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

50.Gün - SwiftUI Networking: Observable Class ve Codable, Haptic Engine

Bugün uygulamamız için kullanıcı arayüzü oluşturmaya geçmeden önce iki tekniği daha ele alacağız. @Observable bir class’a Codable desteği ekleyecek ve Haptic effect’leri inceleyeceğiz.

Bir @Observable Sınıfa Codable Uyumluluğu Ekleme #

Bir türün tüm property’leri zaten Codable’a uygunsa, türün kendisi ekstra bir çalışma gerektirmeden Codable’a uygun olabilir. Swift, türünüzü gerektiği gibi arşivlemek ve arşivden çıkarmak için gereken kodu sentezleyecektir. Ancak, Swift’in kodumuzu yeniden yazma şekli nedeniyle @Observable makrosunu kullanan sınıflarla çalışırken işler biraz daha zorlaşır.

Sorunu uygulamada görmek için, aşağıdaki gibi name adında tek bir property’si olan basit bir observable sınıf oluşturabiliriz.

@Observable
class User: Codable {
    var name = "Taylor"
}

Şimdi bir butona basıldığında sınıfımızın bir instance’ını encode eden ve ortaya çıkan metni yazdıran küçük bir SwiftUI kodu yazabiliriz;

struct ContentView: View {
    var body: some View {
        Button("Encode Taylor", action: encodeTaylor)
    }

    func encodeTaylor() {
        let data = try! JSONEncoder().encode(User())
        let str = String(decoding: data, as: UTF8.self)
        print(str)
    }
}

Göreceğimiz şey beklenmedik:  {"name":“Taylor”,"$observationRegistrar":{}}

name property artık _name, JSON’da bir observation registrar instance’da var.

Unutmayın, @Observable makrosu SwiftUI tarafından izlenebilmesi için sınıfımızı sessizce yeniden yazıyor ve burada bu yeniden yazma sızıntı yapıyor. Bunun gerçekleştiğini görebiliyoruz, bu da her türlü soruna neden olabilir. Örneğin, bir sunucuya bir “name” değeri göndermeye çalışıyorsanız, sunucu “_name” ile ne yapacağını bilemeyebilir.

Bunu düzeltmek için Swift’e verilerimizi tam olarak nasıl kodlaması ve çözmesi gerektiğini söylememiz gerekir. Bu, sınıfımızın içine CodingKeys adında bir enum yerleştirerek ve bunun bir String ham değerine ve CodingKey protokolüne uygun olmasını sağlayarak yapılır. Evet, bu biraz kafa karıştırıcı. enum’un adı CodingKeys ve protokol CodingKey, ancak önemli.

Enum içinde, kaydetmek istediğiniz her property için bir case ve ona vermek istediğiniz adı içeren bir ham değer yazmanız gerekir. Bizim durumumuzda bu, _name’in (name property’nin temel depolama alanı) alt çizgi olmadan “name” string olarak yazılması gerektiğini söylemek anlamına gelir.

@Observable
class User: Codable {
    enum CodingKeys: String, CodingKey {
        case _name = "name"
    }

    var name = "Taylor"
}

Ve işte bu kadar! Kodu tekrar denerseniz, name property’nin doğru adlandırıldığını göreceksiniz ve ayrıca karşımda artık observation registrar yok, sonuç çok daha temiz.

Bu coding key eşlemesi her iki şekilde de çalışır: Codable bazı JSON’larda name gördüğünde, otomatik olarak _name property’ye kaydedilecektir.

Dokunsal (Haptic) Efektler Ekleme #

SwiftUI, telefonu çeşitli şekillerde titreştirmek için Apple’ın Taptic Engine’ini kullanan basit dokunsal efektler için yerleşik desteğe sahiptir. iOS’ta bunun için iki seçeneğimiz var: kolay olan ve eksiksiz olan.

Önemli: Bu dokunsal efektler yalnızca fiziksel iPhone’larda çalışır, Mac’ler ve iPad gibi diğer cihazlar titreşmez.

Kolay seçenekle başlayalım. Sayfalar ve uyarılar gibi, SwiftUI’ye efekti ne zaman tetikleyeceğini söyleriz ve gerisini bizim için halleder.

İlk olarak, bir butona her basıldığında sayaca 1 ekleyen basit bir view yazabiliriz;

struct ContentView: View {
    @State private var counter = 0

    var body: some View {
        Button("Tap Count: \(counter)") {
            counter += 1
        }
    }
}

