스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI 앱에 여러 윈도우 구현
앱의 장면 내에 윈도우를 표시할 수 있도록 하는 최신 SwiftUI API를 확인하세요. SwiftUI를 사용하여 더 많은 종류의 앱을 MenuBarExtra와 같은 장면 유형을 통해 손쉽게 빌드하는 방법을 알아보겠습니다. 또한 앱 윈도우의 표시 및 동작을 맞춤화하여 macOS 앱을 더욱 개선해 주는 제어자를 사용하는 방법을 보여드립니다.
리소스
- Bringing multiple windows to your SwiftUI app
- DocumentGroup
- MenuBarExtra
- NewDocumentAction
- OpenDocumentAction
- OpenWindowAction
- Value and Reference Types
- Window
- WindowGroup
관련 비디오
WWDC22
-
다운로드
♪ 부드러운 힙합 음악 ♪ ♪ 안녕하세요, 여러분 저는 SwiftUI 팀의 공학자 Jeff입니다 오늘 iPadOS와 macOS의 SwiftUI 앱에서 멀티 윈도우 기능의 제공을 소개할 수 있어서 기쁘게 생각합니다 이번 세션에서는 SwiftUI 수명 주기에서 다양한 씬 유형의 개요와 몇 가지 새로운 유형도 소개하고자 합니다 그런 다음 보조 씬을 추가하여 이러한 씬 유형을 함께 구성하는 방법을 보여줄 겁니다 그 후에 앱에서 특정 씬의 윈도우를 여는 몇 가지 새로운 API도 다루겠습니다 그리고는 앱의 씬을 사용자화하는 여러 방법을 소개하는 것으로 마무리하고자 합니다 이제 새로운 씬 유형을 알아보기 전에 기존 씬 유형의 개요부터 살펴보겠습니다 이전 세션에서 SwiftUI에서는 앱이 씬과 뷰로 이루어진 것을 기억하실 겁니다 씬은 일반적으로 윈도우 화면에 콘텐츠를 나타냅니다 읽고 있는 책의 진척도를 표시하는 용도로 만든 앱을 예시로 들어 보죠 단일 윈도우 그룹에서는 플랫폼에 적합한 방식으로 읽기 목록이 나타납니다 멀티 윈도우를 지원하는 iPadOS와 macOS 같은 플랫폼에서는 여러 개의 윈도우에서 씬이 나타나죠 씬의 행동 및 표현 방식은 사용되는 유형에 따라 다릅니다 그 예시로 씬은 플랫폼 기능에 무관하게 단일 인스턴스로만 나타날 수 있습니다 SwiftUI의 기존 씬 유형 목록을 살펴보겠습니다 WindowGroup은 Apple의 모든 플랫폼에서 데이터 기반 애플리케이션을 구축하는 방법을 제공합니다 DocumentGroup은 iOS와 macOS에서 문서 기반 앱을 구축하도록 하죠 그리고 Settings는 macOS에서 앱 내 설정 값을 나타내기 위한 인터페이스를 정의합니다 이러한 씬 유형을 함께 구성하여 앱의 기능을 확장할 수 있습니다 저희는 두 씬을 새로 추가해 씬 목록을 확장했습니다 첫 번째는 Window로 모든 플랫폼에서 단일한 고유 윈도우를 나타내는 씬입니다 두 번째 씬 유형은 macOS용의 MenuBarExtra로 시스템 메뉴 막대에 영구 제어 기능을 렌더링합니다 다른 씬 유형처럼 Window와 MenuBarExtra 둘 다 앱에서 단독으로 쓰거나 다른 씬과 조합해서 쓸 수 있습니다 WindowGroup과 다르게 Window 씬은 단일한 고유 윈도우 인스턴스로만 해당 콘텐츠를 나타냅니다 이 특징은 macOS와 iPadOS에서 WindowGroup의 멀티 윈도우 프레젠테이션 양식과 딱 들어맞지 않는 특정 글로벌 앱 상태에서 씬의 콘텐츠를 나타낼 때 유용할 수 있습니다 콘텐츠 렌더링 때 단일 메인 윈도우만 허용하는 게임 앱이 그 예시죠 MenuBarExtra는 macOS에서만 지원하는 새로운 씬 유형으로 다른 씬과 약간 다르게 작동합니다 윈도우에 콘텐츠를 렌더링하는 대신 콘텐츠의 레이블을 메뉴 막대에 두고 레이블에 고정된 메뉴나 윈도우에 콘텐츠를 표시합니다 또한 이 요소는 해당 앱이 맨 앞에서 실행 중인지는 무관하게 관련 앱이 실행 중일 때 사용할 수 있습니다 MenuBarExtra는 기능에 쉽게 접근할 수 있는 단독 유틸리티 앱을 만드는 데 적합합니다 아니면 다른 씬과 조합해 앱의 기능에 다르게 접근하는 방법을 제공할 수도 있죠 MenuBarExtra는 두 가지 렌더링 양식을 제공합니다 메뉴 막대의 풀다운 메뉴에서 콘텐츠를 표시하는 기본 양식과 메뉴 막대에 고정된 투명 윈도우에 내용을 표시하는 양식이죠 이 두 가지 씬 유형을 새로 추가한 덕분에 SwiftUI 앱은 저희의 모든 플랫폼에서 훨씬 더 풍부한 기능 세트를 표현할 수 있습니다 이러한 새 API를 기존 씬 유형과 함께 어떻게 사용할 수 있는지 알아보겠습니다 앞서 보여 드렸던 BookClub 앱의 정의는 다음과 같습니다 지금은 단일 윈도우 그룹으로 이루어져 있죠 BookClub 앱은 macOS에서 추가 윈도우를 띄우는 것으로 시간에 따른 독서 활동을 표시할 수 있습니다 이는 macOS 앱이 해당 플랫폼에 존재하는 추가 화면 영역과 유연한 윈도우 구성을 어떻게 활용할 수 있는지 보여주는 아주 좋은 예시입니다 이런 인터페이스를 표현하기 위해 앱에 보조 씬을 추가해 봅시다 Activity 윈도우 데이터는 전반적인 앱 상태에서 파생되므로 이런 경우에는 Window 씬이 이상적인 선택지입니다 동일한 상태의 윈도우를 여러 개 여는 것은 기존 설계와는 잘 맞지 않겠죠 씬에 제공한 타이틀은 Window 메뉴 섹션에서 메뉴 항목의 레이블로 사용됩니다 이 항목을 선택하면 아직 열리지 않았을 경우에 씬 윈도우가 열릴 겁니다 이미 열려 있다면 씬 윈도우가 맨 앞에 오고요 지금까지 BookClup 앱에 보조 씬을 추가하는 것을 다루었으므로 새롭게 추가한 몇 가지 씬 프레젠테이션 API와 이를 앱에 통합하여 보다 풍부한 경험을 제공할 방법을 논의하고자 합니다 BookClub 앱은 Content List 창 내부의 모든 책에 적용할 수 있는 빠른 메뉴가 있습니다 이 빠른 메뉴에 포함된 버튼은 윈도우 프레젠테이션을 트리거합니다 자세한 사항은 곧 알려 드리죠 SwiftUI는 앱에 정의된 씬에 연결된 윈도우를 표시하는 환경을 통해 호출 가능한 새로운 유형을 몇 가지 제공합니다 첫 번째는 openWindow 동작으로 WindowGroup이나 Window 씬을 윈도우에 표시합니다 동작에 전달된 식별자는 앱에 정의된 씬의 식별자와 일치해야 합니다 또 openWindow 동작은 프레젠테이션 값을 취할 수 있는데 이 값은 프레젠테이션 씬이 내용을 표시할 때 사용합니다 이러한 동작 형태는 WindowGroup에서만 지원하며 조금 뒤에 살펴볼 새 식별자를 사용합니다 값의 유형은 씬의 식별자에 제공된 유형과 반드시 일치해야 합니다 또한 문서 윈도우를 표시하는 환경에서 두 가지 호출 가능 유형이 있는데 그중 하나인 newDocument 동작은 FileDocument와 ReferenceFileDocument가 새 문서 윈도우를 여는 것을 지원합니다 이 동작을 수행하려면 앱의 해당 DocumentGroup이 편집기 역할로 정의되어 있어야 합니다 이 동작에 제공된 문서는 윈도우가 표시될 때마다 생성됩니다 디스크의 기존 파일에서 콘텐츠를 제공하는 문서 윈도우를 표시할 경우에는 openDocument 동작을 수행하면 됩니다 이 동작은 열려는 파일의 URL을 사용합니다 앱을 화면에 표시하려면 앱이 DocumentGroup을 정의해야 하며 해당 그룹의 문서 유형은 제공된 URL에서 파일 형식을 읽을 수 있어야 합니다 버튼을 재검토하면서 openWindow 환경 속성을 뷰에 추가해 봅시다 이 유형은 호출 가능하기에 버튼의 동작으로 바로 호출할 수 있습니다 Book 유형은 식별이 가능하므로 이것의 식별자를 제시 값으로 전달할 겁니다 더 진행하기 전에 openWindow 동작으로 전달한 값을 설명하겠습니다 제가 UUID 유형의 값인 책 식별자를 전달한다고 언급했었죠 대부분의 경우 모델의 값 자체보다는 식별자를 사용하는 게 더 좋습니다 Book 유형이 값 유형인 것에 주목하세요 따라서 이것을 현재 값으로 사용할 경우 프레젠테이션에서 유래된 윈도우의 복사본이 새 윈도우에 나타납니다 둘 중 하나를 편집해도 다른 하나에는 영향이 없죠 책의 식별자를 사용해 다수의 바인딩을 단일 값에 제공하는 것으로 모델 저장소를 이러한 값의 진실 공급원으로 쓸 수 있습니다 값 유형 의미론에 대한 자세한 내용은 개발자 설명서를 참조해 주세요 또한 제시되는 유형은 해시 가능 프로토콜과 코딩 가능 프로토콜을 모두 준수해야 합니다 해시 가능 준수성은 제시된 값을 열린 윈도우와 연결하는 데 필요하며 코딩 가능 준수성은 상태 복원을 위해 제시된 값을 지속하는 데 필요합니다 이 두 행동은 잠시 후에 더 자세히 설명하겠습니다 마지막으로, 가능하면 가벼운 값을 전달하도록 하세요 이 경우에도 책 식별자가 훌륭한 예시가 됩니다 상태 복원을 위해 SwiftUI에서 이 값을 유지하면 더 작은 값을 사용하는 것으로 앱의 응답성이 향상됩니다 이제 버튼에 세부 윈도우를 표시하는 데 필요한 요소가 생겼지만 버튼을 선택해도 아무것도 나타나지 않을 겁니다 이는 SwiftUI에 특정 데이터 유형의 윈도우를 표시하라고 지시했지만 이를 반영하는 씬을 앱에서 정의하지 않았기 때문이죠 다시 앱으로 돌아가서 변경해 봅시다 기본 WindowGroup과 보조 윈도우와 함께 책 세부 정보를 처리하기 위해 추가 WindowGroup을 추가해 보죠 책 세부 정보 WindowGroup은 새로운 식별자를 사용합니다 제목 외에도 이 그룹이 이 경우엔 UUID인 Book.ID 유형을 위한 데이터를 제시하는 것에 주목해 주세요 이 유형은 앞에서 추가한 openWindow 동작에 전달되는 값과 일치해야 합니다 전달된 값이 프레젠테이션을 위한 WindowGroup에 제공되면 SwiftUI는 해당 값에 맞는 새 하위 씬을 생성하고 해당 씬 윈도우의 루트 정보는 그룹의 뷰 작성기를 통해 해당 값으로 정의됩니다 제시된 각각의 값이 고유할 경우 새로운 씬을 만듭니다 값의 동일성을 통해 새 윈도우를 생성해야 할지 또는 기존 윈도우를 재사용할지 여부를 결정합니다 openWindow가 존재하는 윈도우가 있다는 값을 제시할 경우 그룹은 새 윈도우를 생성하는 대신 해당 윈도우를 사용합니다 BookClub 앱을 예시로 들어 보죠 윈도우에 이미 표시된 책의 빠른 메뉴 동작을 선택하면 동일한 책을 보여 주는 두 번째 윈도우가 뜨는 대신 해당 윈도우가 맨 앞으로 나올 겁니다 또한 제시된 값은 상태 복원을 위해 SwiftUI가 자동으로 유지합니다 처음 제시된 값에 대한 바인딩이 뷰에 전달되는데 이 바인딩은 윈도우가 열려 있는 동안 언제든지 수정할 수 있습니다 상태 복원을 위해 씬을 재생성할 경우 SwiftUI가 가장 최신 값을 윈도우 콘텐츠 뷰에 전달할 겁니다 여길 보시면 Book.ID 바인딩을 상세 뷰에 제공하고 있는데 이를 통해 모델 저장소에서 지정된 항목을 조회하여 표시할 수 있습니다 모든 요소가 준비되었으므로 이제 빠른 메뉴 항목을 선택해 자체 윈도우에서 책의 세부 정보를 볼 수 있습니다 마지막으로 앱의 씬을 사용자화하는 방법 몇 가지를 살펴보도록 하겠습니다 앱을 정의할 때 주 뷰어 윈도우와 세부 뷰어 윈도우라는 두 WindowGroup 씬을 사용했기 때문에 SwiftUI는 기본적으로 파일 메뉴에 각 그룹의 메뉴 항목을 추가합니다 하지만 세부 윈도우의 메뉴 항목은 이번 사용 사례에 알맞진 않죠 앞에서 추가된 빠른 메뉴를 통해서만 윈도우를 열 수 있게 하려고 합니다 새로운 씬 수정자인 commandsRemoved는 File 메뉴와 같은 기본 명령을 더는 제공하지 않도록 씬을 수정할 수 있습니다 이 수정자를 적용하면 File 메뉴에는 기본 WindowGroup에서 윈도우를 여는 항목만 있을 겁니다 독서 활동을 보여 주는 보조 윈도우 씬의 현재 프레젠테이션이 썩 마음에 들지 않으니 이번엔 그 부분에 초점을 맞춰 보죠 여기에 수정자를 몇 개 더 적용할 텐데요 커스텀 씬을 여기에 추출해서 앱 정의를 더 깔끔하게 하겠습니다 윈도우의 이전 상태가 없을 경우 SwiftUI는 기본적으로 화면 중앙에 윈도우를 배치합니다 하지만 Reading Activity 윈도우를 다른 위치에 배치하는 것을 기본값으로 하고 싶으므로 새로운 defaultPosition 수정자를 추가하는 것으로 이전 상태가 없을 경우에 윈도우의 위치를 특정할 수 있습니다 이 위치는 화면 크기에 비례하며 현재 로케일을 고려해 적절한 위치에 윈도우를 배치합니다 이 새 위치는 Activity 윈도우가 떠 있는 다른 윈도우와 구분하는 데 도움이 됩니다 또한 Activity 윈도우를 특정 크기를 기본값으로 지정하되 크기를 조정할 수 있게 하려면 defaultPosition과 함께 defaultSize 수정자를 추가하면 됩니다 여기에 제공된 값은 윈도우의 초기 크기를 유도하도록 레이아웃 시스템에 주어집니다 이제 윈도우의 프레젠테이션을 사용자화했으니 행동을 사용자화하는 수정자를 하나만 더 추가해 보죠 keyboardShortcut 수정자는 씬 유형에서도 작동하도록 기능이 확장되었습니다 해당 씬 수준에서 이 수정자를 사용하면 새 윈도우를 생성하는 명령에 영향을 끼칩니다 여기 나온 대로 Activity 윈도우를 수정하면 option-command-0 단축키로 이 윈도우를 열 수 있습니다 이는 일반적으로 사용되는 씬의 단축키를 제공해 앱을 사용자화하는 좋은 방법이며 앱에 기본 WindowGroup을 추가하는 기본 단축키인 command-N을 사용자화하는 경우에도 사용할 수 있습니다 이것으로 SwiftUI의 새로운 씬과 윈도우 설정 기능을 둘러보는 것을 마치겠습니다 저희는 이 새로운 API의 잠재력에 큰 기대를 걸고 있으며 여러분도 그렇길 바랍니다 iPadOS와 macOS 앱에 기능을 추가하는 방법을 더 자세히 알고 싶으시다면 다음 세션을 참고하시길 바랍니다 "SwiftUI on iPad: Organize your interface" "SwiftUI on iPad: Add toolbars, titles, and more"
시청해 주셔서 감사합니다 ♪
-
-
2:01 - Scene composition
import SwiftUI import UniformTypeIdentifiers @main struct MultiSceneApp: App { var body: some Scene { WindowGroup { ContentView() } #if os(iOS) || os(macOS) DocumentGroup(viewing: CustomImageDocument.self) { file in ImageViewer(file.document) } #endif #if os(macOS) Settings { SettingsView() } #endif } } struct ContentView: View { var body: some View { Text("Content") } } struct ImageViewer: View { var document: CustomImageDocument init(_ document: CustomImageDocument) { self.document = document } var body: some View { Text("Image") } } struct SettingsView: View { var body: some View { Text("Settings") } } struct CustomImageDocument: FileDocument { var data: Data static var readableContentTypes: [UTType] { [UTType.image] } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } self.data = data } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { FileWrapper(regularFileWithContents: data) } }
-
2:34 - Adding a window scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } class ReadingListStore: ObservableObject { }
-
3:01 - Standalone menu bar extra app
import SwiftUI @main struct UtilityApp: App { var body: some Scene { MenuBarExtra("Utility App", systemImage: "hammer") { AppMenu() } } } struct AppMenu: View { var body: some View { Text("App Menu Item") } }
-
3:35 - Windowed app with menu bar extra
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } #if os(macOS) MenuBarExtra("Book Club", systemImage: "book") { AppMenu() } #endif } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct AppMenu: View { var body: some View { Text("App Menu Item") } } class ReadingListStore: ObservableObject { }
-
3:42 - Menu bar extra with default style
import SwiftUI @main struct UtilityApp: App { var body: some Scene { MenuBarExtra("Utility App", systemImage: "hammer") { AppMenu() } } } struct AppMenu: View { var body: some View { Text("App Menu Item") } }
-
3:49 - Menu bar extra with window style
import SwiftUI @main struct UtilityApp: App { var body: some Scene { MenuBarExtra("Time Tracker", systemImage: "rectangle.stack.fill") { TimeTrackerChart() } .menuBarExtraStyle(.window) } } struct TimeTrackerChart: View { var body: some View { Text("Time Tracker Chart") } }
-
4:14 - Book Club app definition
import SwiftUI @main struct BookClubApp: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } class ReadingListStore: ObservableObject { }
-
4:38 - Adding an auxiliary Window Scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } class ReadingListStore: ObservableObject { }
-
5:28 - Open book context menu button
import SwiftUI struct OpenBookButton: View { var book: Book var body: some View { Button("Open In New Window") { } } } struct Book: Identifiable { var id: UUID }
-
5:34 - Opening a window using an identifier
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct OpenWindowButton: View { @Environment(\.openWindow) private var openWindow var body: some View { Button("Open Activity Window") { openWindow(id: "activity") } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } class ReadingListStore: ObservableObject { }
-
5:57 - Opening a window using a presented value
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } } } struct OpenWindowButton: View { var book: Book @Environment(\.openWindow) private var openWindow var body: some View { Button("Open In New Window") { openWindow(value: book.id) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
6:16 - Opening a window with a new document
import SwiftUI import UniformTypeIdentifiers @main struct TextFileApp: App { var body: some Scene { DocumentGroup(viewing: TextFile.self) { file in TextEditor(text: file.$document.text) } } } struct NewDocumentButton: View { @Environment(\.newDocument) private var newDocument var body: some View { Button("Open New Document") { newDocument(TextFile()) } } } struct TextFile: FileDocument { var text: String static var readableContentTypes: [UTType] { [UTType.plainText] } init() { text = "" } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let string = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) } text = string } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = text.data(using: .utf8)! return FileWrapper(regularFileWithContents: data) } }
-
6:41 - Opening a window with an existing document
import SwiftUI import UniformTypeIdentifiers @main struct TextFileApp: App { var body: some Scene { DocumentGroup(viewing: TextFile.self) { file in TextEditor(text: file.$document.text) } } } struct OpenDocumentButton: View { var documentURL: URL @Environment(\.openDocument) private var openDocument var body: some View { Button("Open Document") { Task { do { try await openDocument(at: documentURL) } catch { // Handle error } } } } } struct TextFile: FileDocument { var text: String static var readableContentTypes: [UTType] { [UTType.plainText] } init() { text = "" } init(configuration: ReadConfiguration) throws { guard let data = configuration.file.regularFileContents, let string = String(data: data, encoding: .utf8) else { throw CocoaError(.fileReadCorruptFile) } text = string } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let data = text.data(using: .utf8)! return FileWrapper(regularFileWithContents: data) } }
-
7:03 - Book details context menu button
struct OpenWindowButton: View { var book: Book @Environment(\.openWindow) private var openWindow var body: some View { Button("Open In New Window") { openWindow(value: book.id) } } } struct Book: Identifiable { var id: UUID }
-
7:08 - Book details context menu button
struct OpenWindowButton: View { var book: Book @Environment(\.openWindow) private var openWindow var body: some View { Button("Open In New Window") { openWindow(value: book.id) } } } struct Book: Identifiable { var id: UUID }
-
9:06 - Book Club app with book details Scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
10:32 - Book Club app with book details Scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
11:16 - Removing default commands for the book details scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } Window("Activity", id: "activity") { ReadingActivity(store: store) } WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } .commandsRemoved() } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
11:46 - Extracting reading activity into custom scene
import SwiftUI @main struct BookClub: App { @StateObject private var store = ReadingListStore() var body: some Scene { WindowGroup { ReadingListViewer(store: store) } ReadingActivityScene(store: store) WindowGroup("Book Details", for: Book.ID.self) { $bookId in BookDetail(id: $bookId, store: store) } .commandsRemoved() } } struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } } } struct ReadingListViewer: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading List") } } struct ReadingActivity: View { @ObservedObject var store: ReadingListStore var body: some View { Text("Reading Activity") } } struct BookDetail: View { @Binding var id: Book.ID? @ObservedObject var store: ReadingListStore var body: some View { Text("Book Details") } } struct Book: Identifiable { var id: UUID } class ReadingListStore: ObservableObject { }
-
12:04 - Applying the defaultPosition modifier
struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } .defaultPosition(.topTrailing) } } class ReadingListStore: ObservableObject { }
-
12:32 - Applying the defaultSize modifier
struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } #if os(macOS) .defaultPosition(.topTrailing) .defaultSize(width: 400, height: 800) #endif } } class ReadingListStore: ObservableObject { }
-
12:50 - Applying the keyboardShortcut modifier
struct ReadingActivityScene: Scene { @ObservedObject var store: ReadingListStore var body: some Scene { Window("Activity", id: "activity") { ReadingActivity(store: store) } #if os(macOS) .defaultPosition(.topTrailing) .defaultSize(width: 400, height: 800) #endif #if os(macOS) || os(iOS) .keyboardShortcut("0", modifiers: [.option, .command]) #endif } } class ReadingListStore: ObservableObject { }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.