Bu rehber, swiftui eğitimi kapsamında @Observable tabanlı iOS MVVM mimarisini Swift Testing, XCTest ve Instruments ile test edilebilir hale getirir; render invalidation ölçümü ve CI kapıları için uygulanabilir örnekler sunar.
SwiftUI Eğitimi: @Observable ile iOS MVVM Ekran Testleri Rehberi
SwiftUI eğitimi için @Observable tabanlı iOS MVVM mimarisi
Bir ios eğitimi veya swift eğitimi projesinde ekran durumunu `@Published` ile geniş bir `ObservableObject` üzerinde toplamak yerine, `@Observable` modelini ekran sınırında tutun. Observation sistemi, bir `body` hesaplanırken gerçekten okunan property'leri kaydeder; örneğin ürün listesi sadece `phase` okuyorsa, modeldeki ayrı bir analitik sayacının değişmesi listeyi yeniden hesaplatmaz. Bu davranışın çalışması için view model'i `@State` ile sahiplenmek, bağımlılıkları ise init üzerinden vermek gerekir. Böylece iphone uygulama geliştirme ekipleri ağ katmanını gerçek HTTP istemcisi yerine deterministik stub ile değiştirebilir.
import Foundation
import Observation
struct Product: Identifiable, Equatable, Sendable {
let id: Int
let name: String
}
protocol ProductServing: Sendable {
func products() async throws -> [Product]
}
@MainActor
@Observable
final class ProductListModel {
enum Phase: Equatable {
case idle
case loading
case loaded([Product])
case failed(String)
}
private let service: any ProductServing
private(set) var phase: Phase = .idle
init(service: any ProductServing) {
self.service = service
}
func load() async {
phase = .loading
do {
phase = .loaded(try await service.products())
} catch {
phase = .failed(error.localizedDescription)
}
}
}Buradaki kritik ayrıntı `ProductServing` protokolünün `Sendable` olmasıdır. Servis implementasyonu bir actor veya `URLSession` tabanlı immutable bir yapıysa, view model'den asenkron çağrı yapılırken Swift'in strict concurrency denetimi veri yarışını yakalayabilir. `@MainActor` modelin `phase` yazımlarını ana aktöre sabitler; buna karşılık ağ servisinin tümünü `@MainActor` yapmak, URL çözümleme ve JSON decode gibi işleri gereksiz yere ana aktöre taşır. View tarafında ilk yüklemeyi `.task { await model.load() }` ile başlatın; filtre anahtarı değiştiğinde yeniden istek gerekiyorsa `.task(id: query) { await model.load() }` kullanın. `task(id:)`, eski görevi iptal eder, ancak servis katmanının da `Task.isCancelled` veya iptal fırlatan `URLSession.data(for:)` davranışını bozmaması gerekir.
Swift Testing ile view model durum geçişlerini doğrulama
Swift Testing hedefi ekleyerek ağ, hata ve boş liste senaryolarını view çizmeden doğrulayın. Aşağıdaki test gerçek internet, saat veya rastgele veri kullanmaz; bu nedenle simülatör yükü altında dahi aynı sonucu üretir. `#expect` başarısız olduğunda Swift Testing ilgili expression değerini rapora ekler. Bu, klasik `XCTAssertEqual` ile alınan tek satırlık hataya göre özellikle enum durumlarında daha okunabilir bir failure çıktısı verir.
import Testing
@testable import Catalog
struct StubProductService: ProductServing {
let result: Result<[Product], Error>
func products() async throws -> [Product] {
try result.get()
}
}
@Test @MainActor
func productListLoadsStubbedProducts() async {
let expected = [Product(id: 42, name: "Keyboard")]
let service = StubProductService(result: .success(expected))
let model = ProductListModel(service: service)
await model.load()
#expect(model.phase == .loaded(expected))
}Yaygın hata, hata durumunu doğrudan `Error` olarak enum içinde saklayıp `Equatable` test etmeye çalışmaktır; çoğu `Error` tipi eşitlenebilir değildir ve `NSError` köprülemesi domain veya code farklarını gizleyebilir. Kullanıcıya gösterilecek hata için modelde `String` veya uygulamaya ait `CatalogError` enum'u saklayın, ham hatayı ise logger'a yazın. Örneğin `URLSession` hatalarını `URLError.Code.notConnectedToInternet`, HTTP 401 ve decode hatası olarak normalize edip her biri için ayrı `#expect` yazın. Bu yaklaşım bir swift kursu içindeki örnekten üretim koduna geçerken testlerin locale'e bağlı `localizedDescription` farkları yüzünden kırılmasını engeller.
XCTest UI testlerinde erişilebilirlik ve kararlı senkronizasyon
UI testi yalnızca kullanıcı akışını doğrulamalıdır; veri hazırlığını launch environment ile uygulamaya bildirin. Uygulama başlangıcında `ProcessInfo.processInfo.arguments.contains("-useStubCatalog")` kontrolü yapıp `StubProductService` enjekte edin. Her satıra veritabanı ID'sinden üretilen sabit bir `accessibilityIdentifier` verin: `product-row-42`. Görünen başlık metni yerine bu kimliği sorgulamak, A/B metin değişiklikleri ve yerelleştirme nedeniyle oluşan sahte UI test hatalarını azaltır.
import XCTest
final class CatalogUITests: XCTestCase {
func testStubCatalogShowsProduct() {
let app = XCUIApplication()
app.launchArguments += ["-useStubCatalog"]
app.launch()
let row = app.otherElements["product-row-42"]
let exists = NSPredicate(format: "exists == true")
expectation(for: exists, evaluatedWith: row)
waitForExpectations(timeout: 3)
XCTAssertTrue(row.isHittable)
}
}`sleep(2)` veya `DispatchQueue.main.asyncAfter` ile beklemek yerine `NSPredicate` beklentisi kullanın; XCTest, element görünür olur olmaz devam eder ve testin toplam süresi sabit bekleme kadar uzamaz. Aynı senaryoda yükleme göstergesinin kaybolmasını doğrulamak gerekiyorsa `app.activityIndicators["catalog-loading"]` için `exists == false` predicate'i ekleyin. Test çiftiniz yanlışlıkla `Task.detached` ile ana aktör dışına state yazıyorsa, UI testi aralıklı olarak başarısız olabilir. Bu nedenle stub sonucu döndükten sonra modelin `phase` atamasının `@MainActor` sınırında kaldığını birim testte de koruyun.
Instruments ile SwiftUI render invalidation ölçümü
Test edilebilirlik değişikliğinin çizim maliyetini varsaymayın, Instruments ile önce-sonra kaydı alın. Xcode'da Product > Profile ile Instruments'ı açın, SwiftUI ve Time Profiler araçlarını aynı kayda ekleyin, ardından 30 saniyelik sabit bir senaryoyu çalıştırın: katalog açma, 100 satır kaydırma, filtreyi üç kez değiştirme. Karşılaştırmada View Body hesaplama sayısını, main thread üzerindeki örnek sayısını ve işaretlenmiş yükleme aralığının süresini kaydedin. Aynı simülatör cihazı, aynı stub veri seti ve aynı Release yapılandırması kullanılmazsa iki kayıt karşılaştırılabilir değildir.
import os
private let catalogLog = OSLog(
subsystem: "com.example.catalog",
category: "loading"
)
func loadCatalog() async {
os_signpost(.begin, log: catalogLog, name: "CatalogLoad")
defer { os_signpost(.end, log: catalogLog, name: "CatalogLoad") }
await model.load()
}Signpost aralığını Instruments'ta `CatalogLoad` olarak filtreleyip ağ sonucundan state güncellemesine kadar geçen süreyi ölçün. Sık görülen invalidation hatası, kök `CatalogScreen` içinde her saniye değişen `now` değerini okumaktır; kök view bu property'yi okuduğu için liste de her tick'te body hesaplar. Çözüm, saati ayrı bir `ClockBadge` view'ına taşımak ve `now` state'ini sadece bu view içinde tutmaktır. Önce kayıtta kök ekranın saniyede bir body hesapladığını, sonra kayıtta yalnızca `ClockBadge` hesaplandığını doğrulayın. Bu, sadece `@Observable` kullanmakla otomatik gelmez; observation bağımliliği property'nin okunduğu view sınırında oluşur.
Xcode eğitimi, CI kapıları ve app store yayınlama öncesi kontroller
Bir xcode eğitimi pratiğinde testleri Xcode arayüzünden çalıştırmak yeterli değildir; CI'da şema paylaşılmış olmalı ve komut satırı aynı test paketini üretmelidir. Önce kullanılabilir simülatörleri `xcrun simctl list devices available` ile görün, sonra CI değişkeni olarak tanımlı cihaz adıyla hem Swift Testing hem XCTest hedeflerini çalıştırın. `-resultBundlePath` çıktısı, başarısız UI testinin ekran görüntüsü ve test activity kayıtlarını CI artifact'i olarak saklamanıza imkan verir.
xcodebuild test -scheme Catalog -destination "platform=iOS Simulator,name=${SIMULATOR_NAME}" -resultBundlePath build/CatalogTests.xcresult -only-testing:CatalogTests -only-testing:CatalogUITestsApp store yayınlama adımından önce archive üretimini de ayrı bir CI işi yapın: `xcodebuild archive -scheme Catalog -archivePath build/Catalog.xcarchive -destination 'generic/platform=iOS'`. Test işini archive işinden ayırmak, imzalama veya provisioning arızasının test başarısızlığı gibi raporlanmasını önler. Yayın adayında launch argument ile stub servis seçiminin kapalı olduğunu ayrıca test edin; bunun için üretim bağımlılık bileşiminde `ProcessInfo` bayrağını yalnızca `DEBUG` derlemesinde değerlendirin. Aksi halde bir UI test kolaylığı yanlışlıkla dağıtılmış uygulamada sabit demo verisi gösterebilir.
İlgili Eğitim
YTÜSEM İlgili Eğitim
Sık Sorulan Sorular
SwiftUI eğitimi sırasında @Observable model neden @State ile tutulmalı?
`@State`, view'ın oluşturduğu `@Observable` referansının view kimliği boyunca korunmasını sağlar. Modeli doğrudan `let model = ProductListModel(...)` ile body içinde üretirseniz, SwiftUI yeniden hesaplamalarında servis ve state tekrar oluşturulabilir. Sahiplik view'daysa `@State`, üst katmandan okunuyorsa `@Bindable` kullanın.
iOS MVVM mimarisinde Swift Testing mi XCTest mi kullanmalıyım?
View model, mapper ve hata normalizasyonu için `Swift Testing` ile `@Test` ve `#expect` kullanın. Erişilebilirlik ağacı, navigation ve launch argument davranışı için `XCTest` UI testini kullanın. Aynı HTTP stub'ını iki katmanda da enjekte edin; UI testinin gerçek sunucuya çıkması test sonucunu ağ durumuna bağlar.
iPhone uygulama geliştirme projesinde SwiftUI performansını nasıl ölçerim?
Xcode Product > Profile üzerinden SwiftUI ve Time Profiler araçlarıyla aynı kullanıcı akışını önce-sonra kaydedin. `os_signpost` ile `CatalogLoad` gibi ölçüm aralıkları ekleyin, View Body hesaplama sayısını ve ana thread örneklerini karşılaştırın. Yalnız FPS göstergesine bakmak, gereksiz body hesaplamalarını veya decode maliyetini tek başına ortaya çıkarmaz.
ios kursu projesinde app store yayınlama öncesi hangi test komutu kullanılmalı?
CI'da `xcodebuild test -scheme Catalog -destination "platform=iOS Simulator,name=${SIMULATOR_NAME}" -resultBundlePath build/CatalogTests.xcresult` komutunu çalıştırın. Ardından archive için ayrı bir işte `xcodebuild archive` kullanın. `.xcresult` paketini artifact olarak saklayarak başarısız testte ekran görüntüsü, assertion ve activity kaydını inceleyin.
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.


