web-dev-qa-db-fra.com

UserDefaults Liaison avec Toggle dans SwiftUI

J'essaie de trouver la meilleure façon de créer un écran de paramètres simple lié à UserDefaults .

Fondamentalement, j'ai un Toggle et je veux:

  • la valeur d'un UserDefault à enregistrer chaque fois que ce Toggle est modifié (le UserDefault devrait être la source de vérité)
  • la bascule pour toujours afficher la valeur de UserDefault

Settings screen with Toggle

J'ai regardé de nombreuses sessions SwiftUI WWDC, mais je ne sais toujours pas exactement comment je dois tout configurer avec les différents outils disponibles dans Combine et SwiftUI. Ma pensée actuelle est que je devrais utiliser un objet Bindable afin que je puisse utiliser un chapeau pour encapsuler un certain nombre de paramètres différents.

Je pense que je suis proche, car cela fonctionne presque comme prévu, mais le comportement est incohérent.

Lorsque je le construis et l'exécute sur un appareil, je l'ouvre et j'active la bascule, puis si je fais défiler légèrement la vue de haut en bas, le commutateur bascule (comme s'il ne sauvegardait pas réellement la valeur dans UserDefaults).

Cependant, si j'allume l'interrupteur, je quitte l'application, puis je reviens plus tard, elle est toujours allumée, comme si elle se souvenait du paramètre.

Aucune suggestion? Je poste ceci dans l'espoir que cela aidera d'autres personnes qui sont nouvelles sur SwiftUI et Combine, car je n'ai trouvé aucune question similaire sur ce sujet.

import SwiftUI
import Combine

struct ContentView : View {

    @ObjectBinding var settingsStore = SettingsStore()

    var body: some View {
        NavigationView {
            Form {
                Toggle(isOn: $settingsStore.settingActivated) {
                    Text("Setting Activated")
                }
            }
        }.navigationBarTitle(Text("Settings"))
    }
}

class SettingsStore: BindableObject {

    var didChange = NotificationCenter.default.publisher(for: .settingsUpdated).receive(on: RunLoop.main)

    var settingActivated: Bool {
        get {
            UserDefaults.settingActivated
        }
        set {
            UserDefaults.settingActivated = newValue
        }
    }
}

extension UserDefaults {

    private static var defaults: UserDefaults? {
        return UserDefaults.standard
    }

    private struct Keys {
        static let settingActivated = "SettingActivated"
    }

    static var settingActivated: Bool {
        get {
            return defaults?.value(forKey: Keys.settingActivated) as? Bool ?? false
        }
        set {
            defaults?.setValue(newValue, forKey: Keys.settingActivated)
        }
    }
}

extension Notification.Name {
    public static let settingsUpdated = Notification.Name("SettingsUpdated")
}
5
gohnjanotis

Essayez quelque chose comme ça. Vous pouvez également envisager d'utiliser EnvironmentObject au lieu de ObjectBinding par cette réponse .

import Foundation

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

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

À l'aide de la liaison d'objet, la bascule définira la valeur par défaut de l'utilisateur avec la clé myBoolSetting sur true/false. Vous pouvez voir la valeur actuelle reflétée dans le texte de la vue Text.

import Combine
import SwiftUI

final class SettingsStore: BindableObject {
    let didChange = PassthroughSubject<Void, Never>()

    @UserDefault(key: "myBoolSetting", defaultValue: false)
    var myBoolSetting: Bool {
        didSet {
            didChange.send()
        }
    }
}


struct ContentView : View {
    @ObjectBinding var settingsStore = SettingsStore()

    var body: some View {
        Toggle(isOn: $settingsStore.myBoolSetting) {
            Text("\($settingsStore.myBoolSetting.value.description)")
        }
    }
}
1
JWK

Avec l'aide de cette vidéo d'azamsharp et ce tutoriel de Paul Hudson , j'ai pu produire une bascule qui se lie à UserDefaults et montre quel changement vous avez assigné instantanément.

  • Délégué de scène:

Ajoutez cette ligne de code sous la constante "fenêtre"

var settingsStore = SettingsStore()

Et modifiez window.rootViewController pour afficher cela

window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settingsStore))
  • SettingsStore:
import Foundation

class SettingsStore: ObservableObject {
    @Published var isOn: Bool = UserDefaults.standard.bool(forKey: "isOn") {
        didSet {
            UserDefaults.standard.set(self.isOn, forKey: "isOn")
        }
    }
}
  • SettingsStoreMenu

Si vous le souhaitez, créez une vue SwiftUI appelée ceci et collez:

import SwiftUI

struct SettingsStoreMenu: View {

    @ObservedObject var settingsStore: SettingsStore

    var body: some View {
        Toggle(isOn: self.$settingsStore.isOn) {
            Text("")
        }
    }
}
  • Enfin et surtout

N'oubliez pas d'injecter SettingsStore à SettingsStoreMenu à partir de la vue principale que vous avez, telle que

import SwiftUI

struct MainView: View {

    @EnvironmentObject var settingsStore: SettingsStore

    @State var showingSettingsStoreMenu: Bool = false


    var body: some View {
        HStack {
            Button("Go to Settings Store Menu") {
                    self.showingSettingsStoreMenu.toggle()
            }
            .sheet(isPresented: self.$showingSettingsStoreMenu) {
                    SettingsStoreMenu(settingsStore: self.settingsStore)
            }
        }
    }
}

(Ou de toute autre manière que vous désirez.)

0
esedege

Voici ce que j'ai trouvé après quelques expérimentations, en utilisant PassthroughSubject au lieu d'essayer de faire quelque chose avec les notifications. Il semble fonctionner de manière cohérente et comme prévu.

Je suppose qu'il y a probablement quelques techniques Swift ou SwiftUI pour simplifier les choses, alors veuillez indiquer d'autres idées sur la façon de faire quelque chose comme ça.

import SwiftUI
import Combine

struct ContentView : View {

    @ObjectBinding var settingsStore: SettingsStore

    var body: some View {
        NavigationView {
            Form {
                Toggle(isOn: $settingsStore.settingActivated) {
                    Text("Setting Activated")
                }
            }.navigationBarTitle(Text("Settings"))
        }
    }
}

class SettingsStore: BindableObject {

    let didChange = PassthroughSubject<Void, Never>()

    var settingActivated: Bool = UserDefaults.settingActivated {
        didSet {

            UserDefaults.settingActivated = settingActivated

            didChange.send()
        }
    }
}

extension UserDefaults {

    private struct Keys {
        static let settingActivated = "SettingActivated"
    }

    static var settingActivated: Bool {
        get {
            return UserDefaults.standard.bool(forKey: Keys.settingActivated)
        }
        set {
            UserDefaults.standard.set(newValue, forKey: Keys.settingActivated)
        }
    }
}
0
gohnjanotis

Un problème que je vois est que vous utilisez les mauvaises API pour définir/obtenir une valeur à partir de UserDefaults. Tu devrais utiliser:

static var settingActivated: Bool {
    get {
        defaults?.bool(forKey: Keys.settingActivated) ?? false
    }
    set {
        defaults?.set(newValue, forKey: Keys.settingActivated)
    }
}
0
daltonclaybrook