me icon indicating copy to clipboard operation
me copied to clipboard

学习 MacOS 开发 (Part 12: ObservableObject)

Open nonocast opened this issue 2 years ago • 0 comments

ObservableObject | Apple Developer Documentation

main.swift

import SwiftUI

class Contact: ObservableObject {
  @Published var name: String
  @Published var age: Int

  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }

  func haveBirthday() -> Int {
    age += 1
    return age
  }
}

let john = Contact(name: "John Appleseed", age: 24)
let cancellable = john.objectWillChange.sink { _ in
  print("john property will change")
}

print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

这里有个很妖的事情,就是你必须持有cancellable对象,否则这个return object就被自动释放掉了,释放掉以后就不会接收will change消息,和Furture很类似。

swift - Why objectWillChange has no effect - Stack Overflow

You have to store the AnyCancellable that's returned by sink, otherwise it cancels the subscription as soon as it's deinitialized when you assign to _.

注:

  • objectWillChange可以挂载多个sink,会依次触发
  • 如果只需要listen具体某个属性,采用$方式
let c = john.$age.sink { _ in
  print("#1 john age(\(john.age)) will change")
}
  • 也可以手动触发change消息
class Contact: ObservableObject {
  @Published var name: String
  @Published var age: Int

  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }

  func haveBirthday() -> Int {
    age += 1
    return age
  }

  func fire() {
    objectWillChange.send()
  }
}

$0

import SwiftUI

class Contact: ObservableObject {
  @Published var name: String
  @Published var age: Int

  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }

  func haveBirthday() -> Int {
    age += 1
    return age
  }
}

let john = Contact(name: "John Appleseed", age: 24)
let cancellable = john.$age.sink {
  print($0)
  print("john property will change, from \(john.age) to \($0)")
}

print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

注:

  • $0 表示new value

小结:

  • ObservableObject通过hook property的setter,然后通过objectWillChange触发
  • @ObservedObject标记就是用来关注ObservableObject,当放生变化后就会引起View的render,更新数据

Study Case

我们来看这个案例 如何在 SwiftUI 裡客製一個非同步載入圖片的 View - 法蘭克的iOS世界 - Medium

意图: 实现一个URLImageView,在加载前显示默认图片,加载后自动更新。

解题过程:

  1. 使用方: ContentView
struct ContentView: View {
  var body: some View {
    UrlImage(url: "https://img1.doubanio.com/view/photo/l/public/p2616241137.jpg")
  }
}
  1. 推导出UrlImage, 因为是个View, 所以必须是个轻量对象,不应该有任何业务代码,所以提取出ImageLoader
struct UrlImage: View {
  @ObservedObject var imageLoader = ImageLoader.shared
  var placeholder: Image = Image(systemName: "photo")
  
  init(url: String) {
    self.imageLoader.load(url: url)
  }
  
  var body: some View {
    if let nsImage = imageLoader.image {
      Image(nsImage: nsImage).resizable().scaledToFit()
    } else {
      placeholder
    }
  }
}

注:

  • 原文采用return Image(...),这里的return是需要去掉,否则会不能接scaledToFit()
  1. ImageLoader
import Combine
import Foundation
import Cocoa

class ImageLoader : ObservableObject {
  static var shared = ImageLoader()
  
  @Published var image: NSImage?
  
  func load(url: String) {
    print("now loading: ", url)
    
    guard let url = URL(string: url) else {
      print("url failed.")
      return
    }
    
    let request = URLRequest(url: url)
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
      if let error = error {
        print(error.localizedDescription)
        return
      }
      
      guard let data = data, let _ = response else {
        print("data failed")
        return
      }
      
      DispatchQueue.main.async {
        self.image = NSImage(data: data)
      }
    }
    
    task.resume()
  }
}

注:

  1. 这里用不用shared都ok,我只是做个测试,多一个种选择
  2. SwiftUI的App是在main dispatch,即主线程,UI的操作都必须回到主线程,所以有了main.async,这部分涉及GCD单开文章来说

nonocast avatar May 02 '22 04:05 nonocast