View tree

Chris Eidhof와 Florian Kugler의 『Thinking in SwiftUI』 iOS 17 개정판 2장 전체 번역입니다. 원문 PDF 7~21쪽에 해당합니다.

View tree

원문 7쪽은 2장 「View Trees」의 장 표지입니다.

원문 7쪽과 도표원문 7쪽: View Trees 장 표지

View tree와 render tree는 SwiftUI를 사용하려면 이해해야 하는 가장 기본적이고 중요한 개념이라고 할 수 있습니다. 원하는 layout을 구현하려면 view tree가 어떻게 구성되는지 이해해야 합니다. SwiftUI에서 state가 작동하는 방식을 이해하려면 view의 lifetime과 이것이 구성 중인 view tree와 맺는 관계를 알아야 합니다. 필요한 시점에만 데이터를 불러오고 view를 업데이트하는 효율적인 SwiftUI 코드를 작성할 때도 lifetime에 대한 이해가 똑같이 중요합니다. 마지막으로 animation과 transition에도 view tree에 대한 이해가 필요합니다.

예를 들어 다음 view를 살펴보겠습니다.

Text("Hello")
    .padding()
    .background(Color.blue)

원문 8쪽 도표의 코드 오른쪽에는 그에 대응하는 view tree가 있습니다. background modifier가 view tree의 root에 있습니다. primary subview는 background가 적용되는 view로, padding이 적용된 text이며 위에 그려집니다. secondary subview는 파란색 color이며 primary subview 뒤에 그려집니다. Text view에 padding이나 background 같은 view modifier를 적용할 때마다 또 다른 계층으로 감싸집니다. 위 예제와 같이 view modifier가 이어져 있으면 아래에서 위로 읽어야 결과 view tree를 떠올릴 수 있습니다. 마지막 view modifier가 view tree에서 가장 위에 있는 view가 됩니다. 이 예제에서는 background입니다.

background view modifier 자체는 아무것도 그리지 않습니다. background modifier가 view tree에서 가장 위에 있는 view라고 해도 실제 background인 파란색 color는 여전히 text 뒤에 그려집니다.

이번에는 padding과 background의 순서를 바꿔 예제를 조금 수정해 보겠습니다.

원문 8쪽의 도표 1
원문 8쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 8쪽과 도표원문 8쪽: padding과 background를 적용한 코드, view tree와 preview
Text("Hello")
    .background(Color.blue)
    .padding()

이제 background가 text의 바로 위 parent가 되고, padding이 background의 parent가 됩니다. Layout 장에서 layout이 달라지는 이유를 자세히 살펴보겠지만, 간단히 말하면 서로 다른 view tree를 구성했기 때문에 layout이 바뀐 것입니다.

View builder

SwiftUI는 view 목록을 구성하기 위해 view builder라는 특별한 문법을 사용합니다. View builder는 바로 이 목적을 위해 Swift 언어에 추가된 result builder 기능을 기반으로 합니다. 예를 들어 text 옆에 image를 표시하는 view는 다음과 같이 구성할 수 있습니다.

HStack {
    Image(systemName: "hand.wave")
    Text("Hello")
}

HStack initializer는 closure를 parameter로 받으며, 이 closure에는 @ViewBuilder가 표시되어 있습니다. 이 덕분에 closure 안에 각각 view를 나타내는 여러 expression을 작성할 수 있습니다. 기본적으로 stack에 전달하는 closure는 view 목록을 구성하며, 이 예제에서는 그 목록이 stack의 subview가 됩니다.

ViewBuilder struct의 선언을 살펴보면 두 view로 이루어진 목록을 처리하는 다음 method를 볼 수 있습니다.

extension ViewBuilder {
    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) ->
        TupleView<(C0, C1)> where C0 : View, C1 : View
}
원문 9쪽의 도표 1
원문 9쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 9쪽의 도표 2
원문 9쪽의 도표 2. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 9쪽과 도표원문 9쪽: modifier 순서 변경 결과와 HStack의 view tree

위 예제의 stack에는 view expression이 두 개 있으므로 parameter가 두 개인 view builder의 buildBlock method가 호출됩니다. Return type에서 알 수 있듯이, 이 method는 두 view인 image와 text를 감싸는 TupleView를 구성합니다. View builder는 view 목록을 나타내는 tuple view를 구성하는 메커니즘이라고 생각할 수 있습니다.

