Section 4/181 menit

4. @Model Lanjutan: Attribute, Constraints, dan External Storage

4. @Model Lanjutan: Attribute, Constraints, dan External Storage

@Attribute Options

swift
import SwiftData

@Model
final class Document {
    // Unique constraint — insert duplikat = upsert (update existing)
    @Attribute(.unique)
    var remoteID: String
    
    // External storage — data disimpan di file terpisah, bukan di SQLite row
    // Cocok untuk: gambar, video, file besar (>1MB)
    @Attribute(.externalStorage)
    var thumbnailData: Data?
    
    // Spotlight indexing — nilai ini bisa dicari via Spotlight
    @Attribute(.spotlight)
    var title: String
    
    // Ephemeral — tidak disimpan ke store (hanya in-memory)
    @Attribute(.ephemeral)
    var isExpanded: Bool = false
    
    // Transformable — type custom dengan Codable
    @Attribute(.transformable(by: ColorTransformer.self))
    var accentColor: UIColor?
    
    // Encrypt — enkripsi nilai di SQLite (membutuhkan iOS 18+)
    // @Attribute(.encrypt)
    // var sensitiveNote: String?
    
    var body: String
    var createdAt: Date
    
    init(remoteID: String, title: String, body: String) {
        self.remoteID = remoteID
        self.title = title
        self.body = body
        self.createdAt = Date()
    }
}

// Custom ValueTransformer untuk UIColor
final class ColorTransformer: ValueTransformer {
    override class func transformedValueClass() -> AnyClass { NSData.self }
    override class func allowsReverseTransformation() -> Bool { true }
    
    override func transformedValue(_ value: Any?) -> Any? {
        guard let color = value as? UIColor else { return nil }
        return try? NSKeyedArchiver.archivedData(withRootObject: color, requiringSecureCoding: true)
    }
    
    override func reverseTransformedValue(_ value: Any?) -> Any? {
        guard let data = value as? Data else { return nil }
        return try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: data)
    }
}

Composite Unique Constraint

swift
// Unique constraint pada kombinasi beberapa field (bukan satu field)
@Model
final class CalendarEvent {
    // Unik berdasarkan kombinasi userID + date + title
    @Attribute(.unique)
    var compositeKey: String  // "\(userID)-\(date.timeIntervalSince1970)-\(title)"
    
    var userID: String
    var date: Date
    var title: String
    var notes: String?
    
    init(userID: String, date: Date, title: String) {
        self.userID = userID
        self.date = date
        self.title = title
        self.compositeKey = "\(userID)-\(Int(date.timeIntervalSince1970))-\(title)"
    }
}

// Catatan: SwiftData belum support multi-column unique constraint secara native
// Workaround: composite key string seperti di atas

@Attribute dengan originalName untuk Rename Migration

swift
// Jika kamu rename property TANPA schema versioning (tidak direkomendasikan untuk breaking change),
// gunakan originalName agar SwiftData tahu mapping lama → baru

@Model
final class User {
    // Property lama bernama "name", sekarang diganti "fullName"
    @Attribute(originalName: "name")
    var fullName: String
    
    // Property lama bernama "email_address", sekarang "email"
    @Attribute(originalName: "email_address")
    var email: String
    
    init(fullName: String, email: String) {
        self.fullName = fullName
        self.email = email
    }
}