Как я могу использовать 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 - отличное нативное решение, но недостатком является то, что это своего рода нарушение парадигмы уникального источника истины, поскольку вы должны предоставить значение по умолчанию для каждого свойства.

Другие вопросы по тегам