数据持久化方案解析(十六) —— 基于Realm和SwiftUI的数据持久化简单示例(二)

版本记录

版本号 时间
V1.0 2020.10.19 星期一

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,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<IngredientDB>
  private var boughtIngredientResults: Results<IngredientDB>

  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)
    }
  }
}

后记

本篇主要讲述了基于RealmSwiftUI的数据持久化简单示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352