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

27.Gün - SwiftUI BetterRest Uygulaması

Bu bölümde 100 Days of SwiftUI eğitimimiz kapsamındaki 4 numaralı projemiz olan BetterRest’i çalışır bir uygulama haline getiriyoruz. Bir önceki bölümde öğrendiğimiz Stepper, DatePicker ve Core ML ile birlikte Create ML’nin uygulanmasını gerçek hayatta göreceğiz.

Bu proje aynı zamanda GitHub’da da bulunmaktadır.

GitHub - GorkemGuray/BetterRest: 100 Days of SwiftUI - Project-4

Temel Tasarımın Oluşturulması #

Bu uygulama, bir date picker ve iki stepper ile kullanıcı girişine izin verecek. Kullanıcı bu sayede bize ne zaman uyanacağını, ne kadar uyumak istediğini ve ne kadar kahve içtiğini söyleyecek, biz de karşılığında kullanıcıya uygun bir uyuma saati vereceğiz.

Kontrollerde kullanılacak üç property’yi eklemek ile başlayalım;

@State private var wakeUp = Date.now
@State private var sleepAmount = 8.0
@State private var coffeeAmount = 1

body içine bir VStack ve bir NavigationStack ile sarılmış bileşenler yerleştireceğiz. Aşağıdaki kodları ekleyelim;

NavigationStack {
    VStack {
        Text("When do you want to wake up?")
            .font(.headline)

        DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
            .labelsHidden()

        // more to come
        // daha fazla kod olacak
    }
}

.hourAndMinute yapılandırmasını istedik çünkü, birinin uyanmak istediği saati önemsiyoruz günü değil. labelHidden() modifier ile picker’ın etiketini de gizliyoruz çünkü text view yeterli.

Daha sonra kullanıcıların kabaca ne kadar uyku istediklerini seçmelerini sağlamak için stepper ekleyeceğiz. Stepper’a 4…12 aralığını ve 0,25’lik bir adım vererek mantıklı değerler gireceklerinden emin olabiliriz. Ayrıca formatted() methodu ile birleştirebilir böylece “8.000000” değil “8” gibi sayılar görürüz.

Aşağıdaki kodu // more to come yorumu yerine ekleyin.

Text("Desired amount of sleep")
    .font(.headline)

Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)

Son olarak, ne kadar kahve içtiklerine dair son bir stepper ve etiket ekleyeceğiz. Bu sefer 1’den 20’ye kadar olan aralığı kullanacağız.

Bunları VStack ’in içine, önceki view’ların altına ekleyin;

Text("Daily coffee intake")
    .font(.headline)

Stepper("\(coffeeAmount) cup(s)", value: $coffeeAmount, in: 1...20)

İhtiyacımız olan son şey, kullanıcıların uyumaları gereken en iyi zamanı hesaplamalarını sağlayacak bir buton. Bunu VStack’in sonunda basit bir buton ile yapabiliriz, fakat doğrudan navigation bar’da bir butonun daha iyi duracağı kanısındayım.

Öncelikle butonun çağıracağı bir methoda ihtiyacımız var, bu sebeple aşağıdaki gibi boş bir calculateBedtime() methodunu ekleyin;

func calculateBedtime() {
}

Şimdi navigation bar’a bir buton eklemek için toolbar() modifier’ını kullanmamız gerekiyor. Hazır buradayken en üste bir metin koymak için navigationTitle() ’da kullanabiliriz.

Bu sebeple, aşağıdaki modifier’ları VStack ’e ekleyin;

.navigationTitle("BetterRest")
.toolbar {
    Button("Calculate", action: calculateBedtime)
}

Butonumuz, İngilizce gibi soldan sağa diller için otomatik olarak sağ üst köşeye yerleştirilecek, ancak sağdan sola diller için otomatik olarak diğer tarafa geçecektir.

calculateBedtime() boş olduğu için henüz bir şey yapmayacak, ancak en azından kullanıcı arayüzümüz şu an için yeterince iyi.

SwiftUI ile Core ML’yi Bağlama #

Core ML machine learning işlerini kolaylaştırır. Eğitilmiş bir modelimiz olduğunda, sadece iki satır kodla tahminler elde edebiliriz.

Bizim durumumuzda, XCode’un Create ML uygulamasını kullanarak zaten bir Core ML modeli oluşturduk, bu yüzden bunu kullanacağız. Oluşturduğunuz modeli kaydettiğiniz yerden alarak Xcode’daki proje gezginine sürükleyelim.

