如何在 SwiftUI 中异步更新后触发发布者?
How to fire a publisher after async update in SwiftUI?
问题
当我位于商店 class 中的模型在主线程上异步更新时,SwiftUI 视图不会自动呈现由 ViewModel 提供的模型切片。
假设的解决方案
需要一个自定义发布者/承诺来 link View/ViewModel/Factory/Store 在一起,因为参考不会在异步更新时触发更新。
阻止
我该怎么写?我已经尝试向商店 class 添加承诺,但 Xcode 警告调用 sink(receiveValue:) 的结果未使用。我显然不理解 promises / publishers 并且大多数教程使用 URLSession 及其 dataTaskPublisher(不是我的情况)。
我尝试了示例 in this Apple Dev Forums thread on the factory and store classes, but no dice. I clearly don't understand it. 并更新了一个 @State 变量,但由于我的模型是私有的,因此我缺少该方法的目标。
缩写代码
- 工厂实例化一个具有依赖关系的 ContactStore class;引用传递给 ViewModels。
- 虚拟机通过计算变量控制对私有存储的访问。视图调用 ViewModel 中修改状态的函数,如果同步则效果很好。
Factory.swift
import SwiftUI
import Combine
class MainFactory {
init() {
...
self.contactStore = ContactStore()
}
private var preferences: Preferences
private var contactStore: ContactStore
...
func makeOnboardingVM() -> OnboardingVM {
OnboardingVM(preferences: preferences, contactStore: contactStore)
}
}
ContactStore.swift
final class ContactStore {
private(set) var authorizationStatus: CNAuthorizationStatus = .notDetermined
private(set) var contacts: [Contact] = [Contact]()
private(set) var errors: [Error] = [Error]()
private lazy var initialImporter = CNContactImporterForiOS14(converter: CNContactConverterForiOS14(),
predictor: UnidentifiedSelfContactFavoritesPredictor())
}
// MARK: - IMPORT
extension ContactStore {
/// Full import during app onboarding. Work conducted on background thread.
func requestAccessAndImportPhoneContacts(completion: @escaping (Bool) -> Void) {
CNContactStore().requestAccess(for: .contacts) { [weak self] (didAllow, possibleError) in
guard didAllow else {
DispatchQueue.main.async { completion(didAllow) }
return
}
DispatchQueue.main.async { completion(didAllow) }
self?.importContacts()
}
}
private func importContacts() {
initialImporter.importAllContactsOnUserInitiatedThread { [weak self] importResult in
DispatchQueue.main.async {
switch importResult {
case .success(let importedContacts):
self?.contacts = importedContacts
case .failure(let error):
self?.errors.append(error)
}
}
}
}
}
OnboardingViewModel.swift
import SwiftUI
import Contacts
class OnboardingVM: ObservableObject {
init(preferences: Preferences, contactStore: ContactStore) {
self.preferences = preferences
self.contactStore = contactStore
}
@Published private var preferences: Preferences
@Published private var contactStore: ContactStore
var contactsAllImported: [Contact] { contactStore.contacts }
func processAddressBookAndGoToNextScreen() {
contactStore.requestAccessAndImportContacts() { didAllow in
DispatchQueue.main.async {
if didAllow {
self.go(to: .relevantNewScreen)
else { self.go(to: .relevantOtherScreen) }
}
}
}
...
}
View.swift
struct SelectEasiestToCall: View {
@EnvironmentObject var onboarding: OnboardingVM
var body: some View {
VStack {
ForEach(onboarding.allContactsImported) { contact in
SomeView(for: contact)
}
}
我假设你所说的不工作是指 ForEach
不显示导入的联系人。
问题是当您为 ContactStore.contacts
分配新值时,这不会被检测为 OnboardingVM
中的更改。 @Published
属性 contactStore
没有改变,因为它是 class
- reference-type.
您需要做的是编写代码以手动响应此更改。
你可以做的一件事是让另一个处理程序在添加新联系人时调用,并在收到消息后调用 objectWillChange.send
,这将使观察视图知道此对象将更改(然后会重新计算它的主体)
func processAddressBookAndGoToNextScreen() {
contactStore.requestAccessAndImportContacts(
onAccessResponse: { didAllow in
...
},
onContactsImported: {
self.objectWillChange.send() // <- manually notify of change
}
)
}
(您显然需要对 requestAccessAndImportPhoneContacts
和 importContacts
进行一些修改才能实际调用我添加的 onContactsImported
处理程序)
还有其他通知方法(例如使用委托)。
您可以使用发布者来通知,但它在这里似乎没有用,因为这是一个 one-time 导入,并且 publisher/subscriber似乎有点矫枉过正。
问题
当我位于商店 class 中的模型在主线程上异步更新时,SwiftUI 视图不会自动呈现由 ViewModel 提供的模型切片。
假设的解决方案
需要一个自定义发布者/承诺来 link View/ViewModel/Factory/Store 在一起,因为参考不会在异步更新时触发更新。
阻止
我该怎么写?我已经尝试向商店 class 添加承诺,但 Xcode 警告调用 sink(receiveValue:) 的结果未使用。我显然不理解 promises / publishers 并且大多数教程使用 URLSession 及其 dataTaskPublisher(不是我的情况)。
我尝试了示例 in this Apple Dev Forums thread on the factory and store classes, but no dice. I clearly don't understand it.
缩写代码
- 工厂实例化一个具有依赖关系的 ContactStore class;引用传递给 ViewModels。
- 虚拟机通过计算变量控制对私有存储的访问。视图调用 ViewModel 中修改状态的函数,如果同步则效果很好。
Factory.swift
import SwiftUI
import Combine
class MainFactory {
init() {
...
self.contactStore = ContactStore()
}
private var preferences: Preferences
private var contactStore: ContactStore
...
func makeOnboardingVM() -> OnboardingVM {
OnboardingVM(preferences: preferences, contactStore: contactStore)
}
}
ContactStore.swift
final class ContactStore {
private(set) var authorizationStatus: CNAuthorizationStatus = .notDetermined
private(set) var contacts: [Contact] = [Contact]()
private(set) var errors: [Error] = [Error]()
private lazy var initialImporter = CNContactImporterForiOS14(converter: CNContactConverterForiOS14(),
predictor: UnidentifiedSelfContactFavoritesPredictor())
}
// MARK: - IMPORT
extension ContactStore {
/// Full import during app onboarding. Work conducted on background thread.
func requestAccessAndImportPhoneContacts(completion: @escaping (Bool) -> Void) {
CNContactStore().requestAccess(for: .contacts) { [weak self] (didAllow, possibleError) in
guard didAllow else {
DispatchQueue.main.async { completion(didAllow) }
return
}
DispatchQueue.main.async { completion(didAllow) }
self?.importContacts()
}
}
private func importContacts() {
initialImporter.importAllContactsOnUserInitiatedThread { [weak self] importResult in
DispatchQueue.main.async {
switch importResult {
case .success(let importedContacts):
self?.contacts = importedContacts
case .failure(let error):
self?.errors.append(error)
}
}
}
}
}
OnboardingViewModel.swift
import SwiftUI
import Contacts
class OnboardingVM: ObservableObject {
init(preferences: Preferences, contactStore: ContactStore) {
self.preferences = preferences
self.contactStore = contactStore
}
@Published private var preferences: Preferences
@Published private var contactStore: ContactStore
var contactsAllImported: [Contact] { contactStore.contacts }
func processAddressBookAndGoToNextScreen() {
contactStore.requestAccessAndImportContacts() { didAllow in
DispatchQueue.main.async {
if didAllow {
self.go(to: .relevantNewScreen)
else { self.go(to: .relevantOtherScreen) }
}
}
}
...
}
View.swift
struct SelectEasiestToCall: View {
@EnvironmentObject var onboarding: OnboardingVM
var body: some View {
VStack {
ForEach(onboarding.allContactsImported) { contact in
SomeView(for: contact)
}
}
我假设你所说的不工作是指 ForEach
不显示导入的联系人。
问题是当您为 ContactStore.contacts
分配新值时,这不会被检测为 OnboardingVM
中的更改。 @Published
属性 contactStore
没有改变,因为它是 class
- reference-type.
您需要做的是编写代码以手动响应此更改。
你可以做的一件事是让另一个处理程序在添加新联系人时调用,并在收到消息后调用 objectWillChange.send
,这将使观察视图知道此对象将更改(然后会重新计算它的主体)
func processAddressBookAndGoToNextScreen() {
contactStore.requestAccessAndImportContacts(
onAccessResponse: { didAllow in
...
},
onContactsImported: {
self.objectWillChange.send() // <- manually notify of change
}
)
}
(您显然需要对 requestAccessAndImportPhoneContacts
和 importContacts
进行一些修改才能实际调用我添加的 onContactsImported
处理程序)
还有其他通知方法(例如使用委托)。
您可以使用发布者来通知,但它在这里似乎没有用,因为这是一个 one-time 导入,并且 publisher/subscriber似乎有点矫枉过正。