İçeriğe geç
Educora
İleri25 dk13 / 14

Ağ ve veri saklama

Codable ile JSON'u Swift türlerine çevir, URLSession ve async/await ile sunucudan veri yükle, sonucu ekranda göster ve notları SwiftData ile cihazda sakla.

Kendini test et
Bu derste öğreneceklerin
  • Codable yapılarla JSON okumak ve yazmak
  • URLSession ve async/await ile bir ağ isteği yazmak ve hataları ele almak
  • Yüklemeyi .task ve .refreshable ile ekrana bağlamak
  • SwiftData'da @Model, @Query ve modelContext ile çalışmak

Elvin uçakta Educora'yı açıyor. İnternet yok, ama dün yazdığı notlar yerinde duruyor; iniş yapınca da uygulama yeni dersleri sunucudan sessizce yüklüyor. Bunun için iki beceri gerekir: ağdan veri almak (JSON, URLSession, async/await) ve onu cihazda saklamak (SwiftData). Bu derste ikisini de adım adım kuracağız.

Codable: JSON'dan Swift'e ve geri

Sunucular veriyi genellikle JSON metni olarak gönderir. Bir yapıyı Codable protokolüne uyan olarak tanımlarsan Swift onu JSON'dan okumayı (JSONDecoder) ve JSON'a yazmayı (JSONEncoder) kendisi öğrenir; tek satır fazladan kod gerekmez. Özellik adları JSON'daki anahtarlarla aynı olmalıdır; JSON'daki fazla anahtarlar yalnızca atlanır.

Swift
import Foundation

struct Lesson: Codable {
    let id: Int
    let title: String
    let minutes: Int
}

let json = """
[{"id": 1, "title": "Variables", "minutes": 10},
 {"id": 2, "title": "Loops", "minutes": 15, "level": "beginner"}]
"""

let lessons = try JSONDecoder().decode([Lesson].self, from: Data(json.utf8))
for lesson in lessons {
    print("\(lesson.id). \(lesson.title) - \(lesson.minutes) min")
}

let encoder = JSONEncoder()
encoder.outputFormatting = .sortedKeys
let data = try encoder.encode(lessons[0])
print(String(decoding: data, as: UTF8.self))
Beklenen çıktı
1. Variables - 10 min
2. Loops - 15 min
{"id":1,"minutes":10,"title":"Variables"}
Swift Playgrounds'ta ya da bir Mac'te çalışır. level anahtarı yapıda olmadığı için atlanır; .sortedKeys anahtarları alfabetik sırayla yazar.

URLSession ve async/await

URLSession.shared.data(from:) bir async fonksiyondur: await sözcüğü cevabı beklediği yeri işaretler, ama bu sırada ana iş parçacığı bloke olmaz; ekran tepki vermeye devam eder. Fonksiyon bir (Data, URLResponse) ikilisi döndürür. Ağ kesilirse hata fırlatır; bu yüzden çağrının önüne try da yazılır. Örnekte yapılacaklar, test için herkese açık bir servis olan JSONPlaceholder'dan yüklenir.

Swift
struct Todo: Codable, Identifiable {
    let id: Int
    let title: String
    let completed: Bool
}

enum APIError: Error {
    case badStatus(Int)
}

func fetchTodos() async throws -> [Todo] {
    let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
    let (data, response) = try await URLSession.shared.data(from: url)
    if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
        throw APIError.badStatus(http.statusCode)
    }
    return try JSONDecoder().decode([Todo].self, from: data)
}
Sunucu 200–299 aralığı dışında bir durum kodu döndürürse kendi hatamızı fırlatırız; JSON uymazsa decode hata fırlatır.
Eski: completion handler
func fetchTodos(completion: @escaping (Result<[Todo], Error>) -> Void) {
    let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error {
            completion(.failure(error))
            return
        }
        do {
            let todos = try JSONDecoder().decode([Todo].self, from: data ?? Data())
            completion(.success(todos))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}
Yeni: async/await
func fetchTodos() async throws -> [Todo] {
    let url = URL(string: "https://jsonplaceholder.typicode.com/todos")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Todo].self, from: data)
}
Solda .resume() unutulursa istek hiç gönderilmez; bir completion çağrısını atlamak da ekranı sonsuza dek “yükleniyor” durumunda bırakır. Sağda kod yukarıdan aşağı okunur, hatalar da throws ile kendiliğinden yukarı iletilir.
Swift
@MainActor
@Observable
final class TodosModel {
    var todos: [Todo] = []
    var isLoading = false
    var errorMessage: String?

    func load() async {
        isLoading = true
        defer { isLoading = false }
        do {
            todos = try await fetchTodos()
            errorMessage = nil
        } catch {
            errorMessage = "Could not load the list"
        }
    }
}
@MainActor, modelin özelliklerinin yalnızca ana iş parçacığında değişmesini garanti eder; arayüz de tam orada güncellenir.
Swift
struct TodosView: View {
    @State private var model = TodosModel()

