SwiftUI는 이미 여러가지의 컨테이너를 제공한다.
정적 컨텐츠
List {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")
}
동적 컨텐츠
List {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")
ForEach(otherSongs) { song in
Text(song.title)
}
}
어떤 형태의 컨텐츠던 하나의 컨테이너 안에 넣을 수 있다.
헤더와 푸터를 가진 섹션으로 그룹화하기
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)
}
}
}
컨테이너용 modifier
Section("Other Songs") {
ForEach(otherSongs) { song in
Text(song.title)
.listRowSeparator(.hidden)
}
}
이 세션에서는 새로운 API를 통해서 이러한 기능들을 모두 지원하는 커스텀 컨테이너를 만드는 것을 다룬다.
시작
부르고 싶은 노래를 보여줄 게시판 뷰
커스텀 카드 레이아웃을 통해서 랜덤한 위치에 카드를 배치하게 했다.
@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)
}
}
// DisplayBoard 구현
var data: Data
@ViewBuilder var content: (Data.Element) -> Content
var body: some View {
DisplayBoardCardLayout {
ForEach(data) { item in
CardView {
content(item)
}
}
}
.background { BoardBackgroundView() }
}
지금은 단일 콜렉션만 보여줄 수 있어서 제한적이다.
Composition
SwiftUI의 리스트는 콜렉션을 직접 받을수도 있지만 다른 방법으로도 초기화를 할 수 있다.
// 콜렉션을 받아서 초기화하기
List(songsFromSam) { song in
Text(song.title)
}
// 컨텐츠를 직접 명시해서 초기화하기
List {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")
}
SwiftUI는 여러개의 컨텐츠를 합칠 수 있는 기능을 제공함으로써 이 간극을 메운다.
예를 들어 data-driven 리스트를 ForEach를 사용해서 재작성할 수 있다.
기존과 동작은 같지만 ForEach뷰는 viewbuilder안에 중첩될 수 있다.
List {
ForEach(songsFromSam) { song in
Text(song.title)
}
}
두 리스트의 컨텐츠를 뷰 만으로 표현할 수 있게 되면 두 리스트를 합친 리스트를 만들 수도 있게 된다.
List {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")
ForEach(songsFromSam) { song in
Text(song.title)
}
}
DisplayBoard 컨테이너에도 같은 기능을 추가하고 싶다.
데이터를 받던 것을 뷰 빌더만 받도록한다.
@ViewBuilder var content: Content
var body: some View {
DisplayBoardCardLayout {
ForEach(data) { item in
CardView {
content(item)
}
}
}
.background { BoardBackgroundView() }
}
다음으로 이 content를 사용하도록 해야 하는데, 여기서 ForEach(subviewOf:)라는 새로운 API를 용한다.
@ViewBuilder var content: Content
var body: some View {
DisplayBoardCardLayout {
ForEach(subviewOf: content) { subview in
CardView {
subview
}
}
}
.background { BoardBackgroundView() }
}
덕분에 이제 DisplayBoard도 리스트처럼 구현할 수 있다.
DisplayBoard {
Text("Scrolling in the Deep")
Text("Born to Build & Run")
Text("Some Body Like View")
ForEach(songsFromSam) { song in
Text(song.title)
}
}
어떻게 동작하는가? ForEach(subviewOf:) 에서 말하는 subview란?
다른 뷰 안쪽에 있는 뷰
하지만 ForEach같은 경우는 여러개의 subView를 나타낸다.
리스트에서도 마찬가지다.
이 두가지를 구분하는 게 중요하다.
선언된 subview: 코드상에서 나타나는 subview
결정된 subview: 실제로 화면에 보여지는 subview
SwiftUI의 선언적 시스템에서는 선언된 subView는 결정된 subview를 만드는 레시피 역할을 한다.
ForEach는 특별한 시각적인 영향이나 개별 동작은 없지만 목적 자체가 결정된 subview콜렉션을 만드는 역할을 한다.
내장 컨테이너인 Group도 비슷한 역할을 한다.
결정된 subview가 없는 경우도 있다.(EmptyView)
조건에 따라서 다르게 결정하는 경우도 있다.(if 분기)
ForEach(subviewOf:)는 결정된 subview들을 순회하면서 커스텀 컨테이너가 가능한 모든 조합을 더 적은 코드로 지원할 수 있게 해준다.
더 유연해졌기 때문에 또 다른 리스트를 추가하는 것도 쉽다.
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)
}
}
카드가 너무 많으면 잘 안보이기 떄문에 이때는 크기를 줄이고 싶다.
이때는 Group(subviewsOf:)를 사용해서 결정된 subview 콜렉션을 받아서 처리한다.
@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() }
}
Sections
내장 컨테이너 중 List는 Section을 지원한다.
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)
}
}
}
Section은 그룹과 비슷하지만 헤더와 푸터 등 추가 메타데이터를 지원한다.
DisplayBoard에서도 Section을 지원하고 싶다.
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)
}
}
}
section을 위한 뷰 따로 만들기
@ViewBuilder var content: Content
var body: some View {
DisplayBoardSectionContent {
content
}
.background { BoardBackgroundView() }
}
struct DisplayBoardSectionContent<Content: View>: View {
@ViewBuilder var content: Content
...
}
ForEach(sectionOf: ) API로 섹션 지원
@ViewBuilder var content: Content
var body: some View {
HStack(spacing: 80) {
ForEach(sectionOf: content) { section in
DisplayBoardSectionContent {
section.content
}
}
}
.background { BoardBackgroundView() }
}
헤더 지원
@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() }
}
섹션 지원은 옵셔널이다.
Customization
listRowSeparator modifier 같이 컨테이너 안의 컨텐츠를 커스텀하고 싶다.
이런 modifier를 쓰더라도 실제로 이를 받아서 구현하는 건 컨테이너의 몫이다.
DisplayBoard에서는 특정 카드를 선택하지 않겠다는 것을 나태는 빗금 표시를 커스텀하고 싶다.
이러한 container용 값을 커스텀할 수 있는 Container Value라는 기능이 추가되었다.
modifier정의
컨테이너에서 사용하기
실제로 사용하기