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

54.Gün - SwiftUI Custom UI Bileşeni

Uygulamamızı oluşturmak için öğrendiğimiz yeni teknikleri uygulamaya başlayacağız. Kitap nesnesi oluşturmak için SwiftData’yı ve kullanıcıların her bir kitabı ne kadar beğendiklerini kaydetmeleri için @Binding kullanılarak oluşturulmuş custom RatingView bileşenini kullanacağız.

Bugün, List , @Binding , ve daha fazlası ile yeni edindiğimiz SwiftData becerilerinizi uygulayacağımız üç konu üzerinde çalışacağız.

  • SwiftData ile Kitap (Book) Oluşturma
  • Yıldızlı Derecelendirme Custom Bileşenini Oluşturma
  • @Query ile List Oluşturma

SwiftData ile Kitap (Book) Oluşturma #

Bu projedeki ilk görevimiz kitaplarımız için bir SwiftData modeli tasarlamak ve ardından veritabanına kitap eklemek için yeni bir view oluşturmak olacaktır.

İlk olarak modeli oluşturalım. Book.swift adında yeni bir dosya oluşturun, SwiftData için import ekleyin ve ardında bu kodu yazın.

@Model
class Book {
    var title: String
    var author: String
    var genre: String
    var review: String
    var rating: Int
}

Bu sınıfın tüm property’lerine değer sağlamak için bir initializer ihtiyacı vardır. Xcode içinde in yazmaya başladığınızda xcode size bunu otomatik olarak oluşturabilir.

Bu sınıf, kitabın başlığını, kitap yazarını, türünü, kullanıcının kitap hakkındaki düşüncelerinin kısa bir özetini ve ayrıca kullanıcının kitap için verdiği sayısal puanı saklamak için yeterlidir.

Artık veri modelimiz olduğuna göre, SwiftData’dan bunun için bir model container oluşturmasını isteyebiliriz. Bu BookwormApp.swift dosyasını açmak, dosyanın başına import SwiftData eklemek ve ardından bu modifier’ı WindowGroup’a eklemek anlamına gelir.

.modelContainer(for: Book.self)

Bir sonraki adımımız yeni girişler oluşturulabilecek bir form yazmak. Bu şimdiye kadar öğrendiğimiz pek çok beceriyi bir araya getirecek: Form, @State, @Environment, TextField, TextEditor, Picker, sheet() ve daha fazlası, ayrıca tüm yeni SwiftData bilginiz.

“AddBookView” adında yeni bir SwiftUI view oluşturarak başlayın. Property’ler açısından, model context’e erişmek için bir environment property’ye ihtiyacımız var.

@Environment(\.modelContext) var modelContext

Bu form bir kitabı oluşturmak için gereken tüm verileri depolayacağından, kitabın her bir değeri için @State property’lerine ihtiyacımız var. Şimdi bu property’leri ekleyin;

@State private var title = ""
@State private var author = ""
@State private var rating = 3
@State private var genre = "Fantasy"
@State private var review = ""

Son olarak, tüm olası genre seçeneklerini saklamak için bir property’ye daha ihtiyacımız var, böylece ForEach kullanarak bir picker yapabiliriz. Bu son property’yi AddBookView’a ekleyin;

let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]

Şimdi form’un kendisini yapmaya başlayabiliriz. Mevcut body’yi bununla değiştirin;

NavigationStack {
    Form {
        Section {
            TextField("Name of book", text: $title)
            TextField("Author's name", text: $author)

            Picker("Genre", selection: $genre) {
                ForEach(genres, id: \.self) {
                    Text($0)
                }
            }
        }

        Section("Write a review") {                
            TextEditor(text: $review)

            Picker("Rating", selection: $rating) {
                ForEach(0..<6) {
                    Text(String($0))
                }
            }                
        }

        Section {
            Button("Save") {
                // add the book
            }
        }
    }
    .navigationTitle("Add Book")
}

