UserDefaults 在 SwiftUI 中与 Toggle 绑定

Posted

技术标签:

【中文标题】UserDefaults 在 SwiftUI 中与 Toggle 绑定【英文标题】:UserDefaults Binding with Toggle in SwiftUI 【发布时间】:2019-11-11 15:30:29 【问题描述】:

我正在尝试找出构建绑定到 UserDefaults 的简单设置屏幕的最佳方法。

基本上,我有一个 Toggle 并且我想要:

更改此切换时要保存的 UserDefault 值(UserDefault 应该是事实的来源) 切换为始终显示 UserDefault 的值

我已经观看了许多 SwiftUI WWDC 会议,但我仍然不确定应该如何使用 Combine 和 SwiftUI 中提供的不同工具来设置所有内容。我目前的想法是我应该使用 BindableObject,这样我就可以使用 hat 来封装许多不同的设置。

我想我已经接近了,因为它几乎可以按预期工作,但行为不一致。

当我在设备上构建并运行它时,我打开它并打开 Toggle,然后如果我稍微上下滚动视图,开关就会关闭(好像它实际上并没有保存 UserDefaults 中的值) .

但是,如果我打开开关,离开应用程序,然后稍后再回来,它仍然处于打开状态,就像它记住了设置一样。

有什么建议吗?我发布这篇文章是希望它能帮助其他刚接触 SwiftUI 和 Combine 的人,因为我找不到关于这个主题的任何类似问题。

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

【问题讨论】:

How do I use UserDefaults with SwiftUI?的可能重复 【参考方案1】:

更新

------- iOS 14:-------

ios 14 开始,现在有一种非常简单的方式来读取和写入 UserDefaults。

使用名为 @AppStorage 的新属性包装器

它的使用方法如下:

import SwiftUI

struct ContentView : View 

    @AppStorage("settingActivated") var settingActivated = false

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

就是这样!它是如此简单,非常直接。正在从 UserDefaults 中保存和读取您的所有信息。

-------- iOS 13:---------

Swift 5.1 发生了很多变化。 BindableObject 已完全弃用。此外,PassthroughSubject 也发生了重大变化。

对于任何想使其工作的人,以下是相同的工作示例。我重用了“gohnjanotis”的代码来简化它。

import SwiftUI
import Combine

struct ContentView : View 

    @ObservedObject var settingsStore: SettingsStore

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


class SettingsStore: ObservableObject 

    let willChange = PassthroughSubject<Void, Never>()

    var settingActivated: Bool = UserDefaults.settingActivated 
        willSet 

            UserDefaults.settingActivated = newValue

            willChange.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)
        
    

【讨论】:

当用户默认键实际上并未绑定到屏幕上的任何内容,而是用于重新计算屏幕上显示的其他值时,@AppStorage 和您的 iOS 13 解决方案似乎都不起作用。 对不起,我不明白你是怎么做的。能否请您提供一个样品,以便我也可以检查? @AshleyMills 如何将@AppStorage var 传递给其他子视图?或进入应用程序的环境? @SuperNoob 您不需要将其传递给子视图。每个单独的视图都可以使用单独的 AppStorage,它们都将共享数据。 @atulkhatri 即使您在每个子视图中为它设置了不同的默认值?【参考方案2】:

在this video by azamsharp 和this tutorial by Paul Hudson 的帮助下,我已经能够生成一个绑定到 UserDefaults 的切换开关,并立即显示您分配给它的任何更改。

场景代表:

在'window'变量下添加这行代码

var settingsStore = SettingsStore()

并修改 window.rootViewController 以显示这个

window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settingsStore))
设置商店:
import Foundation

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

设置商店菜单

如果你愿意,创建一个名为 this 的 SwiftUI 视图并粘贴:

import SwiftUI

struct SettingsStoreMenu: View 
    
    @ObservedObject var settingsStore: SettingsStore
    
    var body: some View 
        Toggle(isOn: self.$settingsStore.isOn) 
            Text("")
        
    

最后但并非最不重要的一点

不要忘记从您拥有的任何主视图中将 SettingsStore 注入 SettingsStoreMenu,例如

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)
            
        
    

(或任何你想要的方式。)

【讨论】:

这对我的项目很有帮助。谢谢。 你应该import Combine 加入你的班级SettingsStore 吗?如果这是正确的,可能值得更新您的答案。 @andrewbuilder 对上一个答案感到抱歉,我已经有一段时间没有编写这个代码了——或者任何东西,因为我正在等待 SwiftUI 应用程序生命周期中的核心数据集成——所以乍一看我检查我自己的应用程序代码时感到困惑。这个解决方案在任何地方都不需要import Combine,我只是用我写的信息从头开始创建了一个模型应用程序,它可以工作——Xcode-Beta 12.0 b2 和带有 iOS14 b3(真实设备)的 iPhone SE 2020,但它可以工作以及以前的版本,当时我有它们可用-。 @esedege 我没有看到之前的答案,所以无需道歉。我认为我们需要导入 Combine 来让发布者工作,所以我会做更多的研究。 PS 你不需要等待 Core Data 集成到 SwiftUI...查看这个标题为 “Using Core Data with SwiftUI App protocol” 的 Apple 开发者论坛主题,特别是 my implementation。 @andrewbuilder 哇,太棒了!是时候进行调查了。【参考方案3】:

这个接缝效果很好:

enum BackupLocalisations: String, CaseIterable, Hashable, Identifiable 
    case iPhone = "iPhone"
    case iCloud = "iCloud"
    
    var name: String 
        return self.rawValue
    
    var id: BackupLocalisations self


enum Keys 
    static let iCloudIsOn = "iCloudIsOn"
    static let backupLocalisation = "backupLocalisation"
    static let backupsNumber = "backupsNumber"

    class SceneDelegate: UIResponder, UIWindowSceneDelegate 
    
    var window: UIWindow?
    var settings = Settings()

…/…
    let contentView = ContentView()
            .environmentObject(settings)
… 
class Settings: ObservableObject 
    @Published var iCloudIsOn: Bool = UserDefaults.standard.bool(forKey: Keys.iCloudIsOn) 
        didSet  UserDefaults.standard.set(self.iCloudIsOn, forKey: Keys.iCloudIsOn) 
    
    
    @Published var backupLocalisation: String = UserDefaults.standard.object(forKey: Keys.backupLocalisation) as? String ?? "iPhone" 
        didSet  UserDefaults.standard.set(self.backupLocalisation, forKey: Keys.backupLocalisation) 
    
    
    @Published var backupsNumber: Int = UserDefaults.standard.integer(forKey: Keys.backupsNumber) 
        didSet  UserDefaults.standard.set(self.backupsNumber, forKey: Keys.backupsNumber) 
    

struct ContentView: View 
    @ObservedObject var settings: Settings

    var body: some View 
        NavigationView 
            Form 
                Section(footer: Text("iCloud is \(UserDefaults.standard.bool(forKey: Keys.iCloudIsOn) ? "on" : "off")")) 
                    Toggle(isOn: self.$settings.iCloudIsOn)  Text("Use iCloud") 
                
                Section 
                    Picker(selection: $settings.backupLocalisation, label: Text("\(self.settings.backupsNumber) sauvegarde\(self.settings.backupsNumber > 1 ? "s" : "") sur").foregroundColor(Color(.label))) 
                        ForEach(BackupLocalisations.allCases)  b in
                            Text(b.name).tag(b.rawValue)
                        
                    
                    
                    Stepper(value: self.$settings.backupsNumber) 
                        Text("Nombre de sauvegardes")
                    
                
            .navigationBarTitle(Text("Settings"))
            
        
    

struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        ContentView().environmentObject(Settings())
    

Xcode 11.3.1

【讨论】:

【参考方案4】:

试试这样的。您也可以考虑使用EnvironmentObject 而不是ObjectBinding per this answer。

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)
        
    

使用对象绑定,切换将使用键 myBoolSetting 将用户默认设置为 true / false。您可以看到 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)")
        
    

【讨论】:

@gohnjanotis 刚刚注意到myBoolSetting 的持久化也可能与这种方法不一致。对于那个很抱歉。有时它会起作用。有时不是。正如你所说;)我正在使用模拟器进行测试,因为我没有 iOS 13 设备。您是否尝试过在设备上进行测试? 是的。我看到的结果是在真实设备上。【参考方案5】:

我看到的一个问题是您使用错误的 API 来设置/获取来自 UserDefaults 的值。你应该使用:

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

【讨论】:

【参考方案6】:

这是我经过一些实验后得出的结论,使用PassthroughSubject 而不是尝试对通知进行操作。它似乎始终如一地按预期工作。

我猜可能有一些 Swift 或 SwiftUI 技术可以让这变得更简单,所以请指出任何其他关于如何做这样的事情的想法。

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)
        
    

【讨论】:

【参考方案7】:

您可以扩展@Published 属性包装器以将值存储在UserDefaults 中(如this answer 中所建议的那样):

private var cancellables = [String: AnyCancellable]()

extension Published 
    init(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)
        
    

这是基于发布的问题的示例:

import SwiftUI
import Combine

struct ContentView : View 
    @ObservedObject var settingsStore = SettingsStore()

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


class SettingsStore: ObservableObject 
    @Published(defaultValue: false, key: "SettingActivated")
    var settingActivated: Bool

【讨论】:

但不适用于 swiftUI 的自动更新 :( 尝试在一个视图中使用 Slider 组件,在另一个视图中使用矩形大小。 @Andrew 你是对的。我用一个可行的解决方案更新了我的答案。

以上是关于UserDefaults 在 SwiftUI 中与 Toggle 绑定的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Userdefaults (SwiftUI) 中将新元素附加到数组

如何在 SwiftUI 中将 Stepper 数据保存到 UserDefaults?

在 SwiftUI 中使用 UserDefaults 进行全局更新

SwiftUI 在 UserDefaults 中设置字符串值,但作为`EnvironmentObject`

SwiftUI 小部件中的 UIKit UserDefaults

如何在 SwiftUI 中将颜色选择器值保存到 UserDefaults 中?