如何在 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 变量,但由于我的模型是私有的,因此我缺少该方法的目标。

缩写代码

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
      }
   )
}

(您显然需要对 requestAccessAndImportPhoneContactsimportContacts 进行一些修改才能实际调用我添加的 onContactsImported 处理程序)

还有其他通知方法(例如使用委托)。

可以使用发布者来通知,但它在这里似乎没有用,因为这是一个 one-time 导入,并且 publisher/subscriber似乎有点矫枉过正。