Bunların hepsi eski kodlar, bu yüzden butona her basıldığında tetiklenen bir dokunsal efekt ekleyerek daha ilginç hale getirelim, bu modifier’ı butana ekleyin;

.sensoryFeedback(.increase, trigger: counter)

Bunu gerçek bir cihazda çalıştırmayı deneyin ve butona her bastığınızda hafif dokunsal dokunuşlar hissetmelisiniz.

.increase , yerleşik dokunsal geri bildirim çeşitlerinden biridir ve sayaç gibi bir değeri arttırırken en iyi şekilde kullanılır. Aralarından seçim yapabileceğiniz .success , .warning, .error , .start , .stop ve daha pek çok seçenek vardır.

Bu geri bildirim çeşitlerinin her birinin farklı bir hissi vardır ve hepsini gözden geçitip en çok hoşunuza gidenleri seçmek cazip gelse de, lütfen bunun bilgi aktarmak için dokunsallara güvenen görme engelli kullanıcılar için nasıl kafa karıştırıcı olabileceğini düşünün, uygulamanız bir hatayla karşılaşırsa ancak çok sevdiğiniz success (başarı) dokunsalını oynatırsanız, bu kafa karışıklığına neden olabilir.

Dokunsal efektleriniz üzerinde biraz daha fazla kontrol istiyorsanız, .impact() adlı alternatif vardır. Bu alternatifin iki çeşidi bulunmaktadır: biri nesnenizin ne kadar esnek olduğunu ve efektin ne kadar güçlü olması gerektiğini belirttiğiniz, diğeri ise bir ağırlık ve yoğunluk belirttiğiniz yerdir.

Örneğin, iki yumuşak nesne arasında orta düzeyde bir çarpışma talep edebiliriz;

.sensoryFeedback(.impact(flexibility: .soft, intensity: 0.5), trigger: counter)

Ya da iki ağır nesne arasında yoğun bir çarpışma olduğunu belirtebiliriz;

.sensoryFeedback(.impact(weight: .heavy, intensity: 1), trigger: counter)

Daha gelişmiş haptic feedback için Apple bize Core Haptics adından bir framework sunuyor. Bu şey, arkasındaki Apple ekibi tarafından gerçek bir sevgi emeği gibi hissettiriyor ve bence iOS 13’te tanıtılan gerçek gizli mücevherlerden biriydi.

Core Haptics, dokunuşları, sürekli titreşimleri, parametre eğrilerini ve daha fazlasını birleştirerek son derece özelleştirilebilir hapticler oluşturmamızı sağlar. Biraz konu dışı olduğu için burada çok fazla derinliğine inmek istemiyorum, ancak en azından kendiniz deneyebilmeniz için size bir örnek vereceğim.

İlk olarak ContentView.swift dosyasının üst kısmına bu yeni import’u ekleyin.

import CoreHaptics

Daha sonra, property olarak bir CHHapticEngine instance’ı oluşturmamız gerekir. Bu, titreşimleri oluşturmaktan sorumlu olan gerçek nesnedir, bu nedenle dokunsal efektler istemeden önce onu oluşturmamız gerekir.

Dolayısıyla, bu property’yi ContentView’e ekleyin;

@State private var engine: CHHapticEngine?

Ana view görünür görünmez bunu oluşturacağız. Engine’i oluştururken, uygulama arka plana geçtiğinde olduğu gibi durdurulduğunda etkinliğin devam etmesine yardımcı olmak için handler ekleyebilirsiniz, ancak burada basit tutacağız: mevcut cihaz haptikleri destekliyorsa motoru başlatacağız ve başarısız olursa bir hata yazdıracağız.

Bu methodu ContentView’e ekleyin;

func prepareHaptics() {
    guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }

    do {
        engine = try CHHapticEngine()
        try engine?.start()
    } catch {
        print("There was an error creating the engine: \(error.localizedDescription)")
    }
}

Şimdi işin eğlenceli kısmına gelelim: haptiğin ne kadar güçlü olması gerektiği (.hapticIntensity) ve ne kadar “keskin” olduğunu (.hapticSharpness) kontrol eden parametreleri yapılandırabilir, ardından bunları göreceli bir zaman ofsetine sahip bir haptik olaya yerleştirebiliriz.

Bu methodu ContentView’e ekleyin;

