swiftgrdb

How I can write Swift GRDB model with less boilerplate


I'm new with Swift, coming from web, I want to make a GRDB model that would auto update on edits and can be loaded from json. I ended up writing a model like this. The issue is that I need to repeat each field 8 times, that's a lot of boilerplate. Is there any way to write this shorter with less repetition and more elegant?

import Foundation
import GRDB

class Routine: Identifiable, Codable, FetchableRecord, PersistableRecord, ObservableObject {
  var id: UUID = UUID()
  @Published var name: String {
    didSet { save() }
  }

  // Define the table name for GRDB
  static let databaseTableName = "routines"

  // Define columns as enum for type-safe column access
  enum Columns: String, ColumnExpression {
    case id, name
  }

  // MARK: - Initializers

  required init(row: Row) {
    id = row[Columns.id]
    name = row[Columns.name]
  }

  init(id: UUID, name: String) {
    self.id = id
    self.name = name
  }

  // MARK: - Codable

  enum CodingKeys: String, CodingKey {
    case id, name
  }

  required init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    id = try container.decode(UUID.self, forKey: .id)
    name = try container.decode(String.self, forKey: .name)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(id, forKey: .id)
    try container.encode(name, forKey: .name)
  }

  // MARK: - PersistableRecord

  func encode(to container: inout PersistenceContainer) {
    container[Columns.id] = id
    container[Columns.name] = name
  }

  // MARK: - Auto Save

  func save() {
    do {
      try DatabaseManager.shared.dbQueue.write { db in
        try self.save(db)
      }
    } catch {
      print("Error saving routine:", error)
    }
  }
}



Solution

  • The immediate problem is that the use of @Published prevents the automatic synthesis of Codable conformance, and you have to implement Codable on your own. But...

    Your database records should not be an ObservableObject. If you are working with SwiftUI, Making Routine a struct would be the most convenient. You can store instances of these structs in an ObservableObject if you like.

    struct Routine: Identifiable, Hashable, Codable, PersistableRecord, FetchableRecord {
        let id: UUID
        var name: String {
            didSet {
                save()
            }
        }
        
        static let databaseTableName = "routines"
        
        enum Columns {
            static let id = Column(CodingKeys.id)
            static let name = Column(CodingKeys.name)
        }
        
        init(id: UUID = UUID(), name: String) {
            self.id = id
            self.name = name
        }
        
        func save() {
            // ...
        }
    }
    

    If you absolutely need Routine to be a class (UIKit interop is the only situation I can think of where this is necessary), see the answers to this question. For example, if you made Published conform to Codable, then you can just write

    class Routine: Identifiable, Codable, FetchableRecord, PersistableRecord, ObservableObject {
        let id: UUID
        @Published var name: String {
            didSet { save() }
        }
        
        static let databaseTableName = "routines"
        
        init(id: UUID = UUID(), name: String) {
            self.id = id
            self.name = name
        }
        
        enum Columns {
            static let id = Column(CodingKeys.id)
            static let name = Column(CodingKeys.name)
        }
        
        func save() {
            // ...
        }
    }
    

    You do not need to implement PersistableRecord.encode(to:).