Problem
When my model housed inside a store class is updated on the main thread asynchronously, a SwiftUI view does not automatically render the slice of the model provisioned by the ViewModel.
Assumed Solution
A custom publisher / promise is needed to link the View/ViewModel/Factory/Store together, as a reference doesn't fire updates on asynchronous updates.
Block
How do I write that? I've tried adding a promise to the Store class, but Xcode warns the Result of call to sink(receiveValue:) is unused. I clearly don't understand promises / publishers and most tutorials use URLSession and its dataTaskPublisher (not my case).
I tried the example in this Apple Dev Forums thread on the factory and store classes, but no dice. I clearly don't understand it. In this answer, @Asperi suggested the View listen to a publisher and update an @State variable, but since my model is private I lack a target for that approach.
Abbreviated Code
- A factory instantiates a ContactStore class with dependencies; a reference is passed to ViewModels.
- The VMs gate access to the private store with computed variables. The view calls functions in the ViewModel that modify state, which works well if synchronous.
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)
}
}
store(in:)apeth.com/UnderstandingCombine/subscribers/… and a subject in your model for the view model to subscribe to..sink, which isAnyCancellable. If you don't, it will immediately cancel the subscription. This is typically done with something like:var c = Set<AnyCancellable>()property, which you then usesink { ... }.store(in: &c). But that just seems like a convoluted way of implementing a delegate pattern@Publishedif you made themprivate... (actually it is rhetorical)? The purpose of@Publishedto be interface ofObservableObjectso others could observe them. So either removeprivateand use published as pattern suppose, or remove published (as they don't do anything anyway) and invent manual notification.