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

55.Gün - SwiftUI SwiftData ile Silme ve Sıralama İşlemleri

Bugün SwiftData nesnelerinin nasıl silineceğini, SortDescriptor kullanarak sorguların nasıl sıralanacağını ve uyarılara özel butonların nasıl ekleneceğini öğreneceğiz.

Bugün üzerinde çalışacağımız konu başlıkları;

  • Kitap detaylarını gösterme
  • SortDescriptor ile SwiftData Query Sıralama
  • SwiftData Query silme
  • SwiftUI Alert ile NavigationLink

Kitap Detaylarını Gösterme #

Kullanıcı ContentView’da bir kitaba dokunduğunda, daha fazla bilgi içeren bir ayrıntı view’ı sunacağız - kitabın türü, kısa incelemesi, ve daha fazlası. Ayrıca yeni RatingView’ımızı yeniden kullanacağız ve hatta SwiftUI’nin ne kadar esnek olduğunu görebilmeniz için onu özelleştireceğiz.

Bu ekranı daha ilginç hale getirmek için, uygulamamızdaki her kategoriyi temsil eden bazı resimler ekleyeceğiz. Unsplash’tan bazı çizimler seçtim ve project11-files klasörüne yerleştirdim. Bu dosyaları indirerek asset catalog’a ekleyin.

Ardından, “DetailView” adında yeni bir SwiftUI View oluşturun ve SwiftData’yı import edin. Bu yeni view’ın yalnızca bir property’ye ihtiyacı vardır, bu da göstermesi gereken kitaptır, bu yüzden lütfen bunu şimdi ekleyin;

let book: Book

Sadece bu property’ye sahip olmak bile DetailView.swift’in preview kodunu bozmak için yeterlidir. Önceden bunu düzeltmek kolaydı çünkü sadece örnek bir nesne gönderiyorduk, ancak SwiftData ile işler daha karmaşık hale geldi.

Bu, SwiftData’da gerçekten zor olan ilk şeydir; çalışması için her şeyi tam olarak doğru yapmamız gerekir;

  1. Örnek bir Book nesnesi oluşturmak için öncelikle bir model context oluşturmamız gerekir.
  2. Bu model context, bir model container oluşturmayı gerektirir.
  3. Bir model container oluşturursak, aslında hiçbir şey depolamasını istemeyiz, bu nedenle SwiftData’ya bilgilerini yalnızca bellekte depolamasını söyleyen özel bir yapılandırma oluşturabiliriz. Bu, her şeyin geçici olduğu anlamına gelir.

Bunun kulağa çok fazla geldiğini biliyorum, ancak pratikte sadece birkaç satır koddan ibaret. Model container oluşturmamız ve bunu geçici bellek talep etmemizi sağlayan ModelConfiguration adlı yeni bir tür kullanarak yapmamız gerekiyor. Bunu yaptıktan sonra, normal şekilde bir Book nesnesi oluşturabilir ve ardından model container’ın kendisiyle birlikte DetailView’e gönderebiliriz.

Mevcut preview kodunuzu bununla değiştirin;

#Preview {
    do {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try ModelContainer(for: Book.self, configurations: config)
        let example = Book(title: "Test Book", author: "Test Author", genre: "Fantasy", review: "This was a great book; I really enjoyed it.", rating: 4)

        return DetailView(book: example)
            .modelContainer(container)
    } catch {
        return Text("Failed to create preview: \(error.localizedDescription)")
    }
}

Bir container olmadan SwiftData model nesnesi oluşturmaya çalışmak muhtemelen kodunuzun çökmesine neden olur.

Bunu yaptıktan sonra dikkatinizi daha ilginç sorunlara, yeni view’ın kendisini tasalayamaya çevirebiliriz. Başlangıç olarak, kategori resmini ve türünü bir ZStack içine yerleştireceğiz, böylece birini diğerinin üzerine güzel bir şekilde koyabileceğiz. İyi göründüğünü düşündüğüm bası stilleri seçtim, ancak stilleri istediğiniz gibi deneyebilirsiniz sadece ScrollView’i tutmaya devam edin.