View builder closure에 view expression을 하나만 작성하면 tuple view로 감싸지 않고 그대로 전달합니다. 하지만 이해를 위한 모델에서는 이 예외도 view가 정확히 하나인 목록으로 생각할 수 있습니다.

SwiftUI는 여러 곳에서 view builder를 사용합니다. Stack이나 grid 같은 모든 container view와 background, overlay 같은 modifier는 subview를 구성하기 위해 view builder closure를 받습니다. 또한 각 view의 body property에는 암묵적으로 @ViewBuilder가 표시되어 있으며, view modifier의 body(content:) method도 마찬가지입니다. 곧 살펴볼 예제처럼 직접 작성한 property와 method에도 @ViewBuilder attribute를 사용해 view builder로 표시할 수 있습니다.

SwiftUI가 view 목록을 어떻게 사용하고, view 목록을 어떻게 조합할 수 있는지 더 잘 이해하기 위해 앞선 예제를 조금 확장해 보겠습니다.

HStack(spacing: 20) {
    Image(systemName: "hand.wave")
    Text("Hello")
    Spacer()
    Text("And Goodbye!")
    Image(systemName: "hand.wave")
}

이제 stack에는 subview가 다섯 개 있으며, 다섯 element를 가진 tuple view로 표현됩니다. 실제로는 stack이 커지는 일이 꽤 자주 발생합니다. 가독성을 높이려면 커진 stack을 별도의 component로 나누고 싶을 수 있습니다. 다음은 그렇게 할 수 있는 한 가지 방법입니다. 아래 코드는 원문 10~11쪽에 걸쳐 이어집니다.

struct Greeting: View {
    @ViewBuilder var hello: some View {
        Image(systemName: "hand.wave")
        Text("Hello")
    }
    @ViewBuilder var bye: some View {
        Text("And Goodbye!")
        Image(systemName: "hand.wave")
    }
    var body: some View {
        HStack(spacing: 20) {
            hello
            Spacer()
            bye
        }
    }
}
원문 10쪽의 도표 1
원문 10쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 10쪽과 도표원문 10쪽: 다섯 subview를 가진 HStack과 Greeting 코드의 시작

Property에 @ViewBuilder를 표시하면 body나 stack의 closure에서처럼 property의 body에 view builder 문법을 사용하게 됩니다.

HStack의 view builder closure type을 살펴보면 이제 tuple view, spacer, 또 다른 tuple view라는 세 element를 가진 TupleView가 됩니다.

TupleView<(
    TupleView<(Image, Text)>,
    Spacer,
    TupleView<(Text, Image)>
)>

하지만 HStack이 볼 때는 다섯 view를 모두 stack의 view builder closure 안에 직접 작성했던 이전 코드와 정확히 같습니다. 각 subview 사이에 stack의 spacing이 적용되는 것으로 확인할 수 있듯이, stack의 subview는 여전히 다섯 개입니다.

원문 11쪽의 도표를 제외한 view tree 도표에서는 가독성을 위해 TupleView를 생략했습니다. Parent view와 그 subview 사이의 선을 tuple view로 읽으면 됩니다.

원문 11쪽의 도표 1
원문 11쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 11쪽과 도표원문 11쪽: 중첩된 TupleView의 tree와 다섯 subview의 spacing

이는 view 목록의 특별한 성질입니다. HStack 같은 container view가 view 목록을 순회하면 중첩된 목록이 재귀적으로 펼쳐져 tuple view의 tree가 평평한 view 목록으로 바뀝니다. hello와 bye view builder property를 별도의 view로 refactoring해도 이 성질이 적용됩니다.

struct Hello: View {
    var body: some View {
        Image(systemName: "hand.wave")
        Text("Hello")
    }
}
struct Bye: View {
    var body: some View {
        Text("And Goodbye!")
        Image(systemName: "hand.wave")
    }
}
struct Greeting: View {
    var body: some View {
        HStack(spacing: 20) {
            Hello()
            Spacer()
            Bye()
        }
    }
}

Hello와 Bye view의 body 자체가 두 element를 가진 view 목록이므로, 앞서와 마찬가지로 stack이 subview를 순회할 때 펼쳐집니다.

