版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.10.19 星期一 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
plist
文件(属性列表)、preference
(偏好设置)、NSKeyedArchiver
(归档)、SQLite 3
、CoreData
,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)
4. 数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)
5. 数据持久化方案解析(五) —— 基于Realm的持久化存储(一)
6. 数据持久化方案解析(六) —— 基于Realm的持久化存储(二)
7. 数据持久化方案解析(七) —— 基于Realm的持久化存储(三)
8. 数据持久化方案解析(八) —— UIDocument的数据存储(一)
9. 数据持久化方案解析(九) —— UIDocument的数据存储(二)
10. 数据持久化方案解析(十) —— UIDocument的数据存储(三)
11. 数据持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的数据存储示例(一)
12. 数据持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的数据存储示例(二)
13. 数据持久化方案解析(十三) —— 基于Unit Testing的Core Data测试(一)
14. 数据持久化方案解析(十四) —— 基于Unit Testing的Core Data测试(二)
15. 数据持久化方案解析(十五) —— 基于Realm和SwiftUI的数据持久化简单示例(一)
源码
1. Swift
首先看下工程组织结构
下面就是源码了
1. AppDelegate.swift
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { }
2. SceneDelegate.swift
import UIKit
import SwiftUI
import RealmSwift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
RealmMigrator.setDefaultConfiguration()
if let windowScene = scene as? UIWindowScene {
do {
// 1
let realm = try Realm()
let window = UIWindow(windowScene: windowScene)
// 2
let contentView = ContentView()
.environmentObject(IngredientStore(realm: realm))
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
} catch let error {
// Handle error
fatalError("Failed to open Realm. Error: \(error.localizedDescription)")
}
}
}
}
3. ColorOptions.swift
import SwiftUI
enum ColorOptions: String, CaseIterable {
case rayGreen = "rw-green"
case lightBlue
case lightRed
var color: Color {
Color(rawValue)
}
var name: String {
rawValue
}
var title: String {
switch self {
case .rayGreen:
return "Ray Green"
case .lightBlue:
return "Light Blue"
case .lightRed:
return "Light Red"
}
}
}
4. Ingredient.swift
struct Ingredient: Identifiable {
let id: Int
let title: String
let notes: String
let bought: Bool
let quantity: Int
var colorName = "rw-green"
}
// MARK: Convenience init
extension Ingredient {
init(ingredientDB: IngredientDB) {
id = ingredientDB.id
title = ingredientDB.title
notes = ingredientDB.notes
bought = ingredientDB.bought
quantity = ingredientDB.quantity
colorName = ingredientDB.colorName
}
}
5. IngredientForm.swift
import Foundation
class IngredientForm: ObservableObject {
@Published var title = ""
@Published var notes = ""
@Published var quantity = 1
@Published var color = ColorOptions.rayGreen
var ingredientID: Int?
var updating: Bool {
ingredientID != nil
}
init() { }
init(_ ingredient: Ingredient) {
title = ingredient.title
notes = ingredient.notes
quantity = ingredient.quantity
ingredientID = ingredient.id
color = ColorOptions(rawValue: ingredient.colorName) ?? .rayGreen
}
}
6. IngredientMock.swift
enum IngredientMock {
static let unicornTailHair = Ingredient(
id: 0,
title: "Unicorn Tail Hair",
notes: "Used in Beautification Potion",
bought: false,
quantity: 1)
static let dittany = Ingredient(
id: 1,
title: "Dittany",
notes: "Used in healing potions like Wiggenweld",
bought: false,
quantity: 1)
static let mandrake = Ingredient(
id: 2,
title: "Mandrake",
notes: "Used in a healing potion called the Mandrake Restorative Draught",
bought: false,
quantity: 1)
static let aconite = Ingredient(
id: 3,
title: "aconite",
notes: "Used in the Wolfsbane Potion",
bought: false,
quantity: 1)
static let unicornBlood = Ingredient(
id: 4,
title: "Unicorn blood",
notes: "Used in Rudimentary body potions",
bought: false,
quantity: 1)
static let ingredientsMock = [
unicornTailHair,
dittany,
mandrake,
aconite,
unicornBlood
]
static let roseThorn = Ingredient(
id: 5,
title: "Rose Thorn",
notes: "Used in Love potions",
bought: true,
quantity: 2)
static let rosePetals = Ingredient(
id: 5,
title: "Rose Petals",
notes: "Used in Love potions",
bought: true,
quantity: 2)
static let boughtIngredientsMock = [
roseThorn,
rosePetals
]
}
7. IngredientDB.swift
import Foundation
import RealmSwift
class IngredientDB: Object {
@objc dynamic var id = 0
@objc dynamic var title = ""
@objc dynamic var notes = ""
@objc dynamic var quantity = 1
@objc dynamic var bought = false
@objc dynamic var colorName = "rw-green"
override static func primaryKey() -> String? {
"id"
}
}
8. RealmMigrator.swift
import Foundation
import RealmSwift
enum RealmMigrator {
static private func migrationBlock(
migration: Migration,
oldSchemaVersion: UInt64
) {
if oldSchemaVersion < 1 {
migration.enumerateObjects(ofType: IngredientDB.className()) { _, newObject in
newObject?["colorName"] = "rw-green"
}
}
}
static func setDefaultConfiguration() {
let config = Realm.Configuration(
schemaVersion: 1,
migrationBlock: migrationBlock)
Realm.Configuration.defaultConfiguration = config
}
}
9. IngredientListView.swift
import SwiftUI
struct IngredientListView: View {
@EnvironmentObject var store: IngredientStore
@State private var ingredientFormIsPresented = false
let ingredients: [Ingredient]
let boughtIngredients: [Ingredient]
var body: some View {
List {
Section(header: Text("Ingredients")) {
if ingredients.isEmpty {
Text("Add some ingredients to the list")
.foregroundColor(.gray)
}
ForEach(ingredients) { ingredient in
IngredientRow(ingredient: ingredient)
}
newIngredientButton
}
Section(header: Text("Bought")) {
if boughtIngredients.isEmpty {
Text("Buy some ingredients and list them here")
}
ForEach(boughtIngredients) { ingredient in
IngredientRow(ingredient: ingredient)
}
}
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Potions Master")
}
var newIngredientButton: some View {
Button(action: openNewIngredient) {
HStack {
Image(systemName: "plus.circle.fill")
Text("New Ingredient")
.bold()
}
}
.foregroundColor(.green)
.sheet(isPresented: $ingredientFormIsPresented) {
IngredientFormView(form: IngredientForm())
.environmentObject(self.store)
}
}
}
// MARK: - Actions
extension IngredientListView {
func openNewIngredient() {
ingredientFormIsPresented.toggle()
}
}
#if DEBUG
struct IngredientListView_Previews: PreviewProvider {
static var previews: some View {
Group {
IngredientListView(
ingredients: IngredientMock.ingredientsMock,
boughtIngredients: IngredientMock.boughtIngredientsMock)
IngredientListView(ingredients: [], boughtIngredients: [])
}
}
}
#endif
10. ContentView.swift
import SwiftUI
struct ContentView: View {
@EnvironmentObject var store: IngredientStore
var body: some View {
NavigationView {
IngredientListView(ingredients: store.ingredients, boughtIngredients: store.boughtIngredients)
}
}
}
11. IngredientRow.swift
import SwiftUI
struct IngredientRow: View {
@EnvironmentObject var store: IngredientStore
@State private var ingredientFormIsPresented = false
let ingredient: Ingredient
var body: some View {
HStack {
Button(action: openUpdateIngredient) {
Text("\(ingredient.quantity)")
.bold()
.padding(.horizontal, 4)
VStack(alignment: .leading) {
Text(ingredient.title)
.font(.headline)
Text(ingredient.notes)
.font(.subheadline)
.lineLimit(1)
}
}
.buttonStyle(PlainButtonStyle())
.sheet(isPresented: $ingredientFormIsPresented) {
IngredientFormView(form: IngredientForm(self.ingredient))
.environmentObject(self.store)
}
Spacer()
// TODO: Insert Circle view here
Circle()
.fill(Color(ingredient.colorName))
.frame(width: 12, height: 12)
Button(action: buyOrDeleteIngredient) {
Image(systemName: ingredient.bought ? "trash.circle.fill" : "circle")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(ingredient.bought ? .red : .gray)
}
}
}
}
// MARK: - Actions
extension IngredientRow {
func openUpdateIngredient() {
if !ingredient.bought {
ingredientFormIsPresented.toggle()
}
}
func buyOrDeleteIngredient() {
withAnimation {
if ingredient.bought {
store.delete(ingredientID: ingredient.id)
} else {
store.toggleBought(ingredient: ingredient)
}
}
}
}
#if DEBUG
struct IngredientRow_Previews: PreviewProvider {
static var previews: some View {
Group {
IngredientRow(ingredient: IngredientMock.ingredientsMock[0])
IngredientRow(ingredient: IngredientMock.boughtIngredientsMock[0])
}
.previewLayout(.sizeThatFits)
}
}
#endif
12. IngredientFormView.swift
import SwiftUI
struct IngredientFormView: View {
@EnvironmentObject var store: IngredientStore
@Environment(\.presentationMode) var presentationMode
@ObservedObject var form: IngredientForm
let quantityOptions = [1, 2, 3, 4, 5]
let colorOptions = ColorOptions.allCases
var body: some View {
NavigationView {
Form {
TextField("Title", text: $form.title)
Picker(selection: $form.quantity, label: Text("Quantity")) {
ForEach(quantityOptions, id: \.self) { option in
Text("\(option)")
.tag(option)
}
}
Picker(selection: $form.color, label: Text("Color")) {
ForEach(colorOptions, id: \.self) { option in
Text(option.title)
}
}
Section(header: Text("Notes")) {
TextField("", text: $form.notes)
}
}
.navigationBarTitle("Ingredient Form", displayMode: .inline)
.navigationBarItems(
leading: Button("Cancel", action: dismiss),
trailing: Button(
form.updating ? "Update" : "Save",
action: form.updating ? updateIngredient : saveIngredient))
}
}
}
// MARK: - Actions
extension IngredientFormView {
func dismiss() {
presentationMode.wrappedValue.dismiss()
}
func saveIngredient() {
store.create(
title: form.title,
notes: form.notes,
quantity: form.quantity,
colorName: form.color.name)
dismiss()
}
func updateIngredient() {
if let ingredientID = form.ingredientID {
store.update(
ingredientID: ingredientID,
title: form.title,
notes: form.notes,
quantity: form.quantity,
colorName: form.color.name)
dismiss()
}
}
}
#if DEBUG
struct IngredientFormView_Previews: PreviewProvider {
static var previews: some View {
Group {
IngredientFormView(form: IngredientForm())
IngredientFormView(form: IngredientForm(IngredientMock.ingredientsMock[0]))
}
}
}
#endif
13. IngredientStore.swift
import Foundation
import RealmSwift
final class IngredientStore: ObservableObject {
private var ingredientResults: Results
private var boughtIngredientResults: Results
init(realm: Realm) {
ingredientResults = realm.objects(IngredientDB.self)
.filter("bought = false")
boughtIngredientResults = realm.objects(IngredientDB.self)
.filter("bought = true")
}
var ingredients: [Ingredient] {
ingredientResults.map(Ingredient.init)
}
var boughtIngredients: [Ingredient] {
boughtIngredientResults.map(Ingredient.init)
}
}
// MARK: - CRUD Actions
extension IngredientStore {
func create(title: String, notes: String, quantity: Int, colorName: String) {
objectWillChange.send()
do {
let realm = try Realm()
let ingredientDB = IngredientDB()
ingredientDB.id = UUID().hashValue
ingredientDB.title = title
ingredientDB.notes = notes
ingredientDB.quantity = quantity
ingredientDB.colorName = colorName
try realm.write {
realm.add(ingredientDB)
}
} catch let error {
// Handle error
print(error.localizedDescription)
}
}
func toggleBought(ingredient: Ingredient) {
objectWillChange.send()
do {
let realm = try Realm()
try realm.write {
realm.create(
IngredientDB.self,
value: ["id": ingredient.id, "bought": !ingredient.bought],
update: .modified)
}
} catch let error {
// Handle error
print(error.localizedDescription)
}
}
func update(
ingredientID: Int,
title: String,
notes: String,
quantity: Int,
colorName: String
) {
objectWillChange.send()
do {
let realm = try Realm()
try realm.write {
realm.create(
IngredientDB.self,
value: [
"id": ingredientID,
"title": title,
"notes": notes,
"quantity": quantity,
"colorName": colorName
],
update: .modified)
}
} catch let error {
// Handle error
print(error.localizedDescription)
}
}
func delete(ingredientID: Int) {
// 1
objectWillChange.send()
// 2
guard let ingredientDB = boughtIngredientResults.first(
where: { $0.id == ingredientID })
else { return }
do {
let realm = try Realm()
try realm.write {
realm.delete(ingredientDB)
}
} catch let error {
// Handle error
print(error.localizedDescription)
}
}
}
后记
本篇主要讲述了基于
Realm
和SwiftUI
的数据持久化简单示例,感兴趣的给个赞或者关注~~~