Geçerli body property’sini bununla değiştirin.

ScrollView {
    ZStack(alignment: .bottomTrailing) {
        Image(book.genre)
            .resizable()
            .scaledToFit()

        Text(book.genre.uppercased())
            .font(.caption)
            .fontWeight(.black)
            .padding(8)
            .foregroundStyle(.white)
            .background(.black.opacity(0.75))
            .clipShape(.capsule)
            .offset(x: -5, y: -5)
    }
}
.navigationTitle(book.title)
.navigationBarTitleDisplayMode(.inline)
.scrollBounceBehavior(.basedOnSize)

Bu, genre (tür) adını ZStack’in sağ alt köşesine, arka plan rengi, kalın yazı tipi ve öne çıkmasına yardımcı olmak için biraz dolgu ile yerleştirilir.

Bu ZStack’in altına yazarı, incelemeyi ve derecelendirmeyi ekleyeceğiz. Kullanıcıların burada derecelendirmeyi ayarlayabilmesini istemiyoruz, bunun yerine bunu basit bir salt okunur view’a dönüştürmek için başka bir constant binding kullanabiliriz. Daha da iyisi, rating view oluşturmak için SF Symbols kullandığımızdan, sahip olduğumuz tüm alanı daha iyi kullanmak için basit bir font() modifier ile bunları sorunsuz bir şekilde ölçeklendirebiliriz.

Dolayısıyla, bu view’ları doğrudan önceki ZStack’in altına ekleyin;

Text(book.author)
    .font(.title)
    .foregroundStyle(.secondary)

Text(book.review)
    .padding()

RatingView(rating: .constant(book.rating))
    .font(.largeTitle)

Bookworm app detailview

DetailView tamamlandığına göre, List view’e bir navigation destination eklemek için ContentView.swift’e geri dönebiliriz;

.navigationDestination(for: Book.self) { book in
    DetailView(book: book)
}

Şimdi uygulamayı tekrar çalıştırın, çünkü yeni detail view’da göstermek için girdiğiniz kitaplardan herhangi birine dokunabilmeniz gerekir.

SortDescriptor ile SwiftData Query Sıralama #

SwiftData’dan nesneleri çekmek için @Query kullandığınızda, verilerin nasıl sıralanmasını istediğinizi belirtebilirsiniz. Örneğin alfabetik veya önce en yüksek sayılar olacak şekilde sayısal mı? Neyi seçerseniz seçin, kullanıcılarınızın öngörülebilir bir deneyim yaşaması için bir şey seçmek her zaman iyi bir fikirdir.

Bu projede, sıralama amacıyla yararlı olabilecek çeşitli alanlarımız var: kitabın adı, yazarı veya derecelendirmesi mantıklı ve iyi seçimler olacaktır, ancak yalnızca birine güvenmek zorunda değiliz - birden fazla belirtebilirsiniz, böylece önce en yüksek puan alan kitapları isteyebilir, ardından eşitlik bozucu olarak adlarını kullanabilirsiniz.

Query sıralaması iki şekilde yapılabilir: yalnızca bir sıralama alanına izin veren basit bir seçenek veya SortDescriptor adı verilen yeni bir türden bir array’e izin veren daha gelişmiş bir tür ile.

Basit versiyonu kullanarak, kitaplarımızın başlıklarına göre alfabetik sırayla sağlanmasını isteyebiliriz;

@Query(sort: \Book.title) var books: [Book]

Ya da derecelendirmeye göre en yüksekten en düşüğe doğru sıralanmalarını isteyebiliriz;

@Query(sort: \Book.rating, order: .reverse) var books: [Book]

Bu, tek bir alan istediğinizde işe yarar, ancak genellikle yedek bir alana sahip olmanın daha iyi bir fikir olduğunu söyleyebilirim, “derecelendirmeye göre sırala, sonra başlığa göre” demek uygulamanıza ekstra bir öngörülebilirlik düzeyi ekler, bu da her zaman iyi bir şeydir.

Bu, bir veya iki değerden oluşturabileceğimiz SortDescriptor türü kullanılarak yapılır: sıralamak istediğimiz property ve isteğe bağlı olarak tersine çevrilip çevrilmeyeceği. Örneğin, title property’si üzerinde alfabetik olarak şu şekilde sıralama yapabiliriz;

@Query(sort: [SortDescriptor(\Book.title)]) var books: [Book]

SortDescriptor kullanarak sonuçları sıralamak varsayılan olarak artan sırada yapılır, yani metin için alfabetik sıra kullanılır, ancak sıralama düzenini tersine çevirmek isterseniz bunu yerine aşağıdakini yaparsınız;

@Query(sort: [SortDescriptor(\Book.title, order: .reverse)]) var books: [Book]

Birden fazla Sort Descriptor belirtebilirsiniz ve bunlar sağladığınız sıraya göre uygulanacaktır. Örneğin, kullanıcı Pete Hamill’in “Forever” kitabını ekledikten sonra Judy Blume’un “Forever” kitabını eklediyse o zaman ikinci bir sıralama alanı belirtmek yararlı olacaktır.

Bu nedenle, kitap başlığının önce artan, ardından kitap yazarının ikinci olarak artan şekilde sıralanmasını isteyebiliriz;

@Query(sort: [
    SortDescriptor(\Book.title),
    SortDescriptor(\Book.author)
]) var books: [Book]

Benzer değerlere sahip çok sayıda veriniz olmadığı sürece ikinci veya üçüncü bir sıralama alanına sahip olmanın performansa etkisi yok denecek kadar azdır. Örneğin kitaplar verimizde, neredeyse her kitabın benzersiz bir başlığı olacaktır, bu nedenle ikincil bir sıralama alanına sahip olmak performans açısından aşağı yukarı önemsizdir.

SwiftData Query silme #

SwiftData nesnelerini bir SwiftUI List’e yerleştirmek için @Query’yi zaten kullandık ve sadece biraz daha çalışarak hem silmek için kaydırmayı hem de özel bir Edit/Done butonunu etkinleştirebiliriz.

Tıpkı normal veri array’lerinde olduğu gibi, işin çoğu ForEach’e bir onDelete(perform:) modifier ekleyerek yapılır, ancak yalnızca bir array’den öğeleri kaldırmak yerine, query’de istenen nesneyi bulmamız ve ardından model context’te delete() methodunu çağırmak için kullanmamız gerekir. Tüm nesneler silindiğinde, SwiftData’nın otomatik kaydetme sistemi devreye girecek ve değişiklikleri kalıcı olarak uygulayacaktır.

Bu yüzden, bu methodu ContentView’e ekleyerek başlayın;

func deleteBooks(at offsets: IndexSet) {
    for offset in offsets {
        // find this book in our query
        let book = books[offset]

        // delete it from the context
        modelContext.delete(book)
    }
}

ContentView’in ForEach’ine bir onDelete(perform:) modifier ekleyerek bunu tetikleyebiliriz, ancak unutmayın: List’e değil ForEach’e gitmesi gerekir.

Bu modifier’ı şimdi ekleyin;

.onDelete(perform: deleteBooks)

Bu bize silmek için kaydırma sağlar. Bir Edit/Done butonu ekleyerek de bir adım daha ileri gidebiliriz. ContentView’de toolbar() modifier’ını bulun ve başka bir ToolbarItem ekleyin;

ToolbarItem(placement: .topBarLeading) {
    EditButton()
}

Artık uygulamayı çalıştırabilir ve daha önce eklediğiniz kitapları da silebilirsiniz.

NavigationLink’in custom view, Text veya Image gibi bir detail view’e geçmemizi nasıl sağladığını zaten görmüştük. Bir NavigationStack içinde olduğumuz için iOS, kullanıcıların önceki ekrana geri dönmelerini sağlamak için otomatik olarak bir “Back” butonu sağlar ve ayrıca geri dönmek için kenardan kaydırabilirler. Ancak bazen programsal olarak geri dönmek yani önceki ekrana kullanıcı kaydırdığında değil de istediğimiz zaman geri dönmek yararlı olabilir.

