18

I want a @Published variable to be persisted, so that it's the same every time when I relaunch my app.

I want to use both the @UserDefault and @Published property wrappers on one variable. For example I need a '@PublishedUserDefault var isLogedIn'.

I have the following propertyWrapper

import Foundation

@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T

    init(_ key: String, defaultValue: T) {
        self.key = key
        self.defaultValue = defaultValue
    }

    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

This is my Settings class

import SwiftUI
import Combine

 class Settings: ObservableObject {

   @Published var isLogedIn : Bool = false

 func doLogin(params:[String:String]) {

        Webservice().login(params: params) { response in

            if let myresponse = response {                    
                    self.login = myresponse.login
                    }
               }
         }

}

My View class

struct HomeView : View {
    @EnvironmentObject var settings: Settings
    var body: some View {
        VStack {
            if settings.isLogedIn {
            Text("Loged in")
            } else{
            Text("Not Loged in")
            }
        }
    }
}

Is there a way to make a single property wrapper that covers both the persisting and the publishing?

Sreeni
  • 205
  • 2
  • 6

5 Answers5

36
import SwiftUI
import Combine

fileprivate var cancellables = [String : AnyCancellable] ()

public extension Published {
    init(wrappedValue defaultValue: Value, key: String) {
        let value = UserDefaults.standard.object(forKey: key) as? Value ?? defaultValue
        self.init(initialValue: value)
        cancellables[key] = projectedValue.sink { val in
            UserDefaults.standard.set(val, forKey: key)
        }
    }
}

class Settings: ObservableObject {
    @Published(key: "isLogedIn") var isLogedIn = false
    ...
}

Sample: https://youtu.be/TXdAg_YvBNE

Version for all Codable types check out here

Andrew_STOP_RU_WAR_IN_UA
  • 9,318
  • 5
  • 65
  • 101
Victor Kushnerov
  • 3,706
  • 27
  • 56
  • Avoiding the hassle of passing in `objectWillChange` which a custom `propertyWrapper` would mean by using a custom init which just uses the existing storage and stores on set via sink on change. I didn't think of it. :-) My solution is more cumbersome where u have to set `objectWillChange` inside `init()` before the propertyWrapper is used. – Fabian Sep 18 '19 at 05:38
  • Thank you for this, it makes it much more simpler ^_^ – ygee Sep 26 '19 at 11:13
  • Thanks @victor! it should be possible to make it two way so that user defaults changes updates the value. – Kugutsumen Oct 02 '19 at 23:27
  • I have a small question about your answer, adding the Singelton of Userdefaults in published extension will that cause the app to keep all items accessible everywhere? will it have any performance impact if I did the same but with Keychain? – Mohammed Zaki Nov 19 '20 at 01:32
  • `@AppStorage("isLoggedIn") private var isLoggedIn = false` – kwiknik Mar 09 '23 at 16:42
10

To persist your data you could use the @AppStorage property wrapper. However, without using @Published your ObservableObject will no longer put out the news about the changed data. To fix this, simply call objectWillChange.send() from the property's willSet observer.

import SwiftUI

class Settings: ObservableObject {
    @AppStorage("Example") var example: Bool = false {
        willSet {
            // Call objectWillChange manually since @AppStorage is not published
            objectWillChange.send()
        }
    }
}
Nik
  • 9,063
  • 7
  • 66
  • 81
Pim
  • 2,128
  • 18
  • 30
1

It should be possible to compose a new property wrapper:

Composition was left out of the first revision of this proposal, because one can manually compose property wrapper types. For example, the composition @A @B could be implemented as an AB wrapper:

@propertyWrapper
struct AB<Value> {
  private var storage: A<B<Value>>

  var wrappedValue: Value {
    get { storage.wrappedValue.wrappedValue }
    set { storage.wrappedValue.wrappedValue = newValue }
  }
}

The main benefit of this approach is its predictability: the author of AB decides how to best achieve the composition of A and B, names it appropriately, and provides the right API and documentation of its semantics. On the other hand, having to manually write out each of the compositions is a lot of boilerplate, particularly for a feature whose main selling point is the elimination of boilerplate. It is also unfortunate to have to invent names for each composition---when I try the compose A and B via @A @B, how do I know to go look for the manually-composed property wrapper type AB? Or maybe that should be BA?

Ref: Property WrappersProposal: SE-0258

Mycroft Canner
  • 1,828
  • 1
  • 11
  • 24
0

You currently can't wrap @UserDefault around @Published since that is not currently allowed.

The way to implement @PublishedUserDefault is to pass an objectWillChange into the wrapper and call it before setting the variable.

Fabian
  • 5,040
  • 2
  • 23
  • 35
  • In [Property Wrappers](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) the example `struct Field`. It takes a `String` as an argument. You have to pass in `objectWillChange` instead. The pretty much only thing special about `@Published` is that it calls `objectWillChange.send()` at the appropriate time. – Fabian Aug 28 '19 at 07:08
  • @Fabian I posted my solution below – Victor Kushnerov Sep 17 '19 at 22:10
0
struct HomeView : View {
    @StateObject var auth = Auth()
    @AppStorage("username") var username: String = "Anonymous"

    var body: some View {
        VStack {
            if username != "Anonymous" {
                Text("Logged in")
            } else{
                Text("Not Logged in")
            }
        }
        .onAppear(){
             auth.login()
        }
    }
}


import SwiftUI
import Combine

class Auth: ObservableObject {

 func login(params:[String:String]) {

        Webservice().login(params: params) { response in

            if let myresponse = response {          
                    UserDefaults.standard.set(myresponse.login, forKey: "username")`
                    }
               }
         }

}
malhal
  • 26,330
  • 7
  • 115
  • 133