    var body: some View {
        List(model.todos) { todo in
            Label(todo.title, systemImage: todo.completed ? "checkmark.circle.fill" : "circle")
        }
        .overlay {
            if model.isLoading { ProgressView() }
        }
        .task { await model.load() }
        .refreshable { await model.load() }
    }
}
Ekranda: önce dönen bir gösterge, sonra dolu ya da boş daireli yapılacaklar listesi; listeyi aşağı çekince yeniden yüklenir.

defer bloğu, fonksiyon nasıl biterse bitsin (başarıyla ya da hatayla) en sonda çalışır; bu yüzden gösterge ekranda asla “takılı” kalmaz. errorMessage dolduğunda listenin yerine iOS 17'nin hazır boş durum görünümünü göstermek kullanışlıdır: ContentUnavailableView("No connection", systemImage: "wifi.slash"). Altına load()'u yeniden çağıran bir “Try again” düğmesi koyabilirsin.

SwiftData: cihazda saklamak

SwiftData (iOS 17+), bir Swift sınıfını veritabanında saklamanın en kısa yoludur. Sınıfın önüne @Model yazarsın, uygulamada .modelContainer(for:) ile bir depo oluşturursun, görünümde de veriyi @Query ile okursun. Yeni nesne modelContext.insert(...) ile eklenir, delete ile silinir; değişiklikler kendiliğinden kaydedilir ve @Query listeyi kendisi günceller.

Swift
import SwiftData
import SwiftUI

@Model
final class Note {
    var text: String
    var createdAt: Date

    init(text: String, createdAt: Date = .now) {
        self.text = text
        self.createdAt = createdAt
    }
}

@main
struct EducoraNotesApp: App {
    var body: some Scene {
        WindowGroup {
            NotesView()
        }
        .modelContainer(for: Note.self)
    }
}
Swift
struct NotesView: View {
    @Environment(\.modelContext) private var context
    @Query(sort: \Note.createdAt, order: .reverse) private var notes: [Note]

    var body: some View {
        NavigationStack {
            List {
                ForEach(notes) { note in
                    Text(note.text)
                }
                .onDelete { offsets in
                    for index in offsets { context.delete(notes[index]) }
                }
            }
            .navigationTitle("Notes")
            .toolbar {
                Button("Add", systemImage: "plus") {
                    context.insert(Note(text: "New note"))
                }
            }
        }
    }
}
Ekranda: en yenisi üstte olan bir “Notes” listesi; + yeni not ekler, sola kaydırmak siler. Uygulamayı kapatıp açınca notlar yerinde kalır.
Ne saklanırNerede
küçük ayarlar: tema, günlük hedef@AppStorage (UserDefaults)
notlar, sonuçlar, aranan listelerSwiftData
parolalar ve giriş belirteçleriKeychain
tüm cihazlarda aynı olması gereken ilerlemesunucu
Örneğin: @AppStorage("dailyGoal") private var dailyGoal = 15, değeri uygulama kapandıktan sonra da saklar.
  1. 1
    SwiftData'lı bir proje

    Yeni proje oluştururken Storage alanında SwiftData'yı seç; Xcode örnek bir Item modeli, .modelContainer ve bir liste ekranı oluşturur.

  2. 2
    Önizleme için bellekte depo

    Önizleme gerçek veritabanına dokunmasın diye şunu yaz: #Preview { NotesView().modelContainer(for: Note.self, inMemory: true) }.

  3. 3
    Hata ayıklayıcıyla adım adım ilerle

    load() içinde bir kesme noktası koy, uygulamayı çalıştır ve F6 ile satır satır ilerle: todos dizisinin nasıl dolduğunu görürsün.

hata ayıklayıcıda sonraki satıra geçmek — Step OverF6
çağrılan fonksiyonun içine girmek — Step IntoF7
durduktan sonra çalışmayı sürdürmekCtrl+Cmd+Y
tüm projede aramakCmd+Shift+F

Önemli noktalar

  • Codable bir yapı JSON'dan JSONDecoder ile okunur, JSONEncoder ile yazılır; fazla anahtarlar atlanır.
  • try await URLSession.shared.data(from:) (Data, URLResponse) döndürür ve ekranı bloke etmez.
  • Model @MainActor @Observable olur; .task yüklemeyi başlatır ve kendini iptal eder, .refreshable yenileme hareketi ekler.
  • SwiftData: bir @Model sınıf, uygulamada .modelContainer(for:), görünümde @Query ve modelContext.insert/delete.
  • Küçük ayarlar @AppStorage'da, parolalar Keychain'de tutulur; yalnızca HTTPS kullan.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
try await URLSession.shared.data(from: url) ne döndürür?