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

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

SwiftShaders 2.0: SwiftUI modifier'ı olarak Metal efektleri ve 1.0'ın neden gitmesi gerekti

Özet

SwiftShaders 2.0, 31 Metal efektini SwiftUI'nin shader modifier'ları üzerine nasıl kuruyor, argümanları ve pikselleri nasıl test ediyor; 1.0'da ne bozuktu.

  • iOS 17 ve macOS 14'ten beri SwiftUI, üç modifier aracılığıyla her view üzerinde Metal shader çalıştırabiliyor: colorEffect, distortionEffect ve layerEffect.
  • MTKView yerine SwiftUI'nin shader modifier'ları.
  • Kaynak kod: [github.com/muhittincamdali/SwiftShaders](https://github.com/muhittincamdali/SwiftShaders)
  • 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 2.0: SwiftUI modifier'ı olarak Metal efektleri ve 1.0'ın neden gitmesi gerekti

İç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] ile colors[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.

  • ssDither piksel 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 SwiftShaders
2import SwiftUI
3 
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.yml hazır), test edildikten

sonra tvOS ve visionOS, ve herhangi bir şekli izleyen maskeli bir progressive blur.

Kaynak kod: github.com/muhittincamdali/SwiftShaders

Etiketler

#swift#swiftui#metal#shaders#testing
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