View 목록에 view modifier를 적용할 수도 있지만, 그 동작은 다소 예상 밖일 수 있습니다. 예를 들어 Hello view에 border를 적용할 수 있습니다.

원문 12쪽과 도표원문 12쪽: Hello와 Bye를 별도의 view로 추출한 전체 코드
HStack(spacing: 20) {
    Hello()
        .border(.blue)
    Spacer()
    Bye()
}

이렇게 하면 view 목록의 각 element에 border가 적용되어 image와 text 각각의 둘레에 별도의 border가 그려집니다. 이런 동작을 흔히 만나는 상황 중 하나가 Group을 사용할 때입니다. Group은 view builder를 감싸는, layout에 관여하지 않는 abstraction입니다.

struct Greeting: View {
    var body: some View {
        HStack {
            Group {
                Image(systemName: "hand.wave")
                Text("Hello")
            }
            .border(.blue)
        }
    }
}

Group의 결과가 두 element를 가진 tuple view이므로 두 view 각각에 border가 적용됩니다. 각 view에 같은 modifier를 적용하고 싶을 때 이 기법을 활용할 수 있습니다. 다만 저희 경험으로는 이 방식을 과도하게 사용하면 금방 혼란스러워집니다. 다른 모든 상황에서 일반적으로 기대하는 동작과 modifier의 동작이 너무 다르기 때문입니다.

여기에는 예외가 하나 있으며, 저희도 의도된 동작인지는 확실하지 않습니다. Modifier까지 포함한 group을 root view로 배치하거나 scroll view 안의 유일한 subview로 배치하면 group은 VStack처럼 동작하고, modifier가 group 안의 각 view에 개별적으로 적용되지 않습니다. Group을 overlay나 background 안에 배치하면 암묵적인 ZStack처럼 동작하는데, 이것도 이 규칙의 또 다른 예외입니다.

원문 13쪽의 도표 1
원문 13쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 13쪽의 도표 2
원문 13쪽의 도표 2. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 13쪽과 도표원문 13쪽: Hello와 Group의 각 view에 개별 적용되는 border

동적 콘텐츠

View builder로 구성하는 view 목록은 동적일 수도 있습니다. View를 조건부로 포함하는 방법은 다음과 같습니다.

HStack {
    Image(systemName: "hand.wave")
    if showText {
        Text("Hello")
    }
}

도표를 보면 HStack에는 여전히 image와 optional text라는 두 subview가 있습니다. 이 view tree를 통해 SwiftUI는 stack의 첫 번째 subview로 image가 항상 있고, 두 번째 subview로 text가 있을 수도 있다는 사실을 알 수 있습니다.

if statement 대신 if let, switch, if/else 같은 다른 statement로도 conditional view를 만들 수 있습니다.

HStack {
    Image(systemName: "hand.wave")
    if let g = greeting {
        Text(g)
    }
}

이 장과 책 전반에 나오는 view tree 도표는 view의 type으로부터 자동 생성했습니다. SwiftUI 대부분의 곳에서 사용하는 opaque return type인 some View는 view tree의 정확한 구조를 인코딩하는 복잡하게 중첩된 view type을 감춥니다. View의 type에는 어떤 부분이 정적이고 어떤 부분이 동적인지도 정확히 명시됩니다. 덕분에 SwiftUI는 어떤 view를 동적으로 삽입하거나 제거할 수 있는지 완전히 알 수 있습니다.

원문 14쪽의 도표 1
원문 14쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 14쪽의 도표 2
원문 14쪽의 도표 2. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 14쪽과 도표원문 14쪽: if와 if let으로 구성한 optional Text의 view tree

Render tree

SwiftUI는 view tree를 사용해 지속적으로 유지되는 render tree를 구성합니다. View tree 자체는 일시적입니다. 반복해서 구성되고 버려지므로 저희는 view tree를 설계도처럼 생각합니다. 반면 계속 유지되는 render tree의 node는 더 긴 lifetime을 가집니다. View가 다시 render되어도 남아 있으며, 현재 state를 반영하도록 업데이트됩니다.

둘을 구분하기 위해 view tree의 element를 이야기할 때는 view라고 하고, render tree의 element를 이야기할 때는 node라고 하겠습니다. 그러면 view를 node로 변환하는 과정을 ‘rendering’이라고 부를 수 있습니다. Render tree는 SwiftUI 내부에 있으므로 직접 다루는 일은 없다는 점에 유의해 주세요.

