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

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

Proje-1 boyunca, SwiftUI’nin temellerine bir giriş yapacağız. Proje boyunca WeSplit isimli uygulamayı oluşturacağız. Bu uygulama hesabı ortak olarak ödemek maksadıyla hesabı kişi sayısına bölmekte ve bahşişi hesaplamaktadır. Bugünkü yazımızda Form , NavigationStack ve @State kavramlarını inceleyeceğiz.

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

GitHub - GorkemGuray/WeSplit: 100 Days of SwiftUI - Project-1

Xcode Yeni SwiftUI Projesi Nasıl Oluşturulur #

Xcode’u başlatın ve ardından “Create New Project” seçeneğini seçin.

xcode create new project

Burada karşımıza bazı seçenekler listesi çıkacak buradan önce iOS’u ardından App kısmını seçerek Next butonuna basıyoruz.

choose ios and app click next

Karşımıza aşağıdaki gibi bir ekran çıkacak;

project settings

Bu ekranda yapmamız gerekenler;

  • Product Name için “WeSplit” yazın.
  • Organization Identifier için istediğinizi girebilirsiniz, ancak bir web siteniz varsa bileşenleri ters çevrilmiş olarak girmelisiniz “gorkem.co” , “co.gorkem” olacaktır. Eğer bir alan adınız yoksa, bu kısma “me.soyad.isim” şeklinde de giriş yapabilirsiniz.
  • Interface için SwiftUI’ı seçin.
  • Language için Swift’i seçin.
  • Storage için None seçeneğini seçin.
  • Alttaki tüm onay kutularının işaretli olmadığından emin olun.

Bir SwiftUI Uygulamasının Temel Yapısı #

Xcode’da soldaki bölüm project navigator olarak adlandırılmaktadır. Burada göreceğimiz dosyalar;

  • WeSplitApp.swift : Uygulamayı başlatacak (launch) kodu içerir. Eğer uygulama başlatıldığında bir şey oluşturup ve uygulama çalıştığı süre boyunca bunu canlı tutmak istersek buraya koymamız gerekir.
  • ContentView.swift : Uygulamamız için ilk kullanıcı arayüzünü (User Interface-UI) içerir ve bu projede tüm işi yapacağımız yerdir.
  • Asset.xcassets : asset kataloğudur. Asset, uygulamada kullanmak istediğimiz resimlerden oluşan bir koleksiyondur. Buraya renkleri, uygulama simgelerini, iMessage sticker gibi şeyleri ekleyebiliriz.
  • Preview Content : Bu bir gruptur. İçinde Preview Assets.xcassets bulunur. Bu da başka bir asset kataloğudur. Fakat bu sefer kullanıcı arayüzlerini tasarlarken kullanmak istediğimiz örnek resimler içindir, uygulama çalışırken nasıl görünebilecekleri hakkında bir fikir verir.

Not : Project navigator ‘de bulunan dosyalarınızın uzantılarını görmüyorsanız, Xcode > Settings > General sekmesinde bulunan File Extension seçeneğini kontrol edin.

xcode project navigator file extension

Bu proje için tüm çalışmalarımız Xcode’un bizim için oluşturduğu ContentView.swift dosyasında gerçekleşecek. Xcode’un bu dosyada oluşturduğu kod aşağıdadır;

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

Kendi kodumuzu yazmaya başlamadan önce tüm bunların ne işe yaradığını gözden geçirelim.

  1. import SwiftUI : Swift’e SwiftUI Framework tarafından bize verilen tüm işlevleri kullanmak istediğimizi söyler. Apple bize birçok framework sağlamaktadır; machine learning, ses oynatma, görüntü işleme ve daha bir sürü şey. Bu sebeple programımızın her şeyi kullanmak istediğini varsaymak yerine, hangi parçaları kullanmak istediğimizi söyleriz, böylece sadece o kısımlar yüklenebilir.

  2. struct ContentView: View ContentView adında yeni bir struct oluşturur ve bunun View protocol’e uygun olduğunu söyler. View , yukarıda import ettiğimiz SwfitUI’dan gelmektedir ve ekranda çizmek istediğimiz her şey tarafından benimsenmesi gereken temel protocol’dür.

  3. var body: some View body adında yeni bir computed property(hesaplanmış değişken) tanımlar ve bu property ilginç bir türe sahiptir : some View . Bu bizim layout’umuzun View protocol’e uygun bir şey döndüreceği anlamına gelir. Arka planda layout’umuzdaki herşeye bağlı olan karmaşık bir veri türünün döndürülmesiyle sonuçlanacaktır ancak some View bu konuda bütün işi devralarak gerisini halleder. Bakınız: Opaque Return Type

    View protocol’ün tek bir gereksinimi vardır o da some View döndüren body adında bir computed variable’a sahip olmamızdır. view struct’larımıza daha fazla property ve method ekleyebiliriz tabiki, ancak body protocol tarafından zorunlu tutulan tek şeydir.

  4. VStack ve içindeki kod altında “Hello, world!” metni bulunan bir küre görüntüsü gösterir. Bu küre görüntüsü Apple’ın SF Symbols icon setinden gelmektedir. Text view’lar ekrana çizilen basit statik metin parçalarıdır ve gerektiğinde otomatik olarak birden fazla satır olabilirler.

  5. imageScale()foregroundStyle() ve padding() görüntü ve VStack üzerinde çağrılan methodlardır. SwiftUI’ın modifier olarak adlandırdığı bu methodların küçük bir farkı bulunmaktadır: her zaman hem orijinal verilerimizi hem de istediğimiz ekstra değişikliği içeren yeni bir view döndürürler.

