• 22.08.2026 23:20:28
  • Admin Admin

Bu SwiftUI eğitimi, App Intents komutlarını kalıcı veri, MVVM sınırı, Instruments ölçümü ve dağıtım kontrolleriyle ele alır. Siri ve Kestirmelerden gelen yazma işlemlerini test edilebilir hale getirir.

SwiftUI Eğitiminde App Intents ile Uygulama Komutlarını Tasarlamak

SwiftUI eğitimi için App Intents komut sınırını doğru kurmak

App Intent'i doğrudan ViewModel'e veya ekrandaki @State verisine bağlamayın. Kestirmelerden gelen komut, uygulama arayüzü görünür değilken de çalışabilir; bu nedenle intent ile iş kuralı arasına kalıcı depoya yazan bir command katmanı koyun. Bu ayrım, Swift eğitimi içinde sık atlanan fakat üretimde kritik olan bir detaydır: intent'in yaşam döngüsü, SwiftUI View yaşam döngüsü değildir.

import AppIntents

struct CompleteTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Complete task"
    static var openAppWhenRun = false

    @Parameter(title: "Task identifier")
    var taskID: String

    func perform() async throws -> some IntentResult & ProvidesDialog {
        let command = try AppEnvironment.makeTaskCommand()
        let completed = try await command.complete(taskID: taskID)

        guard completed else {
            throw TaskCommandError.taskNotFoundOrAlreadyCompleted
        }

        return .result(dialog: "Task completed")
    }
}

Buradaki taskID bir liste indeksinden veya kullanıcıya görünen başlıktan üretilmemelidir. SQLite tablosunda saklanan UUID gibi değişmeyen bir anahtar kullanın. Bir kullanıcı aynı başlıklı iki görev oluşturduğunda başlıkla arama belirsiz sonuç verir; liste sırası ise senkronizasyon, filtreleme ve silme sonrası değişir. Intent parametresine UUID metni geçirmek, komutun yeniden çalıştırılabilirliğini korur.

App Group ve SQLite ile süreçler arası tutarlı veri yazımı

Intent'in çalıştığı bağlam ile ana uygulama arasında yalnızca bellek içi singleton paylaşmak güvenli değildir. App Group altındaki SQLite dosyası, iki sürecin aynı veriyi görmesini sağlar. GRDB'nin DatabasePool sınıfı SQLite kilitlerini kullanır; actor ise sadece kendi Swift süreciniz içindeki eşzamanlı çağrıları sıralar. Bu ayrımı yapmak önemlidir: actor, ayrı bir süreçte çalışan ikinci writer'ı kilitlemez.

import Foundation
import GRDB

enum SharedDatabase {
    static func makePool() throws -> DatabasePool {
        guard let directory = FileManager.default.containerURL(
            forSecurityApplicationGroupIdentifier: "group.com.opendart.tasks"
        ) else {
            throw CocoaError(.fileNoSuchFile)
        }

        var configuration = Configuration()
        configuration.busyMode = .timeout(2.0)
        return try DatabasePool(
            path: directory.appendingPathComponent("tasks.sqlite").path,
            configuration: configuration
        )
    }
}

actor TaskCommand {
    private let db: DatabasePool

    init(db: DatabasePool) {
        self.db = db
    }

    func complete(taskID: String) throws -> Bool {
        try db.write { database in
            try database.execute(
                sql: "UPDATE task SET completedAt = ? WHERE id = ? AND completedAt IS NULL",
                arguments: [Date(), taskID]
            )
            return database.changesCount == 1
        }
    }
}

Xcode'da ana uygulama hedefi ve intent'i içeren hedef için aynı App Group capability değerini ekleyin. Ayrıca migration'ı tek bir bootstrap fonksiyonunda çalıştırın ve intent ilk kez tetiklendiğinde de çağrıldığından emin olun. Sadece uygulamanın launch akışında migration çalıştırmak, kullanıcının uygulamayı hiç açmadan Kestirme çalıştırdığı durumda 'no such table' hatasına yol açar. GRDB migrator ile şemayı sürümlü uygulayın ve migration testinde boş veritabanından son şemaya geçişi doğrulayın.