Render tree가 실제로 존재하는 것은 아닙니다. SwiftUI의 작동 방식을 이해하는 데 유용한 모델입니다. 실제 SwiftUI에는 attribute graph라는 것이 있으며, 여기에는 render된 view 외에도 state가 들어 있고 dependency도 추적합니다. Apple은 render tree의 node를 attribute라고 부릅니다.

SwiftUI view를 처음 표시할 때 구성하는 render tree는 대부분 view tree를 일대일로 표현합니다. 앞서 본 예제를 살펴보겠습니다.

HStack {
    Image(systemName: "hand.wave")
    if let g = greeting {
        Text(g)
    }
}

greeting 값이 nil이면 위 view의 render tree에서 HStack 내부에는 image node라는 subview 하나만 있습니다.

원문 15쪽의 도표 1
원문 15쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 15쪽과 도표원문 15쪽: 지속되는 render tree의 개념과 optional Text 예제

greeting이 nil이 아닌 값으로 바뀌면 view를 다시 구성하고, 새로운 view tree를 기반으로 render tree를 업데이트합니다. SwiftUI는 HStack이 항상 있다는 사실을 알고 있으므로 render tree의 이 부분을 건드릴 필요가 없습니다. 첫 번째 subview로 image가 항상 있다는 사실도 알고 있습니다. 이 두 view는 완전히 정적입니다.

View 업데이트 메커니즘이 conditional view를 살펴보면 condition이 바뀌었을 수 있다는 사실을 알게 됩니다. Condition이 nil에서 nil이 아닌 값으로 바뀌면 SwiftUI는 render tree에 Text node를 삽입합니다. 마찬가지로 condition이 nil이 아닌 값에서 nil로 바뀌면 render tree에서 Text node를 제거합니다. Render tree에서 node를 제거하면 연결된 모든 state도 사라집니다. State 장에서 이 내용을 더 다루겠습니다.

이 예제에서 render tree를 업데이트하는 경우가 하나 더 있습니다. 업데이트 전후에 greeting 값이 모두 nil이 아니면 render tree에도 업데이트 전후로 동일한 text node가 존재합니다. 다만 greeting 값이 바뀌었다면 text node의 string을 업데이트합니다.

Lifetime

앞서 설명했듯이 view tree 자체는 일시적이므로 여기에 lifetime이라는 개념을 적용하는 것은 의미가 없습니다. 하지만 render tree의 node에는 구체적인 lifetime이 있습니다. 처음 render될 때부터 화면 표시에 더는 필요하지 않을 때까지입니다.

그러나 render tree node의 lifetime과 화면에 보이는 기간은 같지 않습니다. Scroll view 안에 큰 VStack을 render하면 현재 화면에 보이는지와 관계없이 render tree에 VStack의 모든 subview에 대한 node가 포함됩니다.

원문 16쪽의 도표 1
원문 16쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 16쪽의 도표 2
원문 16쪽의 도표 2. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 16쪽과 도표원문 16쪽: greeting의 nil 여부에 따른 view tree와 render tree 비교

VStack은 LazyVStack과 달리 자신의 콘텐츠를 즉시 render합니다. 하지만 lazy stack에서도 node가 화면 밖으로 나가면 state를 유지하기 위해 render tree에 보존됩니다. 이 내용은 State 장에서 더 자세히 살펴보겠습니다. 핵심은 render tree의 node에 lifetime이 있지만, 그 lifetime을 개발자가 제어하지는 못한다는 것입니다.

실제로 사용할 수 있도록 SwiftUI는 lifetime event에 연결하는 세 가지 hook을 제공합니다.

  1. onAppear는 view가 화면에 나타날 때마다 실행됩니다. Render tree에서 해당 view를 뒷받침하는 node가 계속 남아 있더라도 하나의 view에 대해 여러 번 호출될 수 있습니다. 예를 들어 LazyVStack이나 List 안의 view를 스크롤해 화면 밖으로 보냈다가 다시 화면 안으로 가져오는 일을 반복하면 매번 onAppear가 호출됩니다. TabView에서 tab을 바꿀 때도 마찬가지입니다. Tab이 처음 표시될 때만이 아니라, 그 tab으로 전환할 때마다 onAppear가 호출됩니다.
  2. onDisappear는 view가 화면에서 사라질 때 실행됩니다. onAppear에 대응하며 같은 규칙으로 동작합니다. 해당 view를 뒷받침하는 node가 사라지지 않더라도 여러 번 호출될 수 있습니다.
  3. task는 비동기 작업을 위해 앞의 두 가지를 결합합니다. 이 modifier는 onAppear가 호출될 시점에 새로운 task를 생성하고, onDisappear가 호출될 시점에 그 task를 취소합니다.

