数据持久化方案解析(十六) —— 基于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
  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)
    }
  }
}

后记

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

你可能感兴趣的:(数据持久化方案解析(十六) —— 基于Realm和SwiftUI的数据持久化简单示例(二))