İçindekiler
- Problem
- Tasarım kararları ve alternatifler
- İki katmanlı renk token'ları
- Luminance için `Color.resolve(in:)`
- Enjekte edilebilen Observable yöneticiler
- Piksel snapshot yerine fingerprint
- `ImageRenderer`'ın yetmediği yer
- Bilinçli bir sınır
- Ne ters gitti: toast zamanlayıcısı
- Kullanım
- Sınırlar
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.id3let delay = Duration.seconds(min(toast.duration, Self.maximumDuration))4dismissTimers[id] = Task { [weak self] in5 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 tests8 // keep the main actor busy, the timer can run seconds after 50 ms.9 let timer = try #require(manager.dismissTimers["short"])10 await timer.value11 #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 DesignSystemPro2import SwiftUI3 4struct ContentView: View {5 @State private var notificationsOn = true6 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,TextEditorkullandığı 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

