Модель представления ObservedObject все еще находится в памяти после закрытия представления - PullRequest
2 голосов
/ 19 июня 2020

У меня проблемы с управлением памятью в SwiftUI и Combine.

Например, если у меня есть NavigationView, а затем перейдите к подробному представлению с помощью TextField, введите значение в TextField и коснитесь на кнопке "Назад", в следующий раз, когда я go перейду к этому представлению, TextField будет иметь ранее введенное значение.

Я заметил, что модель представления все еще находится в памяти после того, как подробное представление отклонено, и, вероятно, поэтому TextField по-прежнему содержит значение.

В UIKit при отклонении ViewController модель представления будет освобождена, а затем создана снова, когда ViewController будет представлен. Кажется, здесь это не так.

Я прилагаю минимальный воспроизводимый код для этой проблемы.

import SwiftUI
import Combine

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: OtherView()) {
                Text("Press Here")
            }
        }
    }
}

struct OtherView: View {

    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack {
            TextField("Something", text: $viewModel.enteredText)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("Tap")
            }) {
                Text("Tapping")
            }.disabled(!viewModel.isValid)
        }
    }
}

class ViewModel: ObservableObject {

    @Published var enteredText = ""
    var isValid = false

    var cancellable: AnyCancellable?

    init() {
        cancellable = textValidatedPublisher.receive(on: RunLoop.main)
            .assign(to: \.isValid, on: self)
    }

    deinit {
        cancellable?.cancel()
    }

    var textValidatedPublisher: AnyPublisher<Bool, Never> {
        $enteredText.map {
            $0.count > 1
        }.eraseToAnyPublisher()
    }


}

Я также заметил, что если, например, я добавляю другое представление, скажем, SomeOtherView после OtherView, затем каждый раз, когда я ввожу TextField из OtherView, вызывается деинициализация из модели просмотра SomeOtherView. Может ли кто-нибудь объяснить, почему это происходит?

1 Ответ

2 голосов
/ 19 июня 2020

Более того, я заметил, что если я изменю ContetView и представление будет переоценено, то у меня будут две модели ViewModels в памяти

Это связано с перекрестной ссылкой в ​​ViewModel, здесь фиксированный вариант

struct OtherView: View, Constructable {

    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        VStack {
            TextField("Something", text: $viewModel.enteredText)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("Tap")
            }) {
                Text("Tapping")
            }.disabled(!viewModel.isValid)
        }
        .onDisappear {
            self.viewModel.invalidate()     // << here !!
        }
    }
}

class ViewModel: ObservableObject {

    @Published var enteredText = ""
    var isValid = false

    var cancellable: AnyCancellable?

    init() {
        print("[>>] created")
        cancellable = textValidatedPublisher.receive(on: RunLoop.main)
            .assign(to: \.isValid, on: self)
    }

    func invalidate() {
        cancellable?.cancel()
        cancellable = nil
        print("[<<] invalidated")
    }

    deinit {
//        cancellable?.cancel()     // not here !!!
        print("[x] done")
    }

    var textValidatedPublisher: AnyPublisher<Bool, Never> {
        $enteredText.map {
            $0.count > 1
        }.eraseToAnyPublisher()
    }
}

-

Обновление:

есть ли способ создать экземпляр OtherView при навигации ?

Вот решение (протестировано с Xcode 11.4 / iOS 13.4), но это только полдела, потому что после создания он будет активен до тех пор, пока не будет повторно подтверждена ссылка навигации (ie. на задней панели остается в памяти до следующей навигации)

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: 
               // create wrapper view with type of view which creation
               // is deferred until navigation
               DeferCreatingView(of: OtherView.self)) {
                Text("Press Here")
            }
        }
    }
}

protocol Constructable {
    init()
}

struct DeferCreatingView<T: View & Constructable>: View {
    var ViewType: T.Type

    init(of type: T.Type) {
        ViewType = type
    }

    var body: some View {
        ViewType.init()     // << create only here
    }
}


struct OtherView: View, Constructable {
    // .. not changed code from first part
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...