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

17.Gün - SwiftUI Temelleri Proje-1 Bölüm-2

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ü;

  1. Text Field’ın checkAmount property’ye two-way binding’i vardır.
  2. checkAmount property, değerindeki değişiklikleri izleyen @State ile işaretlenmiştir.
  3. Bir @State property değiştiğinde SwiftUI body property’yi yeniden çağıracaktır (yani kullanıcı arayüzünü yeniden yükleyecektir.)
  4. 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

picker default view

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)
}

section as text

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 ve tipSelection 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;

  1. .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)
  2. 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.
  3. 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.

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