XCode’a bir .mlmodel dosyası eklediğimizde, otomatik olarak aynı isimde bir Swift sınıfı oluşturacaktır. Sınıfı göremeyiz ve görmemiz de gerekmez, derleme işleminin bir parçası olarak otomatik olarak oluşturulur. Bununla birlikte, model dosyamız garip bir şekilde adlandırılmışsa, otomatik olarak oluşturulan sınıf adının da garip bir şekilde adlandırlacağı anlamına gelir.

Model dosyamızın adı ne olursa olsun SleepCalculator.mlmodel olarak adlandıralım, böylece otomatik oluşturulan sınıf adı SleepCalculator olur.

Bunun sınıf adı olduğundan nasıl emin olabiliriz? Model dosyamızın kendisini seçtiğimizde Xcode bize dosya bilgilerini gösterecektir.

ML Model Class Name

Birazdan calculateBedtime() methodunu doldurmaya başlayacağız, ancak buna başlamadan önce CoreML kütüphanesini projemize dahil etmeliyiz.

Bu yüzden, ContentView.swift dosyasının en üstüne gidin ve SwiftUI için import satırının öncesine bunu ekleyin.

import CoreML

CoreML yi SwiftUI’den önce eklemek için bir zorunluluğumuz yok sadece alfabetik olarak sıralısı olması açısından böyle yaptık.

calculateBedTime() methodunda ilk olarak SleepCalculator sınıfından bir instance oluşturalım;

do {
    let config = MLModelConfiguration()
    let model = try SleepCalculator(configuration: config)

    // more code here
    // daha fazla kod
} catch {
    // something went wrong!
    // birşeyler ters gitti!
}

Bu model instance, tüm verilemizi okuyan ve tahmin çıktısı verecek olan şeydir. Yapılandırma (configuration), oldukça belirsiz olan birkaç seçeneği etkileştirme ihtiyacı için bulunmaktadır. Açıkçası doğrudan machine learning alanında çalışmıyorsanız bu seçeneklere pek fazla ihtiyaç duymayacaksınız.

do / catch bloklarına odaklanalım, çünkü CoreML kullanmak iki yerde hata verebilir. Modelin yüklenmesi esnasında ve tahminleri istediğimizde.

Modelimizi aşağıdaki alanları içeren bir CSV dosyası ile eğittik:

  • “wake”: kullanıcının ne zaman uyanmak istediği. Bu gece yarısından itibaren saniye sayısı olarak ifade edilir. Bu nedenle sabah 8:00 , 8x60x60 = 28800 olur.
  • “estimatedSleep”: kullanıcının kabaca ne kadar uyumak istediği, çeyreklik artışlarla 4’ten 12’ye kadar değerler olarak saklanır.
  • “coffee”: kullanıcının günde kaç fincan kahve içtiği.

Dolayısıyla modelimizden bir tahmin elde etmek için bu değerleri doldurmamız gerekir.

Daha önce oluşturduğumuz property’lerden sleepAmount ve coffeeAmount yeterince iyi sadece coffeeAmount ’ı tamsayıya dönüştürmeliyiz.

Ancak uyanma zamanını bulmak daha fazla düşünmeyi gerektirir, çünkü wakeUp property saniye sayısını temsil eden bir Double değil, bir Date ’dir. Bu noktada Swift’in DateComponents devereye giriyor. Bir tarihi temsil etmek için gereken tüm parçaları ayrı değerler olarak depoluyor, yani saat ve dakika bileşenlerini okuyabilir ve geri kalanını göz ardı edebiliriz. Daha sonra tek yapmamız gereken dakikayı 60 ile çarpmak ve saati 60x60 ile çarpmak.

Aşağıdaki method çağrısı ile Date ’den DateComponents alabiliriz : Calendar.current.dateComponenets() . Daha sonra saat ve dakika bileşenlerini talep edebilir ve uyanma tarihimizi iletebiliriz. Geri dönen DateComponents instance’ı optional veriler barındırdığından, unwrap işlemi gerçekleştireceğiz.

Bu sebeple calculateBedtime() içindeki // more code here yorumunun yerine aşağıdakini yazalım;

let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUp)
let hour = (components.hour ?? 0) * 60 * 60
let minute = (components.minute ?? 0) * 60

Bu kod, hour veya minute okunamazsa 0 kullanır, ancak gerçekte bu asla gerçekleşmeyecektir.