ContentView struct’ın altında, içinde ContentView() olan #Preview ’ı göreceksiniz. Bu aslında App Store’a giden uygulamamızın bir parçası olmayacak, bunun yerine özellikle Xcode’un oluşturduğumuz kullanıcı arayüzünün önizlemesini (preview) gösterebilmesi için özel bir kod parçasıdır.

Bu önizlemeler, genellikle kodumuzun sağında görünen canvas adı verilen Xcode özelliğini kullanır.İstersek önizleme kodunu özelleştirebiliriz, bu yalnızca canvas’ı etkiler çalıştırılan gerçek uygulamayı değiştirmez.

Önemli : Eğer Canvas’ı göremiyorsanız, Xcode penceresindeki Editor menüsünden Canvas’ı seçebilirsiniz.

Canvas open close

Çoğu zaman kodumuzdaki bir hatanın Xcode canvas güncellemesini durduğunu göreceğiz. Bunu düzeltmek için refresh butonunu ya da Option+Cmd+P klavye kısayolunu kullanabiliriz.

SwiftUI Form Oluşturma #

Pek çok uygulama, kullanıcılarının bir tür girdi (input) girmesini gerektirir. SwiftUI bize bunun için Form adında özel bir view sunar. Formlar, metin ve resimler gibi statik kontrollerin scroll listeleridir. Ancak text field, toggle switch, button vb. gibi kullanıcı etkileşimli kontrolleri de içerebilir.

Bir text view’ı aşağıdaki gibi Form ’un içine koyarak temel bir Form oluşturabiliriz.

var body: some View {
    Form {
        Text("Hello, world!")
    }
}

Xcode canvasına baktığımızda oldukça değiştiğini görebiliriz;

Xcode Form

Burada, tıpkı Ayarlar uygulamasında göreceğimiz gibi bir liste başlangıcı görmekteyiz. Daha da fazla satır ekleyebiliriz;

Form {
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
}

Hatta, bir formun içine istediğimiz kadar çok şey bulundurabiliriz, Örneğin bu kod on satırlık bir liste gösterecektir.

Form {
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
    Text("Hello, world!")
}

Xcode multi element form

Eğer Formumuzu tıpkı Ayarlar uygulamasında olduğu gibi görsel parçalara bölmek istiyorsak, Section ’ı kullanabiliriz.

Form {
    Section {
        Text("Hello, world!")
    }

    Section {
        Text("Hello, world!")
        Text("Hello, world!")
    }
}

xcode form section

Bir formu ne zaman bölümlere ayırmamız gerektiği ile ilgili kesin ve katı bir kural yoktur. Sadece ilgili öğeleri görsel olarak gruplandırmak için vardır.

SwiftUI Navigation Bar Ekleme #

iOS üstteki sistem saatinin ve alttaki ana ekran göstergesinin altı da dahil olmak üzere ekranın herhangi bir yerine içerik yeleştirmemize izin verir. Fakat bu alanların hepsini kullanırsak hoş olmayan görünümler ortaya çıkabilir. Bu sebeple SwiftUI varsayılan olarak bileşenlerin sistem kullanıcı arayüzü veya cihazın yuvarlatılmış köşeleri tarafından kapatılmayacakları bir alana yerleştirilmesini sağlıyor. Bu alan safe area olarak isimlendirilmektedir.

ios vs android app layout

