- Görkem Güray/
- 100 Günde SwiftUI Notları/
- 51.Gün - SwiftUI Networking: URLSession Kullanarak Uygulamayı Tamamlayalım/
51.Gün - SwiftUI Networking: URLSession Kullanarak Uygulamayı Tamamlayalım
Table of Contents
Bu bölümde URLSession
’u kullanarak internet üzerinden veri gönderme ve almayı göreceğiz.
Geçerli Bir Address Olup Olmadığını Kontrol Etme #
Projemizdeki ikinci adım, kullanıcının adresini bir forma girmesine izin vermek olacak, ancak bunun bir parçası olarak bazı doğrulamalar ekleyeceğiz - yalnızca adresleri iyi görünüyorsa üçüncü adıma geçmek istiyoruz.
Bunu, daha önce oluşturduğumuz AddressView
struct’a dört text field içerecek bir Form
ekleyerek gerçekleştirebiliriz: name, street address, city ve zip. Daha sonra, kullanıcının son fiyatını göreceği ve ödeme yapabileceği bir sonraki ekrana geçmek için bir NavigationLink
ekleyebiliriz.
CheckoutView
adında yeni bir view ekleyerek başlayacağız. Bu view’ı address view push edecek. Şimdilik bunu bir yer tutucu olarak ele alacağız.
CheckoutView
adında yeni bir SwiftUI view oluşturun ve ona AddressView
’ın sahip olduğu Order
property’nin ve preview’in aynısını verin.
struct CheckoutView: View {
var order: Order
var body: some View {
Text("Hello, World!")
}
}
#Preview {
CheckoutView(order: Order())
}
Buna daha sonra döneceğiz, ancak önce AddressView
’ı uygulayalım. Dediğim gibi, bunun Order
nesnemizden dört property’ye bağlı dört text filed içeren bir forma ve kontrolü checkout view’e akataran bir NavigationLink
’e sahip olması gerekiyor.
İlk olarak, teslimat ayrıntılarını saklamak için Order
’da dört yeni property’ye ihtiyacımız var
var name = ""
var streetAddress = ""
var city = ""
var zip = ""
Şimdi AddressView
’in mevcut body
‘sini bununla değiştirin;
Form {
Section {
TextField("Name", text: $order.name)
TextField("Street Address", text: $order.streetAddress)
TextField("City", text: $order.city)
TextField("Zip", text: $order.zip)
}
Section {
NavigationLink("Check out") {
CheckoutView(order: order)
}
}
}
.navigationTitle("Delivery details")
.navigationBarTitleDisplayMode(.inline)
Gördüğünüz gibi, bu işlem order
nesnemizi bir seviye daha derine, CheckoutView
’a aktarır; bu da artık aynı veriye işaret eden üç view’ımız olduğu anlamına gelir.
Bu kod birçok hataya yol açacaktır, ancak bunları düzeltmek için sadece küçük bir değişiklik yeterlidir, order property’yi şu şekilde değiştirin.
@Bindable var order: Order
Daha önce, bu property’ler @Observable
makrolarını kullanan sınıflar olsa bile Xcode’un yerel @State
property’lerine bind olmasına nasıl izin verdiğini görmüştünüz. Bunun nedeni, @State
property wrapper’ın bizim için otomatik olarak $
sözdizimi aracılığıyla eriştiğimiz two way binding oluşturmasıdır.
AddressView
’de @State
kullanmadık çünkü sınıfı burada oluşturmuyoruz, sadece başka bir yerden alıyoruz. Bu da SwiftUI’nin normalde kullandığımız two way binding’e erişimi olmadığı anlamına gelir ki bu da bir sorundur.
Şimdi, bu sınıfın @Observable
makrosunu kullandığını biliyoruz, bu da SwiftUI’nin bu verileri değişiklikler için izleyebileceği anlamına geliyor. Dolayısıyla, @Bindable
property wrapper’ın yaptığı şey bizim için eksik binding’leri oluşturmaktır. Yani local veri oluşturmak için @State
kullanmak zorunda kalmadan @Observable
makrosu ile çalışabilen two way binding üretir. Burada mükemmeldir ve gelecekteki projelerinizde çok kullanacaksınız.
Devam edin ve uygulamayı tekrar çalıştırın, çünkü tüm bunların neden önemli olduğunu görmenizi istiyorum. İlk ekrana biraz veri girin, ikinci ekrana biraz veri girin, sonra başa dönüp ilerlemeyi deneyin
Görmeniz gereken şey, hangi ekranda olursanız olun girdiğiniz tüm verilerin kayıtlı kaldığıdır. Evet, bu verilerimiz için bir sınıf kullanmanın doğal bir yan etkisidir, ancak uygulamamızda herhangi bir çalışma yapmak zorunda kalmadan kazandığımız bir özelliktir. Local state kullansaydık, girdiğimiz tüm adres ayrıntıları orijinal görünüme geri döndüğümüzde kaybolurdu.
Artık AddressView çalıştığına göre, bazı koşullar yerine getirilmediği sürece kullanıcının ödeme sayfasına ilerlemesini durdurmanın zamanı geldi. Hangi koşul? Buna karar vermek bize düşüyor. Dört text field’ın her biri için uzunluk kontrolleri yazabilsek de, bu genellikle insanları yanıltır. Bazı isimler çok kısa olabilir.
Bunun yerine, siparişimizin name
, streetAddress
, city
ve zip
property’lerinin boş olup olmadığını kontrol edeceğiz. Verilerimin içine bu tür karmaşık bir kontrol eklemeyi tercih ediyorum, bu da Order
’a bunun gibi yeni bir hesaplanmış özellik eklememiz gerektiği anlamına geliyor.
var hasValidAddress: Bool {
if name.isEmpty || streetAddress.isEmpty || city.isEmpty || zip.isEmpty {
return false
}
return true
}
Artık bu koşulu SwiftUI’nin disabled()
modifier’ı ile birlikte kullanabiliriz.
Bizim durumumuzda, kontrol etmek istediğimiz koşul az önce yazdığımız hasValidAddress
computed property’dir. Eğer bu property false ise, NavigationLink
’imizi içeren form bölümünün devre dışı bırakılması gerekir, çünkü kullanıcıların önce teslimat bilgilerini doldurmaları gerekir.
Dolayısıyla, bu modifier’ı AddressView’daki ikinci bölümün sonuna ekleyelim;
.disabled(order.hasValidAddress == false)
Kod şu şekilde görülmelidir.
Section {
NavigationLink("Check out") {
CheckoutView(order: order)
}
}
.disabled(order.hasValidAddress == false)
Şimdi uygulamayı çalıştırırsanız, devam etmek için dört text field’da da en az bir karakter içermesi gerektiğini göreceksiniz. Daha da iyisi, SwiftUI koşul doğru olmadığında butonu otomatik olarak grileştiriyor ve kullanıcıya etkileşimli olup olmadığı konusunda net bir geri bildirim veriyor.
Checkout için Hazırlık #
Uygulamamızdaki son ekran CheckoutView
’dir ve iki bölümden oluşur. İlk yarı, sizin için çok az gerçek zorluk sağlayacak olan temel kullanıcı arayüzüdür, ancak ikinci yarı tamamen yenidir. Order
sınıfımızı JSON’a encode etmemiz, internet üzerinden göndermemiz ve bir yanıt almamız gerekir.
Encode ve aktarma (transferring) işinin tamamına yakında bakacağız, ancak önce kolay kısmı ele alalım: CheckoutView
’e bir kullanıcı arayüzü vermek. Daha spesifik olarak, bir resim, siparişlerinin toplam fiyatı ve network oluşturmayı başlatmak için bir Place Order butonu içeren bir ScrollView
oluşturacağız.
Görüntü için sunucuma AsyncImage
ile uzaktan alacağımız bir cupcake görüntüsü yükledim. Bunu uygulamada saklayabiliriz, ancak uzak bir görüntüye sahip olmak, mevsimsel alternatifler ve promosyonlar için dinamik olarak değiştirebileceğimiz anlamına gelir.
Sipariş maliyetine gelince, aslında verilemizde keklerimiz için herhangi bir fiyatlandırma yok, bu yüzden sadece bir tane icat edebiliriz. Kullanacağımız fiyatlandırma aşağıdaki gibidir.
- Kek başına 2 dolarlık bir taban fiyat var.
- Daha karmaşık kekler için ücrete biraz ekleme yapacağız.
- Ekstra krema kek başına 1 dolar tutacaktır.
- Süsleme eklemek kek başına 50 sent daha tutacaktır.
Tüm bu mantığı Order için aşağıdaki gibi yeni bir computed property’de toplayabiliriz.
var cost: Decimal {
// $2 per cake
var cost = Decimal(quantity) * 2
// complicated cakes cost more
cost += Decimal(type) / 2
// $1/cake for extra frosting
if extraFrosting {
cost += Decimal(quantity)
}
// $0.50/cake for sprinkles
if addSprinkles {
cost += Decimal(quantity) / 2
}
return cost
}
Gerçek view’ın kendisi basittir: dikey bir ScrollView
içinde bir VStack
, ardından görüntümüz, maliyet metni ve sipariş vermek için buton kullanacağız.
Butonun action kısmına döneceğiz, önce temel layout’u tamamlayalım. CheckoutView
’in mevcut body
’sini bununla değiştirelim;
ScrollView {
VStack {
AsyncImage(url: URL(string: "https://hws.dev/img/[email protected]"), scale: 3) { image in
image
.resizable()
.scaledToFit()
} placeholder: {
ProgressView()
}
.frame(height: 233)
Text("Your total is \(order.cost, format: .currency(code: "USD"))")
.font(.title)
Button("Place Order", action: { })
.padding()
}
}
.navigationTitle("Check out")
.navigationBarTitleDisplayMode(.inline)
Bunların hepsi artık sizin için eski olmalı ancak bu ekranla işimiz bitmeden önce size buraya ekleyebileceğimiz küçük ama kullanışlı bir SwiftUI modifier göstermek istiyorum: scrollBounceBehavior()
Scroll view kullanmak, kullanıcının etkinleştirdiği Dinamik Tip boyutu ne olursa olsun layout’un harika çalışmasını sağlamanın güzel bir yoludur, ancak küçük bir sıkıntı yaratır: view’ler tek bir ekrana tam olarak sığdığında, kullanıcı üzerinde yukarı ve aşağı hareket ettiğinde yine de biraz zıplar.
scrollBounceBehavior()
modifier’i, kaydırılacak bir şey olmadığında bu zıplamayı devre dışı bırakmamıza yardımcı olur. Bunu .navigationBarTitleDisplayMode(.inline)
’ın altına ekleyin.
.scrollBounceBehavior(.basedOnSize)
Bu şekilde, gerçekten kayan içeriğimiz olduğunda güzel bir kaydırma zıplaması elde edeceğiz, akdi takdirde scroll view orada yokmuş gibi davranır.
Bu bölümüde bitirdiğimize göre son kısım olan network oluşturmayı inceleyebiliriz.
İnternet Üzerinden Veri Gönderme ve Alma #
iOS, networkleri yönetmek için bazı harika işlevlerle birlikte gelir, özellikle URLSession
sınıfı veri göndermeyi ve almayı şaşırtıcı derecede kolaylaştırır. Swift nesnelerini JSON’a ve JSON’dan dönüştürmeyi Codable
ile birleştirirsek, verilerin tam olarak nasıl gönderilmesi gerektiğini yapılandırmak için yeni bir URLRequest
struct kullanabiliriz, yaklaşık 20 satır kodla harika şeyler başarabiliriz.
İlk olarak, Place Order butonumuzdan çağırabileceğimiz bir method oluşturalım, bunu CheckoutView
’e ekleyin;
func placeOrder() async {
}
Tıpkı URLSession
kullanarak veri indirirken olduğu gibi, yükleme de asynchronous olarak yapılır.
Şimdi Place Order butonunu şu şekilde değiştirin;
Button("Place Order", action: placeOrder)
.padding()
Bu kod çalışmayacaktır ve Swift bunun nedenini oldukça açık bir şekilde ortaya koyacaktır: asynchronous’u desteklemeyen bir fonksiyondan asynchronous bir fonksiyon çağırılmaktadır. Bunun anlamı butonumuzun action’ı hemen çalıştırabilmeyi beklediği ve asynchronous bir şeyi nasıl bekleyeceğini anlamadığıdır. await placeOrder()
yazsak bile çalışmaz, çünkü buton beklemek istemez.
Daha önce onAppear() fonksiyonunun bu asenkron fonksiyonlarla çalışmadığından ve bunun yerine task()
modifier’ını kullanamamız gerektiğinden bahsetmiştim. Burada sadece modifier eklemek yerine bir action yürüttüğümüz için bu bir seçenek değil, ancak Swift bir alternatif sunuyor: hiç yoktan yeni bir görev oluşturabiliriz ve tıpkı task()
modifier gibi bu da istediğimiz her türlü asynchronous kodu çalıştıracaktır.
Aslında tek yapmamız gereken await
çağrımızı aşağıdaki gibi bir görevin içine yerleştirmektir.
Button("Place Order") {
Task {
await placeOrder()
}
}
Ve şimdi her şey hazır bu kod placeOrder()
methodunu asynchronous olarak çağıracak. Tabi ki, bu fonksiyon aslında henüz hiçbir şey yapmıyor, bu yüzden şimdi bunu düzeltelim.
placeOrder()
içinde üç şey yapmamız gerekiyor;
- Mevcut sipariş nesnemizi gönderebilecek bazı JSON verilerine dönüştürün.
- Swift’e bu verileri bir network çağrısı üzerinden nasıl göndereceğini söyleyin.
- Bu isteği çalıştırın ve yanıtı işleyin.
Bunlardan ilki basittir, bu yüzden onunla başlayalım. Bu kodu placeOrder()
’a ekleyerek siparişimizi arşivlemek için JSONEncoder
’ı kullanacağız;
guard let encoded = try? JSONEncoder().encode(order) else {
print("Failed to encode order")
return
}
Order
sınıfı Codable
protokolüne uymadığı için bu kod henüz çalışmayacaktır. Yine de bu kolay bir değişikliktir, sınıf tanımını şu şekilde değiştirelim;
class Order: Codable {
İkinci adım, URLRequest
adında yeni bir tür kullanmaktır. Bu tür bize, istek türü, kullanıcı verileri gibi ekstra bilgiler eklemek için seçenekler sunan bir URL
gibidir.
Sunucunun doğru şekilde işleyebilmesi için verileri çok özel bir şekilde eklememiz gerekiyor, bu da siparişimizin ötesinde iki ekstra veri sağlamamız gerektiği anlamına geliyor.
- Bir isteğin HTTP methodu, verilerin nasıl gönderileceğini belirler. Birkaç HTTP methodu vardır, ancak pratikte GET (”Veri okumak istiyorum”) ve POST (”Veri yazmak istiyorum”) çok kullanılır. Biz burada veri yazmak istiyoruz, bu yüzden POST kullanacağız.
- Bir isteğin içerik türü, ne tür bir veri gönderildiğini belirler ve bu da sunucunun verilerimizi ele alma şeklini etkiler. Bu, başlangıçta e-postalarda ek göndermek için yapılmış olan MIME türü olarak adlandırılan bir türde belirtilir e son derece spesifik birkaç bin seçeneği vardır.
Bu nedenle, placeOrder()
için bir sonraki kod, bir URLRequest
nesnesi oluşturmak ve ardından bir HTTP POST isteği kullanarak JSON verilerini gönderecek şekilde yapılandırmak olacaktır. Daha sonra bunu URLSession
kullanarak verilerimizi yüklemek ve geri gelenleri işlemek için kullanabiliriz.
Tabiki asıl soru isteğimizi nereye göndereceğimizdir. https://reqres.in adlı gerçekten yararlı bir web sitesini kullanacağız, istediğimiz herhangi bir veriyi göndermemize izin veriyor ve otomatik olarak geri gönderiyor. Bu, network kodunun prototipini oluşturmanın harika bir yoludur, çünkü gönderdiğiniz her şeyden gerçek verileri geri alırsınız.
Bu kodu şimdi placeOrder()
methoduna ekleyin;
let url = URL(string: "https://reqres.in/api/cupcakes")!
var request = URLRequest(url: url)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
İlk satır URL(string:)
initializer için bir force unwrap içeriyor, bu da “bu optional bir URL döndürür, ancak optional olmaması için zorlayın” anlamına geliyor. String’lerden URL oluşturmak, bazı anlamsız ifadeler ekleme ihtimalimiz dolayısyla başarısız olabilir, ancak burada URL’yi elle yazdım, böylece her zaman doğru olacağını görebiliyorum.
Bu noktada, URLSession.shared.upload()
adlı yeni bir yöntem var ve az önce yaptığımız URL request’i kullanarak yapacağımız network isteğimizi yapmaya hazırız. Şimdi devam edin ve bunu placeOrder()
’a ekleyin;
do {
let (data, _) = try await URLSession.shared.upload(for: request, from: encoded)
// handle the result
} catch {
print("Checkout failed: \(error.localizedDescription)")
}
Şimdi önemli işe gelelim: Her şeyin doğru çalıştığı zamanlar için isteğimizin sonucunu okumamız gerekiyor. Bir şeyler ters giderse belki de internet bağlantısı olmadığı için o zaman catch
bloğumuz çalıştırılacaktır, bu yüzden burada bunun için endişelenmenize gerek yok.
ReqRes.in kullandığımız için, aslında gönderdiğimiz siparişin aynısını geri alacağız, bu da bunu JSON’da bir nesneye dönüştürmek için JSONDecoder
’ı kullanabileceğimiz anlamına geliyor.
Her şeyin doğru çalıştığını doğrulamak için siparişimizin bazı ayrıntılarını içeren bir uyarı göstereceğiz, ancak ReqRes.in’den geri aldığımız decoded Order’ı kullanacağız. Evet bu bizim gönderdiğimizle aynı olmalı, eğer değilse encode’de bir hata yapmışız demektir.
Bir uyarının gösterilmesi, mesajı ve görünür olup olmadığını saklamak için property’ler gerektirir, bu nedenle lütfen bu iki yeni property’yi CheckoutView
’e şimdi ekleyin;
@State private var confirmationMessage = ""
@State private var showingConfirmation = false
Ayrıca bu Boolean’ı izlemek ve doğru olduğu anda bir uyarı göstermek için bir alert()
modifier eklememiz gerekir. Bu modifier’ı CheckoutView’deki navigation title altına ekleyin;
.alert("Thank you!", isPresented: $showingConfirmation) {
Button("OK") { }
} message: {
Text(confirmationMessage)
}
Ve şimdi network kodumuzu tamamlayabiliriz: geri gelen verilerin decode edeceğiz, confirmation message property’yi ayarlamak için kullanacağız, ardından uyarının görünmesi için showingConfirmation
öğesini true olarak ayarlayacağız. Decode işlemi başarısız olursa yani sunucu herhangi bir nedenle order olmayan bir şey gönderirse bir hata mesajı yazdıracağız.
Bu son kodu placeOrder()
methoduna ekleyin ve //handle the result
yorumunu değiştirin.
let decodedOrder = try JSONDecoder().decode(Order.self, from: data)
confirmationMessage = "Your order for \(decodedOrder.quantity)x \(Order.types[decodedOrder.type].lowercased()) cupcakes is on its way!"
showingConfirmation = true
Şimdi çalıştırmayı denerseniz, tam olarak istediğiniz kekleri seçebilmeli, teslimat bilgilerinizi girebilmeli ve ardından bir uyarı görmek için Place Order butonuna basabilmelisiniz, her şey güzel çalışıyor.
Yine de işimiz tam olarak bitmedi, çünkü şu anda ağımızın küçük ama görünmez bir sorunu var. Bunun ne olduğunu görmek için sizi Xcode ile küçük bir debugging işlemi ile tanıştırmak istiyorum. Uygulamamızı duraklatacağız, böylece belirli bir değeri inceleyebileceğiz.
İlk olarak let url = URL...
satırının yanındaki satır numarasına tıklayın. Orada mavi bir ok görünmelidir, bu Xcode’un oraya bir kesme noktası yerleştirdiğimizi söyleme şeklidir. Bu Xcode’a o satıra ulaşıldığında yürütmeyi duraklatmasını söyler, böylece tüm verilemizi kurcalayabiliriz.
Şimdi devam edin ve uygulamayı tekrar çalıştırın, sipariş verilerini girin ve ardından sipariş verin. Her şey yolunda gittiğinde uygulamanız duraklamalı, Xcode öne gelmeli ve çalıştırmak üzere olduğu için bu kod satırı vurgulanmalıdır.
Her şey yolunda giderse, Xcode penceresinin sağ lat kısmında Xocde’un hata ayıklama konsolunu görmelisiniz- normalde Apple’ın tüm dahili günlük mesajlarının göründüğü yerdir, ancak şu anda “(lldb)” yazmalıdır. LLDB, Xcode’un hata ayıklayıcısının adıdır ve verilerimizi keşfetmek için burada komutlar çalıştırabiliriz.
Orada şu komutu çalıştırmanızı istiyorum : p String(decoding: encoded, as: UTF8.self)
Bu, kodlanmış verilerimizi tekrar bir string’e dönüştürür ve yazdırır. Bize @Observable
makrosu tarafından sağlanan observation registrar ile birlikte çok sayıda altı çizili değişken adı olduğunu görmelisiniz.
Kodumuz aslında bunu önemsemiyor, çünkü tüm property’leri altı çizili isimlerle gönderiyoruz, ReqRes.in sunucusunu bunları bize aynı isimlerle geri gönderiyor ve biz de bunları altı çizili property’lere geri decode ediyoruz. Ancak gerçek bir sunucu ile çalışırken bu isimler önemlidir. Yani @Observable
makrosu tarafından üretilen garip versiyonlar yerine gerçek isimleri göndermemiz gerekir.
Bu, Order
sınıfı için bazı özel kodlama anahtarları oluşturmamız gerektiği anlamına gelir. Bu, özellikle bunun gibi birkaç property’yi kaydetmek ve yüklemek istediğimiz sınıflar için oldukça sıkıcıdır, ancak ağımızın düzgün bir şekilde yapıldığından emin olmanın en iyi yoludur.
Order
sınıfını açın ve bu iç içe enumu buraya ekleyin.
enum CodingKeys: String, CodingKey {
case _type = "type"
case _quantity = "quantity"
case _specialRequestEnabled = "specialRequestEnabled"
case _extraFrosting = "extraFrosting"
case _addSprinkles = "addSprinkles"
case _name = "name"
case _city = "city"
case _streetAddress = "streetAddress"
case _zip = "zip"
}
Kodu tekrar çalıştırırsanız, yukarı imleç tuşuna ve return tuşuna basarak p komutunu tekrar çalıştırabileceğinizi ve bu kez gönderilen ve alınan verilerin çok daha temiz olduğunu göreceksiniz.
Bu son kod ile birlikte hem ağımız hem de uygulamamız tamamlanmış oldu.
Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.