【问题标题】:Swift: Should ViewModel be a struct or class?Swift:ViewModel 应该是结构还是类?
【发布时间】:2016-10-21 04:48:38
【问题描述】:

我正在尝试在我的新项目中使用 MVVM 模式。第一次,我创建了我所有的视图模型来构建。但是当我使用闭包实现异步业务逻辑(例如 fetchDataFromNetwork)时,闭包会捕获旧的视图模型值,然后更新为该值。不是新的视图模型值。

这是 Playground 中的测试代码。

import Foundation
import XCPlayground

struct ViewModel {
  var data: Int = 0

  mutating func fetchData(completion:()->()) {
    XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
    NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
      result in
      self.data = 10
      print("viewModel.data in fetchResponse : \(self.data)")
      completion()
      XCPlaygroundPage.currentPage.finishExecution()
      }.resume()
  }
}

class ViewController {
  var viewModel: ViewModel = ViewModel() {
    didSet {
      print("viewModel.data in didSet : \(viewModel.data)")
    }
  }

  func changeViewModelStruct() {
    print("viewModel.data before fetch : \(viewModel.data)")

    viewModel.fetchData {
      print("viewModel.data after fetch : \(self.viewModel.data)")
    }
  }
}

var c = ViewController()
c.changeViewModelStruct()

控制台打印

viewModel.data before fetch : 0
viewModel.data in didSet : 0
viewModel.data in fetchResponse : 10
viewModel.data after fetch : 0

问题是 ViewController 中的 View Model 没有新的 Value 10。

如果我将 ViewModel 更改为 class,则不会调用 didSet,但 ViewController 中的 View Model 具有新值 10。

【问题讨论】:

    标签: swift mvvm struct closures


    【解决方案1】:

    你应该使用一个类。

    如果您使用带有变异函数的结构,则该函数不应在闭包内执行变异;您应该执行以下操作:

    struct ViewModel {
      var data: Int = 0
    
      mutating func myFunc() {
          funcWithClosure() {
              self.data = 1
          }
      }
    }
    

    如果我将 ViewModel 更改为 class,则不会调用 didSet

    这里没有错 - 这是预期的行为。


    如果你喜欢使用struct,你可以这样做

      func fetchData(completion: ViewModel ->()) {
        XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
        NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
          result in
          var newViewModel = self
          newViewModel.data = 10
          print("viewModel.data in fetchResponse : \(self.data)")
          completion(newViewModel)
          XCPlaygroundPage.currentPage.finishExecution()
          }.resume()
      }
    
    
      viewModel.fetchData { newViewModel in
         self.viewModal = newViewModel
          print("viewModel.data after fetch : \(self.viewModel.data)")
        }
    

    还要注意,提供给dataTaskWithURL 的闭包不会在主线程上运行。您可能想在其中调用dispatch_async(dispatch_get_main_queue()) {...}

    【讨论】:

    • 所以没有办法使用带有异步API调用@Code的struct?因为我更喜欢使用结构而不是类。
    • @Paul 编辑了我的帖子(再次)。
    • 是的,这是一个糟糕的设计。 :( 在这种情况下我应该使用类。谢谢@Code。
    • 尽管它的设计很糟糕,但使用类,编写单元测试要容易得多。 (struct 的 Mocking 方法确实是一项令人沮丧的任务)
    【解决方案2】:

    您可以通过两种方式获得self.data:或者在闭包中为fetchResponse 使用返回参数(使用viewModel 作为struct)或者您可以创建自己的设置方法/闭包并使用它在你的init 方法中(使用viewModel 作为class)。

    class ViewModel {
    var data: Int = 0
    func fetchData(completion:()->()) {
        XCPlaygroundPage.currentPage.needsIndefiniteExecution = true
        NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://stackoverflow.com")!) {
            result in
            self.data = 10
            print("viewModel.data in fetchResponse : \(self.data)")
            completion()
            XCPlaygroundPage.currentPage.finishExecution()
            }.resume()
        }
    }
    
    class ViewController {
        var viewModel: ViewModel! { didSet { print("viewModel.data in didSet : \(viewModel.data)") } }
    
        init( viewModel: ViewModel ) {
            // closure invokes didSet
            ({ self.viewModel = viewModel })()
        }
    
        func changeViewModelStruct() {
            print("viewModel.data before fetch : \(viewModel.data)")
    
            viewModel.fetchData {
                print("viewModel.data after fetch : \(self.viewModel.data)")
            }
        }
    }
    
    let viewModel = ViewModel()
    var c = ViewController(viewModel: viewModel)
    c.changeViewModelStruct()
    

    控制台打印:

    viewModel.data in didSet : 0
    viewModel.data before fetch : 0
    viewModel.data in fetchResponse : 10
    viewModel.data after fetch : 10
    

    Apple Document 是这样说的:

    第一次初始化属性时不会调用 willSet 和 didSet 观察者。仅当属性的值在初始化上下文之外设置时才会调用它们。

    【讨论】:

    • 是的,我知道。但我更喜欢使用 didSet 观察者而不是使用 fetchData 函数进行委托或回调。所以我使用结构而不是类。如果没有办法使用带有异步变异功能的结构,我应该使用类。
    猜你喜欢
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 1970-01-01
    • 2016-10-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-09
    • 1970-01-01
    相关资源
    最近更新 更多