func complexSuccess() {
    // make sure that the device supports haptics
    guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
    var events = [CHHapticEvent]()

    // create one intense, sharp tap
    let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1)
    let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 1)
    let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 0)
    events.append(event)

    // convert those events into a pattern and play it immediately
    do {
        let pattern = try CHHapticPattern(events: events, parameters: [])
        let player = try engine?.makePlayer(with: pattern)
        try player?.start(atTime: 0)
    } catch {
        print("Failed to play pattern: \(error.localizedDescription).")
    }
}

Özel haptiklerimizi denemek için ContentView’in body property’sini şu şekilde değişitirin;

Button("Tap Me", action: complexSuccess)
    .onAppear(perform: prepareHaptics)

onAppear() methodunun eklenmesi, dokunma hareketinin doğru şekilde çalışması için haptik sisteminin başlatıldığından emin olunmasını sağlar.

Haptiklerle daha fazla deneme yapmak istiyorsanız, let intensity, let sharpness ve let event satırlarını istediğiniz haptiklerle değiştirin. Örneğin, bu üç satırı aşağıdaki kodla değiştirirseniz, artan ve azalan yoğunluk ve keskinlikte birkaç dokunuş elde edersiniz;

for i in stride(from: 0, to: 1, by: 0.1) {
    let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(i))
    let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(i))
    let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: i)
    events.append(event)
}

for i in stride(from: 0, to: 1, by: 0.1) {
    let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: Float(1 - i))
    let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: Float(1 - i))
    let event = CHHapticEvent(eventType: .hapticTransient, parameters: [intensity, sharpness], relativeTime: 1 + i)
    events.append(event)
}

Core Haptics ile denemeler yapmak çok eğlenceli, ancak ne kadar fazla çalışma gerektirdiğini göz önünde bulundurarak, mümkün olduğunca yerleşik efektlere bağlı kalacağınızı düşünüyorum.

Bu bizi bu proje için genel bakışın sonuna getiriyor, bu yüzden lütfen ContenView.swift’i orijinal durumuna geri getirin, böylece ana projeyi oluşturmaya başlayabiliriz.

Temel Sipariş Detaylarının Alınması #

Bu projedeki ilk adım, bir siparişin temel ayrıntılarını alan bir sipariş ekranı oluşturmak olacaktır: kaç tane cupcake istedikleri, ne tür istedikleri ve herhangi bir özelleştirme olup olmadığı.

Kullanıcı arayüzüne geçmeden önce, veri modelini tanımlayarak başlamamız gerekiyor. Daha önce doğru sonucu elde etmek için struct ve sınıfları beraber kullanmıştık, ancak burada farklı bir çözüm uygulayacağız: tüm verilerimizi depolayan ve ekrandan ekrana aktarılacak tek bir sınıfımız olacak. Bu, uygulamamızdaki tüm ekranların aynı verileri paylaştığı anlamına gelir ve göreceğiniz gibi gerçekten iyi çalışacaktır.

Şimdilik bu sınıfın çok fazla propert’ye ihtiyacı olmayacak:

  • Cake türü, ayrıca tüm olası seçeneklerin statik bir array’i
  • Kullanıcının kaç kek sipariş etmek istediği.
  • Kullanıcının, kullanıcı arayüzümüzde ekstra seçenekleri gösterecek veya gizleyecek özel isteklerde bulunmak isteyip istemediği.
  • Kullanıcının cake’lerinin üzerine ekstra krema isteyip istemediği.
  • Kullanıcının cake’lerinin üzerine serpme eklemek isteyip istemediği.

Bunların her birinin değiştirildiğinde kullanıcı arayüzünü güncellemesi gerekir; bu da sınıfın @Observable makrosunu kullandığından emin olmamız gerektiği anlamına gelir.

Bu nedenle, lütfen Order.swift adından yeni bir Swift dosyası oluşturun, Foundation importunu SwiftUI olarak değiştirin ve bu kodu yazın;

@Observable
class Order {
    static let types = ["Vanilla", "Strawberry", "Chocolate", "Rainbow"]

    var type = 0
    var quantity = 3

    var specialRequestEnabled = false
    var extraFrosting = false
    var addSprinkles = false
}

Şimdi bu property’yi ekleyerek ContentView içinde bunun tek bir instance’ını oluşturabiliriz.

@State private var order = Order()

Siparişin oluşturulacağı tek yer burasıdır uygulamamızdaki diğer tüm ekranlara bu property aktarılacaktır, böylece hepsi aynı verilerle çalışacaktır.

