简体   繁体   中英

How to observe changes in UserDefaults?

I have an @ObservedObject in my View:

struct HomeView: View {

    @ObservedObject var station = Station()

    var body: some View {
        Text(self.station.status)
    }
 

which updates text based on a String from Station.status :

class Station: ObservableObject {
    @Published var status: String = UserDefaults.standard.string(forKey: "status") ?? "OFFLINE" {
        didSet {
            UserDefaults.standard.set(status, forKey: "status")
        }
    }      

However, I need to change the value of status in my AppDelegate , because that is where I receive my Firebase Cloud Messages :

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
  // If you are receiving a notification message while your app is in the background,
  // this callback will not be fired till the user taps on the notification launching the application.

  // Print full message.
  let rawType = userInfo["type"]

  // CHANGE VALUE OF status HERE
}

But if I change the status UserDefaults value in AppDelegate - it won't update in my view.

How can my @ObservedObject in my view be notified when status changes?

EDIT: Forgot to mention that the 2.0 beta version of SwiftUI is used in the said example.

Here is possible solution

import Combine

// define key for observing
extension UserDefaults {
    @objc dynamic var status: String {
        get { string(forKey: "status") ?? "OFFLINE" }
        set { setValue(newValue, forKey: "status") }
    }
}

class Station: ObservableObject {
    @Published var status: String = UserDefaults.standard.status {
        didSet {
            UserDefaults.standard.status = status
        }
    }

    private var cancelable: AnyCancellable?
    init() {
        cancelable = UserDefaults.standard.publisher(for: \.status)
            .sink(receiveValue: { [weak self] newValue in
                guard let self = self else { return }
                if newValue != self.status { // avoid cycling !!
                    self.status = newValue
                }
            })
    }
}

Note: SwiftUI 2.0 allows you to use/observe UserDefaults in view directly via AppStorage , so if you need that status only in view, you can just use

struct SomeView: View {
    @AppStorage("status") var status: String = "OFFLINE"
    ...

I would suggest you to use environment object instead or a combination of both of them if required. Environment objects are basically a global state objects. Thus if you change a published property of your environment object it will reflect your view. To set it up you need to pass the object to your initial view through SceneDelegate and you can work with the state in your whole view hierarchy . This is also the way to pass data across very distant sibling views (or if you have more complex scenario).

Simple Example

In your SceneDelegate.swift:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        
    let contentView = ContentView().environmentObject(GlobalState())

    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: contentView)
        self.window = window
        window.makeKeyAndVisible()
    }
}

The global state should conform ObservableObject . You should put your global variables in there as @Published .

class GlobalState: ObservableObject {
    @Published var isLoggedIn: Bool
    
    init(isLoggedIn : Bool) {
        self.isLoggedIn = isLoggedIn
    }
}

Example of how you publish a variable, not relevant to the already shown example in SceneDelegate

This is then how you can work with your global state inside your view. You need to inject it with the @EnvironmentObject wrapper like this:

struct ContentView: View {
    
    @EnvironmentObject var globalState: GlobalState

    var body: some View {
        Text("Hello World")
    }
}

Now in your case you want to also work with the state in AppDelegate. In order to do this I would suggest you safe the global state variable in your AppDelegate and access it from there in your SceneDelegate before passing to the initial view. To achieve this you should add the following in your AppDelegate:

var globalState : GlobalState!
    
static func shared() -> AppDelegate {
    return UIApplication.shared.delegate as! AppDelegate
}

Now you can go back to your SceneDelegate and do the following instead of initialising GlobalState directly:

let contentView = ContentView().environmentObject(AppDelegate.shared().globalState)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM