如何在 iOS 和 macOS 应用中进行数据持久化
本文简单介绍了在 iOS 和 macOS 应用开发中持久化数据的集中方式,包括:UserDefaults、PropertyList、Archiver、SQLite、CoreData 以及 SwiftData。
2024-01-25
具体代码可以在 Github 上查看。
使用 UserDefaults 接口将数据保存到应用程序的 defaults 存储中
在苹果应用开发中, 提供了一个与系统 UserDefaults
defaults
defaults
defaults
在应用程序运行时,可以使用
UserDefaults
UserDefaults
UserDefaults
NSData
NSString
NSNumber
NSDate
NSArray
NSDictionary
NSKeyedArchiver
NSData
UserDefaults
Float
Double
Int
Boolean
URL
UserDefaults
import Foundation
import SwiftUI
struct UserSettings {
var username: String
var notificationsEnabled: Bool
}
class UserDefaultsManager {
static let shared = UserDefaultsManager()
private let userDefaults = UserDefaults.standard
private let keyUsername = "Username"
private let keyNotificationsEnabled = "NotificationsEnabled"
func saveUserSettings(_ settings: UserSettings) {
userDefaults.set(settings.username, forKey: keyUsername)
userDefaults.set(settings.notificationsEnabled, forKey: keyNotificationsEnabled)
userDefaults.synchronize()
}
func loadUserSettings() -> UserSettings? {
guard let username = userDefaults.string(forKey: keyUsername) else { return nil }
let notificationsEnabled = userDefaults.bool(forKey: keyNotificationsEnabled)
return UserSettings(username: username, notificationsEnabled: notificationsEnabled)
}
}
struct UserDefaultsView : View {
@State var settings: UserSettings = UserSettings(username: "", notificationsEnabled: false)
var body: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("USERNAME", text: $settings.username).textFieldStyle(RoundedBorderTextFieldStyle())
Toggle(isOn: $settings.notificationsEnabled, label: {
Label("NOTIFICATION ENABLED", systemImage: "info.circle")
})
HStack {
Spacer()
Button("UPDATE") {
updateUserSettings()
}
}
}.padding().onAppear() {
if let settings = UserDefaultsManager.shared.loadUserSettings() {
self.settings = settings
} else {
print("No user settings found.")
}
}
}
private func updateUserSettings() {
UserDefaultsManager.shared.saveUserSettings(settings)
}
}
#Preview {
UserDefaultsView()
}
UserDefaults.swift
使用 PropertyList 和 NSKeyedArchiver 将数据持久化到文件系统
在苹果应用中默认会启用 App Sandbox 应用沙盒技术。
应用沙盒是一种访问控制技术,其主要功能是通过限制应用程序在运行时能访问的特权集来减少恶意程序对系统的危害。当用户首次启动应用程序时,系统会在
~/Library/Containers
~/Downloads
~/Pictures
下面是使用 FileManager 获取沙盒文件夹根路径的方法:
import Foundation
func getAppDocumentsURL() -> URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
}
Utils.swift
使用 PropertyList 进行数据持久化
PropertyList
.plist
使用
PropertyList
Array
Dictionary
String
Int
Double
Date
Data
PropertyList
Codable
import Foundation
import SwiftUI
/// Person 和 Address 实现了 Codable 协议
class Address: Codable {
var city: String
var district: String
var id: Int?
var line: String
var province: String
var content: String {
get { return "\(province), \(city), \(district), \(line)" }
}
}
class Person: Codable {
var name: String
var age: Int
var address: Address
var content: String {
get { return "name: \(name), age: \(age), address: \(address.content)" }
}
}
/// 将实现了 Codable 协议的对象数组保存到特定路径
func saveToPropertyList<T: Codable>(data: [T], fileRelativePath: String) -> URL {
let documentDirectory = getAppDocumentsURL()
let fp = documentDirectory.appendingPathComponent(fileRelativePath)
do {
let data = try PropertyListEncoder().encode(data)
try data.write(to: fp)
print("Array/Dictionary successfully written to: \(fp.path)")
} catch {
print("Error writing array/dictionary to file: \(error.localizedDescription)")
}
return fp
}
/// 从特定路径解析实现了 Codable 协议的对象数组
func loadPropertyList<T: Codable>(fileRelativePath: String) -> [T]? {
let documentDirectory = getAppDocumentsURL()
let fp = documentDirectory.appendingPathComponent(fileRelativePath)
do {
let data = try Data(contentsOf: fp)
let decodedStruct = try PropertyListDecoder().decode([T].self, from: data)
print("Array/Dictionary read from file: \(decodedStruct)")
return decodedStruct
} catch {
print("Error reading array/dictionary from file: \(error.localizedDescription)")
return nil
}
}
struct PropertyListView: View {
var plistDest: String = "example.plist"
var archiveDest: String = "example.archive"
@State var person = PersonInput()
@State var loadedPersons: [Person] = []
var body: some View {
VStack {
HStack {
Button("Save") {
let person = Person(data: person);
loadedPersons.append(person)
let _ = saveToPropertyList(data: loadedPersons, fileRelativePath: plistDest)
}
Button("Load") {
if let persons: [Person] = loadPropertyList(fileRelativePath: plistDest) {
loadedPersons = persons
}
}
Spacer()
}.padding(8)
Divider()
VStack {
PersonEditorView(person: $person)
Divider()
Spacer()
PersonListView(persons: $loadedPersons)
}
}.padding()
}
}
#Preview {
PropertyListView()
}
PropertyList.swift
使用 NSKeyedArchiver 进行数据持久化
NSKeyedArchiver
Foundation
如果要使用
NSKeyedArchiver
NSSecureCoding
- :每次编码对象时会调用此方法。在此方法中指定如何归档对象中的每个实例变量;
func encode(with aCoder: NSCoder)
- :每次解码对象时会调用此方法。在此方法中指定如何将二进制数据解码为对象实例。
required convenience init?(coder decoder: NSCoder)
下面是使用
NSKeyedArchiver
import Foundation
import SwiftUI
class Address: NSObject, NSSecureCoding, Codable {
static var supportsSecureCoding: Bool = true
var city: String
var district: String
var line: String
var province: String
var content: String {
get { return "\(province), \(city), \(district), \(line)" }
}
init(city: String, district: String, line: String, province: String) {
self.city = city
self.district = district
self.line = line
self.province = province
}
// MARK: - NSSecureCoding
required convenience init?(coder decoder: NSCoder) {
guard let city = decoder.decodeObject(of: NSString.self, forKey: "city") as String? else { return nil }
guard let district = decoder.decodeObject(of: NSString.self, forKey: "district") as String? else { return nil }
guard let id = decoder.decodeInteger(forKey: "id") as Int? else { return nil }
guard let line = decoder.decodeObject(of: NSString.self, forKey: "line") as String? else { return nil }
guard let province = decoder.decodeObject(of: NSString.self, forKey: "province") as String? else { return nil }
self.init(city: city, district: district, line: line, province: province)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(province, forKey: "province")
aCoder.encode(city, forKey: "city")
aCoder.encode(district, forKey: "district")
aCoder.encode(line, forKey: "line")
}
}
class Person: NSObject, NSSecureCoding {
static var supportsSecureCoding: Bool = true
var name: String
var age: Int
var address: Address
var content: String {
get { return "name: \(name), age: \(age), address: \(address.content)" }
}
init(address: Address, age: Int, name: String) {
self.address = address
self.age = age
self.name = name
}
// MARK: - NSSecureCoding
required convenience init?(coder decoder: NSCoder) {
guard let address = decoder.decodeObject(of: Address.self, forKey: "address") as Address? else { return nil }
guard let age = decoder.decodeInteger(forKey: "age") as Int? else { return nil }
guard let name = decoder.decodeObject(of: NSString.self, forKey: "name") as String? else { return nil }
self.init(address: address, age: age, name: name)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "name")
aCoder.encode(age, forKey: "age")
aCoder.encode(address, forKey: "address")
}
}
func archiveData<T: NSObject & NSSecureCoding>(data: [T], fileRelativeURL: String) {
let documentsDirectory = getAppDocumentsURL()
let destURL = documentsDirectory.appendingPathComponent(fileRelativeURL)
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
try data.write(to: destURL)
print("归档成功:\(destURL)")
} catch {
print("归档失败:\(error)")
}
}
func unarchiveData<T: NSObject & NSSecureCoding>(fileRelativeURL: String) -> [T]? {
let documentsDirectory = getAppDocumentsURL()
let dataURL = documentsDirectory.appendingPathComponent(fileRelativeURL)
if let data = try? Data(contentsOf: dataURL) {
do {
if let unarchived = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: T.self, from: data) {
return unarchived
} else {
print("解档失败:未能还原对象")
}
} catch {
print("解档失败:\(error)")
}
}
return nil
}
struct ArchiverView: View {
var plistDest: String = "example.plist"
var archiveDest: String = "example.archive"
@State var person = PersonInput()
@State var loadedPersons: [Person] = []
var body: some View {
VStack {
HStack {
Button("Add") {
let person = Person(data: person)
loadedPersons.append(person)
archiveData(data: loadedPersons, fileRelativeURL: archiveDest)
}
Button("Load") {
if let persons: [Person] = unarchiveData(fileRelativeURL: archiveDest) {
loadedPersons = persons
}
}
Spacer()
}.padding(8)
Divider()
VStack {
PersonEditorView(person: $person)
Divider()
Spacer()
PersonListView(persons: $loadedPersons)
}
}.padding()
}
}
#Preview {
PropertyListView()
}
Archiver.swift
基于数据库(SQLite3)的持久化存储
对于简单的业务逻辑,只需要使用上述方法进行存储即可。但是如果需要处理复杂的对象间关系、或者需要构建复杂的条件查询,最好还是使用数据库的方式进行存储。
使用 SQLite 数据库进行存储
SQLite3 是常见的轻量级数据库。在 XCode 中,可以通过以下两种方式将 SQLite3 导入项目中:
- 在项目的 中搜索并添加
Frameworks, Libraries and Embeded Content
;libsqlite3.0.tbd
- 使用 Swift Package Manager 或其他 Swift 包管理软件添加 或
SQLite.swift
。FMDB
以下以 为例演示如何进行数据持久化操作:SQLite.swift
import SQLite
import SwiftUI
// Users 表
struct Users {
let db: SQLite.Connection
var table: SQLite.Table {
get { return Table("persons") }
}
var age: SQLite.Expression<Int> {
get { return Expression<Int>("age") }
}
var id: SQLite.Expression<Int> {
get { return Expression<Int>("id") }
}
var name: SQLite.Expression<String> {
get { return Expression<String>("name") }
}
var addressId: SQLite.Expression<Int> {
get { return Expression<Int>("address_id") }
}
func createTbl() {
do {
try db.run(
table.create(ifNotExists: true) { table in
table.column(id, primaryKey: .autoincrement)
table.column(addressId)
table.column(age)
table.column(name)
}
)
} catch {
print("\(error)")
}
}
func insert(person: Person) -> Int64 {
do {
return try db.run(table.insert(
addressId <- person.address.id!,
age <- person.age,
name <- person.name
))
} catch {
print("\(error)")
}
return -1
}
}
struct Addresses {
let db: SQLite.Connection
var table: SQLite.Table {
get { return Table("addresses") }
}
var id: Expression<Int> {
get { return Expression<Int>("id") }
}
var province: Expression<String> {
get { return Expression<String>("province") }
}
var city: Expression<String> {
get { return Expression<String>("city") }
}
var district: Expression<String> {
get { return Expression<String>("district") }
}
var line: Expression<String> {
get { return Expression<String>("line") }
}
func createTbl() {
do {
try db.run(
table.create(ifNotExists: true) { table in
table.column(id, primaryKey: .autoincrement)
table.column(city)
table.column(district)
table.column(line)
table.column(province)
}
)
} catch {
print("\(error)")
}
}
func insert(address: Address) -> Int64 {
do {
return try db.run(table.insert(
city <- address.city,
district <- address.district,
line <- address.line,
province <- address.province
))
} catch {
print("\(error)")
}
return -1
}
}
class SQLiteManager {
private var db: Connection!
private var persons: Users!
private var addresses: Addresses!
init() {
let documentsDirectory = getAppDocumentsURL()
let path = documentsDirectory.appendingPathComponent("db.sqlite3")
do {
db = try Connection(path.absoluteString)
persons = Users(db: db)
addresses = Addresses(db: db)
persons.createTbl()
addresses.createTbl()
} catch {
fatalError("Error initializing database: \(error)")
}
}
func insertPerson(person: Person) -> Person {
let addressId = addresses.insert(address: person.address)
person.address.id = Int(addressId)
let personId = persons.insert(person: person)
person.id = Int(personId)
return person
}
// 查询所有用户数据
func search(age: Int) -> [Person] {
let query = persons.table
.join(
addresses.table,
on: persons.table[persons.addressId] == addresses.table[addresses.id]
)
.filter(persons.table[persons.age] >= age)
.order(persons.table[persons.name].asc)
var result: [Person] = []
do {
for row in try db.prepare(query) {
let address = Address(
id: try row.get(addresses.table[addresses.id]),
city: try row.get(addresses.table[addresses.city]),
district:try row.get(addresses.table[addresses.district]),
line: try row.get(addresses.table[addresses.line]),
province: try row.get(addresses.table[addresses.province])
)
let person = Person(
id: try row.get(persons.table[persons.id]),
address: address,
age: try row.get(persons.table[persons.age]),
name: try row.get(persons.table[persons.name])
)
result.append(person)
}
} catch {
print("\(error)")
}
return result
}
}
struct SQLiteView: SwiftUI.View {
@State var person: PersonInput = PersonInput()
@State var loadedPersons: [Person] = []
let db: SQLiteManager = SQLiteManager()
var body: some SwiftUI.View {
VStack {
HStack {
Button("Add") {
let person = Person(data: person)
let inserted = db.insertPerson(person: person)
print("\(inserted.content) \(inserted.id ?? -1)")
}
Button("Load") {
loadedPersons = db.search(age: 0)
}
Spacer()
}.padding(8)
Divider()
VStack {
PersonEditorView(person: $person)
Divider()
Spacer()
PersonListView(persons: $loadedPersons)
}
}
.padding()
}
}
#Preview {
SQLiteView()
}
SQLite.swift
虽然使用
[SQLite.swift]
在使用进行 iOS、macOS 应用开发时,可以使用
CoreData
SwiftData
使用 CoreData 进行存储
CoreData 是苹果提供的一套用于管理应用中的数据的 ORM 工具。
相较于前面的持久化方式,CoreData 并不需要开发者手动处理配置存储路径,并且支持使用不同的存储后端,如内存、二进制文件和 sqlite。
相较于使用
SQLite.swift
CoreData
CoreData
Cloudkit
在使用
CoreData
在完成配置后,点击
Product > Clean Build Folder
接下来在
App.swift
CoreData
environment
import CoreData
import SwiftUI
@main
struct playgroundApp: App {
let cdContainer = NSPersistentContainer(name: "CoreDataExample")
init() {
cdContainer.loadPersistentStores { (_, error) in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.environment(\.managedObjectContext, cdContainer.viewContext)
}
}
app_playgroundApp.swift
使用
CoreData
import CoreData
import Foundation
import SwiftUI
/// 向 CoreData 容器中插入一个数据
func addPerson(ctx: NSManagedObjectContext, data: PersonInput) {
let address = AddressCD(context: ctx)
address.city = data.address.city
address.district = data.address.district
address.line = data.address.line
address.province = data.address.province
address.city = data.address.city
let person = PersonCD(context: ctx)
person.name = data.name
person.age = Int64(data.age)
person.address = address
try? ctx.save()
}
struct CoreDataView: View {
// 从 Environment 中读取 container
@Environment(\.managedObjectContext) private var dataController
// 使用 FetchRequest 从 container 中读取数据,
@FetchRequest(sortDescriptors: []) private var persons: FetchedResults<PersonCD>
@State var person = PersonInput()
var personsDisplay: [Person] {
get {
persons.map({ person in
let address = Address(
city: person.address?.city ?? "N/A",
district: person.address?.district ?? "N/A",
line: person.address?.line ?? "N/A",
province: person.address?.province ?? "N/A"
)
let item = Person(
address: address,
age: Int(person.age),
name: person.name ?? "N/A"
)
return item
})
}
}
var body: some View {
VStack {
HStack {
Button("Add") {
addPerson(ctx: dataController, data: person)
}
Button("Load") {}
Spacer()
}.padding(8)
Divider()
VStack {
PersonEditorView(person: $person)
Spacer()
Divider()
PersonListPreview(persons: personsDisplay)
}
}
.padding()
}
}
CoreData.swift
使用 SwiftData 进行存储
SwiftData 是苹果最新推出的用于管理应用数据的 ORM 框架。
SwiftData
CoreData
CoreData
SwiftData
SwiftData
使用
SwiftData
import Foundation
import SwiftData
import SwiftUI
@Model
class PersonSD {
var age: Int
var name: String
var address: AddressSD
init(age: Int, name: String, address: AddressSD) {
self.age = age
self.name = name
self.address = address
}
}
@Model
class AddressSD {
var city: String
var district: String
var line: String
var province: String
init(data: AddressInput) {
self.city = data.city
self.district = data.district
self.line = data.line
self.province = data.province
}
}
struct SwiftDataView: View {
@Environment(\.modelContext) private var sdContext
@Query() var persons: [PersonSD]
@State var person: PersonInput = PersonInput()
var personsDisplay: [Person] {
get {
persons.map({ person in
let address = Address(
city: person.address.city,
district: person.address.district,
line: person.address.line,
province: person.address.province
)
let item = Person(
address: address,
age: Int(person.age),
name: person.name
)
return item
})
}
}
var body: some View {
VStack {
HStack {
Button("Add") {
addPerson()
}
Spacer()
}.padding(8)
Divider()
VStack {
PersonEditorView(person: $person)
Divider()
Spacer()
PersonListPreview(persons: personsDisplay)
}
}.padding()
}
private func addPerson() {
let address = AddressSD(data: person.address)
sdContext.insert(address)
let person = PersonSD(age: person.age, name: person.name, address: address)
sdContext.insert(person)
do {
try sdContext.save()
} catch {
print("save error \(error)")
}
}
}
#Preview {
SwiftDataView().modelContainer(for: [PersonSD.self, AddressSD.self])
}
SwiftData.swift