Butonun action kısmını doldurmak söz konusu olduğunda, formumuzdaki tüm değerleri kullanarak Book sınıfının bir instance’ını oluşturacağız ve ardından nesneyi model context’e ekleyeceğiz.

Bu kodu //add the book yorumunun yerine ekleyin;

let newBook = Book(title: title, author: author, genre: genre, review: review, rating: rating)
modelContext.insert(newBook)

Bu şimdilik form’u tamamlıyor, ancak yine de bu formu göstermenin ve gizlemenin bir yolunu bulmalıyız.

AddBookView’i göstermek için ContentView.swift’e dönmek ve bir sheet için olağan adımları izlemek gerekir;

  1. Sheet’in gösterilip gösterilmediğini izlemek için bir @State property ekleme.
  2. Bu property’yi değiştirmek için bir buton ekleme, (bu durumda toolbar’a)
  3. Property true olduğunda AddBookView öğesini gösteren bir sheet() modifier

ContentView.swift’e SwiftData için bir import ekleyerek başlayın, ardından bu property’leri ContentView struct’a ekleyin;

@Environment(\.modelContext) var modelContext
@Query var books: [Book]

@State private var showingAddScreen = false

Bu bize daha sonra kitapları silmek için kullanabileceğimiz bir model context, elimizdeki tüm kitapları okuyan bir sorgu (böylece her şeyin çalıştığını test edebiliriz) ve ekleme ekranının gösterilip gösterilmediğini izleyen bir Boolean verir.

ContentView body için bir navigation stack kullanacağız, böylece sağ üst köşede bir başlık ve bir buton ekleyebileceğiz, ancak bunun dışında sadece books array’de kaç öğemiz olduğunu gösteren bazı metinler tutacak. Unutmayın, gerektiğinde bir AddBookView göstermek için sheet() modifier’ını eklememiz gereken yer burasıdır.

ContentView’ın mevcut body property’sini bununla değiştirin;

 NavigationStack {
    Text("Count: \(books.count)")
        .navigationTitle("Bookworm")
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                Button("Add Book", systemImage: "plus") {
                    showingAddScreen.toggle()
                }
            }
        }
        .sheet(isPresented: $showingAddScreen) {
            AddBookView()
        }
}

Şu an SwiftData modelimizi tasarladık, veri eklemek için bir form oluşturduk, ardından ContentView’ı gerektiğinde formu sunabilecek şekilde güncelledik. Son adım, kullanıcı bir kitap eklediğinde formun kendisini kapatmasını sağlamaktır.

Bunu daha önce de yapmıştık, umarım ne yapacağınızı biliyorsunuzdur. Mevcut view’ı kapatabilmek için AddBookView’a başka bir environment property ekleyerek başlamamız gerekiyor.

@Environment(\.dismiss) var dismiss

Son olarak, Save butonumuzun action closure’unun sonuna bir dismiss() çağrısı ekleyin.

Uygulamayı şimdi çalıştırabilmeli ve örnek bir kitap ekleyebilmelisiniz. Kitap ekledikçe Count 1 artmalıdır.

Yıldızlı Derecelendirme Custom Bileşenini Oluşturma #

SwiftUI, custom UI bileşenleri oluşturmayı gerçekten kolaylaştırır, çünkü bunlar okumamız için bir tür @Binding ’e sahip olan view’lardır.

Bunu göstermek için, kullanıcının resimlere dokunarak 1 ile 5 arasında puanlar girmesine olanak tanıyan bir yıldız derecelendirme view’ı oluşturacağız. Ayrıca bu view’ı her yerde kullanabilmek için esnek bir yapıya çevireceğiz. Altı özelleştirilebilir property yapacağız;

  • Derecelendirmeden önce hangi etiketin yerleştirilmesi gerektiği (varsayılan boş string)
  • Maksimum tamsayı derecesi (varsayılan:5)
  • Yıldız vurgulandığında veya vurgulanmadığında kullanılacak görüntüleri belirleyen off ve on image’ler (varsayılan: off image için nil ve on image için dolu bir yıldız; off imagede nil bulursak, o zaman on image’ı kullanırız)
  • Yıldız vurgulandığında veya vurgulanmadığında kullanılacak renkleri belirleyen off ve on renkler (varsayılan: off için gri, açık için sarı)

