SwiftUIで非同期更新後にパブリッシャーを起動するにはどうすればよいですか?

Aug 23 2020

問題

ストアクラス内に格納されているモデルがメインスレッドで非同期に更新されると、SwiftUIビューはViewModelによってプロビジョニングされたモデルのスライスを自動的にレンダリングしません。

想定される解決策

参照は非同期更新で更新を起動しないため、View / ViewModel / Factory / Storeをリンクするにはカスタムパブリッシャー/ Promiseが必要です。

ブロック

どうやって書くの?Storeクラスにpromiseを追加しようとしましたが、Xcodeはsink(receiveValue :)の呼び出しの結果が使用されていないと警告します。私はプロミス/パブリッシャーを明確に理解しておらず、ほとんどのチュートリアルはURLSessionとそのdataTaskPublisherを使用しています(私の場合ではありません)。

このAppleDev Forumsスレッドの例をファクトリークラスとストアクラスで試しましたが、サイコロはありませんでした。私は明らかにそれを理解していません。この回答では、@ Asperiは、ビューがパブリッシャーをリッスンして@State変数を更新することを提案しましたが、私のモデルはプライベートであるため、そのアプローチのターゲットがありません。

省略コード

  • ファクトリは、依存関係を持つContactStoreクラスをインスタンス化します。参照がViewModelsに渡されます。
  • VMは、計算された変数を使用してプライベートストアへのアクセスをゲートします。ビューは、状態を変更する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)
            }
        }

回答

1 NewDev Aug 23 2020 at 08:08

ForEach動作しないということは、インポートされた連絡先が表示されないということだと思います。

問題は、に新しい値を割り当てている間ContactStore.contacts、これがの変更として検出されないことOnboardingVMです。@Publishedプロパティには、contactStoreそれはですので、変更されていないclass参照型- 。

あなたがする必要があるのは、この変更に手動で反応するコードを書くことです。

できることの1つは、新しい連絡先が追加されたときに呼び出される別のハンドラーを用意し、ニュースを受信しobjectWillChange.sendたら、を呼び出すことです。これにより、このオブジェクトが変更されることを監視ビューに通知します(その後、本体を再計算します)。

func processAddressBookAndGoToNextScreen() {
   contactStore.requestAccessAndImportContacts(
      onAccessResponse: { didAllow in
        ...
      }, 
      onContactsImported: { 
         self.objectWillChange.send() // <- manually notify of change
      }
   )
}

(あなたは明らかにいくつかの変更を加える必要があるだろうrequestAccessAndImportPhoneContactsし、importContacts実際に呼び出すためにonContactsImported私が追加したハンドラ)

通知する方法は他にもあります(デリゲートの使用など)。

パブリッシャーを使用して通知することもできますが、これは1回限りのインポートであり、パブリッシャー/サブスクライバーはやり過ぎのように思われるため、ここでは役に立たないようです。