使用 User Defaults 存储用户设置信息

了解如何使用苹果提供的 User Defaults 持久化存储轻量级的用户数据,并使用 SwiftUI 的 @AppStorage 属性包装器简化数据存取。

使用 User Defaults 存储用户设置信息

Swift 为应用开发提供了多种本地数据存储方案。如果需要持久化存储较大结构化数据,首先考虑使用 2023 年新推出的 SwiftData 框架,它与 SwiftUI 有非常好的集成,你可以查看我的系列文章:

SwiftData 教程与实例:简化 iOS 开发数据持久化(一)
SwiftData 是 Apple 为 iOS 开发者提供的新型类 ORM 工具,简化数据库管理,无需编写 SQL,本文包含 SwiftData 教程和使用实例。

如果是存储用户的偏好设置、应用配置这种小规模数据,则适合使用 UserDefaults 。本文将详细介绍如何使用 UserDefaults 存储用户设置信息,并探讨一些最佳实践,确保数据存储安全和高效。

什么是 User Defaults

UserDefaults 是 iOS 和 macOS 中用于存储应用小型数据的简单持久化机制。它可以存储一些基本数据类型,如字符串、整数、布尔值、数组和字典等。

UserDefaults 允许我们在应用的多个会话之间保存轻量级的键值对数据,这使得用户的设置信息能够在应用关闭后依然被保留。 因此,非常适合保存用户偏好设置、应用配置、状态标志等不需要复杂数据结构的场景。

适合 User Defaults 的使用场景

一些常见的 UserDefaults 使用场景:

  • 保存用户选择的主题(如深色模式或浅色模式)
  • 记录用户的语言偏好
  • 存储是否显示应用的欢迎提示
  • 保存应用的配置信息,例如音量设置、通知开关等

使用 User Defaults 存储数据与读取数据

要将数据存储在 UserDefaults 中,只需使用 set 方法并指定键值对。如下所示:

// 存储用户的首选主题设置
UserDefaults.standard.set("dark", forKey: "userTheme")

// 存储用户的音量设置
UserDefaults.standard.set(0.8, forKey: "volumeLevel")

// 存储是否启用通知
UserDefaults.standard.set(true, forKey: "isNotificationsEnabled")

UserDefaults 中读取数据非常简单,通过 object(forKey:) 或者相应的类型方法即可检索数据:

// 获取用户的首选主题
let userTheme = UserDefaults.standard.string(forKey: "userTheme") ?? "light"

// 获取音量设置
let volumeLevel = UserDefaults.standard.double(forKey: "volumeLevel")

// 获取通知设置
let isNotificationsEnabled = UserDefaults.standard.bool(forKey: "isNotificationsEnabled")

UserDefaults 的数据管理方法,和我们之前讲过的 UIPasteboard 非常相似:

使用 UIPasteboard 管理粘贴板数据
了解如何使用 UIPasteboard 为你的 iOS 添加复制、剪切与粘贴能力,并监听粘贴板数据从而实现高级自定义功能。

使用 User Defaults 存储用户设置案例

为了更好地管理用户设置,通常会创建专门的类来定义和封装这些参数。

为了创建类似上面这样的设置页面,我们需要:

  • 创建一个 SettingsManager 类,用于集中管理 UserDefaults 的键值(否则键值多了之后就会混乱)。它封装了与 UserDefaults 的交互,确保各个设置项的存储和读取操作简单易用。
  • 创建一个 SettingsView,用户可以通过该视图修改应用的设置。提供了用户界面,用于调整主题、音量和通知设置。每次用户在界面上修改设置时,都会自动更新到 UserDefaults 中。

SettingsManager

这个类将负责与 UserDefaults 进行交互,并封装对用户设置的读取和存储逻辑。

import Foundation

class SettingsManager {
    static let shared = SettingsManager()

    private let userThemeKey = "userTheme"
    private let volumeLevelKey = "volumeLevel"
    private let notificationsEnabledKey = "isNotificationsEnabled"

    // 存储用户的首选主题
    func setUserTheme(_ theme: String) {
        UserDefaults.standard.set(theme, forKey: userThemeKey)
    }

    // 读取用户的首选主题
    func getUserTheme() -> String {
        return UserDefaults.standard.string(forKey: userThemeKey) ?? "light" // 默认主题为浅色模式
    }

    // 存储用户的音量设置
    func setVolumeLevel(_ level: Float) {
        UserDefaults.standard.set(level, forKey: volumeLevelKey)
    }

    // 读取用户的音量设置
    func getVolumeLevel() -> Float {
        return UserDefaults.standard.float(forKey: volumeLevelKey)
    }

    // 存储通知设置
    func setNotificationsEnabled(_ isEnabled: Bool) {
        UserDefaults.standard.set(isEnabled, forKey: notificationsEnabledKey)
    }

