이번 포스팅에서는 SwiftUI의 앱을 구성하고 있는 App, Scene, Window, View의 정의와 관계에 대해 알아보겠습니다.
또한, Scene의 종류에 대해 알아보고 macOS에서 메뉴 바 TodoList 앱을 만들면서 Scene을 활용하는 방법을 알아보겠습니다.
App, Scene, Window, View
SwiftUI의 앱은 크게 App, Scene, View로 이루어져 있습니다. 아래 그림과 같이 View들이 모여서 Scene이 되고 Scene들이 모여 App을 구성합니다.
View
View는 유저가 앱을 봤을 때 보이는 모든 것이 View라고 할 수 있습니다. UI의 이미지, 텍스트, 컨테이너 뷰 등 모든 것이 뷰입니다.
Window
Window는 기기의 스크린 위에 나타나는 물리적인 창을 의미합니다. Window는 뷰의 컨테이너 역할을 합니다. iOS에서는 전체 스크린을 꽉 채우는 하나의 Window가 있고 Window내의 뷰가 변화하면서 UI를 표시합니다. 반면, macOS에서는 여러 개의 Window를 통해 앱의 UI를 표시하는 것이 일반적입니다.
Scene
Scene은 스크린의 특정 구역을 의미합니다. 일반적으로 iOS, tvOS, watchOS에서는 하나의 Scene이 전체 스크린을 차지합니다. 반면, iPadOS, macOS에서는 여러 개의 Scene이 기기 스크린 위에 나타날 수 있습니다. 이 Scene은 Window의 컨테이너 역할을 합니다.
App
주로 Xcode에서 새로운 프로젝트를 만들 때 SwiftUI 프레임워크를 UI 프레임워크로 SwiftUI를 선택하면 App 파일이 생성됩니다.
App 프로토콜을 채택하는 구조체가 있고 이 구조체는 앱의 시작 지점(Entry point) 역할을 하게 됩니다. App 구조체는 하나 이상의 Scene으로 구성됩니다.
Mutliple Window 구현하기 (Scene)
SwiftUI에서는 Scene을 활용해 여러 개의 Window를 지원하는 앱을 만들 수 있습니다. 아래는 일반적으로 사용하는 5개의 Scene을 정리한 표입니다.
유형 | 설명 | 지원 플랫폼 | 동작 방식 |
---|---|---|---|
WindowGroup | 모든 플랫폼에서 데이터 기반 앱을 구축. | 모든 Apple 플랫폼 | 멀티 윈도우 프레젠테이션을 지원하며, 여러 창이 필요한 앱에 유용 (예: macOS, iPadOS). |
DocumentGroup | 문서 기반 앱을 구축. | iOS, macOS | 문서를 관리하며, 텍스트 에디터나 파일 기반 앱에 유용. |
Settings | 앱 내 설정 인터페이스 정의. | macOS | 앱 설정을 나타내는 인터페이스 제공. |
Window | 단일, 고유한 창을 나타냄. | 모든 Apple 플랫폼 | 글로벌 앱 상태에 적합하며, 단일 창만 필요할 때 유용. 멀티 윈도우 동작을 허용하지 않음. |
MenuBarExtra | 시스템 메뉴 바에 지속적인 앱을 컨트롤할 수 있는 아이콘(레이블)을 나타냄. | macOS (전용) | 앱이 실행 중이면 항상 사용 가능하며, 앱이 스크린에 있지 않아도 사용 가능. |
두 개 이상의 Scene을 동시에 (보조 Scene으로) 사용할 수도 있습니다. 아래와 같이 App의 body에 Scene을 이어 작성하기만 하면 됩니다.
앱이 처음 실행될 때 처음 작성된 Scene이 화면에 나타나게 되기 때문에 Scene을 작성하는 순서가 중요합니다.
struct MenuBarTodoDemoApp: App {
var body: some Scene {
MenuBarExtra("App", systemImage: "checklist") {
ContentView()
}
.menuBarExtraStyle(.window)
Window("About", id: "about") {
AboutView()
}
.windowResizability(.contentSize)
WindowGroup(id: "detail", for: TodoItem.self) { $item in
if let item {
DetailView(item: .constant(item))
}
}
.windowResizability(.contentSize)
}
}
위 예시에서는 MenuBarExtra가 제일 상단에 위치하기 때문에 앱 실행 시 어떤 Window도 나타나지 않습니다.
그렇다면 하나의 Scene에서 다른 Scene으로 즉, 하나의 Window에서 다른 Window를 어떻게 띄울 수 있을까요?
SwiftUI에서는 openWindow()라는 SwiftUI Environment내의 호출 가능한 타입을 지원합니다.
@Environment(\.openWindow) private var openWindow
...
openWindow(id: "about")
Menubar Todo App 예시
여러 가지 Scene을 활용해 macOS의 Menubar에서 접근할 수 있는 간단한 Todo 리스트 앱을 만든다고 가정해 보겠습니다.
- Menubar 클릭 시 Todo리스트 메인 Window(View) 표시
- Todo 항목 클릭 시 DetailView 표시
- Info 버튼 클릭 시 AboutView 표시
App은 아래와 같이 MenubarExtra, WindowGroup, Window 3개의 Scene으로 구성할 수 있습니다.
struct MenuBarTodoDemoApp: App {
@StateObject private var todoListStore = TodoListStore()
var body: some Scene {
MenuBarExtra("App", systemImage: "checklist") {
ContentView(todoListStore: todoListStore)
}
.menuBarExtraStyle(.window)
WindowGroup(id: "detail", for: TodoItem.ID.self) { $itemId in
DetailView(itemId: itemId ?? UUID(), todoListStore: todoListStore)
}
.windowResizability(.contentSize)
Window("About", id: "about") {
AboutView()
}
.windowResizability(.contentSize)
}
}
Menubar를 탭할 시 일반적인 Menu가 아닌 Window를 표시하기 위해 .menuBarExtraStyle(.window) 모디파이어를 사용합니다.
AboutView의 경우 여러 개의 Window로 표시하는 것은 불필요하기에 Window Scene을 사용합니다. 이때 id는 나중에 해당 Window를 다른 Window에서 열 때 사용됩니다. AboutView()의 경우 크기 조절하는 것도 불필요하기에 .windowResizability(.contentSize)을 사용해 해당 Window의 크기 조절을 막습니다.
DetailView도 마찬가지로 여러 개의 Window로 표시하는 것은 불필요하지만, WindowGroup에서만 data를 주입해서 Window를 열 수 있기 때문에 WindowGroup을 사용합니다. 이때 ID, String과 같은 가벼운 타입을 사용하기를 권장합니다.
Window를 열기 위해선 SwiftUI View내부에서 openWindow(:id) 함수를 호출하면 됩니다.
@Environment(\.openWindow) var openWindow
...
Button {
openWindow(id: "about")
} label: {
Image(systemName: "info.circle.fill")
.foregroundColor(.blue)
}
WindowGroup을 통해 Window를 열기 위해서는 id와 value를 넣어줍니다. 이때 value에 들어가는 타입은 앞서 WindowGroup에서 정의한 타입과 일치해야 합니다.
@Environment(\.openWindow) var openWindow
...
ForEach($todoListStore.todoItems) { $item in
HStack {
Button {
openWindow(id: "detail", value: item.id)
} label: {
HStack {
Text(item.text)
.foregroundColor(.primary)
.strikethrough(item.isCompleted)
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.gray)
}
}
.buttonStyle(PlainButtonStyle())
}
}
참고자료:
'기술 & 언어' 카테고리의 다른 글
Swift - SwiftUI로 이미지 크롭 뷰 구현하기 (0) | 2024.12.02 |
---|---|
Swift: 앱 평가 요청하기 (0) | 2024.11.16 |
Swift: 동시성 프로그래밍 (0) | 2024.08.31 |
Swift: 제네릭, Generics (0) | 2024.08.28 |
Swift: Escaping closure (0) | 2024.08.27 |