Take SwiftUI to the next dimension

DAMIN KIM
daily-monster
Published in
10 min readApr 27, 2024

안녕하세요 수요괴물 damin 입니다! 개인적인 사정으로 수요일이 아닌 토요일에 글을 업로드하게 되었습니다ㅠ

이번주는 visionOS WWDC ‘Take SwiftUI to the next dimension’ 영상을 보고 간단히 정리하였습니다~

visionOS의 기본 개념 중 위처럼 3가지의 방식으로 콘텐츠를 보여줄 수 있죠?
이 중에서 본 세션에서는 volume에 대해서 다뤄 볼거고, 다른 session은 아래 wwdc 를 참고해주세요.

- Elevate your windowed app for spatial computings — WWDC23
- Go beyond the window with SwiftUI — WWDC23

Volume

volume은 3D 모델을 보여주기 위한 컨테이너로 Window와 달리 main glass window가 없으나 대신 scene에 바로 3D content를 control panel과 함께 추가합니다.

Volume은 고정된 크기의 컨테이너를 제공하며 Window와 달리 동적으로 거리에 따라 크기가 바뀌고 어느 거리에서든 같은 사이즈를 유지합니다.
-> 여기서 크기가 바뀌는데 같은 사이즈를 유지한다? 라고 했는데
제가 이해하기로는 거리에 따라 알아서 사이즈를 바꾸면서 유저가 봤을때 같은 사이즈로 보이도록 유지해준다 라는 의미인것 같습니다. ( 거리가 멀면 커지고 가까우면 작아지면서 같은 사이즈로 보이도록 해줌)

또한 Volume은 수평으로 align 되어 있고 어느 방향에서 보든 보여지며 Immersive Space처럼 공간을 점유하지 않고도 3D content를 보여줄 수 있습니다.

WindowGroup(id: Module.globe.name) {
Globe()
.environment(model)
}
.windowStyle(.volumetric)

이렇게 windowGroup에 windowStyle만 volumetric으로 지정해주면 Volume 만들 수 있습니다.

Model3D

- Use Model3D to load simple 3D scenes
Model3D는 간단한 3D를 씬에 로드할때 씁니다.
- Provides phases for handling loading
loading 처리를 위해 phase 를 제공합니다.

Model3DAsyncImage와 대응관계라고 생각하면 된다고 합니다.
이게 복잡한 geometry 일을 대신 처리해주기 때문에 앱이 스무스하게 동작할 수 있게 해줍니다.

AsyncImage 는 비동기적으로 이미지를 보여주는 뷰인데 Model3D 가 비동기적으로 3D 콘텐츠를 보여주는 것처럼 AsyncImage는 이미지용 Model3D인 것이다.

AsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.frame(width: 50, height: 50)

3D views and layout

Model3D를 사용하는 실제 코드인데 phase 별로 case처리를 할 수 있습니다.

각 케이스를 보면
3D 콘텐츠를 로드할때 까지는 .empty 케이스에 ProgressiveView()를 띄워주고
로드에 실패했을때는 .failure 케이스에서 error 메시지 띄워주고
성공했을때는 .success 케이스에서 model을 받아서 이미지에서와 비슷하게 resizable 로 layout system한테 이 모델이 resizable하다고 말해주고, 모델이 가용 공간에 딱 맞도록 scaledToFit() 을 사용합니다.

여러개의 3D content를 한번에 띄우는 방법

위 코드에서는 name, size를 갖는 객체를 만들고(Equatable, Hashable)
@State로 객체의 배열을 만들어서 로드하고자하는 모델의 이름과 사이를 배열에 추가 HStack에 ForEach로 배열에 있는 객체를 위에서 만든 뷰를 이용해서 띄워주고
각 뷰의 사이즈를 다르게 하기 위해 frame으로 객체의 사이즈를 적용합니다.

뒤쪽 정렬 (default)

각 3D의 사이즈는 달라졌으나 뒤쪽 기준으로 정렬이 되어있는데 이게 swiftUI에서 3D콘텐츠의 기본 정렬 방식입니다.

만약 기본 정렬 방식을 변경하려면 frame(depth: , alignment: ) modifier를 이용해서 바꿀 수 있습니다.

정렬로 바뀜

