Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

57.Gün - SwiftUI ve SwiftData

En sonki projemizde SwiftUI ile birlikte SwiftData’nın kullanımını incelemiştik, bu projede ise daha fazla ayrıntıya gireceğiz: özel yönetilen nesne alt sınıfları ve benzersizliğin sağlanması gibi konuları inceleyeceğiz.

Bugün SwiftUI ile SwiftData nesnelerini nasıl düzenleyeceğimizi, #Predicate kullanarak verilerinizi nasıl filtreleyeceğinizi ve daha fazlasını öğreneceğimiz üç konumuz var.

  1. SwiftData Giriş
  2. SwiftData Model Nesnelerini Düzenleme
  3. Predicate kullanarak @Query Filtreleme

SwiftData Giriş #

Bu teknik proje, SwiftData’yı daha ayrıntılı bir şekilde keşfedecek, bazı temel tekniklerin bir özeti ile başlayacak ve daha karmaşık sorunların üstesinden gelmeye kadar ilerleyecektir.

Görebileceğiniz gibi SwiftData, verileri verimli bir şekilde depolamamızı kolaylaştırmak için hem Swift’in hem de SwiftUI’nin gelişmiş özelliklerini gerçekten zorluyor. Yine de her zaman kolay değildir ve doğru şekilde kullanmak için biraz düşünmeyi gerektiren birkaç yer vardır.

Keşfedecek çok şeyimiz var, bu yüzden lütfen deneyebileceğimiz yeni bir proje oluşturun. Buna “SwiftData” değil, “SwiftDataProject” adını verin çünkü bu Xcode’un kafasının karışmasına neden olur.

SwiftData for Stroge’ı etkinleştirmediğinizden emin olun. Yine, her şeyin nasıl çalıştığını görebilmeniz için bunu sıfırdan oluşturacağız.

SwiftData Model Nesnelerini Düzenleme #

SwiftData’nın model nesneleri, @Observable sınıflarının çalışmasını sağlayan aynı observation system tarafından desteklenmektedir, bu da model nesnelerinizdeki değişikliklerin SwiftUI tarafından otomatik olarak alınacağı anlamına gelir, böylece verilerimiz ve kullanıcı arayüzümüz senkronize kalır.

Bu destek, daha önce incelediğimiz @Bindable property wrapper’a kadar uzanır.

Bunu göstermek için, birkaç property’ye sahip basit bir User sınıfı oluşturabiliriz. User.swift adında yeni bir dosya oluşturun, SwiftData için en üste bir import ekleyin ve ardından bu kodu ekleyin;

@Model
class User {
    var name: String
    var city: String
    var joinDate: Date

    init(name: String, city: String, joinDate: Date) {
        self.name = name
        self.city = city
        self.joinDate = joinDate
    }
}

Şimdi App struct dosyasına başka bir import SwiftData ekleyerek ve ardından şu şekilde modelContainer() kullanarak bunun için model container’i ve model context’i oluşturabiliriz.

WindowGroup {
    ContentView()
}
.modelContainer(for: User.self)

Kullanıcı nesnelerini düzenlemek söz konusu olduğunda, EditUserView gibi bir adla yeni bir view oluşturur, ardından bunun için binding oluşturmak üzere @Bindable property wrapper’ı kullanırız. Yani, bunun gibi bir şey;

struct EditUserView: View {
    @Bindable var user: User

    var body: some View {
        Form {
            TextField("Name", text: $user.name)
            TextField("City", text: $user.city)
            DatePicker("Join Date", selection: $user.joinDate)
        }
        .navigationTitle("Edit User")
        .navigationBarTitleDisplayMode(.inline)
    }
}

Bu, normal bir @Observable sınıfını nasıl kullandığımızla aynıdır ve yine de SwiftData tüm değişikliklerimizi otomatik olarak kalıcı depolama alanına yazmaya devam eder - bu bizim için tamamen şeffaftır.

Xcode’un Prewiew’ını kullanabilmek için aşağıdaki kodu ekleyin;

#Preview {
    do {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try ModelContainer(for: User.self, configurations: config)
        let user = User(name: "Taylor Swift", city: "Nashville", joinDate: .now)
        return EditUserView(user: user)
            .modelContainer(container)
    } catch {
        return Text("Failed to create container: \(error.localizedDescription)")
    }
}

Bir butona basıldığında yeni bir kullanıcı ekleyerek ve ardından uygulamayı düzenleme için doğrudan yeni kullanıcıya götürmek için programmatic navigation kullanarak bundan gerçekten basit bir kullanıcı düzenleme uygulaması yapabiliriz.

Bunu adım adım oluşturalım. Öncelikle ContentView.swift dosyasını açın ve SwiftData için bir import ekleyin, tüm User nesnelerini yükleyin, ardından bir NavigationStack’e bind edebileceğimiz bir path’i depolayın.