    // 读取通知设置
    func areNotificationsEnabled() -> Bool {
        return UserDefaults.standard.bool(forKey: notificationsEnabledKey)
    }
}

SettingsView 视图

SettingsView 提供了一个界面,用户可以通过它来修改应用的设置。我们将使用 SwiftUI 创建这个视图,并通过 SettingsManager 进行数据的保存和读取。

import SwiftUI

struct SettingsView: View {
    @State private var selectedTheme = SettingsManager.shared.getUserTheme()
    @State private var volumeLevel = SettingsManager.shared.getVolumeLevel()
    @State private var notificationsEnabled = SettingsManager.shared.areNotificationsEnabled()

    var body: some View {
        Form {
            Section(header: Text("主题设置")) {
                Picker("选择主题", selection: $selectedTheme) {
                    Text("浅色模式").tag("light")
                    Text("深色模式").tag("dark")
                }
                .pickerStyle(SegmentedPickerStyle())
                .onChange(of: selectedTheme) { newValue in
                    SettingsManager.shared.setUserTheme(newValue)
                }
            }

            Section(header: Text("音量设置")) {
                Slider(value: $volumeLevel, in: 0...1) {
                    Text("音量")
                }
                .onChange(of: volumeLevel) { newValue in
                    SettingsManager.shared.setVolumeLevel(newValue)
                }
            }

            Section(header: Text("通知设置")) {
                Toggle("启用通知", isOn: $notificationsEnabled)
                    .onChange(of: notificationsEnabled) { newValue in
                        SettingsManager.shared.setNotificationsEnabled(newValue)
                    }
            }
        }
        .navigationTitle("用户设置")
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
    }
}

这个设计的好处是将用户设置的存取逻辑与界面逻辑分离,增强了代码的可维护性和清晰度。

使用 @AppStorage 简化数据存取

@AppStorage 是 SwiftUI 提供的一个属性包装器,用于简化对 UserDefaults 数据的绑定和管理(存取)。它能够直接在 SwiftUI 视图中绑定用户设置,只要存储的值发生变化,就会自动更新视图。它还充当@State变量,触发视图中的更改。

@AppStorage 只能用来绑定 UserDefaults 中的数据。

使用 @AppStorage 修改实现

在之前的 SettingsView 和 SettingsManager 代码中,我们需要手动存取键值,并在更新时,手动调用 onChange 与 set 方法来存储值:

// 存储用户的首选主题
func setUserTheme(_ theme: String) {
    UserDefaults.standard.set(theme, forKey: userThemeKey)
}

// 读取用户的首选主题
func getUserTheme() -> String {
    return UserDefaults.standard.string(forKey: userThemeKey) ?? "light" // 默认主题为浅色模式
}
 @State private var selectedTheme = SettingsManager.shared.getUserTheme()

 .onChange(of: selectedTheme) { newValue in
    SettingsManager.shared.setUserTheme(newValue)
}

如果使用 @AppStorage,你无需再手动处理数据更新,SwiftUI 会自动存储数据的更新与存储。@AppStorage 可以在视图中直接使用,视图将在数据变化时自动刷新。因此,我们不再需要 SettingManager 类,只需在 SettingView 中实现即可:

SettingView:

import SwiftUI

struct SettingsView: View {
    @AppStorage("userTheme") private var selectedTheme: String = "light"
    @AppStorage("volumeLevel") private var volumeLevel: Float = 0.5
    @AppStorage("isNotificationsEnabled") private var notificationsEnabled: Bool = false

    var body: some View {
        Form {
            Section(header: Text("主题设置")) {
                Picker("选择主题", selection: $selectedTheme) {
                    Text("浅色模式").tag("light")
                    Text("深色模式").tag("dark")
                }
                .pickerStyle(SegmentedPickerStyle())
            }

            Section(header: Text("音量设置")) {
                Slider(value: $volumeLevel, in: 0...1) {
                    Text("音量")
                }
            }

            Section(header: Text("通知设置")) {
                Toggle("启用通知", isOn: $notificationsEnabled)
            }
        }
        .navigationTitle("用户设置")
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
    }
}

以上代码实现了相同的功能,但相比之前的方式更为简洁高效,代码量大大减少。

什么场景推荐使用 @AppStorage

@AppStorage 本质也是使用 UserDefault 来存储数据,所以一个常见的问题是:我们什么时候应该使用 @AppStorage,什么场景又应该采取之前这种通过类来统一管理的方案?

按照我的经验,@AppStorage 非常适合于你的 App 只有少量的设置项、逻辑简单的场景。当值改变时,SwiftUI 会自动更新 UI,使用起来非常方便。

但如果你需要对设置进行复杂的逻辑处理,或者设置选项较多(例如 10 个以上),则推荐使用类来集中管理设置键值对的存取。