Bir sonraki adım, değerlerimizi Core ML’ye beslemek ve ne çıkacağını görmektir. Bu, modelimizin prediction() methodu kullanılarak yapılır. Bu method, bir tahminde bulunmak için gereken uyanma süresi, tahmini uyku ve kahve miktarı değerlerini ister ve bunların bazıları Int64 bazıları da Double değerleri olarak sağlanır. hour ve minute saniye olarak hesapladık, bu yüzden göndermeden önce bunları toplayacağız.

Aşağıdakini, önceki kodun hemen altına ekleyelim.

let prediction = try model.prediction(wake: Int64(hour+minute), estimatedSleep: sleepAmount, coffee: Int64(coffeeAmount))

// more code here

prediction artık kullanıcıların ne kadar uykuya ihtiyaç duyduklarını içeriyor. Bu Core ML algoritması tarafından dinamik olarak hesaplanan bir değerdir.

Ancak bu kullanıcılar için yararlı bir değer değildir çünkü saniye cinsinden bir değer olacaktır. İstediğimiz şey, bunu yatmaları gereken saate dönüştürmektir. Bu da saniye cinsinden olan değeri uyanmaları gereken saatten çıkarmamız gerektiği anlamına gelir.

Apple’ın güçlü API’leri sayesinde bu sadece bir satırlık koddur. Bir Date ’den saniye cinsinden değer çıkabilir ve yeni bir Date elde edebiliriz. Bu durumda aşağıdaki kodu ekleyebiliriz.

let sleepTime = wakeUp - prediction.actualSleep

Artık tam olarak ne zaman uyumaları gerektiğini biliyoruz. Son görevimiz bunu kullanıcıya göstermek. Bunu bir uyarı ile yapacağız.

Uyarıyı göstermek içini uyarı başlığı, mesajı ve gösterilip gösterilmeyeceğini belirleyen üç property ekleyerek başlayalım;

@State private var alertTitle = ""
@State private var alertMessage = ""
@State private var showingAlert = false

Bu değerleri hemen calculateBedtime() içinde kullanabiliriz. Hesaplamamız yanlış giderse yani bir tahmini okumak hata verirse // something went wrong yorumunu yararlı bir hata mesajı oluşturan bazı kodlarla değiştirebiliriz.

alertTitle = "Error"
alertMessage = "Sorry, there was a problem calculating your bedtime."

Tahminin işe yarayıp yaramadığına bakılmaksızın uyarıyı göstermeliyiz. Tahminin sonucunu veya hata mesajını içerebilir. Bu yüzden aşağıdaki kodu calculateBedtime() fonksiyonunun sonuna, yani catch bloğunun hemen sonrasına koyun;

showingAlert = true

Tahmin işe yaradıysa yatağa gitmeleri gereken zamanı içeren sleepTime adlı bir sabit oluştururuz. Ancak bu, düzgün bir şekilde biçimlendirilmiş bir string yerine bir Date olduğundan, insan tarafından okunabilir olduğundan emin olmak için formatted() methodundan geçireceğiz ve ardından alertMessage ’a atayacağız.

Bu sebeple, bu son kod satırlarını calculateBedtime() methoduna, sleepTime sabitini ayarladığımız yerden hemen sonra yerleştirin;

alertTitle = "Your ideal bedtime is…"
alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)

Uygulamanın bu aşamasını tamamlamak için, showingAlert true olduğunda alertTitle ve alertMessage öğelerini gösteren bir alert() modifier eklememiz yeterlidir.

Aşağıdaki modifier’ı VStack ’e ekleyelim;

.alert(alertTitle, isPresented: $showingAlert) {
    Button("OK") { }
} message: {
    Text(alertMessage)
}

Uygulamamız her ne kadar iyi gözükmese de şu an çalışıyor.

Kullanıcı Arayüzü Geliştirmeleri #

Uygulamamız şu anda çalışıyor olsa da App Store’a göndermek isteyebileceğimiz bir durumda değil. Ayrıca en azından bir tane kullanılabilirlik sorunu var.

Önce kullanılabilirlik sorununa bakalım. Date.now öğesini okuduğumuzda, otomatik olarak geçerli tarih ve saate ayarlanır. Dolayısıyla, wakeUp property’imizi oluşturduğumuzda varsayılan uyandırma saati şu anda saat kaçsa o olacaktır.

Uygulamanın her türlü zamanı idare edebilmesi gerekse de sabah 6 ile 8 arasında bir yerde varsayılan bir uyanma saatinin, kullanıcıların büyük çoğunluğu için daha yararlı olacağını söyleyebiliriz.

Bunu düzletmek için ContentView struct’a, geçerli günün sabah 7’sini referans alan bir Tarih değeri içeren computed property ekleyeceğiz. Yeni DateComponents bileşenlerimizi oluşturabilir ve bu bileşenleri tam bir tarihe dönüştürmek için Calendar.current.date(from:) öğesini kullanabiliriz.