Identity

SwiftUI의 view tree는 본래의 identity를 가진 reference type인 object로 구성되지 않으므로, SwiftUI는 view tree 안의 위치를 사용해 view에 identity를 부여합니다. 이런 identity를 implicit identity라고 합니다. 이를 설명하기 위해 위 예제를 조금 수정한 버전을 살펴보겠습니다.

HStack {
    Image(systemName: "hand.wave")
    if let g = greeting {
        Text(g)
    } else {
        Text("Hello")
    }
}

이제 view tree에는 단순한 optional text 대신 두 subview를 가진 ConditionalContent view가 들어 있습니다. 하나는 nil이 아닌 경우의 text이고, 다른 하나는 nil인 경우의 text입니다. View tree의 각 view는 tree 안의 위치로 고유하게 식별할 수 있습니다.

원문 17쪽의 도표 1
원문 17쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 17쪽과 도표원문 17쪽: lifetime event hook과 if else의 ConditionalContent 구조

이 개념을 설명하기 위해 각 view를 식별하는 ‘path’ string을 구성한다고 생각해 보겠습니다.

HStack {
    Image(systemName: "hand.wave") // 0
    if let g = greeting {
        Text(g) // 1.ifBranch
    } else {
        Text("Hello") // 1.elseBranch
    }
}

Image는 HStack의 첫 번째 subview이므로 "0"입니다. if let statement에서 nil이 아닌 branch의 Text는 "1.ifBranch"입니다. ConditionalContent가 HStack의 두 번째 subview이고, 그 Text는 ConditionalContent의 첫 번째 subview이기 때문입니다.

SwiftUI 내부에서 이러한 path string으로 implicit identity를 구현한다고 주장하는 것은 아닙니다. Implicit identity의 의미를 설명하기 위한, 사람이 이해하기 쉬운 모델일 뿐입니다.

이제 if let statement의 두 branch에 있는 두 text view를 생각해 보겠습니다. 두 view는 identity가 다르므로 SwiftUI는 서로 별개의 view로 간주합니다. Condition이 바뀌면 기존 text를 render tree에서 제거하고 새로운 text를 삽입합니다. 이는 state, animation, transition에 여러 영향을 미치며, 이 내용은 뒤에서 다루겠습니다.

같은 예제를 조금 다르게 작성해 보겠습니다.

HStack {
    Image(systemName: "hand.wave")
    Text(greeting ?? "Hello")
}

View tree가 더 단순해졌다는 사실을 바로 알 수 있습니다. HStack에는 image와 text라는 두 개의 정적 subview가 있습니다.

원문 18쪽의 도표 1
원문 18쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 18쪽의 도표 2
원문 18쪽의 도표 2. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 18쪽과 도표원문 18쪽: implicit identity의 path와 단일 Text를 사용하는 view tree

이제 greeting 값이 nil인지 아닌지에 따른 차이는 text view에 표시하는 string뿐입니다. Implicit identity로 설명하면 HStack의 두 번째 subview인 text view 자체는 항상 유지되며, greeting 값의 어떤 변화에도 영향을 받지 않습니다.

View는 implicit identity와 함께 explicit identity를 가질 수도 있습니다. 이는 주로 ForEach의 view에 사용합니다. ForEach의 각 item에는 explicit identifier를 부여합니다. 예를 들어 기반 데이터의 고유 identifier를 사용할 수 있습니다. Item이 Identifiable protocol을 따르게 하거나 고유 identifier를 가리키는 key path를 제공하는 방식입니다. 하지만 id modifier를 사용해 explicit identifier를 직접 부여할 수도 있습니다.

id parameter에는 어떤 Hashable 값이든 사용할 수 있습니다. 아래 예제에서는 greeting 값을 nil과 비교한 Boolean을 사용합니다. nil이면 explicit identifier는 true이고, 그렇지 않으면 false입니다. 즉, identifier가 바뀌면 SwiftUI는 text view를 다른 view로 간주합니다. 이 경우에도 render tree에서 이전 text node를 제거하고 새로운 node를 삽입합니다.

