스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI에서 윈도우 처리하기
visionOS, macOS, iPadOS에서 멋진 단일 윈도우 및 멀티 윈도우 앱을 만드는 방법을 알아보세요. 윈도우 열고 닫기, 위치 및 크기 조정하기, 특정 윈도우를 다른 윈도우로 교체하기 등의 작업을 프로그래밍 방식으로 수행하는 데 사용할 수 있는 도구를 살펴봅니다. 또한 사용자가 앱을 자신의 워크플로 안에서 사용할 수 있게 지원하는 윈도우의 디자인 원칙도 확인해 보세요.
챕터
- 0:00 - Introduction
- 1:19 - Fundamentals
- 6:15 - Placement
- 9:43 - Sizing
- 12:03 - Next steps
리소스
관련 비디오
WWDC24
WWDC23
-
다운로드
안녕하세요 SwiftUI 일을 하는 Andrew입니다 SwiftUI 앱에서 윈도우 작업을 설명하게 되어 기쁘게 생각합니다
윈도우는 앱 콘텐츠를 담는 컨테이너입니다
윈도우로 사람들은 익숙한 제어 기능으로 앱을 관리할 수 있습니다
위치를 재조정할 수 있는 것처럼
크기를 조절하거나
닫을 수 있습니다
저와 동료들이 작업 중인 SwiftUI 앱 BOT-anist입니다
여기 시뮬레이터에는 로봇을 맞춤화할 수 있는 BOT-anist 로봇 편집기가 있습니다
플레이어는 로봇을 게임에 가져와 식물을 가꾸도록 도울 수 있습니다
이 앱은 iOS, iPadOS, visionOS macOS에 맞춤 환경을 제공합니다
제가 설명할 개념은 멀티 윈도우 플랫폼에 적용됩니다 하지만 이 비디오에서는 visionOS에 중점을 두겠습니다
윈도우를 정의하고, 열고 사용하는 방법을 설명하겠습니다
윈도우 초기 배치를 제어하는 방법과 윈도우 크기를 조정할 수 있는 다양한 방법을 알아봅니다
먼저, 기본 사항입니다
개별 윈도우를 통해 앱의 여러 부분을 동시에 사용할 수 있습니다
동일 인터페이스, 몇 인스턴스로 강력한 성능을 가질 수 있습니다
시스템 제어기로 각 윈도우를 독립적으로 조작할 수 있습니다
크기 조절과 같이 위치 재조정, 배율 조정이 가능합니다
각 윈도우에서는 플랫폼별 기능을 활용할 수도 있습니다 예를 들어 visionOS에서 볼륨 윈도우 스타일로 윈도우에 3D 콘텐츠를 포함할 수 있습니다
여러 윈도우 사용도 좋지만 TabView 같은 단일 최상위 뷰로 환경을 단순화할 수 있습니다
TabView 및 기타 최상위 뷰를 알아보려면 ’공간 컴퓨팅에 맞게 윈도우형 앱 향상하기’를 확인하세요
visionOS에 멀티 윈도우가 적절한 시기는 ‘공간 UI 디자인하기’를 확인하세요
BOT-anist는 visionOS에서 두 가지 주요 장면이 있는데
편집기 윈도우와 게임 볼륨입니다
장면은 WindowGroup으로 정의되고
앱이 로봇 편집기 WindowGroup의 인스턴스로 열립니다
이 윈도우의 버튼으로 ’게임’ WindowGroup 인스턴스가 열립니다
볼륨 윈도우는 visionOS에서 윈도우를 입체적으로 만듭니다
BOT-anist에 두 가지 새 기능을 추가하려고 합니다
첫 번째는 로봇에 대한 영상이 포함된 새 윈도우가 열리는 겁니다
이 영상은 포털에 포함된 3D 장면이 될 것입니다
앱 본문에 3D 장면 뷰를 포함하는 새 WindowGroup을 추가합니다 이 WindowGroup을 식별하기 위해 ’movie’라는 ID를 부여했습니다 이 ID로 윈도우를 열겠습니다
ID를 환경 작업에 전달하겠습니다 이 작업은 SwiftUI 계층 구조 어느 지점에든 사용할 수 있습니다 윈도우 관리를 위해 몇몇 다른 환경 작업이 사용 가능합니다
openWindow로 윈도우를 엽니다
dismissWindow로 닫습니다
pushWindow로 윈도우를 열고 원래 윈도우를 숨길 수 있습니다
openWindow를 사용하여 새 영상 윈도우를 열어 보겠습니다
로봇 편집기 뷰에 키 경로 openWindow 환경 속성을 생성하여 환경에서 OpenWindowAction을 검색합니다 새 버튼 내에, 윈도우 그룹을 정의한 ID ’movie’를 전달하여 OpenWindowAction을 수행할 수 있습니다
이제 편집기에서 버튼을 탭하면 영상 포털이 별도의 윈도우로 열립니다
이제 보니 편집기가 영상 뷰와 동시에 표시되지 않아야 할 것 같습니다 대신 pushWindow 환경 작업을 사용해 윈도우를 표시하겠습니다
그러면 원래 윈도우 대신 새 윈도우가 열립니다
새로운 윈도우를 닫으면 원래 윈도우가 다시 나타납니다
영상 윈도우를 열 때 편집기를 숨기려면 환경 속성 키 경로 openWindow를 pushWindow로 변경하고 버튼을 업데이트하여 이 작업을 대신 호출합니다
이제 TV 버튼을 탭하면 영상 윈도우가 푸시되고 로봇 편집기 윈도우가 숨겨집니다
이제 디자인한 로봇이 방해 없이 연기를 시작하는 모습을 지켜볼 수 있습니다
닫기 버튼을 탭하면 편집기로 돌아갑니다 이 동작을 하는 데 추가 로직이 필요하지 않습니다 현재 윈도우와 동시에 표시할 필요가 없는 콘텐츠를 표시할 때 이 작업을 사용하는 것이 좋습니다
윈도우를 정의하고 연 상태에서 Freeform이 도구 막대 오너먼트를 사용해 윈도우 하단을 따라 제어기 표시 방법 또는 ToolbarTitleMenu가 캔버스를 채우지 않고 문서와 관련된 작업을 표시하는 방법처럼
플랫폼별 기능으로 더욱 친숙하게 느껴지도록 개선할 수 있습니다
윈도우 막대와 닫기 버튼은 기본적으로 항상 표시됩니다 하지만 영상 뷰의 경우 사람들이 영상에 집중할 수 있도록 .persistentSystemOverlays 한정자를 사용하여 숨겼습니다
이 API는 visionOS에서 유용한 윈도우 향상 방법입니다
macOS 윈도우를 개선하는 방법은 ’SwiftUI로 macOS 윈도우 다듬기’를 확인하세요
영상 윈도우가 멋져 보이네요 다음으로 게임에 선택적 제어 패널을 추가하고 싶습니다 이 패널엔 로봇을 움직이기 위한 추가 제어기와 점프 또는 흔들기 같은 동작을 수행하는 몇 가지 버튼이 있습니다
제어기를 표시하는 새로운 윈도우 그룹을 추가했습니다
또한 게임 볼륨에서 openWindow를 호출합니다
게임에서 버튼을 탭하면 제어기가 새 윈도우에서 열립니다
게임 볼륨 관계없이 위치를 변경한다는 게 마음에 드네요
하지만 윈도우가 처음 열리면 볼륨을 덮고 멀리 배치될 수 있습니다
visionOS는 제어 패널과 같은 새 윈도우를 원래 윈도우 앞에 배치합니다
반면 macOS는 화면 중앙에 새 윈도우가 열립니다
이는 defaultWindowPlacement 한정자로 맞춤화할 수 있습니다
프로그래밍으로 윈도우 초기 위치, 크기를 설정 가능합니다
플랫폼별 여러 방식으로 윈도우를 배치하고 크기 조절이 가능합니다
선행 또는 후행 위치처럼 다른 윈도우를 기준으로 배치할 수 있는데
visionOS의 utilityPanel처럼 윈도우가 가까이 있고 일반적으로 직접 터치 범위 내에 위치하도록 사람을 기준으로 배치하거나
macOS의 오른쪽 위 사분면처럼 화면 기준 배치도 가능합니다
visionOS에서 게임 제어기가 플레이어와 가깝게 보이도록
’controller’ 그룹에 defaultWindowPlacement를 적용하고
이로부터 위치가 .utilityPanel인 WindowPlacement를 반환합니다
이 반환을 if 조건으로 래핑하여 이 배치가 visionOS에만 적용되도록 합니다
이제 윈도우가 처음 열릴 때마다 제어기가 가까이 표시됩니다 또한 원하는 경우 윈도우를 초기 배치에서 이동할 수 있습니다
이 새로운 제어기를 사용하면 완전히 새로운 방식으로 로봇과 상호 작용할 수 있습니다
이 버튼을 누르면 BOT-anist가 손을 흔드는 것처럼 말이죠
visionOS의 컨트롤러 윈도우가 멋지게 보입니다 다음, macOS에서 이 윈도우의 위치를 수동 계산해 보겠습니다
defaultWindowPlacement 한정자는 컨텍스트를 제공합니다 플랫폼에 따라 다른 정보가 포함됩니다 macOS에는 컨텍스트에 기본 디스플레이 정보가 포함됩니다 여기에 접근하여 .visibleRect를 가져옵니다 이는 콘텐츠를 배치하기 안전한 위치를 나타냅니다
sizeThatFits 메서드를 사용하여 윈도우에 어떤 콘텐츠를 담을지 어떤 크기로 할지 요청합니다
displayBounds와 size 변수를 사용하여 디스플레이 하단 바로 위 가로 중앙의 위치를 계산합니다
계산된 위치, 크기가 포함된 WindowPlacement를 반환 가능합니다
이제 macOS에서도 제어기가 편안하게 배치됩니다
재생하는 동안 플레이어는 윈도우 위치를 재조정하거나 별도의 화면에 배치할 수 있습니다
새 윈도우 배치가 마음에 드네요 콘텐츠가 항상 최상의 모습으로 보이도록 윈도우 크기를 조정하는 방법도 변경하고 싶습니다
윈도우는 시스템에 의해 초기 크기가 결정됩니다 몇 가지 방법으로 기본 크기를 변경할 수 있습니다
화면 크기나 다른 윈도우에 따라 크기가 달라지는 경우 macOS의 컨트롤러 윈도우처럼 기본 윈도우 배치 API를 통해 초기 크기를 지정할 수 있습니다
또는 defaultSize 한정자로 초기 크기를 변경할 수 있습니다
이 기본 크기는 윈도우 배치 API 제공 크기와 같은 다른 크기 제약이나 장면이 복원되는 경우 사용되지 않습니다
앞서 추가한 윈도우와 같이 푸시된 윈도우의 경우 defaultSize는 원래 윈도우의 크기와 동일합니다 이 경우 원래 윈도우는 로봇 편집기입니다
저는 기본 크기에 만족하지만 플레이어는 영상 윈도우 크기를 조정하고 싶을 수 있습니다 영상이 항상 멋지게 보이게 몇몇 제한을 두겠습니다
’movie’ WindowGroup에 .contentSize .windowResizability를 지정하면 윈도우가 포함된 콘텐츠의 최소 및 최대 크기로 제한됩니다
MovieContentView에 min, maxWidth min, maxHeight를 추가합니다
이제 영상 윈도우의 크기를 정사각형으로 축소하고 적절하게 크기를 늘릴 수 있습니다
온종일 BOT-anist를 볼 수 있겠죠 하지만 이제 제어기 윈도우에 집중해야 합니다
크기가 너무 크게 변경해서 볼륨을 방해할 수 있습니다
이 윈도우 크기는 포함 콘텐츠의 크기와 일치하는 것이 좋습니다
영상 WindowGroup에 했던 것처럼 컨트롤러 윈도우 그룹에 windowResizability를 추가하고
이제 컨트롤러 모드를 변경하면 콘텐츠 크기에 맞게 윈도우 크기가 조정됩니다
각 모드의 뷰는 최소 및 최대 크기가 아닌 고정 크기라 이 윈도우는 플레이어가 크기를 조정할 수 없습니다
BOT-anist가 정말 잘하고 있네요 visionOS, macOS용 앱에 몇 가지 큰 개선이 이루어졌습니다 이제 앱은 윈도우와 이를 지원하는 API를 활용 가능합니다
윈도우 뷰와 최상위 뷰 중 앱에 가장 적합한 걸 고려하세요 초기 레이아웃을 제공하려면 윈도우 배치 API를 사용합니다 콘텐츠별 윈도우 크기 조정 해당 조정 방법 제한을 설정합니다
또한 플랫폼별 윈도우 기능을 활용하여 앱을 더 친숙하게 만듭니다
시청해 주셔서 감사합니다 즐거운 윈도우 작업을 하시길 바랍니다
-
-
2:36 - BOT-anist scenes
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) } }
-
3:09 - Creating the movie WindowGroup
@main struct BOTanistApp: App { var body: some Scene { WindowGroup(id: "editor") { EditorContentView() } WindowGroup(id: "game") { GameContentView() } .windowStyle(.volumetric) WindowGroup(id: "movie") { MovieContentView() } } }
-
3:55 - Opening a movie window
struct EditorContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("Open Movie", systemImage: "tv") { openWindow(id: "movie") } } }
-
4:45 - Pushing a movie window
struct EditorContentView: View { @Environment(\.pushWindow) private var pushWindow var body: some View { Button("Open Movie", systemImage: "tv") { pushWindow(id: "movie") } } }
-
5:34 - Toolbar
CanvasView() .toolbar { ToolbarItem { Button(...) } ... }
-
5:40 - Title menu
CanvasView() .toolbar { ToolbarTitleMenu { Button(...) } ... }
-
5:48 - Hiding window controls
WindowGroup(id: "movie") { ... } .persistentSystemOverlays(.hidden)
-
6:28 - Creating the controller window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } WindowGroup(id: "controller") { ControllerContentView() } } }
-
6:34 - Opening the controller window
struct GameContentView: View { @Environment(\.openWindow) private var openWindow var body: some View { ... Button("Open Controller", systemImage: "gamecontroller.fill") { openWindow(id: "controller") } } }
-
7:46 - Positioning the controller window
WindowGroup(id: "controller") { ControllerContentView() } .defaultWindowPlacement { content, context in #if os(visionOS) return WindowPlacement(.utilityPanel) #elseif os(macOS) ... #endif }
-
8:45 - Positioning the controller window continued
WindowGroup(id: "controller") { ControllerContentView() } .defaultWindowPlacement { content, context in #if os(visionOS) return WindowPlacement(.utilityPanel) #elseif os(macOS) let displayBounds = context.defaultDisplay.visibleRect let size = content.sizeThatFits(.unspecified) let position = CGPoint( x: displayBounds.midX - (size.width / 2), y: displayBounds.maxY - size.height - 20 ) return WindowPlacement(position, size: size) #endif }
-
10:12 - Default size
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() } .defaultSize(width: 1166, height: 680) } }
-
10:49 - Setting resize limits on the movie window
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "movie") { MovieContentView() .frame( minWidth: 680, maxWidth: 2720, minHeight: 680, maxHeight: 1020 ) } .windowResizability(.contentSize) } }
-
11:37 - Controller window resizability
@main struct BOTanistApp: App { var body: some Scene { ... WindowGroup(id: "controller") { ControllerContentView() } .windowResizability(.contentSize) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.