iPhone 15’te safe area dinamik adanın hemen altından ana ekran göstergesinin hemen üstüne kadar olan alanı kapsıyor. Bunun gibi bir kod ile bunu rahatlıkla görebiliriz.

Bu kodu iOS simülatörde çalıştırılalım. Simülatörü çalıştırmak için Xcode penceresinin sol üstünde bulunan play tuşuna basabilir veya Cmd+R kısayolunu kullanabiliriz.

Formun dinamik adanın altında başladığını göreceksiniz, bu nedenle varsayılan olarak formumuzdaki satır tamamen görünür durumdadır. Formlar kaydırılabilir (scroll), simülatörde formu yukarı doğru kaydırırsak, satırı saatin altına girecek şekilde yukarı taşıyabiliriz, ki bu da istemediğimiz bir durumdur. Çünkü her ikisinin de okunmasını zorlaştıracaktır.

Bunu düzeltmenin yaygın yolu, ekranın üst kısmına bir navigation bar eklemekten geçer. Navigation bar başlıklara ve düğmelere sahip olabilir. Ayrıca SwiftUI’de kullanıcı bir eylem gerçekleştirdiğinde bize yeni görünümler gösterme yeteneği de verir.

Navigation bar’ı şu şekilde ekleyebiliriz.

var body: some View {
    NavigationStack {
        Form {
            Section {
                Text("Hello, world!")
            }
        }
    }
}

Yukarıdakş kodu yazdığımızda bir önceki ile birebir aynı gözükecektir. Fakat genellikle navigation bar’larda başlık’da kullanırız. Bu başlığı (title) modifier yardımıyla ekleyebiliriz.

NavigationStack {
    Form {
        Section {
            Text("Hello, world!")
        }
    }
    .navigationTitle("SwiftUI")
}

.navigationTitle() modifier’ını form’a eklediğimizde, Swift aslında bir navigation bar ve sağladığımız tüm mevcut içeriklere sahip yeni bir form oluşturur.

large navigation title

Navigation bar’a bir title eklediğimizde, bu title için büyük bir yazı tipi kullanıldığını fark edeceğiz. Başka bir modifier ekleyerek küçük bir yazı tipi elde edebiliriz.

.navigationBarTitleDisplayMode(.inline)

inline navigation title

SwiftUI State nedir? Program State’in Değiştirilmesi #

View’lar state’lerinin bir fonksiyonudur. (Views are a function of their state)

SwiftUI’nin view’larının state’lerinin bir fonksiyonu olduğunu söylediğimizde, kullanıcı arayüzünün nasıl göründüğünün programımızın state’i tarafından belirlendiğini kastediyoruz. Örneğin, kullanıcılar bir text field’a kendi isimlerini girene kadar Devam butonuna dokunamazlar.

Bunu, ismi ve dokunulduğunda çalıştırılacak bir action closure’u olan buton ile gösterebiliriz.

