【问题标题】:Why is an old view model responding to a notification?为什么旧视图模型会响应通知?
【发布时间】:2018-01-12 16:27:10
【问题描述】:

我正在使用 MVVM 创建一个举重计算器应用程序 (Swift 4),并且已经尝试了 2 天来弄清楚为什么应该死掉的视图模型仍然响应 UserDefaults.defaultsDidChange 事件通知。

  1. 我启动应用程序:
    • 在启动时,我在 AppDelegate 中创建了一个新的 lift 事件对象,并使用它为 `CalculatorViewController' 初始化一个新的 CalculatorLiftEventViewModelFromLiftEvent
  2. 计算升力并保存
  3. 我点击 + 按钮来创建一个新的电梯:
    • 这会导致创建一个新的空电梯事件对象
    • 这个新的提升事件对象用于初始化一个新的CalculatorLiftEventViewModelFromLiftEvent对象
    • 然后将这个新的CalculatorLiftEventViewModelFromLiftEvent 分配给CalculatorViewController 的viewModel 属性,替换应用启动时创建的那个
    • 计算器屏幕上的值已清零,准备输入新的电梯事件
  4. 我点击“设置”按钮转到“设置”,在其中更改与当前提升事件关联的公式。
  5. 新公式保存为默认值并触发UserDefaults.defaultsDidChange 通知
  6. 这是我无法弄清楚的部分:原始视图模型仍然存在,并且仍在侦听 UserDefault 通知。当我关闭“设置”屏幕并返回“计算器”视图时,先前已清除的提升事件中的值现在重新出现。

点击计算器屏幕上的 +(新)按钮时会发生以下情况:

@objc fileprivate func onNewButtonTapped(_ sender: UIBarButtonItem) {
    let newLiftEvent = dataManager.createNewLiftEvent()
    viewModel = CalculatorLiftEventViewModelFromLiftEvent(withLiftEvent: newLiftEvent, dataManager: dataManager)

    setupView()
}

CalculatorLiftEventViewModelFromLiftEvent 的初始化方式如下:

init(withLiftEvent liftEvent: LiftEventRepresentable, dataManager: CoreDataHelper) {
    self.modelLiftEvent = liftEvent
    self.liftName = Dynamic("\(modelLiftEvent.lift.liftName)")
    self.weightLiftedTextField = Dynamic(modelLiftEvent.liftWeight.value)
    self.repetitionsTextField = Dynamic("\(modelLiftEvent.repetitions)")
    self.oneRepMaxTextField = Dynamic(modelLiftEvent.oneRepMax.value)
    self.unitsTextField = Dynamic("\(UserDefaults.weightUnit())")
    self.weightPercentages = Dynamic( [ : ] )
    self.dataManager = dataManager

    super.init()

    subscribeToNotifications()
}

更新:这是deinitCalculatorLiftEventViewModelFromLiftEvent 中的addObservers。请注意,我没有使用基于块的观察。

deinit {
    print("I got to the deinit method")
    unsubscribeFromNotifications()
}

func subscribeToNotifications() {
        NotificationCenter.default.addObserver(self,
                                                    selector: #selector(liftNameDidChangeNotification(_:)),
                                                    name: NSNotification.Name(rawValue: LiftEventNotifications.LiftNameDidChangeNotification),
                                                    object: nil)

        NotificationCenter.default.addObserver(self,
                                                    selector: #selector(weightUnitDidChangeNotification(_:)),
                                                    name: NSNotification.Name(rawValue: LiftEventNotifications.WeightUnitDidChangeNotification),
                                                    object: nil)

        NotificationCenter.default.addObserver(self,
                                                    selector: #selector(roundingOptionDidChangeNotification(_:)),
                                                    name: NSNotification.Name(rawValue: UserDefaultsNotifications.roundingOptionDidChangeNotification),
                                                    object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.defaultsDidChange), name: UserDefaults.didChangeNotification,
                                                    object: nil)
    }

--- 结束更新

我在转到SettingsViewController 时传递了modelLiftEvent:

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let identifier = segue.identifier {
            switch identifier {
            case a:...
            case b:...
            case "SettingsSegue":
            if let nav = segue.destination as? UINavigationController {
                let destinationViewController = nav.topViewController as! SettingsViewController
                destinationViewController.dismissalDelegate = self

                let settingsViewModel = SettingsViewModelFromLiftEvent(withLiftEvent: self.viewModel.modelLiftEvent)
                destinationViewController.settingsViewModel = settingsViewModel
                destinationViewController.dataManager = dataManager
                settingsViewModel.dataManager = dataManager
            }

最后,在CalculatorLiftEventViewModelFromLiftEvent 中,我在这里设置了一个断点,因为当视图模型听到UserDefaults.defaultsDidChange 通知时会调用它。此时,我也验证了这个CalculatorLiftEventViewModelFromLiftEvent是旧的,而不是我点击+按钮时创建的新的:

@objc func defaultsDidChange(_ notification: Notification) {
    let oneRepMax = modelLiftEvent.calculateOneRepMax()

    guard oneRepMax.value != 0.0 else { return }

    let weightPercentages = getWeightPercentages(weight: oneRepMax.value)
    self.weightPercentages.value = weightPercentages

    weightLiftedTextField.value = modelLiftEvent.liftWeight.value
    repetitionsTextField.value = "\(modelLiftEvent.repetitions)"
    oneRepMaxTextField.value = modelLiftEvent.oneRepMax.value
}

我已经阅读了一堆关于对象生命周期的文档,但没有找到任何有用的信息。我希望当新的CalculatorLiftEventViewModelFromLiftEvent 被创建并分配给`CalculatorViewController' 的viewModel 属性时,它将替换对旧的引用并且它将不复存在。显然,这不是正在发生的事情。

有谁知道为什么当我从没有值(0.0 除外)的“计算器”视图(第 3 步)转到“设置”然后返回时,会显示之前的提升事件值?

【问题讨论】:

  • 没有足够的代码来确定,但看起来 CalculatorLiftEventViewModelFromLiftEvent 已注册为通知的观察者(通过调用 subscribeToNotifications() 提示)。如果为真,那将持有对保持其活动状态的对象的引用(并且,根据您的断点证据,继续向其发送通知以采取行动。)
  • 谢谢,@pauln。我很高兴添加更多您认为可能相关的代码。我查看了 Apple 关于基于选择器的观察 API 的文档,他们说从 iOS 9.0 开始,您不需要在其 dealloc 方法中取消注册观察者。他们接着说,“否则,您应该在观察者或传递给此方法的任何对象被释放之前调用 removeObserver(_:name:object:)。”不确定“否则”是什么意思。不然呢?
  • 有趣的是,我今天刚刚在这里oleb.net/blog/2018/01/notificationcenter-removeobserver/… 读到了这个。基本上,尽管文档的一部分说不,但 Foundation 注释和 addObserver 文档都说是。所以现在你仍然需要这样做,反正也没有什么坏处。
  • “否则”是指目标在 iOS 9.0 和 macOS 10.11 之前的应用程序。尽管如此,这还是很可疑的,尝试注销可能会发现问题。另外,您的视图模型类型是对象还是结构?
  • @UpholderOfTruth - 哈!我今天早上也读到了。这实际上是我听起来好像我知道我在说什么的原因。我会尝试并报告。

标签: swift mvvm notifications


【解决方案1】:

我已经解决了之前的liftEvent在清除计算器、更改默认公式并返回计算器屏幕后显示的问题。

CalculatorViewController 上,当点击 + 按钮时,我没有创建新的 viewModel 并将其分配给 viewModel 属性,而是要求我的 AppDelegate 使用launchCalculatorViewController 在应用启动时执行此操作的方法。

CalculatorViewController中的原始代码:

@objc fileprivate func onNewButtonTapped(_ sender: UIBarButtonItem) {

    let newLiftEvent = dataManager.createNewLiftEvent()
    viewModel = CalculatorLiftEventViewModelFromLiftEvent(withLiftEvent: newLiftEvent, dataManager: dataManager)

    self.percentagesTableView.reloadData()

    setupView()
}

现在CalculatorViewController中的新代码:

@objc fileprivate func onNewButtonTapped(_ sender: UIBarButtonItem) {

    (UIApplication.shared.delegate as? AppDelegate)?.launchCalculatorViewController()

}

AppDelegate:

func launchCalculatorViewController() {
    self.window = UIWindow(frame: UIScreen.main.bounds)
    let mainStoryboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    if let initialViewController: CalculatorViewController = mainStoryboard.instantiateInitialViewController() as? CalculatorViewController {

        self.window?.rootViewController = initialViewController

        let liftEvent = dataManager.createNewLiftEvent()
        let viewModel = CalculatorLiftEventViewModelFromLiftEvent(withLiftEvent: liftEvent, dataManager: dataManager)
        initialViewController.viewModel = viewModel
        initialViewController.dataManager = dataManager
        self.window?.makeKeyAndVisible()

    }
}

不幸的是,我确定 CalculatorLiftEventViewModelFromLiftEvent 对象永远不会被释放,这告诉我我有一个不会放手的强大引用循环:

那一定是另一个 SO 问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-01-31
    • 2011-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多