Ayrıca bir @Binding tamsayısını saklamak için ekstra bir property’ye ihtiyacımız var, böylece kullanıcının seçimini yıldız derecelendirmesini kullanan her şeye geri bildirebiliriz.

Bu yüzden, “RatingView” adında yeni bir SwiftUI view oluşturun ve bu property’leri vererek başlayın;

@Binding var rating: Int

var label = ""

var maximumRating = 5

var offImage: Image?
var onImage = Image(systemName: "star.fill")

var offColor = Color.gray
var onColor = Color.yellow

body property’yi doldurmadan önce, lütfen kodu oluşturmayı deneyin - başarısız olduğunu göreceksiniz, çünkü #Preview kodumuz rating için kullanılacak bir binding iletmiyor.

SwiftUI bunun için constant binding adı verilen özel ve basit bir çözüme sahiptir. Bunlar sabit değerlere sahip bağlardır, bu da bir yandan kullanıcı arayüzünde değiştirilemeyecekleri anlamına gelirken diğer yandan bunları önemsiz bir şekilde oluşturabileceğimiz anlamına gelir- önizlemeler için mükemmeldir.

Bu yüzden, mevcut preview kodunu bununla değiştirin;

#Preview {
    RatingView(rating: .constant(4))
}

Şimdi body property’ye dönelim. Bu, sağlanan herhangi bir label’in yanı sıra talep edilen sayıda yıldız içeren bir HStack olacak - ancak elbette istedikleri herhangi bir resmi içerebilir, bu nedenle yıldız hiçte olmayabilir.

Hangi resmin gösterileceğini seçme mantığı oldukça basittir, ancak kodumuzun karmaşıklığını azaltmak için kendi methodunu oluşturmak mükemmeldir. Mantık şudur;

  • Aktarılan sayı geçerli rating’ten büyükse, ayarlanmışsa off görüntüyü döndürür, aksi takdirde on görüntüyü döndürür.
  • Aktarılan sayı geçerli rating’e eşit veya daha küçükse, on görüntüyü döndürür.

Bunu tek bir method ile kapsülleyebiliriz, bu yüzden bunu şimdi RatingView’a ekleyin;

func image(for number: Int) -> Image {
    if number > rating {
        offImage ?? onImage
    } else {
        onImage
    }
}

Ve şimdi body property’yi uygulamak şaşırtıcı derecede kolaydır: lable’da herhangi bir text varsa onu kullanının, ardından 1’den maksimum rating’e artı 1’e kadar saymak için ForEach kullanın ve image(for:) öğesini tekrar çağırın. Ayrıca rating’e bağlı olarak bir ön plan rengi uygulayacağız ve her yıldız derecelendirmeyi ayarlayan bir butonun içine saracağız.

Mevcut body property’yi bununla değiştiirn;

HStack {
    if label.isEmpty == false {
        Text(label)
    }

    ForEach(1..<maximumRating + 1, id: \.self) { number in
        Button {
            rating = number
        } label: {
            image(for: number)
                .foregroundStyle(number > rating ? offColor : onColor)
        }
    }
}

Bu, rating view’ı zaten tamamlıyor, bu yüzden onu eyleme geçirmek için AddBookView’e geri dönün ve ikinci section’ı bununla değiştirin.

Section("Write a review") {
    TextEditor(text: $review)
    RatingView(rating: $rating)
}

Varsayılan değerlerimiz mantıklıdır, bu nedenle kutudan çıkar çıkmaz harika görünür-devam edin ve şimdi deneyin.

Muhtemelen işlerin tam olarak yolunda gitmediğini göreceksiniz: hangi yıldız rating’e basarsanız basın, 5 yıldız seçecektir.