struct ContentView: View {
    var tapCount = 0

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

Bu kod oldukça makul görünüyor: “Tap Count” yazan bir buton oluşturun ve düğmeye kaç kez dokunulduğunu belirtin, ardından butona her dokunulduğunda tapCount değişkenine 1 ekleyin.

Ancak bu kod Xcode tarafından derlenmeyecektir. Gördüğünüz gibi ContentView sabit olarak oluşturulan bir struct’tır. Eğer struct’lar hakkında öğrendiklerimizi hatırlarsak, bu onun değişmez (immutable) olduğu anlamına gelir (değelerini serbestçe değiştiremeyiz).

Property’lerini değiştirmek isteyen struct methodları oluştururken, mutating keyword’ünü eklememiz gerekir: mutating func doSomeWork() gibi. Ancak Swift mutated computed property yapmamıza izin vermez, bu da mutating var body: some View yazamayacağımız anlamına gelir.

Bu, bir çıkmaza girmişiz gibi görünebilir: Programımız çalışırken değerleri değiştirebilmek istiyoruz, ancak view’lar struct olduğu için Swift bize izin vermiyor.

Neyse ki Swift bize property wrapper adı verilen özel bir çözüm sunuyor: property’lerimizin önüne yerleştirebileceğimiz ve onlara süper güçler kazandıran özel bir nitelik. Bir butona kaç kez dokunulduğu gibi basit program state’lerini depolamak için SwiftUI’den @State adlı bir property wrapper ‘ı aşağıdaki gibi kullanabiliriz.

struct ContentView: View {
    @State var tapCount = 0

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

Bu küçük değişiklik programımızın çalışması için yeterlidir. Şimdi onu oluşturabilir ve derleyebiliriz.

@State struct’ların sınırlamalarını aşmamızı sağlar. Struct’lar sabit (constant) olduğu için property’lerini değiştiremeyeceğimizi biliyoruz, ancak @State bu değerin SwiftUI tarafından değiştirilebilecek bir yerde ayrı olarak saklanmasına izin verir.

İpucu : SwiftUI’da program state’ini saklamanın çeşitli yolları vardır. @State özellikle tek bir view’da depolanan basit property’ler için tasarlanmıştır. Sonuç olarak Apple, bu property’lere aşağıdaki gibi private access control eklememizi önerir.

@State private var tapCount = 0

State’i Kullanıcı Arayüzü Kontrollerine Bağlama (Binding State to User Interface Controls) #

SwiftUI’nin @State property wrapper’ı, view struct’ları özgürce değiştirmemize olanak tanır. Bu da programımız değiştikçe view property’lerini de buna uygun olarak güncelleyebileceğimiz anlamına gelir.

Ancak, kullanıcı arayüzü kontrollerinde işler biraz daha karmaşıktır. Örneğin, kullanıcıların içine yazabileceği düzenlenebilir bir metin kutusu oluşturmak istiyorsak, aşağıdaki gibi bir SwiftUI view’ı oluşturabiliriz.

struct ContentView: View {
    var body: some View {
        Form {
            TextField("Enter your name")
            Text("Hello, world!")
        }
    }
}

Yukarıdaki kod bir text field ve text view oluşturmaya çalışır. Ancak, SwiftUI text field alanındaki metnin nerede saklanacağını bilmek istediği için kod derlenemez.

Unutmayın view’lar state’lerinin bir fonksiyonudur. Bu text field yalnızca programımızda depolanan bir değeri yansıtıyorsa bir şey gösterebilir. SwiftUI’nin istediği şey, text field içinde gösterilebilecek ve kullanıcının text field’a yazdığı her şeyi saklayacak olan struct’daki bir string property’dir.

Şu şekilde bir değişiklik yapabiliriz;

struct ContentView: View {
    var name = ""

    var body: some View {
        Form {
            TextField("Enter your name", text: name)
            Text("Hello, world!")
        }
    }
}

Bu, name property ekler ve ardından bunu text field’ı oluşturmak için kullanır. Ancak kod yine de çalışmayacaktır çünkü Swift’in name property’sini kullanıcının text field’a yazdığı her şeyle eşleşecek şekilde güncelleyebilmesi gerekir. Bu sebeple @State ’i şu şekilde kullanabiliriz.

@State private var name = ""

Ancak bu hala yeterli değil ve kodumuz hala derlenmiyor.

Sorun, Swift’in “bu property’nin değerini burada göster” ile “bu property’nin değerini burada göster, ancak tüm değişiklikleri property’ye geri yaz” arasında ayrım yapmasıdır.

Text Field söz konusu olduğunda, Swift’in metinde bulunan her şeyin name property’de de bulunduğundan emin olması gerekir, böylece view’larımızın state’lerinin bir fonksiyonu olduğuna dair sözünü gerçekleştirebilir.

Buna two-way binding denir. text field’ı property’mizin değerini gösterecek şekilde bağlarız (bind), ancak aynı zamanda text field’da yapılan herhangi bir değişikliğin property’yi güncelleyeceği şekilde de bağlarız.

Swift’te bu two-way binding’leri daha görünür olmaları için özel bir sembolle işaretleriz: $

Bu Swift’e property’nin değerini okuması gerektiğini ama aynı zamanda herhangi bir değişiklik olduğunda bunu geri yazması gerektiğini söyler.

Yani struct’ın doğru versiyonu şu şekildedir.

struct ContentView: View {
    @State private var name = ""

