Как я могу использовать UserDefaults с SwiftUI?
struct ContentView: View {
@State var settingsConfiguration: Settings
struct Settings {
var passwordLength: Double = 20
var moreSpecialCharacters: Bool = false
var specialCharacters: Bool = false
var lowercaseLetters: Bool = true
var uppercaseLetters: Bool = true
var numbers: Bool = true
var space: Bool = false
}
var body: some View {
VStack {
HStack {
Text("Password Length: \(Int(settingsConfiguration.passwordLength))")
Spacer()
Slider(value: $settingsConfiguration.passwordLength, from: 1, through: 512)
}
Toggle(isOn: $settingsConfiguration.moreSpecialCharacters) {
Text("More Special Characters")
}
Toggle(isOn: $settingsConfiguration.specialCharacters) {
Text("Special Characters")
}
Toggle(isOn: $settingsConfiguration.space) {
Text("Spaces")
}
Toggle(isOn: $settingsConfiguration.lowercaseLetters) {
Text("Lowercase Letters")
}
Toggle(isOn: $settingsConfiguration.uppercaseLetters) {
Text("Uppercase Letters")
}
Toggle(isOn: $settingsConfiguration.numbers) {
Text("Numbers")
}
Spacer()
}
.padding(.all)
.frame(width: 500, height: 500)
}
}
Итак, у меня есть весь этот код здесь, и я хочу использовать UserDefaults для сохранения настроек при каждом изменении переключателя или скольжения слайдера и для извлечения всех этих данных при запуске приложения, но я не знаю, как мне поступить с использованием UserDefaults с SwiftUI (Или UserDefaults в целом, я только начал изучать его, чтобы я мог использовать его для своего приложения SwiftUI, но все примеры, которые я вижу, относятся к UIKit, и когда я пытаюсь реализовать их в SwiftUI, я просто сталкиваюсь с кучей ошибок).
7 ответов
Подход caram в целом нормальный, но с кодом так много проблем, что SmushyTaco не работает. Ниже вы найдете рабочее решение "Из коробки".
// 1. UserDefaults propertyWrapper
import Foundation
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
init(_ key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
// 2. Класс UserSettings
final class UserSettings: BindableObject {
let didChange = PassthroughSubject<Void, Never>()
@UserDefault("ShowOnStart", defaultValue: false)
var showOnStart: Bool {
didSet {
didChange.send()
}
}
}
// 3. SwiftUI view
struct SettingsView : View {
@ObjectBinding var settings = UserSettings()
var body: some View {
VStack {
Toggle(isOn: $settings.showOnStart) {
Text("Show welcome text")
}
if settings.showOnStart{
Text("Welcome")
}
}
}
}
Начиная с Xcode 12.0 (iOS 14.0) вы можете использовать @AppStorage
обертка свойств для таких типов: Bool, Int, Double, String, URL
а также Data
. Вот пример использования для хранения значения String:
struct ContentView: View {
static let userNameKey = "user_name"
@AppStorage(Self.userNameKey) var userName: String = "Unnamed"
var body: some View {
VStack {
Text(userName)
Button("Change automatically ") {
userName = "Ivor"
}
Button("Change manually") {
UserDefaults.standard.setValue("John", forKey: Self.userNameKey)
}
}
}
}
Здесь вы объявляете userName
свойство со значением по умолчанию, которое не попадает в UserDefaults
сам. Когда вы впервые измените его, приложение запишет это значение вUserDefaults
и автоматически обновит представление с новым значением.
Также есть возможность установить индивидуальный UserDefaults
провайдер при необходимости через store
такой параметр:
@AppStorage(Self.userNameKey, store: UserDefaults.shared) var userName: String = "Mike"
а также
extension UserDefaults {
static var shared: UserDefaults {
let combined = UserDefaults.standard
combined.addSuite(named: "group.myapp.app")
return combined
}
}
Примечание: если это значение изменится вне приложения (скажем, вручную открыв файл plist и изменив значение), View не получит это обновление.
PS Также есть новое расширение на View
что добавляет func defaultAppStorage(_ store: UserDefaults) -> some View
что позволяет изменить хранилище, используемое для просмотра. Это может быть полезно, если есть много@AppStorage
properties и настроить индивидуальное хранилище для каждого из них - это обременительно.
Приведенный ниже код адаптирует отличное решение Мохаммада Азама из этого видео:
import SwiftUI
struct ContentView: View {
@ObservedObject var userDefaultsManager = UserDefaultsManager()
var body: some View {
VStack {
Toggle(isOn: self.$userDefaultsManager.firstToggle) {
Text("First Toggle")
}
Toggle(isOn: self.$userDefaultsManager.secondToggle) {
Text("Second Toggle")
}
}
}
}
class UserDefaultsManager: ObservableObject {
@Published var firstToggle: Bool = UserDefaults.standard.bool(forKey: "firstToggle") {
didSet { UserDefaults.standard.set(self.firstToggle, forKey: "firstToggle") }
}
@Published var secondToggle: Bool = UserDefaults.standard.bool(forKey: "secondToggle") {
didSet { UserDefaults.standard.set(self.secondToggle, forKey: "secondToggle") }
}
}
Во-первых, создайте оболочку свойств, которая позволит нам легко установить связь между вашим классом настроек и UserDefaults:
import Foundation
@propertyWrapper
struct UserDefault<Value: Codable> {
let key: String
let defaultValue: Value
var value: Value {
get {
let data = UserDefaults.standard.data(forKey: key)
let value = data.flatMap { try? JSONDecoder().decode(Value.self, from: $0) }
return value ?? defaultValue
}
set {
let data = try? JSONEncoder().encode(newValue)
UserDefaults.standard.set(data, forKey: key)
}
}
}
Затем создайте хранилище данных, в котором хранятся ваши настройки:
import Combine
import SwiftUI
final class DataStore: BindableObject {
let didChange = PassthroughSubject<DataStore, Never>()
@UserDefault(key: "Settings", defaultValue: [])
var settings: [Settings] {
didSet {
didChange.send(self)
}
}
}
Теперь, по вашему мнению, доступ к вашим настройкам:
import SwiftUI
struct SettingsView : View {
@EnvironmentObject var dataStore: DataStore
var body: some View {
Toggle(isOn: $settings.space) {
Text("\(settings.space)")
}
}
}
Я уверен, что никто не написал новый способ, во всяком случае, Apple перешла на этот метод сейчас, и вам не нужен весь старый код, вы можете читать и писать в него так:
@AppStorage("example") var example: Bool = true
это эквивалентно чтению/записи в старом
Если вы сохраняете одноразовую структуру, так что оболочка свойств избыточна, вы можете закодировать ее как JSON. При декодировании используйте пустойData
экземпляр для случая отсутствия данных.
final class UserData: ObservableObject {
@Published var profile: Profile? = try? JSONDecoder().decode(Profile.self, from: UserDefaults.standard.data(forKey: "profile") ?? Data()) {
didSet { UserDefaults.standard.set(try? JSONEncoder().encode(profile), forKey: "profile") }
}
}
Еще одно отличное решение - использовать неофициальный API статических индексов
@propertyWrapper
вместо
wrappedValue
что значительно упрощает код. Вот определение:
@propertyWrapper
struct UserDefault<Value> {
let key: String
let defaultValue: Value
init(wrappedValue: Value, _ key: String) {
self.key = key
self.defaultValue = wrappedValue
}
var wrappedValue: Value {
get { fatalError("Called wrappedValue getter") }
set { fatalError("Called wrappedValue setter") }
}
static subscript(
_enclosingInstance instance: Preferences,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<Preferences, Value>,
storage storageKeyPath: ReferenceWritableKeyPath<Preferences, Self>
) -> Value {
get {
let wrapper = instance[keyPath: storageKeyPath]
return instance.userDefaults.value(forKey: wrapper.key) as? Value ?? wrapper.defaultValue
}
set {
instance.objectWillChange.send()
let key = instance[keyPath: storageKeyPath].key
instance.userDefaults.set(newValue, forKey: key)
}
}
}
Затем вы можете определить свой объект настроек следующим образом:
final class Settings: ObservableObject {
let userDefaults: UserDefaults
init(defaults: UserDefaults = .standard) {
userDefaults = defaults
}
@UserDefaults("yourKey") var yourSetting: SettingType
...
}
Однако будьте осторожны с такой реализацией. Пользователи обычно помещают все настройки своего приложения в один из таких объектов и используют его в каждом представлении, которое зависит от одного параметра. Это может привести к замедлению работы из-за слишком большого количества ненужных
objectWillChange
уведомления во многих представлениях. Вам определенно следует разделить проблемы, разбив свои настройки на множество небольших классов.
В
@AppStorage
- отличное нативное решение, но недостатком является то, что это своего рода нарушение парадигмы уникального источника истины, поскольку вы должны предоставить значение по умолчанию для каждого свойства.