我正在尝试找出构建绑定到UserDefaults的简单设置屏幕的最佳方法。
基本上,我有一个Toggle,我想要:
我已经看过许多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")
}
答案 0 :(得分:5)
在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)
}
}
}
}
(或您想要的其他方式。)
答案 1 :(得分:3)
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)
}
}
}
答案 2 :(得分: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
答案 3 :(得分:1)
尝试类似这样的方法。您也可以考虑使用EnvironmentObject
代替每个this answer的ObjectBinding
。
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)")
}
}
}
答案 4 :(得分:0)
我看到的一个问题是您使用了错误的API来从UserDefaults
设置/获取值。您应该使用:
static var settingActivated: Bool {
get {
defaults?.bool(forKey: Keys.settingActivated) ?? false
}
set {
defaults?.set(newValue, forKey: Keys.settingActivated)
}
}
答案 5 :(得分:0)
这是经过一些试验后我想到的,使用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)
}
}
}
答案 6 :(得分:0)
您可以扩展@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
}