    var body: some View {
        Form {
            TextField("Enter your name", text: $name)
            Text("Hello, world!")
        }
    }
}

Devam etmeden önce, text view’ı kullanıcının adını doğrudan text field’ın altında gösterecek şekilde değiştirelim;

Text("Your name is \(name)")

Bunun $name yerine name şeklinde kullanıldığına dikkat ettiniz mi? Bunun nedeni burada two-way binding istememizdir. Değeri okumak istiyoruz evet, ancak bir şekilde geri yazmak istemiyoruz, çünkü bu text view değişmeyecektir.

Bu sebeple, bir property adından önce $ gördüğümüzde, bunun two-way binding olduğunu unutmamalıyız: property’nin değeri okunur ve aynı zamanda yazılır.

Döngü (Loop) içinde View Oluşturma #

Bir döngü içinde birkaç SwiftUI view’ı oluşturmayı istemek yaygındır. Örneğin, bir isim Array’i üzerinde döngü yapmak ve her birinin bir metin görünümü olmasını isteyebiliriz.

SwiftUI bize bu amaç için ForEach adı verilen özel bir view türü sunar Bu Array ve range üzerinde döngü yaparak gerektiği kadar görünüm oluşturabilir.

ForEach üzerinde döndüğü her öğe için bir kez closure çalıştırır ve geçerli döngü öğesini geçirir. Örneğin, 0’dan 100’e kadar döngü yaparsak, önce 0 sonra 1, sonra 2 ve bu şekilde devam eder.

Örneğin bu 100 satırlı bir form oluşturur;

Form {
    ForEach(0..<100) { number in
        Text("Row \(number)")
    }
}

ForEach closure içine parametre geçtiğinden, parametre adı için aşağıdaki gibi kısa sözdizimi kullanabiliriz.

Form {
    ForEach(0 ..< 100) {
        Text("Row \($0)")
    }
}

ForEach özellikle SwiftUI’nin Picker view’ı ile çalışırken kullanışlıdır. Picker kullanıcıların aralarından seçim yapabileceği çeşitli seçenekler göstermemizi sağlar.

Bunu göstermek için bir view tanımlayacağız;

  1. Olası öğrenci adlarından oluşan bir Array’e sahiptir.
  2. O an seçili olan öğrenciyi saklayan @State property’si vardır.
  3. Kullanıcılardan favorilerini seçmelerini isteyen bir Picker view oluşturur ve @State property’ye two-way binding kullanır.
  4. Tüm olası öğrenci adları üzerinde döngü yapmak için ForEach’i kullanır ve bunları bir text view’e dönüştürür.

İşte kod;

struct ContentView: View {
    let students = ["Harry", "Hermione", "Ron"]
    @State private var selectedStudent = "Harry"

    var body: some View {
        NavigationStack {
            Form {
                Picker("Select your student", selection: $selectedStudent) {
                    ForEach(students, id: \.self) {
                        Text($0)
                    }
                }
            }
        }
    }
}

Burada çok fazla kod yok fakat bazı şeyleri açıklığa kavuşturmak gerekir;

  1. students Array’inin @State ile işaretlenmesine gerek yoktur çünkü bu bir sabittir, değişmeyecektir.
  2. selectedStudent property “Harry” değeri ile başlar ancak değişebilir, bu sebeple @State ile işaretlenmiştir.
  3. Picker kullanıcılara ne yaptığını söyleyen ve aynı zamanda ekran okuyucuları için açıklayıcı bir metin olan “Select your student” etiketine sahiptir.
  4. Picker ’ın selectedStudent’e iki yönlü bağı vardır (two-way binding), yani “Harry” seçimini göstermeye başlayacak ancak kullanıcı başka bir şey seçtiğinde property’yi güncelleyecektir.
  5. ForEach içinde tüm students Array üzerinde döngü yaparız.
  6. Her student için o student’in adını gösteren bir text view oluşturuyoruz.

Buradaki tek kafa karıştırıcı kısım şudur : ForEach(students, id: \.self) Bu, students Array’i üzerinde döngü yapar, böylece her bir için bir text view oluşturabiliriz, ancak id: \.self kısmı önemlidir. Çünkü SwiftUI’nin ekrandaki her görünümü benzersiz bir şekilde tanımlayabilmesi gerekir, böylece işler değiştiğinde bunu algılayabilir.

Örneğin, array’i önece Ron gelecek şekilde düzenlersek, SwiftUI text view’ı aynı anda hareket ettirecektir. Bu nedenle, SwiftUI’ye string array’deki her bir öğeyi benzersiz bir şekilde nasıl tanımlayabileceğini söylememiz gerekir. Her bir stringi benzersiz kılan nedir?

Sadece basit stringlerden oluşan bir array’imiz var ve string ile ilgili tek benzersiz şey string’in kendisidir. Array’deki her bir string farklıdır, bu nedenle string’ler doğal olarak benzersizdir.

Dolayısıyla, birçok view oluşturmak için ForEach kullandığımızda ve SwiftUI bize string array’deki her bir öğeyi benzersiz yapan tanımlayıcının ne olduğunu sorduğunda, cevabımız \.self, yani “stringlerin kendileri benzersizdir” olacaktır. Elbette, bu students array’e yinelenen string’ler eklersek sorun yaşayabileceğimiz anlamına gelir, ancak bu örnekte sorun yok.


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

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