Wzorce przestrzenne visionOS: poza oknem
Większość aplikacji wydawanych na visionOS trafia na tę platformę ścieżką zgodności „Designed for iPad”: istniejąca aplikacja iPadowa działa jako płaski panel unoszący się w przestrzeni 3D, a deweloper zaznacza pole wyboru, zamiast budować doświadczenie natywne dla visionOS. Dla użytkownika to rozwiązanie wystarczające — aplikacja działa — ale nie oddaje sprawiedliwości platformie. Natywna warstwa visionOS daje deweloperom trzy sposoby prezentacji (okna, wolumeny i przestrzenie immersyjne) oraz strukturalne prymitywy UI (ornamenty, attachments), których SDK iPada nie oferuje.4 Aplikacje, które z nich korzystają, sprawiają wrażenie natywnych; te, które tego nie robią, wyglądają jak iPad przeniesiony na Vision Pro.
Ten tekst przechodzi przez słownik przestrzenny visionOS, zestawiając go z dokumentacją Apple. Ramą jest pytanie „co platforma faktycznie oferuje aplikacji SwiftUI”, a nie wprowadzenie do visionOS. Wpis RealityKit i przestrzenny model myślowy z tego samego cyklu opisuje warstwę treści 3D; tutaj chodzi o warstwę SwiftUI, która ją zawiera.
TL;DR
- Aplikacje visionOS składają się z trzech typów scen:
WindowGroup(okna),WindowGroupz.windowStyle(.volumetric)(wolumeny) orazImmersiveSpace(przestrzenie immersyjne)1. - Okno to płaszczyzna 2D, wolumen to ograniczony obszar 3D, a przestrzeń immersyjna otacza użytkownika. Każdy typ rządzi się innymi prawami: wolumen ma po utworzeniu niezmienny rozmiar, przestrzeń immersyjna wymaga jawnego otwarcia i zamknięcia, a okno zachowuje się najbardziej podobnie do iPadowego.
- Immersja występuje w trzech stylach:
.mixed(treść współistnieje z pokojem),.full(pokój zastąpiony wirtualnym otoczeniem),.progressive(rozwiązanie pośrednie, zachowujące peryferyjne zakotwiczenie w rzeczywistości)2. - Ornamenty to płaszczyzny UI równoległe do okna, wysunięte przed nie na osi z. W ten sposób visionOS realizuje paski narzędzi i paski kart3. Attachments osadzają widoki SwiftUI wewnątrz treści 3D w RealityView — to most między płaskim interfejsem a geometrią przestrzenną.
- Antywzorzec „aplikacji-panelu”: wydanie interfejsu iPadowego jako pojedynczego okna, bez wolumenów, przestrzeni immersyjnych i ornamentów. Użytkownik może z aplikacji korzystać, ale prawdziwa wartość platformy pozostaje niewykorzystana.
Trzy typy scen
Ciało App w aplikacji visionOS składa sceny z trzech klas. Każda z nich buduje u użytkownika inny model myślowy.
Okna: płaszczyzna 2D
WindowGroup tworzy domyślnie dwuwymiarowe okno w szklanej ramie visionOS. Okno jest umieszczane w przestrzeni — system stawia je przed miejscem, na które patrzy użytkownik — a przesuwanie i zmiana rozmiaru odbywają się standardowymi gestami systemowymi. Z punktu widzenia SwiftUI okno jest odpowiednikiem okna macOS: płaską powierzchnią treści pokrytą szklanym materiałem świadomym głębi.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Domyślne okno otacza treść szklanym materiałem. Aplikacje, które potrzebują całkowicie przezroczystej powierzchni, sięgają po .windowStyle(.plain):
WindowGroup {
ContentView()
}
.windowStyle(.plain)
Okna w stylu plain tracą systemową szklaną ramę. Warto ich używać wtedy, gdy treść sama zapewnia własny pojemnik wizualny; w pozostałych przypadkach właściwe jest ustawienie domyślne.
Wolumeny: ograniczony obszar 3D
Wolumen to obszar 3D mieszczący treść świadomą głębi: model, scenę z wieloma obiektami albo interfejs, który zyskuje na trzeciej osi. Scena wolumenu również jest WindowGroup, tyle że w innym stylu:
WindowGroup(id: "globe") {
GlobeView()
}
.windowStyle(.volumetric)
.defaultSize(width: 0.6, height: 0.6, depth: 0.6, in: .meters)
Modyfikator .defaultSize(width:height:depth:in:) określa granice wolumenu w jednostkach świata rzeczywistego (metrach). Domyślnie granice ustalane są w momencie otwarcia: użytkownik może wolumen przesunąć, ale nie zmienić jego rozmiaru. visionOS 2 i nowsze dodały opcjonalną ścieżkę przez .windowResizability(.contentSize) i powiązane API dla aplikacji, które chcą udostępnić skalowanie wolumenu; domyślny stały rozmiar pozostaje jednak przypadkiem najczęstszym. Wniosek: rozmiar domyślny należy dobierać uważnie, bo bez jawnej decyzji dewelopera większości wolumenów nie da się przeskalować.
Dobrymi kandydatami na wolumen są aplikacje, w których przestrzenne ograniczenie stanowi część doświadczenia: wirtualna rzeźba, wokół której użytkownik chodzi, miarka przypięta do prawdziwej ściany, scena treningowa z celami rozstawionymi w głąb. Aplikacje, którym zależy jedynie na szerszym płótnie, nic na wolumenie nie zyskają — właściwą odpowiedzią jest większe okno.
Przestrzenie immersyjne: otoczenie
ImmersiveSpace to scena zajmująca otoczenie użytkownika.5 W odróżnieniu od okna i wolumenu — widocznych obok innych aplikacji w przestrzeni współdzielonej (Shared Space) — przestrzeń immersyjna przejmuje otoczenie i uniemożliwia jednoczesne korzystanie z okien pozostałych aplikacji.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
ImmersiveSpace(id: "training") {
TrainingScene()
}
.immersionStyle(selection: .constant(.mixed), in: .mixed, .progressive, .full)
}
}
Modyfikator .immersionStyle(...) wybiera poziom doświadczenia:
.mixed. Wirtualna treść pojawia się obok rzeczywistego pokoju. Sprawdza się tam, gdzie użytkownik korzysta z obu kontekstów naraz..progressive. Immersja częściowa, regulowana pokrętłem Digital Crown. Użytkownik zachowuje peryferyjną świadomość pokoju, podczas gdy centralny widok jest wirtualny..full. Pokój zostaje zastąpiony wirtualnym otoczeniem. Stosowane w doświadczeniach w pełni immersyjnych: medytacji, symulacjach treningowych, grach.
Otwarcie przestrzeni immersyjnej jest zawsze jawne. Aplikacja wywołuje @Environment(\.openImmersiveSpace) z identyfikatorem id przestrzeni, a system zajmuje się animacją przejścia i zamknięciem ewentualnej kolidującej przestrzeni:
@Environment(\.openImmersiveSpace) var openImmersiveSpace
@Environment(\.dismissImmersiveSpace) var dismissImmersiveSpace
Button("Start Session") {
Task {
await openImmersiveSpace(id: "training")
}
}
W danej chwili aktywna może być tylko jedna przestrzeń immersyjna na aplikację. Przejście między przestrzeniami — na przykład z .mixed do .full — wymaga jawnego zamknięcia starej i otwarcia nowej.
Ornamenty: płaszczyzny UI wokół okna
Ornamenty to widoki SwiftUI przypięte do krawędzi okna i wysunięte nieco przed jego płaszczyznę na osi z. W ten sposób visionOS realizuje paski narzędzi, paski kart i kontrolki pomocnicze. System korzysta z nich wszędzie: sterowanie odtwarzaniem w aplikacji TV, kontrolka segmentowa w Music, pasek narzędzi w Mail.
ContentView()
.ornament(
attachmentAnchor: .scene(.bottom),
contentAlignment: .center
) {
HStack {
Button("Previous", systemImage: "backward.fill") { ... }
Button("Play", systemImage: "play.fill") { ... }
Button("Next", systemImage: "forward.fill") { ... }
}
.padding()
.glassBackgroundEffect()
}
Parametr attachmentAnchor: określa położenie ornamentu względem okna: .scene(.top), .scene(.bottom), .scene(.leading), .scene(.trailing). Za oprawę wizualną ornamentu odpowiada deweloper; .glassBackgroundEffect() daje natywny dla visionOS szklany materiał, zgodny z ramą okna.
Ornamenty rozwiązują realny problem visionOS: kontrolki umieszczone wewnątrz okna zagęszczają treść, a wyniesione do osobnego okna zmuszają użytkownika do przekierowania wzroku. Ornament unosi się na obrzeżach pola widzenia — można go wskazać spojrzeniem, ale nie rywalizuje z główną treścią o centrum uwagi.
Attachments w RealityView: SwiftUI wewnątrz przestrzeni 3D
Gdy aplikacja potrzebuje widoków SwiftUI wewnątrz sceny 3D — etykiety na modelu, przycisku unoszącego się obok wirtualnego obiektu, odczytu pomiaru przypiętego do rzeczywistej powierzchni — mostem jest mechanizm attachments w RealityView.
RealityView { content, attachments in
let model = ModelEntity(...)
content.add(model)
if let label = attachments.entity(for: "label") {
label.position = [0, 0.5, 0]
model.addChild(label)
}
} attachments: {
Attachment(id: "label") {
Text("Vintage Globe, 1872")
.padding()
.glassBackgroundEffect()
}
}
Domknięcie attachments: deklaruje widoki SwiftUI ze stabilnymi identyfikatorami. Wewnątrz głównego domknięcia RealityView metoda attachments.entity(for:) zwraca widok jako trójwymiarową encję Entity, którą można umieścić w układzie współrzędnych sceny. Widok bierze udział w cyklu aktualizacji SwiftUI — zmiana stanu powoduje jego przerysowanie — a jednocześnie renderowany jest jako oteksturowana płaszczyzna w scenie 3D.
Ten mechanizm jest właściwy dla każdego interfejsu osadzonego w świecie: etykiety podążającej za ruchomym obiektem, adnotacji pomiarowej, przycisku kontekstowego. Sposób pisania widoków SwiftUI pozostaje bez zmian; pozycjonowanie 3D dzieje się w warstwie RealityView.
Antywzorzec „aplikacji-panelu”
Najczęstszy błąd przy wydawaniu na visionOS to właśnie aplikacja-panel: iPadowa aplikacja, która trafia na visionOS ścieżką zgodności „Designed for iPad” i ukazuje się jako pojedyncze okno — bez wolumenu, bez przestrzeni immersyjnej, bez ornamentów. Aplikacja działa, ale na platformę sobie nie zapracowała.
Trzy sygnały, że mamy do czynienia z aplikacją-panelem:
Jedna scena okna. Brak .windowStyle(.volumetric), brak zadeklarowanej ImmersiveSpace. Aplikacja jest płaską powierzchnią i niczym więcej.
Brak ornamentów. Pasek kart znajduje się wewnątrz treści okna, a nie poza nią. Efekt jest ciaśniejszy niż w aplikacji natywnej dla visionOS o tej samej gęstości treści.
Brak funkcji czysto przestrzennych. Aplikacja do niczego nie wykorzystuje trzeciej osi: żadnych modeli 3D w wolumenie, żadnej sceny środowiskowej w przestrzeni immersyjnej, żadnego interfejsu rozmieszczonego na osi z przez attachments. Robi dokładnie to samo co na iPadzie, tyle że unosi się w powietrzu.
Aplikacje-panele nie są porażką — dla kategorii treści, które nic nie zyskują na przetwarzaniu przestrzennym (komunikator, notatnik, narzędzie do ustawień), to decyzja słuszna. Porażką jest wydanie aplikacji-panelu i przypisywanie jej natywnego dla visionOS autorytetu. Wpis Macierz platform Apple z tego cyklu przekonuje, że obecność na platformie jest decyzją produktową; w przypadku visionOS pytanie brzmi: „czy ta aplikacja ma zapracować na powierzchnię przestrzenną, czy panel w zupełności wystarczy?”.
Typowe błędy
Trzy wzorce, które psują UX na visionOS:
Wolumeny, które w istocie są treścią 2D z marginesem głębi. Interfejs „3D”, który wypełnia wolumen, ale renderuje wewnątrz płaskie płaszczyzny, marnuje przestrzeń. Wolumeny służą treści trójwymiarowej; płaska treść należy do okna.
Styl immersji walczący ze scenariuszem użycia. Aplikacja do medytacji oferująca wyłącznie .full wyrywa użytkownika z otoczenia nawet przy krótkich sesjach. Aplikacja treningowa wyłącznie w .mixed nie idzie dość daleko przy ćwiczeniach wymagających pełnego skupienia. Styl immersji trzeba dopasować do rzeczywistego przebiegu sesji.
Ornamenty rywalizujące z treścią. Ornament jest z założenia peryferyjny. Taki, który domaga się uwagi w centrum — migający kolor, animowany ruch — przeczy własnemu przeznaczeniu. Ornamenty służą stabilnym kontrolkom, czytelnym jednym spojrzeniem.
Co ten wzorzec oznacza dla aplikacji visionOS
Trzy wnioski.
-
Typ sceny wybiera się według modelu myślowego użytkownika, nie według tego, co łatwiejsze. Płaska lista elementów to okno. Model 3D, który użytkownik ogląda, to wolumen. Otaczające środowisko to przestrzeń immersyjna. Łączenie ich w jednej aplikacji — okno z wolumenem otwieranym na żądanie, przestrzeń immersyjna dostępna spod przycisku w oknie — jest wzorcem natywnym dla visionOS.
-
Do pasków narzędzi i interfejsu pomocniczego warto sięgać po ornamenty. To nimi visionOS komunikuje, że dany interfejs jest uzupełnieniem; paski narzędzi wciśnięte w treść okna zdradzają iPadowy rodowód. Integracja jest niewielka, a różnica wizualna ogromna.
-
W RealityView interfejs osadzony w świecie należy budować na attachments. Etykiety na obiektach 3D, przyciski przy wirtualnej treści, odczyty kontekstowe. Most między SwiftUI a przestrzenią 3D jest już zbudowany; błędem jest z niego nie korzystać i kończyć na doraźnym renderowaniu tekstu w 3D.
Pełny cykl o ekosystemie Apple: typowane App Intents; serwery MCP; pytanie o routing; Foundation Models; LLM między środowiskiem wykonawczym a warstwą narzędziową; trzy powierzchnie; wzorzec pojedynczego źródła prawdy; Dwa serwery MCP; hooki w pracy na platformach Apple; Live Activities; środowisko wykonawcze watchOS; z czego zbudowane jest SwiftUI; przestrzenny model myślowy RealityKit; dyscyplina schematu SwiftData; wzorce Liquid Glass; wydawanie na wiele platform; macierz platform; framework Vision; słownik Symbol Effects; wnioskowanie Core ML na urządzeniu; API Writing Tools; Swift Testing; Privacy Manifest z bliska; dostępność jako funkcja platformy; typografia SF Pro; o czym odmawiam pisać. Stroną zbiorczą cyklu jest Apple Ecosystem Series. Szerszy kontekst pracy nad iOS z agentami AI opisuje przewodnik po tworzeniu agentów na iOS.
FAQ
Czym różni się wolumen od przestrzeni immersyjnej?
Wolumen to ograniczony obszar 3D, który żyje w przestrzeni współdzielonej obok innych aplikacji. Można go obejść dookoła, system go kadruje, a okna pozostałych aplikacji nadal są widoczne. Przestrzeń immersyjna otacza użytkownika, przejmuje otoczenie i uniemożliwia jednoczesną pracę z innymi aplikacjami. Wolumen mówi „popatrz na ten obiekt 3D”, przestrzeń immersyjna — „bądź w tym otoczeniu”.
Czy można otworzyć kilka wolumenów jednocześnie?
Tak. Jednocześnie otwartych może być wiele scen WindowGroup ze stylem .volumetric, każda z własnym rozmiarem i własną treścią. System rozmieszcza je w przestrzeni niezależnie od siebie.
Czy można otworzyć kilka przestrzeni immersyjnych jednocześnie?
Nie. W danej chwili aktywna może być tylko jedna przestrzeń immersyjna na aplikację. Przełączenie wymaga jawnego zamknięcia bieżącej i otwarcia nowej przez @Environment(\.openImmersiveSpace) oraz @Environment(\.dismissImmersiveSpace).
Czy rozmiar wolumenu naprawdę jest niezmienny?
Domyślnie granice wolumenu ustalane są w momencie otwarcia. Wytyczne HIG dla visionOS tłumaczą to tak: wolumen reprezentuje konkretną treść 3D o świadomie dobranych granicach, a dowolne skalowanie przez użytkownika zaburzyłoby jej zamierzoną skalę. visionOS 2 i nowsze dodały opcjonalny mechanizm skalowalnych wolumenów przez .windowResizability(.contentSize) i powiązane API, więc aplikacje potrzebujące przestrzennych pojemników o zmiennym rozmiarze mogą o to poprosić. Większość wolumenów jest jednak wydawana z domyślnym stałym rozmiarem, który HIG nadal zaleca dla treści o konkretnej skali: wirtualnej rzeźby czy modelu w wymiarach fizycznych.
Jak dodać pasek kart do okna visionOS?
Wewnątrz okna można użyć TabView dla kart osadzonych w treści (wzorzec iPadowy) albo ornamentu z własnym rzędem przycisków dla peryferyjnego, natywnego dla visionOS paska kart. Ścieżkę z ornamentem wybierają aplikacje samego Apple (Music, Mail) i to ona wydaje się użytkownikom visionOS najbardziej naturalna.
Czy attachments w RealityView współpracują ze śledzeniem dłoni?
Tak. Po umieszczeniu w scenie attachments są encjami 3D i uczestniczą w tym samym systemie gestów oraz testowania trafień co pozostałe encje RealityKit. Gesty dotknięcia, przeciągnięcia i najechania podpina się do nich standardowymi modyfikatorami gestów SwiftUI; wzorce integracji ze śledzeniem dłoni opisuje wpis o RealityKit z tego cyklu.
Źródła
-
Apple Developer: Meet SwiftUI for spatial computing (sesja 10109 na WWDC 2023). Wprowadzenie WindowGroup, wolumetrycznego WindowGroup i ImmersiveSpace jako trzech typów scen visionOS. ↩
-
Dokumentacja Apple Developer:
ImmersionStyle. Trzy style immersji (.mixed,.progressive,.full) oraz API modyfikatora.immersionStyle(selection:in:). ↩ -
Dokumentacja Apple Developer:
ornament(visibility:attachmentAnchor:contentAlignment:ornament:). Modyfikator widoku SwiftUI, który dodaje do okna płaszczyznę UI ornamentu w zadanym punkcie zaczepienia. ↩ -
Apple Developer: Go beyond the window with SwiftUI (sesja 10111 na WWDC 2023). Sesja o wolumenach, przestrzeniach immersyjnych i wzorcach wychodzenia poza płaski interfejs panelowy na visionOS. ↩
-
Dokumentacja Apple Developer: Creating an immersive space in visionOS with SwiftUI. Kompletny przewodnik po definiowaniu i otwieraniu przestrzeni immersyjnych. ↩