iOS MVVM mimarisi içinde Intent yazımlarını SwiftUI ekranına yansıtmak

iOS MVVM mimarisi açısından ViewModel'in görevi intent sonucunu tahmin etmek değil, kalıcı depodan yeniden okumaktır. Intent bir görevi tamamladıktan sonra ViewModel koleksiyonundan yalnızca ilgili satırı silmek, başka süreçte gelen değişiklikler veya başarısız transaction durumunda ekranda yanlış durum üretir. iPhone uygulama geliştirme sürecinde güvenli varsayım şudur: SQLite kaydı source of truth'tür.

import Observation
import SwiftUI

@MainActor
@Observable
final class TaskListModel {
    private let repository: TaskRepository
    private(set) var tasks: [TaskRow] = []
    private var reloadTask: Task<Void, Never>?

    init(repository: TaskRepository) {
        self.repository = repository
    }

    func reload() {
        reloadTask?.cancel()
        reloadTask = Task {
            do {
                tasks = try await repository.openTasks(limit: 100)
            } catch is CancellationError {
                return
            } catch {
                tasks = []
            }
        }
    }
}

struct TaskListView: View {
    @Environment(\.scenePhase) private var scenePhase
    @State private var model: TaskListModel

    var body: some View {
        List(model.tasks) { task in
            Text(task.title)
        }
        .task { model.reload() }
        .onChange(of: scenePhase) { _, phase in
            if phase == .active { model.reload() }
        }
    }
}

scenePhase active olduğunda yeniden sorgulama, uygulama arka plandayken çalışan bir intent'in yazdığı veriyi ekrana taşımak için basit ve ölçülebilir bir taban davranıştır. Kullanıcı ekranı çok sık aktif hale getiriyorsa reload çağrısını 150-300 ms debounce edin. Daha düşük gecikme gerekiyorsa GRDB ValueObservation ile aynı süreçteki değişimleri izleyin; fakat farklı süreçteki SQLite değişikliği için yalnızca bellek içi observation'a güvenmeyin. Süreçler arası bildirim ihtiyacında Darwin notification kullanın ve bildirimi veri taşımak için değil, yeniden sorgu tetiklemek için kullanın.

App Intents gecikmesini Instruments ve SQL planıyla ölçmek

Bir intent'in yavaşlığını anlamak için Time Profiler ile yalnızca uygulama açılışını değil, intent'in perform() çağrısından veritabanı transaction'ının bitişine kadar olan aralığı kaydedin. os_signpost tabanlı OSSignposter, Instruments Points of Interest görünümünde bu sınırı işaretler. Önce indeks olmadan 30 soğuk ve 30 sıcak çalıştırmanın p50 ile p95 sürelerini alın; sonra sadece bir değişiklik yapıp aynı cihaz ve aynı görev veri setiyle ölçümü tekrarlayın.

import os

private let signposter = OSSignposter(
    subsystem: "com.opendart.tasks",
    category: "AppIntent"
)

func complete(taskID: String) throws -> Bool {
    let state = signposter.beginInterval("completeTask")
    defer { signposter.endInterval("completeTask", state) }

    return try db.write { database in
        try database.execute(
            sql: "UPDATE task SET completedAt = ? WHERE id = ? AND completedAt IS NULL",
            arguments: [Date(), taskID]
        )
        return database.changesCount == 1
    }
}

Açık görevleri updatedAt sırasıyla çekiyorsanız partial index kullanın ve planı doğrudan kontrol edin. Örneğin migration içine CREATE INDEX IF NOT EXISTS idx_task_open_updated ON task(updatedAt DESC) WHERE completedAt IS NULL; ekleyin. Ardından App Group içindeki veritabanının bir kopyasında şu komutu çalıştırın: sqlite3 tasks.sqlite "EXPLAIN QUERY PLAN SELECT id, title, updatedAt FROM task WHERE completedAt IS NULL ORDER BY updatedAt DESC LIMIT 100;". Çıktıda SCAN TABLE görüyorsanız indeks sorguya uymuyordur; SEARCH TABLE veya indeks kullanımı görmeden sadece CPU profiline bakmak yanlış teşhis üretir. Bu ölçüm disiplini, bir Xcode eğitimi laboratuvarında tekrar edilebilir performans deneyi olarak kullanılabilir.

