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