5

Launching myself into Swift and SwiftUI, I find the process of migrating from UIKit quite hard. Presently stomped by UserDefaults, even after trying to make sense of the many tutorials I found on the web.

Please tell me what I'm doing wrong here : VERY simple code to :

  1. register a bool value to a UserDefault,
  2. display that bool in a text !

Doesn't get any simpler than that. But I can't get it to work, as the call to UserDefaults throws this error message :

Instance method 'appendInterpolation' requires that 'Bool' conform to '_FormatSpecifiable'

My "app" is the default single view app with the 2 following changes :

1- In AppDelegate, I register my bool :

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

    UserDefaults.standard.register(defaults: [
    "MyBool 1": true
    ])


    return true
}

2- in ContentView, I try to display it (inside struct ContentView: View) :

let defaults = UserDefaults.standard

var body: some View {
    Text("The BOOL 1 value is : Bool 1 = \(defaults.bool(forKey: "MyBool 1"))")
}

Any ideas ?

Thanks

mybrave
  • 1,662
  • 3
  • 20
  • 37
Esowes
  • 287
  • 2
  • 13
  • https://www.hackingwithswift.com/example-code/system/how-to-save-user-settings-using-userdefaults – Enzo N. Digiano Apr 17 '20 at 13:39
  • Thanks for the help below, I am moving on to another UserDefaults issue still unresolved : [Data flow between UserDefaults and datePicker](https://stackoverflow.com/questions/61285454/userdefaults-insanity-take-2-with-a-datepicker) – Esowes Apr 18 '20 at 07:12
  • @Enzo N. digiano : thanks but I had seen that tutorial. Alas, things are not that straightforward... – Esowes Apr 18 '20 at 16:02

7 Answers7

6

Your issue is that the Text(...) initializer takes a LocalizedStringKey rather than a String which supports different types in its string interpolation than plain strings do (which does not include Bool apparently).

There's a couple ways you can work around this.

You could use the Text initializer that takes a String and just displays it verbatim without attempting to do any localization:

var body: some View {
    Text(verbatim: "The BOOL 1 value is : Bool 1 = \(defaults.bool(forKey: "MyBool 1"))")
}

Alternatively, you could extend LocalizedStringKey.StringInterpolation to support bools and then your original code should work:

extension LocalizedStringKey.StringInterpolation {
    mutating func appendInterpolation(_ value: Bool) {
        appendInterpolation(String(value))
    }
}
dan
  • 9,695
  • 1
  • 42
  • 40
1

To solve your problem, just add description variable, like:

var body: some View {
    Text("The BOOL 1 value is : Bool 1 = \(defaults.bool(forKey: "MyBool 1").description)")
}
YardenV4
  • 11
  • 2
  • 1
    You shouldn't use the `description` property. According to apple docs: *Calling this property directly is discouraged. Instead, convert an instance of any type to a string by using the String(describing:) initializer* – Rob C Apr 18 '20 at 00:30
  • concur, you shouldn't use the description property, use String(..) see my answer below. – workingdog support Ukraine Apr 18 '20 at 00:34
  • Making progress ! Using the String wrapper works like a charm. – Esowes Apr 18 '20 at 06:29
  • @Rob Making progress ! Using the String wrapper works like a charm. I'm now having trouble with the data flow. The whole idea was to use a datePicker and instantiate it from a UserDefaults date, but the picker systematically shows the present day as starter upon relaunch of the app (after a kill). I still instantiate my UserDefaults by the register method in appDelegate, but in UIKit, that would not override future settings by the users. Here is my full code : – Esowes Apr 18 '20 at 06:34
  • Cant't seem to post my code in comment...Guess I'll have to start a new topic. – Esowes Apr 18 '20 at 06:35
  • Im aware its not the best practice, but to only solve his error - he could easily just type description, And later on, use private string variable instead. – YardenV4 Apr 18 '20 at 14:16
1

To answer your questions:

1- register a bool value to a UserDefault,

2- display that bool in a text !

I tested the following code and confirm that it works on ios 13.4 and macos using catalyst. Note the String(...) wrapping.

in class AppDelegate

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
//    UserDefaults.standard.register(defaults: ["MyBool 1": true])
   UserDefaults.standard.set(true, forKey: "MyBool 1")
    return true
}

