İçindekiler
Problem
Bir visionOS uygulamasında aynı küçük işler tekrar tekrar karşınıza çıkar. Beş kart, izleyiciye doğru kıvrılan bir yayın üzerinde durmalıdır. Bir düğme immersive space açmalı ve space daha açılırken düğmeye ikinci kez basılınca düğmeyle sistem farklı şeyler düşünür hâle gelmemelidir. Bir model sürüklenebilmeli ama kutusunun dışına çıkmamalı, sıkıştırma hareketi de onu yok denecek kadar küçültmemelidir. Bunların hiçbiri tek başına zor değildir, ama her biri kolayca yanlış yapılan bir geometri ya da state ayrıntısı saklar: bir öğenin hangi yöne baktığı, SwiftUI'nin point'leriyle RealityKit'in metrelerinin ilişkisi, bir gesture'ın her karede ne bildirdiği ve bir konuma NaN girince ne olduğu.
VisionOS-UI-Framework, VisionUI adıyla import edilen ve bu parçaları bir araya getiren bir Swift paketidir. 2.0.0 sürümünde beş layout (arc, ring, grid, sphere ve helix), ring üzerine kurulu bir SpatialCarousel, bir ImmersiveSpaceCoordinator, sınırlı bir ManipulationState ve birkaç bileşen var: SpatialCard, visionOS'ta ornament'a dönüşen bir toolbar ve Apple'ın yönergelerine göre boyutlanan dokunma alanları. Paket visionOS 2, iOS 18 ve macOS 15'i destekler ve hiçbir bağımlılığı yoktur.
Tasarım kararları ve alternatifler
Layout, veri üreten bir değerdir. SpatialLayout'un tek bir gereksinimi var: placements(count:) -> [SpatialPlacement]. Bu fonksiyon her öğe için metre cinsinden bir konum ve bir yön döndürür. Akla ilk gelen alternatif SwiftUI'nin Layout protokolüydü, ama o iki boyutta çalışır: boyut önerir ve nokta yerleştirir; derinlik ya da dönüş ifade edemez ve RealityKit entity'lerine hiç uygulanmaz. Her layout için bir view ve ayrı bir RealityKit yardımcısı yazmak matematiği iki kez yazmak demekti. Ortada saf bir fonksiyon olunca SpatialLayoutView ve Entity.arrangeChildren(using:) aynı çıktıyı kullanıyor, testler de geometriyi view oluşturmadan kontrol ediyor. Bedeli şu: layout, SwiftUI'nin ölçüm adımına katılmaz; bir öğenin ne kadar büyük olduğunu bilmez, bu yüzden aralık bir argüman olarak verilir.
Metre ve RealityKit eksenleri. Konumlar metre cinsindendir; +y yukarı, +z izleyiciye doğrudur. RealityKit zaten metreyle çalışır, visionOS'ta ortamdaki physicalMetrics de metreyi point'e çevirir. SwiftUI'de y ekseni aşağı baktığı için dönüşüm her view'da tekrarlanmak yerine tek bir yerde (SpatialUnits) duruyor.
iOS ve macOS'ta boşluk yerine düz bir projeksiyon. #if os(visionOS) altında view'lar offset(z:) ve rotation3DEffect ile gerçek derinlik kazanır. iOS ve macOS'ta FlatProjection layout'u önden görünüşle çizer: orijinden z metre daha yakın bir öğe distance / (distance - z) kat büyük çizilir, bu oran 0.2...3 aralığına sınırlanır ve derinlik zIndex olur. Bu gerçek 3D değil, bir yaklaşım; ama aynı API'nin iPhone ve Mac'te çalışmasını, testlerin de gözlük olmadan view'ları ImageRenderer ile render edip pikselleri okumasını sağlıyor.
Immersive space için bir durum makinesi. Elle tutulan bir Bool, biri geçiş sırasında iki kez dokunduğu anda sistemden kopar. ImmersiveSpaceCoordinator, tek bir phase tutan @MainActor @Observable bir sınıftır: .closed, .opening(id), .open(id) ya da .closing(id). Geçiş sırasında gelen istek .busy döner ve hiçbir şeyi değiştirmez. Bir space açıkken başka bir space istenirse önce açık olan kapatılır; açık olan tekrar istenirse .alreadyOpen döner. Her istek bir generation sayacını artırır. Sistem cevap verdiğinde coordinator, phase'e dokunmadan önce request == generation koşulunu kontrol eder; böylece yerine yenisi gelmiş bir isteğin geç cevabı yok sayılır. Kullanıcı space'i Digital Crown ile kapatırsa uygulama, space'in onDisappear'ından spaceDidDisappear(_:) çağırır. Sistem çağrıları iki closure'dan oluşan ImmersiveSpaceActions üzerinden geçer, çünkü OpenImmersiveSpaceAction ortamdan gelir ve testte oluşturulamaz. Testler, bir isteği bilerek açık tutan sahte bir sistem kullanır; bu da yarış durumunu tekrarlanabilir kılar.
Değer tipi olarak manipulation. ManipulationState konumu, ölçeği ve yönü tutar; ManipulationConstraints ise sınırları, ölçek aralığını, dönüş modunu (serbest, yalnız yaw ya da kilitli) ve isteğe bağlı bir yaw snap açısını. spatialManipulation(_:) modifier'ı yalnızca SwiftUI gesture'larını drag(translation:), magnify(by:) ve rotate(by:) çağrılarına çevirir. Kuralların bir struct'ta durması, onların gesture olmadan test edilmesini sağlıyor. SwiftUI gesture'ları başladıklarından beri olan toplam değişimi bildirir; bu yüzden state her gesture için ayrı bir başlangıç değeri tutar (dragStart, magnifyStart, rotationStart). Aynı anda yapılan sürükleme ve sıkıştırma birbirinin başlangıç noktasını ezmez.
Savunmacı ve belgelenmiş girdi kontrolü. Bir sayı ya da uzunluk bir dosyadan veya sunucudan gelebilir. Öğe sayısı en fazla 4096, uzunluklar en fazla 10 km'dir; NaN ya da sonsuz argümanlar belgelenmiş varsayılanlara döner. Bir view modifier'ı hata fırlatamadığı için değeri düzeltip bunu dokümantasyonda söylemek en pratik seçenekti. Riski, sessiz düzeltmenin çağıranın hatasını gizleyebilmesi.
Ne ters gitti, ne öğrenildi
Repo, bugün içinde duran paketten daha eski. Ağustos 2025'te açıldı ve ilk ayı çoğunlukla doküman sayfalarına ve workflow'lara gitti. 1.0.0 Şubat 2026'da etiketlendi. 2.0.0 CHANGELOG'unun yazdığı gibi, manifest'i var olmayan bir platform sürümü (.visionOS(.v1_0)) bildiriyordu; Swift Package Manager paketi çözemiyordu. Bu düzeltildiğinde bile kaynaklar visionOS SDK'sında olmayan API'leri çağırıyordu, örneğin RealityViewContent.physicsWorld ve SpatialDragGesture. Haziran 2026'da bileşenler silindi ve yerlerine yer tutucular geldi: bir sürüm sabiti, yalnızca gölge çizen bir modifier ve tek satır yazdıran bir VolumetricWindowManager.present(). 15 Ekim 2026'daki 5c17403 commit'i bunların hepsini kaldırdı ve paket yeniden yazıldı. Her public isim değiştiği için sonuç yeni bir major sürüm oldu. Çıkarılan ders yeni CI'da duruyor: testler, örnek uygulama ve dokümantasyon her pull request'te derleniyor; testler ve örnek uygulama üç platformda.
Yeniden kurulumun da anlatmaya değer bir hatası var. ManipulationState, bir quaternion'u normalize etmeden önce simd_length(vector) > 1e-6 kontrolü yapıyordu. [3e38, 3e38, 0, 0] gibi sonlu ama devasa bir değerde uzunluk sonsuza taşar, kontrol geçer ve simd_normalize sıfır quaternion döndürür. Initializer bunu saklıyordu; matrix'in ilk üç sütunu sıfır oluyor, nesne bir noktaya çöküyordu. Dokümantasyon sıfır quaternion'ların yok sayıldığını söylüyordu, yani kod onunla çelişiyordu. 5f6c475 commit'i iç kullanımlı bir unitQuaternion(_:) ekledi: vektörü ölçmeden önce en büyük bileşenine böler, böylece uzunluk 1 ile 2 arasında kalır; NaN, sonsuz ya da sıfıra çok yakın girdide nil döner. Initializer, constraints değişiklikleri ve rotate(by:) bunu kullanıyor. NaN ve sonsuzluğu kontrol etmek yetmiyordu: taşan sonlu değerler ayrı bir durum.
İkinci bir düzeltme testler hakkında bir şey öğretti. SpatialLayout public olduğu için kullanıcının yazdığı bir layout NaN konum döndürebilir. f3f8c0e commit'i SpatialPlacement.sanitized'ı ve bozuk bir layout'u çizen bir piksel testi ekledi. 75e0e4e commit'inin kaydettiği gibi bu test, düzeltme geri alındığında da geçiyordu, çünkü FlatProjection sonlu olmayan bir konumu zaten orijine çeviriyordu. Testteki layout artık [.nan, 0.025, 0] döndürüyor: sanitize işlemi yalnızca NaN'ı 0 yapar ve renk örneğini merkezin 25 point üstüne çizer; ham yerleşim ise ortaya düşer. Test artık düzeltme olmadan kırılıyor. Yeni yazılmış yeşil bir test, koruduğu değişiklik olmadan kırıldığı görülene kadar pek bir şey kanıtlamaz.
Kullanım
Paketi Xcode'da File › Add Package Dependencies… ile ya da Package.swift içinde ekleyin:
swift
1dependencies: [2 .package(url: "https://github.com/muhittincamdali/VisionOS-UI-Framework.git", from: "2.0.0")3],4targets: [5 .target(name: "YourApp", dependencies: [6 .product(name: "VisionUI", package: "VisionOS-UI-Framework")7 ])8]README'deki hızlı başlangıç örneği beş kartı bir yaya dizer:
swift
1import SwiftUI2import VisionUI3 4struct ContentView: View {5 let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter"]6 7 var body: some View {8 SpatialLayoutView(.arc(radius: 0.6, spacing: 0.17), planets, id: \.self) { planet in9 SpatialCard {10 Text(planet)11 .font(.title)12 }13 }14 }15}Apple Vision Pro simülatöründe ortadaki kart pencerenin düzleminde durur, dıştaki kartlar daha yakına gelir ve yayın merkezine döner. iPhone ya da Mac'te yay önden görünüşle çizilir. README örnekleri Examples/SpatialGallery/SpatialGallery/ReadmeSamples.swift dosyasında duruyor; bu dosyayı örnek uygulama derliyor, test target'ı da sembolik bağ üzerinden derliyor. Böylece derlenmez hâle gelen bir örnek build'i kırar. Aynı dosyadaki sınırlı manipulation örneği:
swift
1struct ManipulationSample: View {2 @State private var globe = ManipulationState(constraints: ManipulationConstraints(3 bounds: SpatialBounds(center: .zero, size: [0.4, 0.3, 0.2]),4 scaleRange: 0.5...2,5 rotation: .yaw,6 yawSnapAngle: .pi / 47 ))8 9 var body: some View {10 Image(systemName: "globe.americas.fill")11 .font(.system(size: 120))12 .foregroundStyle(.blue)13 .spatialManipulation($globe)14 }15}Küre 40 × 30 × 20 cm'lik bir kutunun içinde kalır, yarı boyutla iki kat boyut arasında ölçeklenir, yalnızca dikey eksen etrafında döner ve dönüş bitince 45°'ye oturur.
Sınırlar
- Yalnızca simülatör. Repodaki ekran görüntüleri Apple Vision Pro simülatöründen alınmış ve benchmark yok. Gerçek cihazda gesture hissi ve performans belgelenmemiş.
- visionOS view yolunun otomatik kontrolü daha az. 97 test macOS'ta ve iPhone simülatöründe koşuyor; visionOS simülatöründe piksel testleri derlemeye girmediği için 87'si koşuyor.
physicalMetricsveRotateGesture3Dkullanan visionOS dalları piksel testleriyle değil, state testleri ve örnek uygulamanın derlenmesiyle kapsanıyor. - Coordinator sahte bir sisteme karşı test ediliyor. Sahte sistem durum makinesinin her durumunu çalıştırıyor, ama gerçek
openImmersiveSpacedeğil.ImmersiveSpaceTogglevisionOS'a ya da SwiftUI'de immersive space'in bulunduğu macOS 26'ya ihtiyaç duyuyor. - Düz projeksiyon 3D değil. iOS ve macOS'ta derinliği ölçek ve sırayla taklit ediyor.
- Kapsam dar. Hand tracking, eye tracking, ARKit ya da spatial audio yok. Yol haritasında ARKit'in bulduğu yüzeyleri izleyen layout'lar ve bir yay boyunca kayan carousel var.
- Araç zinciri. Manifest
swift-tools-version: 6.2kullanıyor, yani projenin Xcode 26 ya da daha yenisine ihtiyacı var.
Paket, DocC makaleleri ve örnek uygulama MIT lisansıyla GitHub'da.

