There are many possible variants of this question, but take as an example the CNAuthorizationStatus
returned by CNContactStore.authorizationStatus(for: .contacts)
, which can be notDetermined
, restricted
, denied
, or authorized
. My goal is to always show the current authorization status in my app's UI.
To expose this to SwiftUI, I might make an ObservableObject
called ModelData
with a contacts
property:
final class ModelData: ObservableObject {
@Published var contacts = Contacts.shared
}
Where contacts
contains my contact-specific model code, including Authorization:
class Contacts {
fileprivate let store = CNContactStore()
static let shared = Contacts()
enum Authorization {
case notDetermined
case restricted
case denied
case authorized
}
var authorization: Authorization {
switch CNContactStore.authorizationStatus(for: .contacts) {
case .notDetermined:
return .notDetermined
case .restricted:
return .restricted
case .denied:
return .denied
case .authorized:
return .authorized
@unknown default:
return .notDetermined
}
}
}
And I might add a method that a button could call to request access:
func requestAccess(handler: @escaping (Bool, Error?) -> Void) {
store.requestAccess(for: .contacts) { (granted, error) in
// TODO: tell SwiftUI views to re-check authorization
DispatchQueue.main.async {
handler(granted, error)
}
}
}
And for the sake of simplicity, say my view is just:
Text(String(describing: modelData.contacts.authorization))
So my questions are:
ModelData().contacts.authorization
calls a getter function, not a property, how can I inform the SwiftUI view when I know it's changed (e.g. where the TODO is in the requestAccess()
function)?As @jnpdx pointed out - using @Published
with a class (especially a singleton that never changes) is probably not going to yield any useful results
@Published
behaves like CurrentValueSubject
and it will trigger an update only in case there are changes in the value it is storing/observing under the hood. Since it is storing a reference to the Contacts.shared
instance, it won't provide/trigger any updates for the authorization state changes.
Now to your question -
Given that ModelData().contacts.authorization
calls a getter function, not a property, how can I inform the SwiftUI view when I know it's changed
As long as you are directly accessing a value out of the getter ModelData().contacts.authorization
, it's just a value of Contacts.Authorization
type that does NOT provide any observability.
So even if the value changes over time (from .notDetermined
=> .authorized
), there is no storage (reference point) against which we can compare whether it has changed since last time or not.
We HAVE TO define a storage that can compare the old/new values and trigger updates as needed. This can achieved be by marking authorization
as @Published
like following -
import SwiftUI
import Contacts
final class Contacts: ObservableObject {
fileprivate let store = CNContactStore()
static let shared = Contacts()
enum Authorization {
case notDetermined
case restricted
case denied
case authorized
}
/// Since we have a storage (and hence a way to compare old/new status values)
/// Anytime a new ( != old ) value is assigned to this
/// It triggers `.send()` which triggers an update
@Published var authorization: Authorization = .notDetermined
init() {
self.refreshAuthorizationStatus()
}
private func refreshAuthorizationStatus() {
authorization = self.currentAuthorization()
}
private func currentAuthorization() -> Authorization {
switch CNContactStore.authorizationStatus(for: .contacts) {
case .notDetermined:
return .notDetermined
case .restricted:
return .restricted
case .denied:
return .denied
case .authorized:
return .authorized
@unknown default:
return .notDetermined
}
}
func requestAccess() {
store.requestAccess(for: .contacts) { [weak self] (granted, error) in
DispatchQueue.main.async {
self?.refreshAuthorizationStatus()
}
}
}
}
struct ContentView: View {
@ObservedObject var contacts = Contacts.shared
var body: some View {
VStack(spacing: 16) {
Text(String(describing: contacts.authorization))
if contacts.authorization == .notDetermined {
Button("Request Access", action: {
contacts.requestAccess()
})
}
}
}
}