Tüm Yazılar
KategoriSwiftUI
Okuma Süresi
9 dk
Yayın Tarihi
2026-10-15
Kelime Sayısı
1.344kelime

Kahveni hazırla - bu içerikli bir makale!

DesignSystemPro 2.0: SwiftUI için token'lar, temalar ve testler

Özet

DesignSystemPro 2.0 renk ve tipografi token'larını nasıl kuruyor, render testleri neden fingerprint kullanıyor, kırılan bir toast testi ne öğretti.

  • Bir design system'in değeri, doğru renk ve doğru boyutun her ekranda, light ve dark modda, her yazı boyutunda görünmesinde.
  • PrimitiveColors ham palet (griler, maviler, yeşiller vb.).
  • Paketi Swift Package Manager ile ekleyin (from: "2.0.0"), DesignSystemPro'yu import edin ve tema sağlayıcıyla toast katmanını kurmak için kök view'ı DSApp { ContentView() } ile sarın.
  • Kaynak kod: [github.com/muhittincamdali/SwiftUI-Design-System-Pro](https://github.com/muhittincamdali/SwiftUI-Design-System-Pro)
DesignSystemPro 2.0: SwiftUI için token'lar, temalar ve testler

İçindekiler


Problem

Bir design system'in değeri, doğru renk ve doğru boyutun her ekranda, light ve dark modda, her yazı

boyutunda görünmesinde. Bir DSButton yazmak kolay. Etiketinin dark modun accent renginde hâlâ

okunabildiğini ya da bir tema değişikliğinin alttaki view'lara gerçekten ulaştığını bilmek daha zor.

DesignSystemPro, iOS, macOS, tvOS, watchOS ve visionOS'taki SwiftUI uygulamaları için üç şeyi bir

araya getiren bir Swift paketi: design token'ları (renk, tipografi, spacing, border, gölge, hareket),

light, dark, hazır ve marka temalarını yöneten bir tema motoru ve bu token'larla kurulmuş yaklaşık 40

bileşen view'ı. 2.0.0 sürümü (14 Ekim 2026) aynı zamanda bir kurtarma. CHANGELOG bunu açıkça

yazıyor: Şubat ile Haziran 2026 arasında repo önce yeniden yazıldı, sonra kaynak kodun çoğunu

placeholder dosyalarla değiştiren bir sadeleştirmeden geçti. 2.0, Şubat'taki token, tema ve bileşen

kodunu geri getiriyor, düzeltiyor ve test ediyor. API'si 1.0.0 ile uyumlu değil.

Bu geçmiş sürümü şekillendirdi. Kod daha önce de vardı ama testi yoktu, aşağıda anlatılan hatalar da

baştan beri içindeydi. Bu yüzden 2.0'daki asıl iş, README'deki her iddiayı bir testin kontrol ettiği

bir şeye dönüştürmekti.

Tasarım kararları ve alternatifler

İki katmanlı renk token'ları

Renkler iki katmanda duruyor. PrimitiveColors ham palet (griler, maviler, yeşiller vb.).

SemanticColorToken ise UI anlamını taşıyor: ColorTokens.Background.primary,

ColorTokens.Foreground.onAccent, ColorTokens.Accent.primary, ColorTokens.Status.success.

Bileşenler yalnız anlamsal katmanı kullanıyor.

Alternatif, asset catalog'daki named color'lardı. Bu Xcode'da görsel düzenleme sağlar, ama bir Swift

paketinde resource bundle gerektirir ve iki şeyi zorlaştırır: kontrastı testte ölçmek ve hex

değerinden marka teması üretmek. Kodla tanımlanan token test edilebilir ve diff'te okunur. Bunun

faydası kontrast düzeltmesinde görüldü: dark modun accent rengi (#60A5FA) üzerindeki beyaz etiketler

yaklaşık 2,5:1 ölçülüyordu, yani WCAG AA'nın altındaydı. Foreground.onAccent'in dark değerini

gray900 yapmak bütün dolu butonları tek seferde düzeltti (commit 896bc90).

Her anlamsal token light, dark ve high-contrast değer taşıyor. Çözümleme saf bir fonksiyon:

resolved(for:highContrast:). .dsForegroundColor(_:) ve .dsBackgroundColor(_:) modifier'ları

environment'tan colorScheme ve colorSchemeContrast değerlerini okuyor. Çözümlemenin saf olması,

bir testin view hiyerarşisi kurmadan her şemada kontrast ölçebilmesini sağlıyor.

Luminance için `Color.resolve(in:)`

Şubat kodu luminance'ı UIColor/NSColor ile hesaplıyordu ve bu watchOS'ta çalışmıyordu. 2.0,

doğrusal RGB döndüren Color.resolve(in:) kullanıyor; böylece WCAG formülü her platformda doğrudan

uygulanıyor. Minimum sürümün iOS 17'ye çıkmasının bir nedeni bu API, diğeri Observation framework'ü.

Enjekte edilebilen Observable yöneticiler

ThemeManager ve DSToastManager birer @MainActor @Observable sınıf. DSApp kullandığı için

kolaylık amaçlı bir shared örnekleri var, artık public initializer'ları da var. Kendi örneğinizi

DSThemeProvider(themeManager:) ya da DSToastContainer(manager:) ile verebilirsiniz. Testler de aynı

nedenle kendi örneklerini kuruyor. Geri getirilen Şubat kodu ObservableObject kullanıyordu; 2.0 iki

yöneticiyi de @Observable'a geçiriyor, CHANGELOG da bunu not ediyor.

Piksel snapshot yerine fingerprint

Her bileşen grubu (butonlar, rozetler, kartlar, seçim kontrolleri, geri bildirim) light ve dark modda

render edilip kayıtlı bir referansla karşılaştırılıyor. Karşılaştırma piksel bazlı değil.

RenderFingerprint, görüntüyü boyutuna ve 8×8 ızgaradaki her hücrenin ortalama rengine indiriyor.

Render testi renk kanalı başına 0,03, boyutta 6 point farka izin veriyor. Referanslar

Tests/RenderingReferences altında platform başına JSON dosyaları.

Birebir piksel snapshot'ları OS ve Xcode sürümleri arasındaki anti-aliasing farkları yüzünden kırılır,

bir snapshot kütüphanesi de bağımlılık ekler. Fingerprint yine de yanlış rengi, eksik bileşeni ya da

layout kaymasını yakalıyor. Hiçbir hücrenin ortalamasını değiştirmeyen küçük bir değişikliği

kaçırabilir; ödünleşim bu.

`ImageRenderer`'ın yetmediği yer

ImageRenderer switch'leri placeholder olarak çiziyor, preferredColorScheme'ı uygulamıyor ve bir

view'a focus vermiyor. Üç düzeltme tam da bu davranışlara bağlıydı. Bu yüzden HostedViewTests

(yalnız macOS) view'ları AppKit'te host ediyor, gerçek bir pencerede çiziyor ve pikselleri geri

okuyor. Host edilen text field'lar focus için yarıştığından suite sıralı (serialized) koşuyor.

Bilinçli bir sınır

Paketteki bileşenler aktif temayı değil, renk token'larını okuyor. Hazır ve marka temaları

@Environment(\.dsTheme) üzerinden sizin view'larınızı yeniden renklendiriyor, ama DSButton ya da

DSCard'ı değil. README bunu yazıyor ve yol haritasının ilk maddesi bu. Her bileşeni temaya bağlamak

her bileşenin API'sini ve testlerini değiştirecekti. 2.0'ın hedefi önce çalışan ve test edilen kodu

geri getirmekti.

Ne ters gitti: toast zamanlayıcısı

DSToastManager.show her toast için bir zamanlayıcı başlatıyor. Geri getirilen kodda bununla ilgili

üç sorun vardı ve testi de çalışana kadar iki kez kırıldı.

İlk sorun bir yarıştı. Aynı ID ile değiştirilen ya da erken kapatılan bir toast'ın eski zamanlayıcısı

çalışmaya devam ediyor, sonra yeni toast'ı kapatıyordu. İkincisi bir çökmeydi. Süre

UInt64(toast.duration * 1_000_000_000) ile dönüştürülüyordu; bu .infinity ve çok büyük değerlerde

trap ediyordu (commit 9d51b5e). Düzeltme, zamanlayıcıları ID'ye göre bir sözlükte tutuyor, show

ya da dismiss çağrısında eskisini iptal ediyor, uyandıktan sonra Task.isCancelled kontrol ediyor ve

sonlu olmayan bir süre için hiç zamanlayıcı kurmuyor:

swift
1guard toast.duration > 0, toast.duration.isFinite else { return }
2let id = toast.id
3let delay = Duration.seconds(min(toast.duration, Self.maximumDuration))
4dismissTimers[id] = Task { [weak self] in
5 try? await Task.sleep(for: delay)
6 // A cancelled timer belongs to a replaced or dismissed toast.
7 guard !Task.isCancelled else { return }
8 self?.dismiss(id: id)
9}

maximumDuration 24 saat. Parametreli bir test .infinity, 1e300 ve .nan değerlerini kapsıyor.

Üçüncü sorun testin kendisiydi. Test önce 400 ms bekliyordu ve yüklü bir iOS simulator'ında kırıldı.

2ff8316 commit'i bu beklemeyi en fazla beş saniyelik bir polling ile değiştirdi. Sonra #21 numaralı

pull request'te test, GitHub'ın macos-26 runner'ında (Xcode 26.6) yine kırıldı. 97d7d3b

commit mesajına göre toast, test başladıktan 7,04 saniye sonra hâlâ ekrandaydı. Zamanlayıcı görevi

de polling döngüsü de main actor'da koşuyor; paralel çalışan render ve hosted-view testleri main

actor'ı saniyelerce meşgul ediyordu. Saate dayalı her son tarih bir tahmindi.

Son hâli olayın kendisini bekliyor:

swift
1@Test(.timeLimit(.minutes(1)))
2func autoDismissesAfterDuration() async throws {
3 let manager = DSToastManager()
4 manager.show(DSToastData(id: "short", title: "Bye", duration: 0.05))
5 #expect(manager.toasts.count == 1)
6 
7 // Await the timer itself, not a wall-clock deadline: while other tests
8 // keep the main actor busy, the timer can run seconds after 50 ms.
9 let timer = try #require(manager.dismissTimers["short"])
10 await timer.value
11 #expect(manager.toasts.isEmpty)
12}

Test modülü okuyabilsin diye dismissTimers private(set) oldu; zaman sınırı da zamanlayıcı hiç

bitmezse testi durduruyor. Zamanlayıcıdan dismiss çağrısını kaldırınca test hâlâ kırılıyor. Ders şu:

async bir testte saati değil, beklediğiniz şeyin kendisini await edin.

Benzer bir ders tema sağlayıcıdan geldi. Şubat'taki DSThemeProvider her zaman

preferredColorScheme(themeManager.currentTheme.colorScheme) uyguluyordu; sistemi takip eden bir

uygulama bu yüzden dark moda hiç geçmiyordu. Düzeltme, şemayı yalnız açıkça seçilmiş bir temada

zorluyor (followSystem ? nil : currentTheme.colorScheme). İlk regresyon testi yalnız bu yardımcıyı

çağırıyordu; view onu kullanmayı bıraksa test yine geçerdi. e3443b2 commit'i bunun yerine host

edilen bir test ekledi: pencereyi .aqua'dan .darkAqua'ya çeviriyor ve alt view'ın dark şemayı

gördüğünü kontrol ediyor. Yardımcıyı test etmek ile view'ın davranışını test etmenin farklı şeyler

olduğu ortaya çıktı.

Kullanım

Paketi Swift Package Manager ile ekleyin (from: "2.0.0"), DesignSystemPro'yu import edin ve tema

sağlayıcıyla toast katmanını kurmak için kök view'ı DSApp { ContentView() } ile sarın. Aşağıdaki

README'deki hızlı başlangıç örneği; README'deki diğer Swift parçaları gibi (Package.swift alıntısı

hariç) Tests/DesignSystemProTests/ReadmeExamples.swift içinde derlenip çalıştırılıyor:

swift
1import DesignSystemPro
2import SwiftUI
3 
4struct ContentView: View {
5 @State private var notificationsOn = true
6 
7 var body: some View {
8 VStack(spacing: SpacingTokens.Content.comfortable) {
9 Text("Hello, DesignSystemPro")
10 .dsTypography(TypographyTokens.Heading.h2)
11 
12 DSStatusBadge("Ready", variant: .success)
13 
14 DSToggle(isOn: $notificationsOn, label: "Notifications")
15 
16 DSButton("Continue", isFullWidth: true) {
17 notificationsOn.toggle()
18 }
19 }
20 .padding(SpacingTokens.Layout.screenEdge)
21 }
22}

ThemeManager.shared.setTheme(PresetThemes.ocean) hazır bir temaya geçiyor; BrandConfiguration ve

WhiteLabelConfiguration marka renklerinden tema üretiyor. ColorContrastUtility.contrastRatio(_:_:)

ve meetsWCAG_AA(_:_:) kendi renk çiftlerinizi kontrol ediyor. Examples/DesignSystemProDemo

altındaki örnek uygulama iOS ve macOS'ta çalışıyor; her bileşen ailesini ve bir tema değiştiriciyi

gösteriyor. DocC dokümantasyonu GitHub Pages'te yayında.

v2.0.0 tag'inde swift test 11 suite'te 52 test raporluyor; sürümün merge commit'indeki CI koşusu

da aynı sayıyı veriyor. CI ayrıca paketi iOS, macOS, tvOS, watchOS ve visionOS için derliyor.

Sınırlar

  • Bileşenler henüz aktif temayı okumuyor; hazır ve marka temaları yalnız sizin view'larınızı etkiliyor.
  • Açık palet renklerindeki avatar baş harfleri ve dark modda checkbox tiki hâlâ WCAG AA'nın altında.

İkisi de yol haritasında.

  • 1.x bileşenleri (alert, modal, navigation bar, tab bar, chart, progress indicator) v2 token'larıyla

derlenmediği için geri getirilmedi.

  • DSTextArea, TextEditor kullandığı için tvOS ve watchOS'ta yok.
  • Fingerprint testi bilerek kaba: bir hücrenin ortalama rengini değiştirmeyecek kadar küçük bir

değişiklik testten geçer.

  • Performans benchmark'ı yok. Eski README'deki benchmark tabloları, onları üreten bir benchmark

olmadığı için kaldırıldı.

Kaynak kod: github.com/muhittincamdali/SwiftUI-Design-System-Pro

Etiketler

#swiftui#design-system#design-tokens#swift-testing#accessibility
Muhittin Çamdalı

Muhittin Çamdalı

Senior Mobile Engineer

12+ yıllık deneyime sahip Senior Mobile Engineer. iOS, Flutter ve React Native ile mobil ürünler geliştiriyorum; full-stack ve AI-native çalışıyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Onay e-postasındaki bağlantıyı açıp “Aboneliğimi onayla” düğmesine bastığında aboneliğin başlar. Bültende açılma/tıklama istatistikleri tutulur; dilediğin an tek tıkla ayrılabilirsin. Gizlilik

Paylaş

İlgili İçerik