Ios SwiftUI中带切换的UserDefaults绑定

Ios SwiftUI中带切换的UserDefaults绑定,ios,swiftui,combine,Ios,Swiftui,Combine,我正试图找出最好的方法来构建一个绑定到UserDefaults的简单设置屏幕 基本上,我有一个开关,我想要: 更改此切换时要保存的UserDefault值(UserDefault应为真实值的来源) 切换始终显示UserDefault的值 我已经观看了许多SwiftuiWWDC会议,但我仍然不确定应该如何使用Combine和SwiftUI中提供的不同工具设置所有内容。我目前的想法是,我应该使用BindableObject,这样我就可以使用hat来封装许多不同的设置 我认为我很接近,因为它几乎

我正试图找出最好的方法来构建一个绑定到UserDefaults的简单设置屏幕

基本上,我有一个开关,我想要:

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

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

我认为我很接近,因为它几乎像预期的那样工作,但行为是不一致的

当我在设备上构建并运行它时,我打开它并打开切换,然后如果我向上和向下滚动视图一点,开关就会切换回关闭状态(好像它实际上并没有在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")
}

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

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

试试这样的。您也可以考虑使用<代码>环境对象< /代码>而不是<代码> ObjultBoope<代码>p>
下面是我经过一些实验后得出的结论,使用
PassthroughSubject
,而不是尝试使用通知。它似乎始终如一地工作,并如预期的那样

我猜可能有一些快速或快捷的技术可以使这更简单,所以请指出如何做类似的事情的其他想法

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)
        }
    }
}
导入快捷界面
进口联合收割机
结构ContentView:View{
@ObjectBinding变量settingsStore:settingsStore
var body:一些观点{
导航视图{
形式{
切换(isOn:$settingsStore.settingActivated){
文本(“设置激活”)
}
}.navigationBarTitle(文本(“设置”))
}
}
}
类设置存储:BindableObject{
让didChange=PassthroughSubject()
var settingActivated:Bool=UserDefaults.settingActivated{
迪塞特{
UserDefaults.settingActivated=settingActivated
didChange.send()
}
}
}
扩展用户默认值{
私有结构密钥{
静态let settingActivated=“settingActivated”
}
静态var设置激活:布尔{
得到{
返回UserDefaults.standard.bool(forKey:Keys.settingActivated)
}
设置{
UserDefaults.standard.set(newValue,forKey:Keys.settingActivated)
}
}
}
更新

----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)
        }
    }
}
导入快捷界面
进口联合收割机
结构ContentView:View{
@观察对象变量设置存储:设置存储
var body:一些观点{
导航视图{
形式{
切换(isOn:$settingsStore.settingActivated){
文本(“设置激活”)
}
}.navigationBarTitle(文本(“设置”))
}
}
}
类设置存储:ObservableObject{
let willChange=PassthroughSubject()
var settingActivated:Bool=UserDefaults.settingActivated{
意志{
UserDefaults.settingActivated=newValue
willChange.send()
}
}
}
扩展用户默认值{
私有结构密钥{
静态let settingActivated=“settingActivated”
}
静态var设置激活:布尔{
得到{
返回UserDefaults.standard.bool(forKey:Keys.settingActivated)
}
设置{
UserDefaults.standard.set(newValue,forKey:Keys.settingActivated)
}
}
}

要使此接缝工作良好:

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"
}
Xcode 11.3.1

在和的帮助下,我已经能够生成一个绑定到UserDefaults的切换,并立即显示您分配给它的任何更改

  • 场景代理:
在“window”变量下添加这行代码

var settingsStore = SettingsStore()
并修改window.rootViewController以显示此信息

window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settingsStore))
  • 设置存储:
  • 设置存储菜单
如果愿意,请创建名为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)
            }
        }
    }
}

(或您希望的任何其他方式。)

您可以扩展
@Published
属性包装,以将值存储在
用户默认值中(如中所建议):

下面是基于发布的问题的示例:

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
}

@gohnjanotis的可能复制品刚刚注意到,
myBoolSetting
的持久性也可能与这种方法不一致。很抱歉。有时它是有效的。有时候不是。正如你所说;)我使用模拟器进行测试,因为我没有iOS 13设备。有你
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(Settings())
    }
}
var settingsStore = SettingsStore()
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")
        }
    }
}
import SwiftUI

struct SettingsStoreMenu: View {
    
    @ObservedObject var settingsStore: SettingsStore
    
    var body: some View {
        Toggle(isOn: self.$settingsStore.isOn) {
            Text("")
        }
    }
}
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)
            }
        }
    }
}
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
}