27.Gün - SwiftUI BetterRest Uygulaması
Table of Contents
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.
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)
}
}
}
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.