Şimdi bu property’yi ContentView ’e ekleyelim;

var defaultWakeTime: Date {
    var components = DateComponents()
    components.hour = 7
    components.minute = 0
    return Calendar.current.date(from: components) ?? .now
}

Yeni oluşturduğumuz computed property’yi varsayılan wakeUp değeri için Date.now yerine kullanabiliriz;

@State private var wakeUp = defaultWakeTime

Bu kodu derlemeye çalışırsanız başarısız olduğunu göreceksiniz. Bunun nedeni bir property’ye diğerinin içinden erişiyor olmamızdır. Swift property’lerin hangi sırada oluşturulacağını bilmez, bu nedenle buna izin verilmez.

Buradaki çözüm basit: defaultWakeTime ’ı static bir değişken yapabiliriz, yani bu struct’ın tek bir instance’ı yerine ContentView struct’ın kendisine aittir. Bu da defaultWakeTime ’ın istediğimiz zaman okunabileceği anlamına gelir, çünkü başka herhangi bir property’nin varlığına bağlı değildir.

Bu yüzden property tanımlamasını şu şekilde değiştirelim;

static var defaultWakeTime: Date {

Bu, kullanılabilirlik sorunumuzu çözer, çünkü kullanıcıların çoğu varsayılan uyanma saatinin seçmek istediklerine yakın olduğunu görecektir.

Şimdi biraz kullanıcı arayüzünü değiştirelim. VStack yerine bir Form kullanalım. Bu yüzden aşağıdaki kodu bulun;

NavigationStack {
    VStack {

ve bununla değiştirin;

NavigationStack {
    Form {

Formumuzda her view listede bir satır olarak değerlendiriliyor, oysa daha iyi bir görünüm elde edebiliriz. Burada şunu yapacağız; her bir text view ve control (DatePicker veya Stepper) çiftini VStack ile saracağız, böylece her bir çift, tek bir satır olarak görülecek.

Devam edelim ve alignment için .leading ve spacing için 0 kullanarak her bir çifti VStack ile saralım. body şu şekilde olmalıdır;

var body: some View {
        NavigationStack {
            Form {
                VStack(alignment: .leading, spacing: 0) {
                    Text("When do you want to wake up?")
                        .font(.headline)
                    
                    DatePicker("Please enter a time", selection: $wakeUp, displayedComponents: .hourAndMinute)
                        .labelsHidden()
                }
                
                VStack(alignment:.leading, spacing: 0){
                    Text("Desired amount of sleep")
                        .font(.headline)
                    
                    Stepper("\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 4...12, step: 0.25)
                }
                
                VStack(alignment: .leading, spacing: 0) {
                    Text("Daily coffee intake")
                        .font(.headline)
                    
                    Stepper("\(coffeeAmount) cup(s)", value: $coffeeAmount, in: 1...20)
                }
                
            }
            .navigationTitle("BetterRest")
            .toolbar {
                Button("Calculate", action: calculateBedtime)
            }
            .alert(alertTitle, isPresented: $showingAlert) {
                Button("OK") {}
            } message: {
                Text(alertMessage)
            }
        }

    }

New UI

Yapacağımız son değişiklik küçük ama sihirli. Kullanıcının kaç fincan kahve içitiğini gösteren bu koda tekrar bir göz atın;

Stepper("\(coffeeAmount) cup(s)", value: $coffeeAmount, in: 1...20)

“cup(s)” yazmak işe yarıyor, ancak biraz tembelce. İdeal olarak “1 cup”, “2 cups” vb. göstermeliyiz.

Bunu aşağıdaki gibi ternary operator kullanarak düzeltebiliriz.

Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount) cup(s)", value: $coffeeAmount, in: 1...20)

Ancak SwiftUI’nin daha iyi bir çözümü var. Çoğul kullanımı bizim için halledebilir. Kodumuzu şu şekilde değiştirelim;

Stepper("^[\(coffeeAmount) cup](inflect: true)", value: $coffeeAmount, in: 1...20)

Bu garip syntax, aslında yaygın bir metin biçimi olan Markdown’un özel bir biçimidir. Bu syntax SwiftUI’ye “cup” kelimesinin coffeeAmount değişkeninde ne varsa onunla eşleşecek şekilde çekilmesi gerektiğini söyler. Bu durumda otomatik olarak “cup” kelimesinden “cups” kelimesine uygun şekilde dönüşecektir.


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

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