in ContentView

import SwiftUI

struct ContentView: View {

@State var defaultValue = false   // for testing
let defaults = UserDefaults.standard

var body: some View {
    VStack {
        Text("bull = \(String(UserDefaults.standard.bool(forKey: "MyBool 1")))")
        Text(" A The BOOL 1 value is Bool 1 = \(String(defaultValue))")
        Text(" B The BOOL 1 value is : Bool 1 = \(String(defaults.bool(forKey: "MyBool 1")))")
    }
    .onAppear(perform: loadData)
}

func loadData() {
    defaultValue = defaults.bool(forKey: "MyBool 1")
    print("----> defaultValue: \(defaultValue) ")
}
}
0

Not sure why you use register, but you can just set the bool value like this:

UserDefaults.standard.set(true, forKey: "MyBool1")
  • 1
    Hello Antoni, I use `register` in order to populate UserDefaults with default values at first launch. The user will then override them later... – Esowes Apr 17 '20 at 13:45
0

in SwiftUI I use these:

UserDefaults.standard.set(true, forKey: "MyBool 1")
let bull = UserDefaults.standard.bool(forKey: "MyBool 1")
  • Hello Workingdog.Tried using this, no success. Tried setting the bool in a "onAppear", no joy. – Esowes Apr 17 '20 at 13:53
0

I figured that the best way to use UserDefaults is inside a class. It helps us subscribe to that class from any model using @ObservedObject property wrapper.

Boolean method can be used for rest of the types

//
//  ContentView.swift
//

import SwiftUI

struct ContentView: View {
    @ObservedObject var data = UserData()
    var body: some View {
        VStack {
            Toggle(isOn: $data.isLocked){ Text("Locked") }
            List(data.users) { user in
                Text(user.name)
                if data.isLocked {
                    Text("User is Locked")
                } else {
                    Text("User is Unlocked")
                }
            }
        }
    }
}


//
//  Model.swift
//

import SwiftUI
import Combine

let defaults = UserDefaults.standard
let usersData: [User] = loadJSON("Users.json")
// This is custom JSON loader and User struct is to be defined

final class UserData: ObservableObject {

    // Saving a Boolean

    @Published var isLocked = defaults.bool(forKey: "Locked") {
        didSet {
            defaults.set(self.isLocked, forKey: "Locked")
        }
    }

    // Saving Object after encoding

    @Published var users: [User] {
        // didSet will only work if used as Binding variable. Else need to create a save method, which same as the following didSet code.
        didSet {
            // Encoding Data to UserDefault if value of user data change
            if let encoded = try? JSONEncoder().encode(users) {
                defaults.set(encoded, forKey: "Users")
            }
        }
    }
    init() {
        // Decoding Data from UserDefault
        if let users = defaults.data(forKey: "Users") {
            if let decoded = try? JSONDecoder().decode([User].self, from: users) {
                self.users = decoded
                return
            }
        }
        // Fallback value if key "Users" is not found
        self.users = usersData
    }
    // resetting UserDefaults to initial values
    func resetData() {
        defaults.removeObject(forKey: "Users")
        self.isLocked = false
        self.users = usersData
    }
}

Note: This code is not tested. It is directly typed here.

NikzJon
  • 912
  • 7
  • 25
-1

Try this:

struct MyView {
    private let userDefaults: UserDefaults

    // Allow for dependency injection, should probably be some protocol instead of `UserDefaults` right away
    public init(userDefaults: UserDefaults = .standard) {
        self.userDefaults = userDefaults
    }
}

// MARK: - View
extension MyView: View {

    var body: some View {
        Text("The BOOL 1 value is: \(self.descriptionOfMyBool1)")
    }
}

private extension MyView {
    var descriptionOfMyBool1: String {
        let key = "MyBool 1"
        return "\(boolFromDefaults(key: key))"
    }

    // should probably not be here... move to some KeyValue protocol type, that you use instead of `UserDefaults`...
    func boolFromDefaults(key: String) -> Bool {
        userDefaults.bool(forKey: key)
    }
}
Sajjon
  • 8,938
  • 5
  • 60
  • 94