@Environment(\.modelContext)var modelContext
@Query(sort: \User.name)var users: [User]
@Stateprivatevar path = [User]()

body property’yi aşağıdaki gibi düzenleyin;

NavigationStack(path: $path) {
    List(users) { user in
        NavigationLink(value: user) {
            Text(user.name)
        }
    }
    .navigationTitle("Users")
    .navigationDestination(for: User.self) { user in
        EditUserView(user: user)
    }
}

Ve şimdi sadece kullanıcı eklemenin bir yoluna ihtiyacımız var. Düşünürseniz, ekleme ve düzeltme çok benzerdir, bu nedenle burada yapılacak en kolay şey boş property’lere sahip yeni bir User nesnesini oluşturmak, bunu model context’e eklemek ve ardından path property’yi ayarlayarak hemen buna gitmektir.

Aşağıdaki iki modifier’ı navigation modifier’ın altına ekleyin;

.toolbar {
    Button("Add User", systemImage: "plus") {
        let user = User(name: "", city: "", joinDate: .now)
        modelContext.insert(user)
        path = [user]
    }
}

Gördüğünüz gibi, SwiftData nesneleri ile düzenleme yapmak normal @Observable sınıflarını düzenlemekten farklı değildir- sadece tüm verilerimizin düzgün bir şekilde yüklenmesi ve kaydedilmesi ek bir avantajdır.

Predicate kullanarak @Query Filtreleme #

SwiftData nesnelerini belirli bir sıraya göre sırlamak için @Query ’nin nasıl kullanılabileceğini zaten gördünüz, ancak bu verileri filtrelemek için de kullanılabilir.

Bunun syntax’ı ilk başta biraz garip geliyor, çünkü bu aslında perde arkasında başka bir makro. Swift kodumuzu SwiftData’nın tüm nesnelerini depolayan temel veritabanına uygulayabileceği bir dizi kurala dönüştürüyor.

Daha önce kullandığımız User modelini kullanarak basit bir şeyle başlayalım;

@Model
class User {
    var name: String
    var city: String
    var joinDate: Date

    init(name: String, city: String, joinDate: Date) {
        self.name = name
        self.city = city
        self.joinDate = joinDate
    }
}

Şimdi ContentView’e sahip olduğumuz tüm kullanıcıları gösterebilecek birkaç özellik ekleyebiliriz;

@Environment(\.modelContext) var modelContext
@Query(sort: \User.name) var users: [User]

Ve son olarak, tüm bu kullanıcıları bir listede gösterebiliriz ve ayrıca bazı örnek verileri kolayca eklemek için bir buton ekleyeceğiz;

NavigationStack {
    List(users) { user in
        Text(user.name)
    }
    .navigationTitle("Users")
    .toolbar {
        Button("Add Samples", systemImage: "plus") {
            let first = User(name: "Ed Sheeran", city: "London", joinDate: .now.addingTimeInterval(86400 * -10))
            let second = User(name: "Rosa Diaz", city: "New York", joinDate: .now.addingTimeInterval(86400 * -5))
            let third = User(name: "Roy Kent", city: "London", joinDate: .now.addingTimeInterval(86400 * 5))
            let fourth = User(name: "Johnny English", city: "London", joinDate: .now.addingTimeInterval(86400 * 10))

            modelContext.insert(first)
            modelContext.insert(second)
            modelContext.insert(third)
            modelContext.insert(fourth)
        }
    }
}

İpucu : Bu join date’ler geçmişteki veya gelecekteki bazı gün sayılarını temsil eder ve bu da bize üzerinde çalışabileceğimiz ilginç veriler sağlar.

Bunun gibi örnek verilerle çalışırken, örnek verileri eklemeden önce mevcut serileri silebilmek yararlı olacaktır. Bunu yapmak için, let first = satırından önce aşağıdaki kodu ekleyin:

try? modelContext.delete(model: User.self)

Bu, SwiftData’ya User türündeki tüm mevcut model nesnelerini silmesini söyler, bu da örnek kullanıcıları eklemeden önce veritabanının temiz olduğu anlamına gelir.

Küçük örnek uygulamamızı tamamlamak için, App struct’ın SwiftData’yı doğru bir şekilde kurmak üzere modelContainer() modifier’ını kullandığından emin olmamız gerekiyor:

WindowGroup {
    ContentView()
}
.modelContainer(for: User.self)

Şimdi devam edin ve uygulamayı çalıştırın, ardından dört kullanıcı eklemek için + butonuna basın.

Alfabetik sırada göründüklerini görebilirsiniz, çünkü @Query property’imizde istediğimiz buydu.