각 객체에 레이블을 오버레이로 추가하고, alignment를 bottom으로 해서 3D를 가리지 않게 할 수 있음 추가로 glassBackgroundEffect로 가독성 살리기

레이블을 overlay로 Model3D에 추가

TimeLineView로 시간에 따라 애니메이션을 줄 수 있고, 좀 더 동적으로 보여주기 위해 rotation3DEffect를 이용해 3D모델을 회전시켜준다

attachment in RealityView

더 몰입갑있는 경험을 위해서 RealityView는 앱에서 RealityKit을 최대한 활용하기 위한 SwiftUI의 entry point, 시작점이라고 할 수 있습니다.

RealityView에 대해 더 알고 싶다면 아래 세션을 참고해주세요!
Build spatial experiences with RealityKit — WWDC23
Enhance your spatial computing app with RealityKit — WWDC23

attachment는 RealityView 내에서 tag된 SwiftUI view를 entity와 짝지어줄 수 있습니다. attachment는 주석을 달거나, 어포던스(행동 유도성)을 수정하는데 좋아요

attachments 클로저에서 favoritePlaces 배열의 장소를 ForEach로 장소의 이름을 text 뷰로 보여주고 거기에 glassBackgroundEffect로 가독성을 더하고 tag를 달아줍니다, 이 태그에 들어가는 값은 hashable하기만 하면되고 여기서는 place의 id를 넣어줬습니다.

그리고 update 클로저에서 지정한 태그(place.id)를 사용해 각 attachment 뷰를 호스팅하는 entity를 가져올 수 있어서 그 엔터티를 content에 추가하고 look(at: , from:, relativeTo: ) 메서드를 사용해서 entity를 배치해 줍니다

3D gestures

제스처를 통해 3D에 새로운 place를 추가하는 것을 알아봅시다

Entity에 입력 설정을 해볼게요
InputTargetComponent를 추가해주면 엔터티의 하위 엔터티도 입력을 받을 수 있게 됩니다. CollisionComponent로 엔터티의 상호작용 영역을 설정할 수 있습니다. 그 모양도 3D에 따라 설정할 수 있는데 여기서는 지구본이니까 Sphere로 설정 이렇게 SwiftUI의 제스쳐를 RealityView에서 처리할 수 있도록 해줬습니다.

SwiftUI처럼 제스처를 추가하는데 SpatialTapGesture를 추가하고
여기서 특정 엔터티에서만 제스처를 받을 수 있도록
targetedToEntity로 특정 entity를 지정할 수 있고
이 엔터티 및 하위 엔터티를 제외한 엔터티에 탭 제스처는 실패함

Model3D를 attachment에 추가하기

frame을 이용해 크기도 쉽게 조절 가능하고, 여기서도 attachments에 Text 뷰를 추가 했으니 이 뷰의 tag를 추가해줍니다.

placeEntity와 마찬가지로 attatchments에 추가한 위성 Model3D를 엔터티로 가져와 content에 추가 해줍니다.

3D transform을 리턴하는 제스처를 정의해서 satelite entity의 scale, rotation, position을 정의해줍니다.

그리고 영상에는 안 나왔는데

struct ManipulationState {
var transform: AffineTransform3D = .identity
var active: Bool = false
}

@GestureState var manipulationState = ManipulationState()

위 코드처럼 @GestureState 로 manipulationState를 만들어서 제스처의 상태를 관리할 수 있는 변수를 만들어 준것 같습니다.

그러고 나서 gesture modifier에 방금 만든 manipulationGesture를 추가하고 updating modifier로 gesture의 상태를 트래킹합니다.

updating 클로저에서 value는 제스처에 따라 변화하는 transform 값을 전달하고
state는 updating gesture 상태 관리 state 변수 값을 전달합니다.

그리고 offset 모디파이어를 추가해 manipulationState의 transform 으로 offSet을 지정해주어 업데이트된 transform에 offset을 바꿔주고

마지막으로 animation 모디파이어로 manipulationState의 변화에 따라 spring animation을 실행한다.

또한 manipulationGesture simultaneously modifier를 이용해 확대할 수 있는 MagnifyGesture, 회전시킬 수 있는 RotateGesture3D를 추가하여 이 제스처들에 대한 값을 동시에 받아 처리합니다.

그리고 rotation3DEffect 모디파이어로 manipulationState.transform.rotation 값으로 회전 효과를 줍니다.

--

--