如何在 SwiftUI 中引用外部 iOS 系统状态更新?

Aar*_*ger 5 ios swift cncontact swiftui

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
}
Run Code Online (Sandbox Code Playgroud)

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
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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)
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

And for the sake of simplicity, say my view is just:

Text(String(describing: modelData.contacts.authorization))
Run Code Online (Sandbox Code Playgroud)

So my questions are:

  1. 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 (e.g. where the TODO is in the requestAccess() function)?
  2. Given that the user can toggle the permission in the Settings app (i.e., the value might change out from under me), how can I ensure the view state is always updated? (Do I need to subscribe to an NSNotification and similarly force a refresh? Or is there a better way?)

Tar*_*agi 1

正如 @jnpdx 指出的 -与类(尤其是永不改变的单例)一起使用@Published可能不会产生任何有用的结果

@Published其行为类似于CurrentValueSubject,仅当其在后台存储/观察的值发生变化时才会触发更新。由于它存储对实例的引用Contacts.shared,因此它不会提供/触发授权状态更改的任何更新。

现在回答你的问题 - 鉴于ModelData().contacts.authorization调用的是 getter 函数,而不是属性,当我知道它已更改时,如何通知 SwiftUI 视图

只要您直接从 getter 访问一个值ModelData().contacts.authorization,它就只是一个Contacts.Authorization不提供任何可观察性的类型值。

因此,即使该值随时间变化(从.notDetermined=>.authorized开始),也没有存储(参考点)可供我们比较它自上次以来是否发生了变化。

我们必须定义一个可以比较旧/新值并根据需要触发更新的存储。authorization这可以通过如下标记来实现@Published-

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()
                })
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)