Bu sorunun, ne kadar deneyime sahip olurlarsa olsunlar, yüzlerce insanı etkilediğini gördüm. Sorun şu ki, bir form veya list içinde satırlarımız olduğunda, SwiftUI satırların kendilerinin dokunabilir olduğunu varsaymayı seviyor. Bu, kullanıcılar için seçimi kolaylaştırır, çünkü içindeki butonu tetiklemek için bir satırdaki herhangi bir yere dokunabilirler.

Bizim durumumuzda birden fazla butonumuz var, bu nedenle SwiftUI hepsine sırayla dokunuyor bu sebeple ne olursa olsun 5’te bitiyor.

HStack’e eklenmiş ekstra bir modifier ile tüm “butonları tetiklemek için satıra dokunun” davranışını devre dışı bırakabiliriz.

.buttonStyle(.plain)

Bu, SwiftUI’nin her butonu ayrı ayrı ele almasını sağlar, böylece her şey planlandığı gibi çalışır. Ve kullanımı çok daha daha güzel: yıldız rating’leri daha doğal ve daha yaygın olduğu için burada picker ile ayrıntı görünümüne girmeye gerek yok.

@Query ile List Oluşturma #

Şu anda ContentView’ımızın aşağıdaki gibi bir query property’i var.

@Query var books: [Book]

Ve bunu basit text view ile body içinde kullanıyoruz;

Text("Count: \(books.count)")

Bu ekranı hayat geçirmek için, bu text view, eklenen tüm kitapları, rating’leri, ve yazarlarıyla birlikte gösteren bir List ile değiştireceğiz.

Burada daha önce yaptığımız rating view’ı kullanabilirdik, ancak başka bir şey denemek çok daha eğlenceli. RatingView kontrolü her projede kullanılabilirken, bu projeye özel bir rating görüntüleyen yeni bir EmojiRatingView yapabiliriz. Tek yapacağı, rating’e bağlı olarak beş farklı emojiden birini göstermektir ve SwiftUI’de view kompanizasyonunun ne kadar basit olduğuna dair harika bir örnektir.

Bu yüzden, “EmojiRatingView” adında yeni bir SwiftUI view oluşturun ve ona aşağıdaki kodu verin;

struct EmojiRatingView: View {
    let rating: Int

    var body: some View {
        switch rating {
        case 1:
            Text("1")
        case 2:
            Text("2")
        case 3:
            Text("3")
        case 4:
            Text("4")
        default:
            Text("5")
        }
    }
}

#Preview {
    EmojiRatingView(rating: 3)
}

İpucu : Emojiler e-okuyucularda hasara yol açabildiği için metnimde rakamlar kullandım, ancak bunları çeşitli derecelendirmeleri temsil ettiğini düşündüğünüz emojilerle değiştirebilirsiniz.

Şimdi ContentView’a dönebilir ve kullanıcı arayüzünün ilk geçişini yapabiliriz. Bu, mevcut text view’ı bir list ve books üzerinde bir ForEach ile değiştirecektir. ForEach için bir tanımlayıcı sağlamamıza gerek yok çünkü tüm SwiftData modelleri otomatik olarak Identifiable ile uyumludur.

List’in içinde mevcut book’a işaret eden bir NavigationLink olacak ve bunun içinde yeni EmojiRatingView ile kitabın başlığı ve yazarı yer alacak. Mevcut text filed’ı bununla değiştirin;

List {
    ForEach(books) { bookinNavigationLink(value: book) {
            HStack {
                EmojiRatingView(rating: book.rating)
                    .font(.largeTitle)

                VStack(alignment: .leading) {
                    Text(book.title)
                        .font(.headline)
                    Text(book.author)
                        .foregroundStyle(.secondary)
                }
            }
        }
    }
}

İpucu: Daha önceki modifier’ları yerinde bıraktığınızdan emin olun - navigationTitle() vb.

navigationDestination() işlevini henüz eklemediğimiz için navigasyon tam olarak çalışmayacak, ancak sorun değil yakında bu ekrana geri döneceğiz. Önce detail view’i oluşturalım.


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

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