App Shortcuts kaydı, test otomasyonu ve App Store yayınlama kontrolü

Kestirmeler uygulamasında bulunabilir bir komut için AppShortcutsProvider tanımlayın. Invocation phrase içinde uygulama adını sistemin beklediği yer tutucuyla eklemek gerekir; sabit marka metni yazmak, uygulama adı yerelleştirildiğinde veya yeniden adlandırıldığında phrase kalitesini düşürür. Bu adım, ios kursu veya swift kursu projelerinde genellikle yalnızca simulator üzerinde elle denenir; bunun yerine archive öncesi derleme ve test çıktısını CI'a ekleyin.

import AppIntents

struct TaskShortcuts: AppShortcutsProvider {
    static var appShortcuts: [AppShortcut] {
        AppShortcut(
            intent: CompleteTaskIntent(),
            phrases: [
                "Complete a task in \(.applicationName)",
                "Mark task done in \(.applicationName)"
            ],
            shortTitle: "Complete task",
            systemImageName: "checkmark.circle"
        )
    }
}

App Store yayınlama öncesinde intent'in veri erişimi, boş veritabanı ve zaten tamamlanmış kayıt senaryolarını unit test ile kapsayın. CI'da archive doğrulaması için örnek komut şudur: xcodebuild archive -scheme Tasks -configuration Release -destination 'generic/platform=iOS' -archivePath build/Tasks.xcarchive. Ardından export aşamasından önce App Group entitlement değerinin archive içindeki uygulama hedefiyle uyumunu codesign -d --entitlements :- build/Tasks.xcarchive/Products/Applications/Tasks.app komutuyla kontrol edin. Bu kontrol, entitlement farklılığı nedeniyle intent'in ayrı ve boş bir container'a yazması gibi dağıtımda görünen hataları erken yakalar.

Sık Sorulan Sorular

SwiftUI eğitimi kapsamında App Intent neden doğrudan ViewModel'i çağırmamalı?

App Intent, ekran oluşturulmadan çalışabileceği için ViewModel içindeki @State veya bellek içi cache mevcut olmayabilir. Intent'i TaskCommand gibi bir uygulama servisinin önüne koyun, transaction sonucunu SQLite'a yazın ve ViewModel'i repository sorgusuyla yenileyin.

iOS MVVM mimarisi ile App Intents verisini App Group olmadan paylaşabilir miyim?

Aynı uygulama hedefinde yalnızca uygulama açılarak çalışan bir akışınız varsa varsayılan container yeterli olabilir. Ancak ayrı hedefler veya App Group kullanan bileşenler arasında ortak veri gerekiyorsa aynı group.com... identifier ile App Group container kullanın. UserDefaults, çok süreçli read-modify-write işlemlerinde transaction garantisi vermez; yazma işlemi için SQLite transaction kullanın.

iPhone uygulama geliştirme projesinde App Intent performansını nasıl ölçerim?

perform() çağrısının iş kuralı bölümünü OSSignposter beginInterval ve endInterval ile sarın. Instruments'ta Points of Interest ve Time Profiler kayıtlarını alın, 30 soğuk ve 30 sıcak çalıştırmada p50/p95 değerlerini kaydedin. SQL sorgusu için ayrıca EXPLAIN QUERY PLAN çalıştırarak indeksin gerçekten kullanıldığını doğrulayın.

iOS eğitimi veya swift kursu projesinde app store yayınlama öncesi hangi App Intents kontrolleri yapılmalı?

Release archive üzerinde App Group entitlement değerini codesign ile inceleyin, boş veritabanından migration testini çalıştırın, bulunamayan UUID ve tekrarlanan komut için hata davranışını test edin. AppShortcutsProvider phrase'lerinde \(.applicationName) kullanın ve Kestirmelerden gelen komutun uygulama arayüzü açılmadan da kalıcı veriye yazabildiğini doğrulayın.

AI / LLM Discovery

Bu makale Opendart Akademi iOS / Swift eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt