17.Gün - SwiftUI Temelleri Proje-1 Bölüm-2
Table of Contents
Bu bölümde, hesap bölüşme uygulamamız olan WeSplit’e oluşturmaya başlıyoruz. Bu bölüm ile uygulamamızda, TextField ile metin okuma, Form içinde Picker oluşturma, SwiftUI Segmented Control ve klavye gizlemenin nasıl yapılacağını inceleyeceğiz.
Bu proje aynı zamanda GitHub’da da bulunmaktadır.
GitHub - GorkemGuray/WeSplit: 100 Days of SwiftUI - Project-1
TextField ile Kullanıcıdan Metin Okuma #
Kullanıcıların uygulamamıza veri olarak girmesini beklediğimiz property’leri @State
olarak işaretliyoruz.
ContentView
’e bu üç property’yi ekleyerek başlayalım;
@State private var checkAmount = 0.0
@State private var numberOfPeople = 2
@State private var tipPercentage = 20
Bu property’lere mantıklı varsayılan değerler atadık.
Fakat bahşiş oranı kullanıcı tarafından farklı bir yüzde de seçilebilir, bu sebeple olası seçenekleri bir Array içerisinde belirleyelim ve diğer üç property’nin altına ekleyelim.
let tipPercentages = [10, 15, 20, 25, 0]
Kullanıcıların, hesabın miktarını girebilecekleri bir alanı oluşturmak ile başlayalım. Fakat aşağıdaki kodu yazdığınızda çalışmadığını göreceksiniz.
body
property’sini aşağıdaki gibi değiştirelim;
Form {
Section {
TextField("Amount", text: $checkAmount)
}
}
Yukarıdaki kod çalışmayacak çünkü TextField girdileri metin olarak ele alır ve bir String’e atama eğilimindedir. TextField’den string olarak aldığımız ifadeyi, üzerinde çalışabileceğimiz sayıya dönüştürmeliyiz.
Fakat bu işlemi daha iyi bir yolla yapabiliriz. Double
değerimizi TextField
’a aktarabilir ve girdiyi para birimi olarak ele almasını isteyebiliriz. Burada parametre olarak text
yerine value
kullandığımıza dikkat edin. Eğer value
parametresini kullanıyorsak, format
’ı da beraberinde kullanmalıyız.
TextField("Amount", value: $checkAmount, format: .currency(code: "USD"))
Bu gerçekten iyi duruyor, fakat bir problemimiz var. Bütün kullanıcılar USD para birimini mi kullanıyor? Burada iOS’a kullanıcının para birimini verip veremeyeceğini sorabiliriz.
.currency(code: Locale.current.currency?.identifier ?? "USD"))
Locale
, kullanıcının tüm bölgesel ayarlarını (hangi takvim, metrik sistem mi vb.) saklamaktan sorumlu olan iOS’ta yerleşik bulunan devasa bir struct’tır. Bizim durumumuzda, kullanıcının tercih ettiği para biriminin kodu olup olmadığını soruyoruz ve eğer yoksa “USD” geri döneceğiz.
TextField
’da ilk parametre placeholder olarak adlandırılan bir stringdir. Placeholder TextField içinde gösterilen gri metindir ve kullanıcılara orada ne olması gerektiği hakkında fikir verir. İkinci parametre checkAmount
property two-way bindings’tir, yani kullanıcı yazdıkça bu property güncellenecektir. Buradaki üçüncü parametre, metnin biçimlendirme şeklini kontrol eden ve onu bir para birimi haline getiren parametredir.
@State
property wrapper’ın en güzel yanlarından biri, değişiklikleri otomatik olarak izlemesi ve bir şey olduğunda body
property’yi otomatik olarak yeniden çağırmasıdır. Böylelikle değişen state ‘i yansıtmak için kullanıcı arayüzü yeniden yüklenir. Bu aslında SwiftUI’nin çalışma şeklinin temel bir özelliğidir.
Bunu göstermek için, checkAmount
değerini gösteren text view’ı yeni bir bölümde tekrar ekleyelim.
Form {
Section {
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
}
Section {
Text(checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
}
}
Bu uygulamayı simülatörde çalıştırın. İlk section’da bulunan Text Field’a bir değer girdiğinizde ikinci section’da bulunan text view’ın anında değişikliği yansıttığını göreceksiniz.
Bu senkronizasyon gerçekleşir çünkü;
- Text Field’ın
checkAmount
property’ye two-way binding’i vardır. checkAmount
property, değerindeki değişiklikleri izleyen@State
ile işaretlenmiştir.- Bir
@State
property değiştiğinde SwiftUIbody
property’yi yeniden çağıracaktır (yani kullanıcı arayüzünü yeniden yükleyecektir.) - Bu sebeple text view
checkAmount
’un güncellenmiş değerini alacaktır.
Projeyi simülatörde çalıştırdığınızda, sadece sayı girilmesini beklediğimiz alanda alfabetik bir klavye gözüküyor.
Bu sorunun üstesinden gelmek için bir modifier kullanacağız. Text Field’larda farklı klavyelere zorlayan keyboardType()
modifier’ı bulunmaktadır. Buna istediğimiz klavye türünü belirten bir parametre verebiliriz bu örnekte .numberPad
veya .decimalPad
kullanabiliriz. Bu klavyelerin her ikisi de nümerik klavye gösterir fakat .decimalPad
ondalıklı sayıların girilmesine de müsade eder. Bu bizim örneğimiz için daha uygundur. Bu durumda kodumuzu şu şekilde değiştirelim;
TextField("Amount", value: $checkAmount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
.keyboardType(.decimalPad)
.numberPad
ve.decimalPad
klavye türleri SwiftUI’ye 0 ile 9 arasındaki rakamları ve ondalık sayıyı göstermesini söyler, ancak bu kullanıcıların diğer değerleri girmesini engellemez. Örneğin, bir donanım klavyesi ile istediklerini yazabilirler. Yine de sorun değil Return tuşuna bastıklarında Text Field kötü değerleri otomatik olarak filtreleyecektir.
Form’da Picker Oluşturma #
Uygulamamızda hesabın ne kadar olduğunu belileyen bir formumuz var. Biz buna hesabı kaç kişinin ödeyeceğini belirlemek amacıyla Picker
ekleyeceğiz.
TextField’lar gibi Picker
two-way binding ile bağlanmalıdır. Bu amaçla numberOfPeople
property’sini @State
olarak işaretledik.
Kodumuzu şu şekilde değiştirdik;
Section {
TextField("Amount", value: $checkAmount, format: Locale.current.currency?.identifier ?? "USD"))
.keyboardType(.decimalPad)
Picker("Number of people", selection: $numberOfPeople) {
ForEach(2..<100) {
Text("\($0) people")
}
}
}
Bunu simülatörde çalıştırdığımızda aşağıdaki gibi bir görüntü elde edeceğiz
Burada dikkatimizi bir şey çekmiş olabilir, biz numberOfPeople
property’ye varsayılan değer olarak 2 vermiştik, fakat picker’da 4 gözüküyor. Bu aslında bir hata değildir. Üzerinde biraz düşünelim; picker’ı ForEach
kullanarak şu şekilde oluşturuyoruz;
ForEach(2 ..< 100) {
Bu 2’den 100’e kadar sayar ve satır oluşturur. Fakat bizim 0. satırımızda “2 people” yazmaktadır, biz varsayılan değer olarak 2 verdiğimizde picker’ın 3.satırını [0,1,2…]
kastetmiş oluyoruz. 3. satırda da “4 people” yazdığı için, picker’ın varsayılan görüntüsü bu şekilde olmaktadır.
Picker’ın birçok alternatif stili bulunmaktadır. Burada popüler bir picker stili olan .pickerStyle(.navigationLink)
’i deneyeceğiz. Bu Picker’ın seçeneklerini yeni bir sayfada açacak.
Picker("Number of people", selection: $numberOfPeople) {
ForEach(2 ..< 100) {
Text("\($0) people")
}
}
.pickerStyle(.navigationLink)
Fakat bunu yaptığımızda, picker’ın seçilemez ve gri renkte olduğunu göreceğiz. Çünkü bu modifier, seçenekleri yeni bir sayfada göstermek üzere tasarlanmıştır, fakat bizim uygulamamızda NavigationStack
bulunmadığından, picker yeni bir sayfa açamamakta, dolayısıyla pasif olmaktadır.
Bunun için kodumuza NavigationStack
’i aşağıdaki gibi ekleyebiliriz;
var body: some View {
NavigationStack {
Form {
// Form'un içindeki herşey.
}
}
}
Yukarıdaki değişikliği yaptığımızda, picker seçeneklerinin otomatik yeni bir sayfada gösterildiğini ve varsayılan değerin seçili olduğunu göreceğiz.
Burada gördüğümüz declarative user interface olarak adlandırılan şey sayesinde gerçekleşmiştir. Bu nasıl yapmamız gerektiğini söylemek yerine, ne istediğimizi söylememiz anlamına gelir. İçinde bazı değerler olan bir navigasyon bağlantısı picker’ı istediğimizi söyledik, ancak “tüm öğelerin listesini oluştur, hangisi seçiliyse ona bir onay işareti göster” dememize gerek kalmadı.
Son olarak navigation bar’a bir başlık ekleyebiliriz. Aşağıdaki modifier’ı kullanabiliriz;
.navigationTitle("WeSplit")
SwiftUI Segmented Control #
Uygulamamız da bahşişi yüzdesel olarak beliyeceğiz ve bu belirleme işleminde Segmented Control kullancağız. Uygulamamızdaki Section’un ardından bir section daha ekleyelim;
Section {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(tipPercentages, id: \.self) {
Text($0, format: .percent)
}
}
}
Yukarıdaki kod, tipPercentages
Array’i üzerinde döngü yaparak, her birini .percent
formatında (%) bir metin görünümüne dönüştürecektir ve bunu açılır bir menü olarak bize sunacaktır.
Fakat biz burada segmented control olarak kullanmak istiyoruz. Bu sebeple şu kodu ekleyelim;
.pickerStyle(.segmented)
Evet bu şekilde iyi bir durumda fakat, uygulamamıza kullanıcı gözünden baktığımızda, yüzdelere bir açıklama yazarsak daha hoş duracaktır.
Section {
Text("How much tip do you want to leave?")
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(tipPercentages, id: \.self) {
Text($0, format: .percent)
}
}
.pickerStyle(.segmented)
}
Evet güzel bir fikir olmasına rağmen, başlığımız form’un bir öğesi gibi gözüküyor. Bunun daha iyisini yapabiliriz, SwiftUI bir bölümün üst ve altbilgisine görünümler eklememize izin verir. Kodumuzu şu şekilde revize edebiliriz;
Section("How much tip do you want to leave?") {
Picker("Tip percentage", selection: $tipPercentage) {
ForEach(tipPercentages, id: \.self) {
Text($0, format: .percent)
}
}
.pickerStyle(.segmented)
}
Kişi başı Toplam Tutarın Hesaplanması #
Kişi başına düşen tutarı hesaplayabilmek için computed property kullanabiliriz.
Bu sebeple, totalPerPerson
adında ve Double
türünde bir computed variable oluşturacağız.
body
property’sinden önce totalPerPerson
‘i ekleyebiliriz.
var totalPerPerson: Double {
// calculate the total per person here
return 0
}
Ardından, numberOfPeople
değerini okuyarak ve buna 2 ekleyerek kaç kişi olduğunu bulabiliriz. Bu kısımda aralığımız 2’den 100’e olduğundan fakat biz 0’dan itibaren saydığımızdan dolayı aldığımız değere 2 eklemeliyiz.
// calculate the total per person here
kısmını değiştirmeye başlayalım;
let peopleCount = Double(numberOfPeople + 2)
Elde ettiğimiz değeri Double
’a dönüştürdüğümüzü fark etmişsinizdir, çünkü checkAmount
ile birlikte kullanılacaktır.
Aynı sebeple tipPercentage
’ı da Double
’a dönüştürelim;
let tipSelection = Double(tipPercentage)
Uygulamamızdaki hesaplamayı 3 adımda gerçekleştireceğiz.
- Bahşiş değerini
checkAmount
’u 100’e bölerek vetipSelection
ile çarparak hesaplayabiliriz. - Bahşiş değerini
checkAmount
’a ekleyerek hesabın bahşiş dahil genel toplamını hesaplayabiliriz. - Genel toplamı
peopleCount
’a bölerek, kişi başına düşen miktarı bulabiliriz.
Son olarak return 0
’ı aşağıdaki gibi değiştirebiliriz.
let tipValue = checkAmount / 100 * tipSelection
let grandTotal = checkAmount + tipValue
let amountPerPerson = grandTotal / peopleCount
return amountPerPerson
Artık totalPerPerson
bize doğru değeri verdiğine göre, Formumuza son section’ını da ekleyebiliriz.
Section {
Text(totalPerPerson, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
}
SwiftUI Klavye Gizleme #
Projeyi neredeyse tamamladık fakat sinir bozucu bir durum farketmiş olabilirsiniz, klavye asla kaybolmuyor. Fakat birkaç ekleme ile bu durumun önüne geçebiliriz;
İlk olarak @FocusState
property wrapper’ı kullanmalıyız. Bu kullanıcı arayüzünde input focus’ı işlemek için tasarlanmış olması dışında nomral bir @State
özelliği gibidir.
Yeni property’yi ContentView
’e ekleyelim;
@FocusState private var amountIsFocused: Bool
Şimdi bunu Text Field’a ekleyebiliriz, böylece metin alanı odaklandığında amaountIsFocused
true, aksi takdirde false olur. Bu modifier’ı TextField
’e ekleyelim;
.focused($amountIsFocused)
İkinci olarak, text field aktif olduğunda, toolbara bir buton eklemeliyiz.
.navigationTitle()
modifier’ının altına şu kodu ekleyelim;
.toolbar {
if amountIsFocused {
Button("Done") {
amountIsFocused = false
}
}
}
Bu kodu bir miktar inceleyelim;
.toolbar()
modifier’ı, bir view için toolbar öğesi belirtmemizi sağlar. Bu toolbar öğeleri ekranda çeşitli yerlerde görünebilir ( üstteki navigation bar veya alttaki özel alan gibi)- Koşul,
amountIsFocused
değişkeninin o anda true olup olmadığını kontrol eder, böylece butonu yalnızca Text Field etkin olduğunda gösteriririz. - Burada kullandığımız
Button
view, adı “Done” olan bazı dokunulabilir metinleri gösterir. Ayrıca her butona basıldığında çalışacak bazı kodları içerir.
Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.