İçindekiler
Problem
iOS 17 ve macOS 14'ten beri SwiftUI, üç modifier aracılığıyla her view üzerinde Metal shader
çalıştırabiliyor: colorEffect, distortionEffect ve layerEffect. API küçük, ama onu ham
haliyle kullanmak şu demek: bir .metal dosyası yazmak, onu doğru bundle'dan yüklemek,
argümanları Metal fonksiyonunun tanımladığı sırayla birebir geçmek ve GPU'nun bir NaN'la, sıfıra
bölmeyle ya da 32-bit float'a sığmayan bir değerle ne yaptığını bilmek. Shader'da hata yönetimi
yok: kötü bir değer bozuk bir görüntü çizer ya da hiçbir şey çizmez; argümanları fonksiyonla
uyuşmayan bir çağrı ise hiç çizim yapmaz.
SwiftShaders bu işi paketliyor. Bir efekt, herhangi bir view'a eklenen tek bir modifier:
.halftone(dotSize: 6), .wave(amplitude: 5) ya da .transition(.dissolve) gibi. 2.0.0
sürümü, 31 [[ stitchable ]] Metal fonksiyonuna dayanan 26 view modifier'ı, 3 fill ve 5
transition içeriyor ve hiçbir bağımlılığı yok.
Tasarım kararları ve alternatifler
MTKView yerine SwiftUI'nin shader modifier'ları. Bir MTKView'ı UIViewRepresentable
ile sarmak compute pass'lere ve çok geçişli render'a izin verirdi. Ama efekti SwiftUI'nin view
ağacından da çıkarırdı; layout, animasyon, hit testing, dark mode ve ImageRenderer etrafında
her şeyi yeniden kurmak gerekirdi. colorEffect, distortionEffect ve layerEffect üzerinde
kalmak, efekti sıradan bir view modifier'ı olarak tutuyor. Bedeli SwiftUI'nin sınırları: shader
başına tek geçiş ve en düşük sürüm olarak iOS 17.
Her efektin altında tek bir modifier. Her public efekt, argümanları kontrol edip bir shader'ı
dahili ShaderEffect modifier'ına veren birkaç satırdan ibaret. Bu modifier view'ı
visualEffect ile ölçüyor, animasyonlu efektler için bir TimelineView çalıştırıyor, Reduce
Motion açıkken animasyonu durduruyor ve üç türden birini seçiyor: color, distortion ya da layer.
Alternatif, yani her efekt için ayrı bir ViewModifier, v1.0.0 etiketinin yaptığıydı; 31 shader'la aynı
zamanlama ve ölçüm mantığının 31 kopyası demek olurdu.
Layer efektleri bir compositing group üzerinde. compositingGroup() olmadan SwiftUI bir
layer efektini view'ın her parçasına ayrı ayrı uygular; örneğin arka plana ve önündeki yazıya.
sketch ve emboss gibi komşu pikselleri karşılaştıran shader'lar o zaman parçalar arasında
aslında olmayan kenarlar bulur. Grubun neden orada olduğunu ShaderEffect.swift içindeki yorum
ve "How effects are drawn" DocC makalesi kayıt altına alıyor.
Her public argüman GPU'ya ulaşmadan kontrol ediliyor. Bir view modifier'ı hata fırlatamaz;
bu yüzden paket kötü girdiyi güvenli değerlere çeviriyor. Bu kontrollerin hepsi ShaderInput
içinde: aralıklar sınırlanıyor, NaN efekti kapatıyor, açılar tek bir tura katlanıyor ve konumlar
±1.000.000 noktayla sınırlanıyor. Sample offset'ler view'ın köşegeniyle sınırlı, çünkü SwiftUI
distortion ve layer efektlerini, view'dan her kenarda bu offset kadar büyük bir offscreen
katmana çiziyor. Sınırsız bir offset çok büyük bir katman için bellek ayrılmasına yol açabilir; köşegenden daha uzaktaki
içerik zaten view'ın dışında ve saydam. sampleOffsetsAreBounded testi sekiz efekte .infinity
ve 1e12 veriyor ve offset'in köşegeni aşmamasını bekliyor.
Zaman her saat başı sarılıyor. Shader'lar 32-bit float ile hesap yapıyor ve sayılar
büyüdükçe hassasiyet kayboluyor. ShaderClock geçen süreyi 3.600 saniyede sarıyor ve bunu hızla
çarpmadan önce yapıyor ki çarpım sonlu kalsın. Animasyonlu efektler bir ShaderTime alıyor:
.animated, .animated(speed:) ya da preview'larda, snapshot testlerinde veya kendi sürdüğünüz
bir animasyonda tek kare göstermek için .fixed(_:).
Premultiplied renk disiplini. SwiftUI shader'lara premultiplied renk veriyor. Ton ya da
parlaklık değiştiren efektler ShaderCommon.h içindeki bir yardımcıyla düz renge geçiyor,
işlemi yapıyor ve sonra yeniden alfayla çarpıyor; böylece kenar yumuşatmalı yazı kenarları
kararmıyor. Komşu piksel okuyan efektler okumalarını view'ın içine sınırlıyor; bu da sharpen ya
da sketch uygulanmış view'larda koyu veya parlak bir çerçeve oluşmasını önlüyor.
Ayrılabilir bir blur. progressiveBlur, tek bir 49 × 49 geçiş yerine her biri 49 örnekli
yatay ve dikey iki geçiş çalıştırıyor; piksel başına maliyet yarıçapa bağlı değil.
Shader'lar paketin bundle'ından yükleniyor. ShaderLibrary.swiftShaders,
ShaderLibrary.bundle(.module) demek; her fonksiyon adı ss ile başlıyor ki uygulamadaki
shader'larla çakışmasın.
Ne bozuktu, ne öğrendim
2.0.0'ın CHANGELOG'u bir itirafla açılıyor. Şubat 2026 kodu, yani ilk commit'teki v1.0.0
etiketi ve 3-5 Şubat'ta eklenen etiketsiz shader'lar, bir uygulamada çalışmıyordu. Bu kodu
Haziran sadeleştirmesi (09a2dad) sildi. CHANGELOG'a ve neden geri getirilmediğini açıklayan
ff67521 commit'ine göre bazı .metal dosyaları
SwiftUI_Metal.h'ı include etmeden SwiftUI::Layer kullanıyordu, Swift sarmalayıcıları Metal
imzalarıyla uyuşmayan argümanlar geçiyordu ve shader'lar paketin değil uygulamanın ana
bundle'ında aranıyordu. Eksik include, etiketten sonra eklenen CRTShader.metal ve
SwirlShader.metal gibi etiketsiz dosyalarda; v1.0.0'daki üç .metal dosyası başlığı include
ediyor. Bundle sorunu ise v1.0.0 kaynağında görülebiliyor: shader önbelleği
fonksiyonları ShaderLibrary.default üzerinden çözüyor, .metal dosyaları ise paketin resource
bundle'ında duruyor. Derlenen bir build bile hiçbir efekt göstermezdi.
Haziran 2026'da bu kod düzeltilmedi, yer tutucularla değiştirildi: yalnızca opaklığı düşüren bir
modifier ve shader'la ilgisi olmayan circuit breaker, retry ve event bus tipleri. README hâlâ
geniş bir shader koleksiyonu vaat ediyordu; CI ise yeşil kalıyordu, çünkü swift test adımı
continue-on-error: true olarak işaretliydi. Yeşil bir check hiçbir şey kanıtlamıyordu.
2.0.0, Ekim 2026'da sıfırdan yazıldı ve testleri, Şubat kodunun sessizce bozulduğu yerlerde yüksek
sesle kırılacak şekilde tasarlandı. LibraryTests, derlenmiş default.metallib'i paket
bundle'ından yüklüyor ve içinde Swift kodunun çağırdığı 31 fonksiyon adının birebir bulunduğunu
kontrol ediyor; iki taraftan birinde yapılan bir yeniden adlandırma, hiçbir şey çizmemek yerine
bir testi kırıyor.
Yeni kodun incelemesi yine de iki shader hatası buldu; ikisi de f9205c3 ile düzeltildi:
ssGradientMap,colors[index]ilecolors[index + 1]'i karıştırıyor. Swift API'si her
zaman en az iki renk geçiriyor, ama ShaderLibrary.swiftShaders public; boş ya da tek renkli
bir diziyle yapılan doğrudan çağrı, shader'ın kendisine verilmemiş GPU belleğini okumasına yol
açıyordu. Artık boş dizide orijinal rengi döndürüyor, tek renkte ise orijinali amount oranında o renge
doğru karıştırıyor.
directGradientMapCalls testi shader'ı sıfır ve tek renkle doğrudan çağırıyor.
ssDitherpiksel konumunu unsigned tamsayıya çeviriyordu. Bir color efekti, view'ının dışına
çizilen içerikte negatif konum görebilir ve bu dönüşüm Metal'de tanımsız. Düzeltme, her konum
için 0...3 veren işaretli tamsayı ve bitwise AND kullanıyor.
Bu düzeltmenin dither yarısı testsiz yayınlandı. Ardından gelen 4642ff9 commit'i
ditherNegativePositions testini ekliyor: kısmen negatif x'te çizilen gri bir alan, iki
tarafta da aynı 4 × 4 deseni göstermeli. Commit mesajı, testin eski uint2(...) % 4 ile
kırıldığını, düzeltmeyle geçtiğini not ediyor.
Daha küçük bir bulgu (e4a813c): ShaderLibrary.swiftShaders'ın doküman yorumundaki örnek
ssSepia'yı tek bir float ile çağırıyordu; oysa fonksiyon önce view'ın boyutunu bekliyor, yani
örnek hiçbir şey çizmiyordu. Artık tüm argümanları çağıran tarafın verdiği ssCheckerboard
kullanılıyor.
Bu repodan çıkardığım ders: özelliklerin sayısı, her birinin çalıştığının kanıtından daha az
önemli. Yani efekt bozulduğunda kırılan bir test ve kırmızıya dönmesine izin verilen bir CI.
Kullanım
Paketi Xcode'da File › Add Package Dependencies… ile ya da Package.swift içinde ekleyin:
swift
1.package(url: "https://github.com/muhittincamdali/SwiftShaders.git", from: "2.0.0")README'deki quick start; aynı kod örnek uygulamanın ContentView'ı ve test süiti tarafından da
derlenip render ediliyor:
swift
1import SwiftShaders2import SwiftUI3 4struct ContentView: View {5 var body: some View {6 Image(systemName: "mountain.2.fill")7 .font(.system(size: 96))8 .foregroundStyle(.blue)9 .padding(40)10 .background(.blue.opacity(0.15), in: .rect(cornerRadius: 24))11 .halftone(dotSize: 6, ink: .blue, paper: .white)12 }13}Efektler birleştirilebiliyor ve view'dan dışarı doğru sırayla çalışıyor:
swift
1struct RetroScreenSample: View {2 var body: some View {3 Text("GAME OVER")4 .font(.system(size: 44, weight: .black, design: .monospaced))5 .foregroundStyle(.cyan)6 .padding(32)7 .background(Color(red: 0.02, green: 0.07, blue: 0.2))8 .chromaticAberration(offset: 2)9 .scanlines(spacing: 3, intensity: 0.4)10 .vignette(intensity: 0.7)11 }12}Testler view'ları ImageRenderer ile 1x ölçekte render ediyor ve pikselleri Swift'te hesaplanan
değerlerle karşılaştırıyor. ImageRenderer, hiçbir şey çizmeyen bir view için önceki bir
görüntüyü döndürebildiğinden her view iki kez, siyah ve beyaz zemin üzerinde render ediliyor;
her pikselin rengi ve alfası bu çiftten geri çıkarılıyor. v2.0.0 etiketinde Swift 6.4 ile
swift test, 12 suite'te 80 testin geçtiğini bildiriyor; bunların 3'ü yalnızca README
görsellerini üretiyor ve SCREENSHOT_DIR verilmedikçe atlanıyor, yani 77'si koşuyor.
Sınırlar
- Platformlar: yalnızca iOS 17 ve macOS 14. tvOS ve visionOS
v1.0.0'da listeleniyordu ama hiç
derlenmemişti; test edilene kadar manifest'e girmiyorlar.
- Toolchain:
swift-tools-version: 6.2, yani Xcode 26 ya da üstü. Komut satırında
swift build ve swift test için Swift 6.4 gerekiyor; .metal dosyalarını derleyen Swift
Build motoru orada varsayılan. Eski native build system dosyaları derlemeden kopyalıyor ve
efektler çizilmiyor. Hangi durumda olduğunuzu SwiftShaders.isMetalLibraryAvailable
söylüyor. Aynı nedenle ortak CI workflow'u swift test'i atlıyor; testler macOS'ta ve iPhone
simülatöründe xcodebuild ile koşuyor.
- Shader başına tek geçiş: fluid ya da particle simülasyonu gibi birden çok geçiş veya
compute gerektiren efektler kapsam dışı. Şubat 2026'da, v1.0.0 etiketinden sonra
eklenen fire, raymarching, particle ve fluid shader'ları geri getirilmedi.
- Hız ölçülmedi: benchmark ya da cihaz GPU profili yok. Testler macOS'ta ve simülatörde
piksel doğruluğunu kontrol ediyor, kare süresini değil; piksel çıktısı fiziksel bir cihazda
doğrulanmadı.
- Yol haritası (README'den): Swift Package Index kaydı (
.spi.ymlhazır), test edildikten
sonra tvOS ve visionOS, ve herhangi bir şekli izleyen maskeli bir progressive blur.
Kaynak kod: github.com/muhittincamdali/SwiftShaders