Bu ekran için kullanıcı arayüzünü, cake türü ve miktarıyla başlayarak üç bölümde oluşturacağız. Bu ilk bölümde, kullanıcının Vanilyalı, Çilekli, Çikolatalı ve Gökkuşağı kekleri arasından seçim yapmasına olanak tanıyan bir picker ve ardından miktarı seçmek için 3 ila 20 aralığında bir stepper gösterilecek. Tüm bunlar bir formun içine yerleştirilecek ve kendisi de bir navigation stack içinde yer alacak, böylece bir title belirleyebileceğiz.

Burada ufak bir sorun var: cupcake topping listemiz bir string array’dir, ancak kullanıcının seçimini bir integer olarak saklıyoruz. Bu ikisini nasıl eşleştirebiliriz? Kolay bir çözüm, array’in indices property’sini kullanmaktır, bu da bize her öğenin bir konumunu verir ve daha sonra bunu bir array indeksi olarak kullanabiliriz. Bu, değişken array’ler için kötü bir fikirdir çünkü array’in sırası herhangi bir zamanda değişebilir, ancak burada array sıramız hiç değişmeyecek, bu yüzden güvenlidir.

Bunu şimdi ContentView’in body’sine yerleştirin:

NavigationStack {
    Form {
        Section {
            Picker("Select your cake type", selection: $order.type) {
                ForEach(Order.types.indices, id: \.self) {
                    Text(Order.types[$0])
                }
            }

            Stepper("Number of cakes: \(order.quantity)", value: $order.quantity, in: 3...20)
        }
    }
    .navigationTitle("Cupcake Corner")
}

Formumuzun ikinci bölümünde sırasıyla specialRequestEnabled , extraFrosting ve addSprinkles ‘a bağlı üç toggle switch bulunacaktır. Ancak, ikinci ve üçüncü anahtarlar yalnızca birincisi etkinleştirildiğinde görünür olmalıdır, bu nedenle bunları bir koşul içine alacağız.

Section {
    Toggle("Any special requests?", isOn: $order.specialRequestEnabled)

    if order.specialRequestEnabled {
        Toggle("Add extra frosting", isOn: $order.extraFrosting)

        Toggle("Add extra sprinkles", isOn: $order.addSprinkles)
    }
}

Devam edin ve uygulamayı tekrar çalıştırıp deneyin.

Ancak bir hata var ve bu hatayı biz oluşturduk. Özel istekleri etkinleştirip ardından “extra frosting” ve “extra sprinkles” seçeneklerinden birini veya her ikisini birden etkinleştirirsek, arından özel istekleri devredışı bırakırsak, önceki özel istek seçimimiz etkin kalır. Bu özel istekleri yeniden etkinleştirdiğimizde, önceki özel isteklerin hala etkin olduğu anlamına gelir.

Kodumuzun her katmanı bununun farkındaysa (uygulamanız, sunucunuz, veritabanınız vb. specialRequestEnabled false olarak ayarlandığında extraFrosting ve addSprinkles değerlerini yok sayacak şekilde programlanmışsa) bu tür bir sorunu aşmak zor değildir. Bununla birlikte, daha iyi bir fikir -ayrıca daha güvenli- specialRequestEnabled false olarak ayarlandığında hem extraFrosting hem de addSprinkles değerlerinin false olarak sıfırlandığından emin olmaktır.

specialRequestEnabled öğesine didSet property observer ekleyerek bunu gerçekleştirebiliriz.

var specialRequestEnabled = false {
    didSet {
        if specialRequestEnabled == false {
            extraFrosting = false
            addSprinkles = false
        }
    }
}

Üçüncü bölümümüz en kolayı, çünkü sadece bir sonraki ekrana işaret edene bir NavigationLink olacak. İkinci bir ekranımız yok, ancak bunu yeterince hızlı bir şekilde ekleyebiliriz. “AdressView” adında yeni bir SwiftUI view oluşturun ve buna aşağıdaki gibi bir order property verin.

struct AddressView: View {
    var order: Order

    var body: some View {
        Text("Hello World")
    }
}

#Preview {
    AddressView(order: Order())
}

Bunu kısa süre içinde daha kullanışlı hale getireceğiz, ancak şimdilik ContentView.swift’e dönüp formumuz için son bölümü ekleyeceğiz. Bu, AdressView’e işaret eden bir NavigationLink oluşturacak ve geçerli sipariş nesnesini aktaracaktır.

Bu son bölümü şimdi ekleyin.

Section {
    NavigationLink("Delivery details") {
        AddressView(order: order)
    }
}

Bu ilk ekranımızı tamamlıyor, bu yüzden devam etmeden önce deneyebilirsiniz.


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

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