SwiftUIで非同期更新後にパブリッシャーを起動するにはどうすればよいですか?
問題
ストアクラス内に格納されているモデルがメインスレッドで非同期に更新されると、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)
}
}
回答
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回限りのインポートであり、パブリッシャー/サブスクライバーはやり過ぎのように思われるため、ここでは役に立たないようです。