Designing SwiftUI Previews that use Core Data NSManagedObject

Viewed 159

My SwiftUI app uses Core Data for persistence and I'm uncertain how best to manage my previews so I can iterate quickly on the UI. It seems wasteful to spin up the whole Core Data stack to generate previews in the canvas so I've subclassed the NSManagedObject used to populate the view but it seems a bit hacky and means a separate subclass for every Core Data entity which will need to be kept in sync with its parent in the event the properties change (which they probably will this early in the project!) I guess I could spin up a in-memory persistent store and avoid subclassing altogether? Not sure how much that's going to slow down the preview though. Other thought is to move to MVVM and then I could just stub out the view model...At present that seems to be the best solution.

Current implementation below. It works, but I'm very open to suggestions as to an alternative.

Core Data entity:

@objc(Patient)
class Patient: NSManagedObject {
    @NSManaged private(set) var id: UUID
    @NSManaged private(set) var pmsId: String
    @NSManaged private(set) var name: String
    @NSManaged private(set) var clientName: String
    @NSManaged fileprivate var imageData: Data
    @NSManaged private(set) var species: String
    @NSManaged private(set) var breed: String
    @NSManaged private(set) var dateOfBirth: Date
    @NSManaged fileprivate var sexCode: Int16 
    @NSManaged private(set) var weight: NSDecimalNumber
    
    fileprivate(set) var sex: Sex {
        get {
            guard let s = Sex(rawValue: sexCode) else {
                fatalError("Unknown code for patient sex")
            }
            return s
        }
        set {
            sexCode = newValue.rawValue
        }
    }
    
    fileprivate(set) var image: UIImage {
        get {
            // TODO: - Return SwiftUI Image rather than UIImage?
            // TODO: - Implement species-specific placeholder
            return UIImage(data: imageData, scale: 1.0) ?? UIImage(named: "cat-star")!
        }
        set {
            imageData = newValue.pngData()! // shouldn't ever fail...should it?
        }
    }
}


// MARK: - Extensions
extension Patient: Managed {
    static var defaultSortDescriptors: [NSSortDescriptor] {
        return [ NSSortDescriptor(keyPath: \Patient.name, ascending: true) ]
    }
}

extension Patient {
    static func insert(into context: NSManagedObjectContext,
                       pmsId: String,
                patientName: String,
                clientName: String,
                image: UIImage,
                sex: Sex,
                dateOfBirth: Date,
                species: String,
                breed: String,
                weight: NSDecimalNumber) -> Patient {
        let patient: Patient = context.insertObject()
        patient.id = UUID()
        patient.pmsId = pmsId
        patient.name = patientName
        patient.clientName = clientName
        patient.image = image
        patient.sex = sex
        patient.dateOfBirth = dateOfBirth
        patient.species = species
        patient.breed = breed
        patient.weight = weight
        return patient
    }
}


enum Sex: Int16, CustomStringConvertible, CaseIterable, Identifiable, Hashable {
    case unknown = 0
    case male
    case maleNeutered
    case female
    case femaleNeutered
    
    var description: String {
        switch self {
        case .unknown: return "Unknown"
        case .male: return "Male"
        case .maleNeutered: return "Male Neutered"
        case .female: return "Female"
        case .femaleNeutered: return "Female Neutered"
        }
    }
    
    var pickerDescription: String {
        switch self {
        case .unknown: return "Unknown"
        case .male: return "ME"
        case .maleNeutered: return "MN"
        case .female: return "F"
        case .femaleNeutered: return "FN"
        }
    }
    
    var id: Sex { self }
    
}

Object drives this SwiftUI view (subclassed Patient is at the bottom):

struct PatientCell: View {
    var patient: Patient
    
    private var age: DateComponents {
        let calendar = Calendar.current
        return calendar.dateComponents([.year, .month, .weekOfMonth], from: patient.dateOfBirth, to: Date())
    }
    
    var body: some View {
        HStack {
            Image(uiImage: patient.image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 50, height: 50)
                .clipShape(Circle())
                .overlay(Circle().stroke(Color.gray, lineWidth: 2))
                .padding(.trailing, 10)
            
            VStack {
                HStack(spacing: 4) {
                    Text(patient.name)
                    Text(patient.clientName).bold()
                    Spacer()
                }
                .font(.title3)
                
                HStack {
                    Text(patient.species)
                    Text(patient.sex.pickerDescription)
                    HStack {
                        if age.year! > 0 {
                            Text("\(age.year!)y \(age.month!)m") } else {
                                Text("\(age.month!)m \(age.weekOfMonth!)w")
                            }
                    }
                    Spacer()
                }
                .font(.caption)
            }
            
        }
    }
}

struct PatientCell_Previews: PreviewProvider {
    static var previews: some View {
        PreviewWrapper()
    }
    
    struct PreviewWrapper: View {
        @State private var patient = Patient_Preview()
        
        var body: some View {
            PatientCell(patient: patient)
                .previewLayout(.sizeThatFits)
                .padding()
                .previewDisplayName("Patient list row")
        }
    }
}

fileprivate class Patient_Preview: Patient {
    override var id: UUID { UUID() }
    override var pmsId: String { "1-7756" }
    override var name: String { "Star" }
    override var clientName: String { "Ward" }
    override var image: UIImage { UIImage(named: "cat-star")! }
    override var species: String { "Cat" }
    override var breed: String { "Norweigian Forest Cat" }
    override var dateOfBirth: Date {
        let calendar = Calendar.current
        let dateComponents = DateComponents(
            calendar: calendar,
            year: 2009,
            month: 4,
            weekOfMonth: 3)
        return calendar.date(from: dateComponents)!
    }
    override var sex: Sex { .maleNeutered }
    override var weight: NSDecimalNumber { 5.45 }
}
0 Answers
Related