Swift:ViewModel 应该是一个结构体还是类?

25

我希望在我的新项目中使用MVVM模式。第一次,我将所有的视图模型都创建为结构体。但是当我实现包含闭包的异步业务逻辑(例如使用fetchDataFromNetwork从网络获取数据)时,闭包会捕获旧的视图模型值并更新到那个值上,而不是一个新的视图模型值。

这里是一个播放器中的测试代码。

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中的ViewModel没有新值10。

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

2个回答

16

你应该使用类(class)。

如果你使用一个带有mutating函数的结构体(struct),则该函数不应在闭包(closure)内进行变异(mutation);你不应该做以下操作:

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()) {...}


那么在@Code中没有办法使用结构体来进行异步API调用吗?因为我更喜欢使用结构体而非类。 - Paul
@Paul 又编辑了我的帖子。 - Code
是的,那是一个糟糕的设计。:( 在这种情况下我应该使用类。谢谢 @Code。 - Paul
2
尽管它的设计不太好,但使用类编写单元测试要容易得多。 (为结构体模拟方法确实是一项令人沮丧的任务) - farzadshbfn

1
你可以在两种方式中获得self.data: 要么使用闭包的返回参数(使用 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

苹果文档中这样说:

当属性首次初始化时,不会调用willSet和didSet观察器。它们只在属性的值在初始化上下文之外被设置时调用。


是的,我知道。但是我更喜欢使用didSet观察器而不是委托或回调fetchData函数。所以我使用结构体而不是类。如果没有办法使用异步变异函数来使用结构体,那么我应该使用类。 - Paul

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接