Uygulamamıza, kullanıcının o anda bakmakta olduğu kitabı silen son bir özellik ekleyeceğiz. Bunu yapmak için kullanıcıya kitabı gerçekten silmek isteyip istemediğini soran bir uyarı göstermemiz, ardından istedikleri buysa kitabı mevcut model context’ten silmemiz gerekiyor. Bu yapıldıktan sonra, ilişkili kitap artık mevcut olmadığından mevcut ekranda kalmanın bir anlamı yoktur, bu nedenle mevcut view’i atacağız - NavigationStack yığının en üstünden kaldıracağız, böylece önceki ekrana geri döneceğiz.

İlk olarak, DetailView struct’ta üç yeni property’ye ihtiyacımız var: biri SwiftData model context’i tutmak için (böylece bir şeyleri silebiliriz), biri dismiss eylemimizi tutmak için (böylece view’ı navigaton stacak’ten çıkarabiliriz) ve bir de silme onayı uyarısını gösterip göstermeyeceğimizi kontrol etmek için.

Bu yüzden, DetailView’e bu üç yeni property’yi ekleyerek başlayın;

@Environment(\.modelContext) var modelContext
@Environment(\.dismiss) var dismiss
@State private var showingDeleteAlert = false

İkinci adım, geçerli kitabı model context’ten silen ve geçerli view’ı kapatan bir method yazmaktır. Bu view’ın bir sheet yerine bir navigation link kullanılarak gösteriliyor olması önemli değildir-yine aynı dismiss() kodunu kullanırız.

Bu methodu şimdi DetailView’e ekleyin;

func deleteBook() {
    modelContext.delete(book)
    dismiss()
}

Üçüncü adım, showingDeleteAlert ’i izleyen ve kullanıcıdan eylemi onaylamasını isteyen bir alert() modifier eklemektir. Şimdiye kadar bir kapatma butonunu basit uyarılar ile kullandık, ancak burada iki butona ihtiyacımız var. Biri kitabı silmek için, diğeri de iptal etmek için. Bunların her ikisi de otomatik olarak doğru görünmelerini sağlayan belirli buton rollerine sahiptir, bu yüzden bunları kullanacağız.

Apple, uyarı metnini nasıl etiketlememiz gerektiği konusunda çok net bir rehberlik sunuyor, ancak konu şuna geliyor: basit bir “Anlıyorum” kabülü ise “OK” iyidir, ancak kullanıcıların bir seçim yapmasını istiyorsanız “Yes” ve “No” gibi başlıklardan kaçmalı ve bunun yerine “Ignore”, “Reply” ve “Confirm” gibi fiiller kullanmalısınız.

Bu örnekte, destructive buton için “Delete” kullanacağız, ardından kullanıcıların isterlerse silme işleminden vazgeçebilmeleri için yanında bir “Cancel” butonu sağlayacağız. Bu yüzden, bu modifier’ı DetailView’deki ScrollView’e ekleyin;

.alert("Delete book", isPresented: $showingDeleteAlert) {
    Button("Delete", role: .destructive, action: deleteBook)
    Button("Cancel", role: .cancel) { }
} message: {
    Text("Are you sure?")
}

Son adım, silme işlemini başlatan bir toolbar item eklemektir - bunun sadece showingDeleteAlert boolean’ı toggle etmesi gerekir, çünkü alert() modifier’ımız zaten onu izlemektedir. Bu son modifier’ı ScrollView’e ekleyin;

.toolbar {
    Button("Delete this book", systemImage: "trash") {
        showingDeleteAlert = true
    }
}

Artık ContentView’da kitapları silmek için kaydırma ve edit butonunu kullanarak silebilir veya DetailView’a gidip buradaki özel delete butonuna dokunabilirsiniz.


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

Bu yazı, SwiftUI Day 55 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.