UserDefaults 绑定与 SwiftUI 中的切换

goh*_*tis 19 ios swiftui combine

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

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

  • 每当此 Toggle 更改时要保存的 UserDefault 值(UserDefault 应该是事实来源)
  • Toggle 始终显示 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")
}
Run Code Online (Sandbox Code Playgroud)

atu*_*tri 22

更新

------- 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"))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

就是这样!这非常简单,而且非常简单。您的所有信息都将被保存并从 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)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 当用户默认键实际上并未绑定到屏幕上的任何内容,而是用于重新计算屏幕上显示的其他值时,“@AppStorage”和您的 iOS 13 解决方案似乎都不起作用。 (2认同)

ese*_*ege 5

azamsharp 的这个视频Paul Hudson 的这个教程的帮助下,我已经能够生成一个绑定到 UserDefaults 的切换,并立即显示您分配给它的任何更改。

  • 场景代理:

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

var settingsStore = SettingsStore()
Run Code Online (Sandbox Code Playgroud)

并修改 window.rootViewController 来显示这个

window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settingsStore))
Run Code Online (Sandbox Code Playgroud)
  • 设置存储:
import Foundation

class SettingsStore: ObservableObject {
    @Published var isOn: Bool = UserDefaults.standard.bool(forKey: "isOn") {
        didSet {
            UserDefaults.standard.set(self.isOn, forKey: "isOn")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 设置存储菜单

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

import SwiftUI

struct SettingsStoreMenu: View {
    
    @ObservedObject var settingsStore: SettingsStore
    
    var body: some View {
        Toggle(isOn: self.$settingsStore.isOn) {
            Text("")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 最后但并非最不重要的

不要忘记从您拥有的任何主视图中将 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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

(或您想要的任何其他方式。)


dal*_*ook 0

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

static var settingActivated: Bool {
    get {
        defaults?.bool(forKey: Keys.settingActivated) ?? false
    }
    set {
        defaults?.set(newValue, forKey: Keys.settingActivated)
    }
}
Run Code Online (Sandbox Code Playgroud)