me
me copied to clipboard
学习 MacOS 开发 (Part 12: ObservableObject)
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,在加载前显示默认图片,加载后自动更新。
解题过程:
- 使用方: ContentView
struct ContentView: View {
var body: some View {
UrlImage(url: "https://img1.doubanio.com/view/photo/l/public/p2616241137.jpg")
}
}
- 推导出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()
- 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()
}
}
注:
- 这里用不用shared都ok,我只是做个测试,多一个种选择
- SwiftUI的App是在main dispatch,即主线程,UI的操作都必须回到主线程,所以有了main.async,这部分涉及GCD单开文章来说