스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
SwiftUI 컨테이너 쉽게 이해하기
SwiftUI 컨테이너 뷰의 기능에 대해 알아보고 컨테이너의 하위 보기 관리 방식에 맞춰 심리적 모델을 구축해 보세요. 맞춤형 컨테이너 제작, 컨테이너 콘텐츠 맞춤화를 위한 제어자 생성, 앱을 더욱 돋보이게 하기 위한 컨테이너 조정 등의 작업에 새로운 API를 활용해 보세요.
챕터
- 0:00 - Introduction
- 3:17 - Composition
- 10:42 - Sections
- 13:18 - Customization
- 16:52 - Next steps
리소스
관련 비디오
WWDC23
WWDC21
-
다운로드
안녕하세요, 저는 SwiftUI를 담당하는 Matt입니다 SwiftUI로 맞춤형 컨테이너 보기를 빌드하는 방법을 다루겠습니다 SwiftUI는 API를 통해 여러 기능을 갖춘 컨테이너를 다양하게 제공하죠 List 컨테이너 보기처럼요
컨테이너 보기는 후행 보기 빌더 클로저로 콘텐츠를 래핑합니다
보기 빌더는 콘텐츠가 정적으로 정의되도록 하죠 화면에 표시된 하드 코딩된 Text 보기의 목록처럼요 한편 보기 빌더는 콘텐츠를 동적으로도 정의할 수 있습니다 ForEach 보기를 통해 데이터로부터 Text 보기를 생성하는 경우와 같죠
또한 보기 빌더로 모든 종류의 콘텐츠를 같은 컨테이너 내에 구축할 수 있습니다 일부 컨테이너는 고급 기능도 지원합니다 콘텐츠를 구성 가능한 헤더와 푸터를 포함하는 구분된 섹션으로 그룹화하는 것과 같은 기능을 제공하죠 컨테이너별 한정자로 콘텐츠를 맞춤화할 수도 있습니다 예를 들어, 이 예시에서는 List가 일반적으로 행 사이에 그리는 구분 기호를 숨기고 있죠 이 동영상에서는 몇 가지 새 API를 활용하여 이러한 기능을 모두 지원할 수 있는 맞춤형 컨테이너 보기를 빌드하는 방법 등을 설명해 드리겠습니다
우선 모든 콘텐츠 구성을 지원하는 맞춤형 컨테이너를 만들어 컨테이너의 유연성을 극대화하는 방법을 설명할게요
또한 섹션에 대한 지원을 추가하는 방법을 보여드리겠습니다
그다음 컨테이너 내에서 콘텐츠를 꾸미기 위한 맞춤형 한정자를 정의하는 방법을 다루겠습니다
또한 주제별로 SwiftUI와 API 설계에 관한 핵심 개념을 설명해 드리겠습니다 지금 바로 시작하겠습니다
이건 무엇인가요?
‘SwiftUI의 새로운 기능’ 세션을 담당했던 Sommer와 Sam이 WWDC를 기념하기 위해 노래방 파티를 열겠답니다
참석 여부를 알릴 때 노래할 곡을 제출해야 하는데요
그런데 무슨 노래를 선택해야 할지 모르겠어요 이렇게 곤란한 상황에서는 저는 항상 하던 대로 해결합니다 강력하면서도 유연한 SwiftUI의 선언형 API로 문제를 해결해 보죠 이 동영상에서는 노래방에서 부르기에 적절한 곡도 선택할게요 도움이 될 만한 것을 작업하고 있었거든요 바로 이 유용한 게시판입니다
노래방에서 부를 만한 몇 곡을 미리 고민해 봤습니다
이니셜라이저를 사용하고 있는데요 제가 모아 둔 노래 아이디어를 Text 보기로 매핑해 주죠 이러한 보기는 게시판에 고정되는 카드에 작성됩니다
저는 DisplayBoard의 구현에서 맞춤형 레이아웃을 사용하여 카드를 게시판에서 임의로 배치하고 있습니다 또한 카드 자체는 ForEach 보기를 통해 구성되는데 이는 입력 데이터의 반복을 처리합니다 각 데이터 요소에 대해 콘텐츠 보기를 생성하고 제가 만든 맞춤형 CardView로 래핑했습니다
시작은 좋지만 DisplayBoard 컨테이너로는 하나의 데이터 컬렉션만 사용하여 카드를 생성할 수 있으므로 창의력을 발휘하는 데 한계가 있죠 다양한 콘텐츠 구성에 대한 지원을 추가하면 컨테이너의 유연성을 개선할 수 있습니다
하지만 그 전에 구성의 뜻을 이해하는 것이 중요합니다
이와 같은 SwiftUI 목록이 있다고 가정해 볼게요 Sam이 저에게 추천한 여러 노래를 표시하고 있습니다
이 List는 데이터 컬렉션으로 초기화됩니다 제 DisplayBoard와 마찬가지로요 하지만 SwiftUI에서는 다른 방법으로 List를 만들 수 있습니다
예를 들어 수동으로 보기 세트를 작성하여 List를 만들 수 있습니다 해당 방법으로 이 노래 아이디어 목록을 만들었죠
SwiftUI는 다양한 종류의 콘텐츠를 함께 구성할 수 있는 API를 제공하여 이 두 기법 간의 간극을 해소합니다
예를 들어 ForEach 보기로 데이터 기반 목록을 다시 작성할 수 있죠 이는 이전과 동일한 기능을 지원하지만 ForEach 보기는 보기 빌더 내에 중첩될 수 있습니다
이는 중요합니다 보기만을 사용하여 두 List의 콘텐츠를 정의할 수 있다면
두 List를 하나의 통합된 List로 합쳐 지금까지 모은 모든 노래 아이디어를 표시할 수 있음을 뜻하죠 이 통합된 목록은 구성의 예시입니다
하드 코딩된 Text 보기를 통해 처음 3개의 행을 정적으로 정의하면서 나머지 행은 데이터를 사용하여 동적으로 생성할 수 있죠 같은 목록 내에서요
DisplayBoard 컨테이너에서도 유연한 구성을 지원하고 싶습니다 이를 위해서는 구현을 변경해야 합니다
우선 컨테이너를 리팩터링해야 합니다 보기 빌더만 사용하여 초기화하기 위해서죠
먼저 기존의 데이터 기반 속성을 하나의 일반적인 보기 속성으로 대체하는 것부터 시작하겠습니다
ViewBuilder 속성을 추가하면 기본 이니셜라이저가 후행 보기 빌더 클로저로 콘텐츠를 자동으로 구성합니다
그다음 새 콘텐츠 보기를 사용하도록 보기 본문을 업데이트해야 합니다 여기에서 새 API인 ForEach(subviewOf:)를 사용할 수 있죠
이 새로운 ForEach 이니셜라이저는 1개의 보기 값을 입력으로 허용하고 각 하위 보기를 후행 보기 빌더로 다시 전달하여 다른 유형의 보기로 전환될 수 있도록 합니다 아까 보여드린 카드 보기처럼요
이 새로운 구현을 통해 이제 이전에 만든 노래 아이디어의 List를 가져온 다음
동일한 콘텐츠를 DisplayBoard에서 대신 줄바꿈하고 각 Text 보기를 게시판의 카드로 변환할 수 있습니다
이는 상당한 개선 사항이지만 이 기능이 어떻게 작동하는지 이해하는 것이 중요합니다
구현으로 돌아가서 새 API인
ForEach(subviewOf:에 대해 설명하죠
그런데 하위 보기라는 것은 정확히 무엇일까요?
하위 보기는 단순히 다른 보기에 포함된 보기를 뜻합니다 이 콘텐츠에서 하위 보기는 몇 개 있을까요? 이는 상황에 따라 다릅니다
코드에서 최고 수준의 보기만 고려한다면 4개 있습니다 3개의 Text 보기와 하나의 ForEach 보기가 있죠
그러나 ForEach는 하나의 보기가 아니라 데이터로 생성된 여러 보기의 컬렉션입니다
이 경우 하위 보기의 수는 9개가 됩니다 Sam이 좋아하는 노래마다 1개씩 있죠
즉, 이 DisplayBoard 콘텐츠에는 총 12개의 개별 하위 보기가 있죠 게시판에 12장의 카드가 있으므로 분명하죠
이는 또한 List의 동일한 콘텐츠에서도 확인할 수 있습니다 12개의 개별 행이 표시되어 있으니까요
이 두 가지 종류의 하위 보기의 차이점을 이해하는 것이 중요합니다
주황색으로 강조된 DisplayBoard의 코드에 있는 4개의 하위 보기는 선언형 하위 보기입니다
반면 화면에 파란색으로 강조된 보기는 해결된 하위 보기라고 합니다 여기에는 수동으로 정의된 3개의 Text 보기와 ForEach에서 생성한 9개의 Text 보기가 포함되죠
SwiftUI의 선언형 시스템에서 선언형 하위 보기는 SwiftUI 앱이 실행되는 동안 해결된 하위 보기를 생성하기 위한 방법을 정의합니다
예를 들어, ForEach 보기는 그 자체로는 시각적 형상이나 동작이 없는 선언형 하위 보기입니다 대신 ForEach 보기의 유일한 목적은 해결된 하위 보기의 컬렉션을 생성하는 것이죠
내장 컨테이너의 다른 예로는 Group 보기가 있습니다 이는 해결된 하위 보기의 컬렉션을 나타냅니다 예를 들어, 3개의 Text 보기로 구성된 Group은 정확히 대응되는 3개의 하위 보기로 해결됩니다
EmptyView와 같은 일부 선언형 하위 보기의 경우 해결된 하위 보기를 전혀 생성하지 않을 수 있습니다
또는 조건부로 다른 개수의 하위 보기로 해결될 수 있습니다 if 구문의 다양한 분기문처럼요
새로운 ForEach(subviewOf:) API는 콘텐츠에서 해결된 하위 보기만 반복합니다
이렇게 하면 코드에서 하위 보기를 어떻게 선언하든 SwiftUI가 하위 보기를 해결하는 작업을 대신 수행하므로 가능한 모든 콘텐츠 구성을 훨씬 적은 코드로도 컨테이너에서 지원할 수 있습니다
유연한 구성을 지원하면 게시판에 새 노래를 간편하게 추가할 수 있게 됩니다
Sam뿐만 아니라 Sommer도 자신이 좋아하는 노래 몇 곡을 추천했는데 ForEach 보기를 통해 추가할 수 있습니다 이는 컨테이너의 구현을 추가로 변경할 필요 없이 가능하죠 하지만 새 아이디어를 추가하는 것이 너무 쉬워져서인지 카드를 다 보기 어려워졌습니다 이 문제를 해결하기 위해 게시판에 카드가 많아지면 카드가 작아지도록 하겠습니다
게시판에 추가된 카드가 15개를 넘었을 때 카드의 크기를 줄이려고 합니다 카드의 개수를 세기 위해 또 다른 새로운 API를 사용할 수 있습니다 Group(subviewsOf:)입니다 이 구현에서는 ForEach에 대해 래핑할 수 있죠
아까 설명한 ForEach(subviewOf:) API와 마찬가지로 이 보기는 보기를 입력으로 허용하고 해당 보기의 하위 보기를 해결합니다
그러나 한 번에 하나씩 반복하는 대신 Group(subviewsOf:) API는 해결된 모든 하위 보기의 컬렉션을 다시 전달합니다
컬렉션에 대해 count 속성을 사용하여 총 카드 수를 확인할 수 있고 카드 수가 15개를 넘을 때 더 작은 scale을 사용하도록 CardView를 구성할 수 있습니다
앱을 다시 실행하면 카드가 작아져 덜 겹치게 됩니다 이러면 카드를 읽는 데 도움이 되지만 여전히 게시판이 정리되지 않은 느낌이 약간 드네요
그러므로 다음에는 섹션에 대한 지원을 추가하여 정리하겠습니다
List는 SwiftUI의 Section 보기를 사용하여 섹션을 지원하는 내장 컨테이너의 예입니다 Section 보기의 작동 방식은 Group 보기와 유사하지만 선택 사항인 헤더와 푸터 등 섹션별 메타데이터를 추가로 사용할 수 있죠
저는 게시판에 각 팀원이 좋아하는 노래별로 별도의 섹션을 만들려고 합니다
하지만 맞춤형 컨테이너는 기본적으로 섹션을 지원하지 않으므로 이를 활성화하기 위해 추가 작업을 해야 합니다
제가 고려하고 있는 디자인의 대략적인 그림입니다 섹션을 만들기 위해 게시판을 세로 방향의 열로 나누고 각 열 상단에는 헤더가 표시될 것입니다
이 구현에서는 기존 카드 레이아웃 코드를 자체의 보기로 팩터링하여 시작하겠습니다
개별 섹션 내에서 카드를 배치할 때 이 보기를 다시 사용할 것입니다
그런 다음 섹션 콘텐츠를 가로 방향의 스택으로 줄바꿈하여 게시판을 열로 나누겠습니다 열을 구성하려면 게시판의 콘텐츠 내에 존재하는 모든 Section 보기에 대한 정보에 접근해야 합니다 이를 위해 ForEach에서 다른 새로운 API를 사용할 수 있습니다
ForEach(sectionOf:)입니다 이는 ForEach(subviewOf:)와 유사하게 작동하며 보기 인스턴스를 입력으로 받습니다
그러나 이 API는 보기 내에서 감지되는 각 섹션을 반복하며 보기 빌더에 섹션 구성을 제공합니다
각 섹션에는 콘텐츠 보기를 위한 속성이 있고 이 속성은 카드 배치를 위해 아까 만들어 둔 헬퍼 보기에 전달할 수 있습니다
마무리하기 전에 시각적으로 구분이 쉽도록 섹션마다 맞춤형 배경을 추가하겠습니다
다시 앱을 실행하면 카드가 아까보다 더 정리된 느낌이 드네요 각 섹션이 전용 열 안에 배치되어 있습니다 이제 섹션 헤더를 표시하기 위한 지원을 추가하겠습니다
VStack의 각 섹션을 래핑하여 헤더와 콘텐츠 모두 포함하도록 하겠습니다
그다음 if 구문을 사용하여 각 섹션에 헤더가 있는지 확인합니다 여기에서 isEmpty 속성을 사용하는데 헤더에 해결된 하위 보기가 포함되어 있는지를 반환하죠
헤더가 존재하는 경우 앞서 작성한 맞춤형 헤더 카드 안에 표시하겠습니다
게시판을 확인해 보니 이제 각 섹션 위에 눈에 잘 띄는 헤더 카드가 있습니다
원활한 노래 선택을 위해 선 긋는 기능을 추가하려고 합니다 이는 컨테이너 내 콘텐츠에 대한 맞춤화 지원을 추가하여 구현할 수 있죠
이 동영상을 시작할 때 .listRowSeparator() 한정자를 사용하는 예시를 보여드렸습니다 이 한정자는 List에 있는 모든 보기에 적용되지만 행 사이에 구분 기호를 그릴지를 결정할 때 동작 구현은 List 자체가 담당합니다
게시판에 있는 특정 노래를 부르지 않기로 결정했을 때 해당 카드에 선을 긋는 기능을 지원하려고 합니다
이러한 컨테이너별 한정자를 빌드하기 위한 새 API가 있습니다 Container 값입니다 Container 값은 새로운 유형의 키 저장소로 Environment 및 Preferences와 유사합니다
그러나 Environment 값은 전체 보기 계층 구조에서 하위로 전달되며
Preferences는 포함된 모든 보기에 대해 전체 보기 계층 구조에서 값을 상위로 전달합니다
해결된 하위 보기의 Container 값은 직접 컨테이너를 통해서만 접근할 수 있습니다 컨테이너별 맞춤화 옵션을 구현할 때 사용하면 매우 유용합니다
게시판에서 Container 값을 사용하여 카드에 선을 긋기 위한 맞춤형 보기 한정자를 만들게요 새로운 Container 값을 정의하려면 몇 줄의 코드만 필요합니다
우선 SwiftUI에 새로 도입된 ContainerValues 유형의 extension을 만들겠습니다
extension 내에서 새로운 Entry 매크로를 사용하여 속성을 선언하고 카드의 거부 여부를 추적하는 불리언 값을 저장할게요
새 API인 Entry 매크로는 environment 값, focus 값 등 SwiftUI 키 저장소 유형에 새 값을 추가할 때 편리한 구문을 제공합니다
그다음 속성 설정 시 편의를 위해 맞춤형 보기 한정자를 선언할게요 이 한정자는 새로운 containerValue() API 한정자를 호출하여 속성의 키 경로와 설정할 새 값을 전달합니다
이제 컨테이너 내에서 새 컨테이너 값에 대한 지원을 추가하겠습니다 섹션 구현에서 카드 콘텐츠의 거부 여부에 따라 각 카드 보기를 맞춤화해야 합니다 containerValues 속성을 사용하면 됩니다 containerValues는 해결된 하위 보기 및 섹션 모두에서 읽을 수 있죠
맞춤 값을 CardView의 isRejected 매개변수에 전달하여 카드가 거부되면 맞춤형 선언을 표시하도록 하겠습니다
새 한정자를 구현했으니 노래 몇 곡은 지워보겠습니다
‘스크롤링 인 더 딥’ 노래를 좋아하긴 하지만 제 음역으로 그 노래를 부를 수 있을지는 모르겠네요
해당 노래에 선을 그으면 빨간색의 큰 슬래시로 렌더링됩니다
Sam이 자신이 추천한 노래 중 몇 개는 부르겠다고 했으니 그 노래들도 거부할게요
Sommer는 어떤 노래를 부를지 잘 모르겠네요 만약을 대비해 Sommer의 섹션에 있는 노래를 모두 거부하겠습니다
섹션 전체에 한정자를 적용하면 모든 하위 보기에 해당 값이 설정되죠
즉, Sommer가 추천한 모든 노래에 슬래시가 렌더링됩니다
좋습니다, 저에게 완벽한 노래를 고르는 데 많은 진전을 이루었어요 하지만 아직 최종 결정이 남아 있죠 제가 노래에 대해 고민하는 동안 여러분은 새 API를 사용해 보세요
ForEach 및 Group에 대해 새 이니셜라이저를 사용하여 해결된 하위 보기와 보기 내 섹션을 반복하고 변환하세요 맞춤형 컨테이너의 설계상 지원할 수 있다면 섹션에 대한 지원도 추가해 보세요 그러나 컨테이너에 섹션을 추가하기가 적절하지 않다면 괜찮습니다, 섹션 지원 추가는 선택 사항이니까요
마지막으로 Container 값을 사용하여 개별 콘텐츠를 맞춤화하고 꾸미세요
이러한 새로운 API를 활용하여 수많은 노래 중에 몇 곡만 남겨 두었습니다
방금 생각났어요 아직 고려하지 않은 노래가 하나 있는데요 그 노래가 적합할 것 같아요
위트니 뷰스턴의 아이 윌 올웨이즈 서브뷰죠
이제 Sommer와 Sam에게 제 참석 여부를 전달하고 이 동영상을 마무리해야겠네요 노래를 연습해야 하거든요 다음에 또 뵙겠습니다
-
-
0:20 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
0:36 - SwiftUI Lists
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(otherSongs) { song in Text(song.title) } }
-
0:54 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
1:00 - SwiftUI Lists
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
2:35 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
2:47 - DisplayBoard implementation
// Insert code snvar data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
3:08 - Data-driven DisplayBoard
@State private var songs: [Song] = [ Song("Scrolling in the Deep"), Song("Born to Build & Run"), Song("Some Body Like View"), ] var body: some View { DisplayBoard(songs) { song in Text(song.title) } }
-
3:30 - List composition
List(songsFromSam) { song in Text(song.title) }
-
3:46 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") }
-
3:56 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List(songsFromSam) { song in Text(song.title) }
-
4:05 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } List { ForEach(songsFromSam) { song in Text(song.title) } }
-
4:24 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
4:59 - DisplayBoard implementation
var data: Data @ViewBuilder var content: (Data.Element) -> Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } }
-
5:15 - DisplayBoard implementation
// DisplayBoard implementation @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(data) { item in CardView { content(item) } } } .background { BoardBackgroundView() } } DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } DisplayBoard { ForEach(songsFromSam) { song in Text(song.title) } }
-
5:27 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
5:52 - List composition
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
5:57 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:12 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
6:23 - DisplayBoard subviews
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
6:36 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
7:11 - List subviews
List { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
7:19 - Declared vs. resolved views
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:00 - Resolved ForEach
// 1 declared view ForEach(songsFromSam) { song in Text(song.title) } // 9 resolved subviews Text("I Container Multitudes") … Text("Love Stack")
-
8:16 - Resolved Group
// 1 declared view Group { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } // 3 resolved subviews Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View")
-
8:32 - Resolved EmptyView
// 1 declared view EmptyView() // Zero resolved subviews
-
8:39 - Resolved if expression
// Insert code snippet.
-
8:48 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:11 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } }
-
9:17 - DisplayBoard composition
DisplayBoard { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") ForEach(songsFromSam) { song in Text(song.title) } ForEach(songsFromSommer) { song in Text(song.title) } }
-
9:44 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { ForEach(subviewOf: content) { subview in CardView { subview } } } .background { BoardBackgroundView() } }
-
9:55 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView { subview } } } } .background { BoardBackgroundView() } }
-
10:19 - DisplayBoard implementation
@ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
10:47 - List sections
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) } } }
-
11:03 - DisplayBoard sections
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { song in Text(song.title) } } }
-
11:26 - Implementing DisplayBoard sections
DisplayBoard sections @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in CardView( scale: subviews.count > 15 ? .small : .normal ) { subview } } } } .background { BoardBackgroundView() } }
-
11:35 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { DisplayBoardSectionContent { content } .background { BoardBackgroundView() } } struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content ... }
-
11:42 - Implementing DisplayBoard sections
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in DisplayBoardSectionContent { section.content } } } .background { BoardBackgroundView() } }
-
12:48 - Implementing DisplayBoard section headers
@ViewBuilder var content: Content var body: some View { HStack(spacing: 80) { ForEach(sectionOf: content) { section in VStack(spacing: 20) { if !section.header.isEmpty { DisplayBoardSectionHeaderCard { section.header } } DisplayBoardSectionContent { section.content } .background { BoardSectionBackgroundView() } } } } .background { BoardBackgroundView() } }
-
13:30 - List customization
List { Section("Favorite Songs") { Text("Scrolling in the Deep") Text("Born to Build & Run") Text("Some Body Like View") } Section("Other Songs") { ForEach(otherSongs) { song in Text(song.title) .listRowSeparator(.hidden) } } }
-
14:46 - Custom container values
extension ContainerValues { @Entry var isDisplayBoardCardRejected: Bool = false } extension View { func displayBoardCardRejected(_ isRejected: Bool) -> some View { containerValue(\.isDisplayBoardCardRejected, isRejected) } }
-
15:42 - Implementing DisplayBoard customization
struct DisplayBoardSectionContent<Content: View>: View { @ViewBuilder var content: Content var body: some View { DisplayBoardCardLayout { Group(subviewsOf: content) { subviews in ForEach(subviews) { subview in let values = subview.containerValues CardView( scale: (subviews.count > 15) ? .small : .normal, isRejected: values.isDisplayBoardCardRejected ) { subview } } } } } }
-
16:15 - DisplayBoard customization
DisplayBoard { Section("Matt's Favorites") { Text("Scrolling in the Deep") .displayBoardCardRejected(true) Text("Born to Build & Run") Text("Some Body Like View") } Section("Sam's Favorites") { ForEach(songsFromSam) { song in Text(song.title) .displayBoardCardRejected(song.samHasDibs) } } Section("Sommer's Favorites") { ForEach(songsFromSommer) { Text($0.title) }}} } .displayBoardCardRejected(true) }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.