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

How to reference external iOS system state updates in SwiftUI?

这道题有很多种可能的变体,但以CNContactStore.authorizationStatus(for: .contacts)返回的CNAuthorizationStatus为例,可以是notDeterminedrestricteddenied,或 authorized。我的目标是始终在我的应用的 UI.

中显示当前授权状态

为了将其暴露给 SwiftUI,我可能会创建一个名为 ModelDataObservableObject 和一个 contacts 属性:

final class ModelData: ObservableObject {
    @Published var contacts = Contacts.shared
}

其中 contacts 包含我的联系人特定型号代码,包括授权:

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
        }
    }
}

我可能会添加一个按钮可以调用以请求访问的方法:

    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)
            }
        }
    }

为了简单起见,我的观点是:

Text(String(describing: modelData.contacts.authorization))

所以我的问题是:

  1. 鉴于 ModelData().contacts.authorization 调用了 getter 函数,而不是 属性,当我知道 SwiftUI 视图发生变化时(例如,在哪里TODO 在 requestAccess() 函数中)?
  2. 鉴于用户可以在“设置”应用中切换权限(即,值可能会从我下面改变),我如何确保视图状态始终更新? (我是否需要订阅 NSNotification 并同样强制刷新?或者有更好的方法吗?)

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

@Published 的行为类似于 CurrentValueSubject,并且仅当其 storing/observing 的值发生变化时才会触发更新。由于它正在存储对 Contacts.shared 实例的引用,因此它不会 provide/trigger 授权状态更改的任何更新。

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

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

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

我们必须定义一个可以比较 old/new 值并根据需要触发更新的存储。这可以通过将 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()
                })
            }
        }
    }
}

我认为你已经完成了所有工作。 当用户从“设置”应用程序更改访问级别时调用此行。

Text(String(describing: modelData.contacts.authorization))

因此您的视图始终显示当前状态。