스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
iPad의 SwiftUI: 도구 막대, 제목 등 추가
SwiftUI를 통해 iPad 앱의 도구 막대를 개선해 보세요. 도구 막대를 구조화하여 iPad에서 사용 가능한 공간을 활용하고 사용자의 생산성을 극대화하도록 지원하는 방법을 보여드립니다. 또한 맞춤화에 대해 안내하고, 문서를 나타내는 최신 방법 등에 대해 살펴보겠습니다. 이것은 2부작 시리즈의 두 번째 세션입니다. 이 비디오를 최대한 활용하려면 ‘iPad의 SwiftUI: Organize your interface(인터페이스 구조화)'를 시작하는 것이 좋습니다.
리소스
관련 비디오
WWDC23
WWDC22
-
다운로드
♪ ♪
안녕하세요, 저는 SwiftUI 팀의 엔지니어 Harry예요 iPad를 위한 SwiftUI 시리즈의 두 번째 파트에 오신 걸 환영해요 이 시리즈의 첫 번째 파트에서는 제 동료 Raj가 목록과 테이블, selection 스플릿 뷰를 이용하여 iPad의 커다란 화면과 다양한 입력 기기들에서 여러분의 앱이 빛나게 할 방법을 심도 있게 알아봤습니다 그 세션을 아직 보지 않았다면 꼭 먼저 보시기를 추천합니다 저는 Raj가 Places 앱을 만드는 것을 즐겁게 보았는데 여기에 제가 원하는 몇 가지 기능을 더해보겠습니다 이번 세션에서는 제가 정말 좋아하는 툴바를 알아보겠습니다 SwiftUI에서는 툴바 API로 iOS의 네비게이션바나 바텀바 macOS의 윈도우 툴바와 같은 다양한 시스템바를 설정합니다 툴바는 가장 일반적인 기능에 대한 빠른 작업을 가능케 합니다 좋은 툴바를 만들면 앱 사용자의 생산성을 확연히 증가시킬 수 있어요
저는 툴바에 대해, 그리고 iOS 16의 새로운 툴바 기능을 Places에서 사용할 방법에 대해 많은 시간 동안 생각해 보았습니다 이제 iPad에서 무엇이 가능한지 제가 만든 것을 보여드리며 시작하겠습니다
이는 제가 업데이트한 Places 앱입니다
눈치 채셨겠지만 탐색 제목, 제목 메뉴 제목 메뉴 헤더가 왼쪽에 정렬됐고 툴바 항목들이 가운데 정렬됐습니다 Mac의 사용자라면 이와 같이 툴바를 원하는 대로 만드는 툴바 사용자화 같은 기능에 익숙하실 것입니다 Mac의 이 강력한 기능이 iPad에도 데뷔했습니다
이 세션을 시작하며 툴바 API의 개선점을 보여드리겠습니다 그리고 제목과 문서를 위한 몇 가지 새로운 API를 함께 볼게요 그럼 바로 툴바를 알아봅시다 여러분은 대부분 iOS 앱에서 툴바를 구성해봤을 거예요 그리고 작은 화면에 최적화하기 위해 제가 Place 앱에 한 것과 같이 메뉴를 추가하셨을 것입니다
제 메뉴를 코드로 보면 이렇습니다
ToolbarItem(placement: .primary Action)을 볼 수 있는데 이 안에는 몇 가지 제어를 포함해 더 많은 메뉴가 있습니다 iPad에서 어떻게 보이는지 봅시다 예상하셨듯이 넓은 공간의 이점을 활용하지 못합니다 iOS 16의 좋은 점은 여러분 대신 이 메뉴들을 툴바에 구현한다는 점입니다 이러한 추가 메뉴를 호출해 최대한 활용하기 위해서 툴바의 내용을 다시 구성하겠습니다
먼저 ToolbarItem을 ToolbarItemGroup으로 바꾸고 menu를 삭제한 뒤 그 내용을 ToolbarItemGroup의 내용으로 넣습니다 이 그룹은 개별 툴바 항목들을 각 그룹의 보기에 삽입합니다 iPad나 Mac에서는 이렇게만 하면 필요한 때 항목들을 자동으로 추가 메뉴로 이동시킵니다 더 할 수 있는 것이 있지만 그 전에 툴바 항목의 placement를 생각해보죠 placement는 툴바가 렌더링 될 영역을 정의합니다 다른 placement가 같은 영역이 될 수 있습니다 탐색 막대를 생각해보면 왼쪽, 가운데, 오른쪽의 세 영역으로 확실히 구분됩니다 왼쪽과 오른쪽 영역은 보통 제어를 포함하고 가운데 영역은 앱의 탐색 제목을 포함합니다 이를 Places 앱에서 확인해봅시다
Places 앱에서 primaryAction인 ToolbarItemGroup은 iPad나 Mac에서 오른쪽에 위치합니다 primaryAction은 특정 화면에서 사용자가 가장 일반적으로 수행할 작업을 나타냅니다 iOS 16에는 새로운 plcaement인 secondaryAction이 추가됐습니다 이 항목들은 가장 많이 사용되는 제어는 아니더라도 고유한 툴바 항목들을 나타냅니다 Places 앱에서 편집이나 별표처럼 가장 중요하지는 않은 작업을 secondaryAction으로 설정하겠습니다
기본적으로 secondryAction은 툴바에서 보이지 않고 추가 메뉴에 있습니다 이는 toolbarRole 변경자로 바꿀 수 있습니다
이 변경자는 툴바에 의미를 부여해 동작에 영향을 미칩니다 editor라는 값을 주면 탐색 막대가 컨텐츠 내용 편집에 최적화됩니다 탐색 막대는 이를 해석해 툴바 항목들에 공간을 더 할당해서 탐색 제목을 가운데에서 왼쪽으로 옮깁니다 이렇게 하면 secondaryAction이 추가 메뉴로 가기 전에 가운데에 위치할 공간이 생깁니다 사이즈 클래스가 compact일 때 탐색 막대는 바뀌지 않고 제목을 그대로 가운데에 렌더링합니다
secondaryAction과 toolbarRole API를 사용해 Places 앱이 iPad의 큰 화면을 활용해봅시다 가운데 영역을 사용할 수 있으므로 음소거 버튼이나 휴지통 버튼 좋아요 표시 버튼, 휴지통 버튼 등 툴바에 더 많은 항목을 추가할 수 있습니다 그러나 항목이 너무 많으면 일부 사용자는 툴바를 불편해 합니다 macOS는 사용자가 직접 최적의 항목들을 고를 수 있는 사용자화가 가능한 툴바를 지원해왔습니다 이제 iPadOS에서도 사용자화를 지원함을 알리게 되어 기쁩니다 macOS에서 작동하는 툴바 사용자화 API로 이 기능을 적용할 수 있습니다 한번 봅시다 먼저 ToolbarItem만 사용자화 할 수 있으므로 ToolbarItemGroup을 ToolbarItem으로 분리합니다 이렇게 바꿔도 기능적인 차이는 없습니다 사용자화는 툴바의 모든 항목이 고유한 식별자와 연결돼야 하므로 각 항목에 모두 ID를 추가해줍니다 이 ID는 반드시 고유하고 앱이 실행중인 동안 일관되어야 합니다 사용자가 툴바를 사용자화할 때 SwiftU는 이 ID를 유지하고 이를 사용해 렌더링할 관련 보기를 찾습니다 마지막으로 전체 툴바 변경자에 ID를 추가합니다 이 전체를 통해 툴바가 사용자화를 지원하게 됩니다
사용자화 가능한 툴바의 특별한 기능으로 툴바 항목들이 처음에 툴바에 보이지 않게 할 수 있죠 이 항목들은 나중에 사용자화 팝오버가 나타나면 추가할 수 있습니다 이 항목들은 처음에는 없기 때문에 특정 워크플로우에서 더 유용하며 작업에 좋은 선택지가 됩니다 확인해봅시다
현재 툴바 항목 일부를 숨겨 새 항목이 잘 보이게 하겠습니다
ShareLink를 ToolbarItem에 추가합니다 ShareLink는 Transferable 프로토콜에 의존하는 SwiftUI의 새 요소입니다 Transferable과 ShareLink에 대한 추가적인 정보는 'Transferable 만나기' 세션에서 확인할 수 있습니다 ToolbarItem에 대해 showByDefault: false로 설정해 이 항목이 처음에는 보이지 않도록 합니다
이제 공유 버튼이 사용자화 팝오버에 나타납니다 이를 사용자화 팝오버에서 툴바로 드래그할 수 있습니다 이 기능은 인기가 많을 것입니다 공유 버튼을 위치시키고 나니 툴바 항목 간 관계에 대해 생각하게 되었습니다 공유 버튼을 툴바로 옮긴 후 사진과 지도 조정 버튼이 분리되었는데 이 두 항목은 빠른 편집 제어를 하는 하나의 그룹으로 생각되므로 툴바에서 이 둘의 관계를 모델링하고자 합니다
iOS 16과 macOS Ventura는 ControlGroup으로 이런 관계를 모델링할 수 있습니다 어떻게 하는지 보여드리겠습니다 여기에 사진과 이미지 조정 작업에 대한 두 개의 개별적 ToolbarItem이 있습니다 이 둘을 하나의 그룹으로 묶으려면 하나의 ToolbarItem으로 옮기고 ControlGroup으로 묶어줍니다
사용자는 이제 이를 한 묶음으로 추가하거나 제거할 수 있습니다 여기까지도 멋지지만 ControlGoup에서 사용할 수 있는 새로운 API로 다른 작업도 할 수 있습니다 ControlGroup에 label을 추가해서 이 항목들의 그룹을 추가 메뉴로 이동하기 전에 더 작은 메뉴로 축소할 수 있죠 이제 툴바가 합쳐지기 시작합니다 마지막으로 한 가지 더 추가해보죠 새 장소를 추가하는 작업은 일반적이고 중요한 만큼 툴바에 이를 추가하겠습니다
이를 위해 먼저 툴바에 새로 만들기 버튼을 추가해야 하는데 이번엔 이 작업이 가장 일반적인 작업인 만큼 primaryAction으로 위치시키겠습니다
이 배치는 iOS와 macOS에서 중요하게 구분됩니다 macOS는 사용자화 가능한 툴바 변경자의 모든 항목의 사용자화를 지원하지만 iPadOS는 secondaryAction만 지원합니다 따라서 새로 만들기 버튼은 오른쪽에 렌더링되며 사용자화가 되지 않습니다 우와! 툴바에 대해 많은 내용을 다뤘네요 개선 사항은 이 뿐만이 아닙니다 탐색 제목에도 메뉴, 문서 등을 중심으로 몇 가지 새 기능이 추가됐습니다 문서를 예로 들어보죠 다양한 종류의 문서가 있습니다 DocumentGroup API로 관리되는 문서들에 대해 친숙하실 것입니다 DocumentGroup API는 문서를 나타내고 관리할 수 있는 많은 기능들이 내장돼 있습니다 제가 드릴 말씀은 DocumentGroup을 사용할 때 저절로 알게 됩니다
Places는 앱에서 DocumentGroup API를 사용하지 않지만 개별 장소를 각각의 문서로 여길 수 있습니다 편집할 수 있는 속성이 있고 Places 앱에서 추가하거나 삭제할 수 있으며 친구들과 장소를 공유할 수도 있습니다 DocumentGroup을 사용하지 않은 앱에서 이 관계가 어떻게 보일지 살펴보죠 제가 미리 장소의 이름을 이 보기의 탐색 제목으로 설정해 두었습니다 즉, 장소를 툴바에 연결한 것죠 iOS 16에서는 navigatinTitle 변경자로 그 이상을 할 수 있습니다 이제 navigationTitle로 메뉴를 보여줄 수 있습니다 이는 macOS의 파일 메뉴와 비슷하게 볼 수 있습니다 이 메뉴들 중 하나를 만들려면 일반 메뉴와 비슷하게 navigationTitle에 작업들을 제공하면 됩니다 보시면 제목에 설정한 작업이 나타나는 메뉴 지시자를 확인할 수 있습니다 navigationTitle의 능력은 이뿐만이 아닙니다 제가 제일 좋아하는 것은 navigationTitle의 제목 수정 지원입니다 navigationTitle에 바인딩을 전달해 툴바에서 제목 수정을 지원합니다 그런데 아직은 실제로 제목을 수정할 방법이 없습니다 새로운 RenameButton을 제목 아래 메뉴에 추가해 수정할 방법을 마련합니다
Rename 버튼을 누르면 제목을 바꿀 수 있습니다 탐색 제목을 보기에 연결한 것처럼 이제 내 장소와 같은 문서를 연결할 수 있습니다 문서가 제공되면 제목 메뉴는 문서의 미리보기를 보여주는 특별한 헤더를 렌더링합니다 미리보기를 드래그해서 빠르게 공유할 수 있습니다 이러한 헤더를 얻으려면 navigationDocument 변경자로 이를 보기에 연결해야 합니다 이는 Transferable에 맞는 타입이나 URL을 직접 제공해서 연결할 수 있습니다 지도 앱에서 지금 보고 있는 곳을 보여주는 URL을 넣어보겠습니다 navigationDocument 변경자는 URL이 제공되면 macOS의 윈도우 툴바의 프록시 아이콘도 설정합니다
이제 앱의 툴바 업데이트는 여기까지 하겠습니다 이 시간동안 이 모든 기능을 추가했다는게 믿어지세요? 어서 써보고 싶네요 iPad의 Places 앱 경험을 개선하기 위해 많은 부분을 다뤘습니다 iPad의 툴바에는 추가 메뉴나 사용자화와 같은 많은 기능이 추가됐습니다 secondaryAction과 사용자화 API로 넓은 공간을 더 잘 활용하게 됐습니다
제목을 툴바에서 나타내는 새로운 방법도 다뤘습니다 navigationTitle로 제목 메뉴를 만들고 제목 편집을 지원했습니다 navigationDocument 변경자를 적절히 사용하는 걸 기억해주세요 iPad 시리즈에서의 SwiftUI를 즐기시길 바랍니다 테이블, selection 툴바 등의 개선 사항으로 여러분의 iPad 앱을 다음 단계로 도약시켜보세요 감사합니다 좋은 WWDC22 보내세요
-
-
0:01 - Explicit More Menu
PlaceDetailContent(place: $place) .toolbar { ToolbarItem(placement: .primaryAction) { Menu { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label( "More", systemImage: "ellipsis.circle") } } }
-
0:02 - Menu in ToolbarItemGroup
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .primaryAction) { Menu { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label("More", systemImage: "ellipsis.circle") } } }
-
0:03 - ToolbarItemGroup with Menu Content
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .primaryAction) { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } }
-
0:04 - Secondary Action ToolbarItemGroup
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .secondaryAction) { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } }
-
0:05 - Toolbar Role
PlaceDetailContent(place: $place) .toolbar { ToolbarItemGroup(placement: .secondaryAction) { FavoriteToggle(place: $place) AdjustImageButton(place: $place) AdjustMapButton(place: $place) } } .toolbarRole(.editor)
-
0:06 - Individual ToolbarItems
PlaceDetailContent(place: $place) .toolbar { ToolbarItem(placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(placement: .secondaryAction) { AdjustMapButton(place: $place) } } .toolbarRole(.editor)
-
0:07 - Customizable ToolbarItems
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(id: "map", placement: .secondaryAction) { AdjustMapButton(place: $place) } } .toolbarRole(.editor)
-
0:08 - ShareLink ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(id: "map", placement: .secondaryAction) { AdjustMapButton(place: $place) } ToolbarItem(id: "share", placement: .secondaryAction) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:09 - Non-default ShareLink ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { AdjustImageButton(place: $place) } ToolbarItem(id: "map", placement: .secondaryAction) { AdjustMapButton(place: $place) } ToolbarItem(id: "share", placement: .secondaryAction, showsByDefault: false) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:10 - ControlGroup in ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { ControlGroup { AdjustImageButton(place: $place) AdjustMapButton(place: $place) } } ToolbarItem(id: "share", placement: .secondaryAction, showsByDefault: false) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:11 - ControlGroup in ToolbarItem with Label
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { ControlGroup { AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label("Edits", systemImage: "wand.and.stars") } } } .toolbarRole(.editor)
-
0:12 - NewButton ToolbarItem
PlaceDetailContent(place: $place) .toolbar(id: "place") { ToolbarItem(id: "new", placement: .primaryAction) { NewButton() } ToolbarItem(id: "favorite", placement: .secondaryAction) { FavoriteToggle(place: $place) } ToolbarItem(id: "image", placement: .secondaryAction) { ControlGroup { AdjustImageButton(place: $place) AdjustMapButton(place: $place) } label: { Label("Edits", systemImage: "wand.and.stars") } } ToolbarItem(id: "share", placement: .secondaryAction, showsByDefault: false) { ShareLink(item: place) } } .toolbarRole(.editor)
-
0:13 - Navigation Title
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle(place.name)
-
0:14 - Navigation Title with Menu
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle(place.name) { MyPrintButton() }
-
0:15 - Editable Navigation Title with Menu
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle($place.name) { MyPrintButton() }
-
0:16 - Editable Navigation Title with RenameButton
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle($place.name) { MyPrintButton() RenameButton() }
-
0:17 - Navigation Document
PlaceDetailContent(place: $place) // toolbar customizations ... .navigationTitle($place.name) { MyPrintButton() RenameButton() } .navigationDocument(place.url)
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.