HStack {
    Image(systemName: "hand.wave")
    Text(greeting ?? "Hello")
        .id(greeting == nil)
}

위와 같은 explicit identifier는 view의 implicit identity를 덮어쓰는 것이 아니라 그 위에 추가로 적용된다는 점이 중요합니다. 다시 말해 여러 view에 같은 explicit identifier를 사용해도 SwiftUI가 혼동하지 않습니다. 앞서 살펴본 view의 path는 implicit identity를 구체적으로 표현하는 한 가지 방법이며, explicit identifier는 ‘path에 덧붙이는 것’으로 생각할 수 있습니다.

이제 SwiftUI의 view identity를 더 확실히 이해했으니, 이 주제와 관련한 흔한 문제 두 가지를 살펴보겠습니다.

원문 19쪽의 도표 1
원문 19쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 19쪽과 도표원문 19쪽: id modifier로 추가하는 explicit identity의 view tree

먼저 다음 예제를 살펴보겠습니다.

HStack {
    let v = Text("Hello")
    v
    v
}

여기서는 local variable에 text view를 구성한 다음 HStack 안에서 두 번 사용합니다. 두 text view의 identity라는 관점에서 이는 어떤 의미일까요?

Tree에서 볼 수 있듯이 두 text view는 view tree의 서로 다른 위치에 있습니다. 따라서 implicit identity가 다르고, SwiftUI는 별개의 view로 간주합니다. ‘설계도’라는 생각으로도 이를 설명할 수 있습니다. String이 "Hello"인 text view의 설계도를 만들고, 그 설계도를 두 번 사용하는 것입니다.

다음도 view identity와 관련한 예제입니다. View modifier를 조건부로 적용하는 작은 view extension을 작성할 때 널리 사용하는 pattern입니다.

// 피해야 할 패턴
extension View {
    @ViewBuilder
    func applyIf<V: View>(_ condition: Bool, transform: (Self) -> V) -> some View {
        if condition {
            transform(self)
        } else {
            self
        }
    }
}
원문 20쪽의 도표 1
원문 20쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 20쪽과 도표원문 20쪽: 같은 Text 값을 두 번 사용하는 view tree와 applyIf 코드

applyIf method는 다음과 같이 사용할 수 있습니다.

HStack {
    Image(systemName: "hand.wave")
    Text("Hello")
        .applyIf(highlighted) {
            $0.background(.yellow)
        }
}

결과 view tree를 보면 applyIf modifier를 사용하면서 두 subview를 가진 ConditionalContent가 추가되었다는 사실을 알 수 있습니다. 하나는 수정되지 않은 text이고 다른 하나는 background가 있는 text입니다. 따라서 condition인 highlighted가 바뀌면 화면에 있는 text의 identity도 바뀝니다.

저희는 이 pattern을 사용하지 않기를 강하게 권합니다. 무해해 보이는 applyIf modifier가 view tree에 branch를 추가하면서 이후 동작에 예상치 못한 결과를 일으킬 수 있기 때문입니다. 대신 다음 방식이 훨씬 안전합니다.

HStack {
    Image(systemName: "hand.wave")
    Text("Hello")
        .background(highlighted ? .red : .clear)
}

대부분의 view modifier는 optional을 받으므로 ternary operator pattern으로 값을 지정하거나, 값을 지정하고 싶지 않으면 nil을 사용할 수 있습니다. 예를 들어 frame의 width와 height는 optional이고, foregroundColor의 color도 optional이며, padding의 length도 optional이거나 0으로 설정할 수 있습니다. 같은 이유로 bold나 disabled 같은 view modifier는 Boolean argument를 받습니다. 얼핏 보면 이 argument가 필요하지 않다고 생각할 수도 있습니다.

원문 21쪽의 도표 1
원문 21쪽의 도표 1. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 21쪽의 도표 2
원문 21쪽의 도표 2. 그림을 선택하면 원래 크기로 볼 수 있습니다.
원문 21쪽과 도표원문 21쪽: applyIf가 추가하는 branch와 modifier 값만 바꾸는 대안
이전서문과 독서 안내다음State와 Binding