Şimdi bu verileri filtrelemeyi deneyelim, böylece yalnızca adında büyük R harfi olan kullanıcıları gösterelim. Bunu yapmak için @Query ’ye aşağıdaki gibi bir filter parametresi uygularız;

@Query(filter: #Predicate<User> { user in
    user.name.contains("R")
}, sort: \User.name) var users: [User]

Bunu biraz açalım;

  1. Filtre, #Predicate<User> ile başlar, bu da bir predicate yazdığımız anlamına gelir.
  2. Bu predicate bize kontrol etmemiz için tek bir user instance verir. Pratikte bu, SwiftData tarafından yüklenen her user için bir kez çağrılacak ve bu user’ın sonuçlara dahil edilmesi gerekiyorsa true değerini döndürmemiz gerekecek.
  3. Testimiz, user’ın adının R harfi içerip içermediğini kontrol eder. İçeriyorsa, kullanıcı sonuçlara dahil edilir, aksi takdirde dahil edilmez.

Şimdi kodu çalıştırdığınızda hem Rosa hem de Roy’un listemizde göründüğünü, ancak Ed ve Johnny’nin isimlerinin büyük R harfi içermediği için listede yer almadığını göreceksiniz. contains() methodu büyük/küçük harfe duyarlıdır, bu sebeple küçük r harfini barındıran Ed Sheeran sonuçlara dahil edilmedi.

Bu, basit bir predicate testi için harika çalışır, ancak kullanıcıların büyük harfleri gerçekten önemsediği çok nadirdir. Genellikle sadece birkaç harf yazmak isterler ve büyük küçük harfleri göz ardı ederek sonuçların herhangi bir yerinde bu eşleşmeyi ararlar.

Bu amaçla, iOS bize ayrı bir localizedStandartContains() methodu sunar. Bu da aramak için bir string alır, ancak harf büyüklüğünü otomatik olarak yok sayar, bu nedenle kullanıcı metnine göre filtrelemeye çalıştığınızda çok daha iyi seçenektir.

İşte böyle görünüyor;

@Query(filter: #Predicate<User> { user in
    user.name.localizedStandardContains("R")
}, sort: \User.name) var users: [User]

Küçük test verilerimizde bu, dört kullanıcıdan üçünü göreceğimiz anlamına geliyor, çünkü bu üçünün adlarının bir yerinde “r” harfi var.

Şimdi bir adım daha ileri gidelim: Filtremizi, adında “R” harfi olan ve Londra’da yaşayan kişilerle eşleşecek şekilde yükseltelim;

@Query(filter: #Predicate<User> { user in
    user.name.localizedStandardContains("R") &&
    user.city == "London"
}, sort: \User.name) var users: [User]

Bu, Swift’in “mantıksal ve” operatörünü kullanır. Bu durumda isminde r harfi olan ve Londra’da yaşayan sonuçlar filtrelenir.

Bunun gibi daha fazla kontrol ekleyebilirsiniz, ancak && kullanmak biraz kafa karıştırıcı olur. Neyse ki, bu predicate’ler Swift ifadelerinin sınırlı bir alt kümesini destekler ve bu da okumayı biraz daha kolaylaştırır.

Örneğin, mevcut predicate’i şu şekilde yeniden yazabiliriz;

@Query(filter: #Predicate<User> { user in
    if user.name.localizedStandardContains("R") {
        if user.city == "London" {
            return true
        } else {
            return false
        }
    } else {
        return false
    }
}, sort: \User.name) var users: [User]

Şimdi, bunun biraz ayrıntılı olduğunu düşünüyor olabilirsiniz. Her iki else bloğunu da kaldırabilir ve sadece return true ile bitirebilirsiniz, çünkü user gerçekten predicate ile eşleşirse return true zaten sağlanmış olacak.

İşte böyle görünecek;

@Query(filter: #Predicate<User> { user in
    if user.name.localizedStandardContains("R") {
        if user.city == "London" {
            return true
        }
    }

    return false
}, sort: \User.name) var users: [User]

Ne yazık ki bu kod aslında geçerli değil, çünkü saf Swift kodu çalıştırıyormuşuz gibi görünse de aslında bunun gerçekleşmediğini hatırlamanız önemlidir. #Predicate makrosu aslında kodumuzu, Swift’i dahili olarak kullanmayan veritabanına uygulayabileceği bir dizi test olacak şekilde yeniden yazar.

Dahili olarak neler olduğunu görmek için, değişiklikleri geri alın (⌘ + Z) . Şimdi #Predicate ’e sağ tıklayın ve Expand Macro’yu seçin; büyük miktarda kodun göründüğünü göreceksiniz. Unutmayın, bu oluşturulacak ve çalıştırılacak gerçek koddur.


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 57 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.