Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Intermediate)

14. sheet 시트 사용법 / multiple sheets

by Toughie 2023. 5. 25.

⭐️sheet 시트 사용법 / multiple sheets⭐️

한 화면에서 여러가지 다른 종류의 시트(모달)를 띄우는 경우 의도한 대로 적절한 시트가 띄워지지 않는 문제를 겪을 수 있다.

//  Created by Toughie on 2023/05/25.
//

import SwiftUI

struct SheetModel: Identifiable {
    let id = UUID().uuidString
    let title: String
}


struct MultipleSheetsPrac: View {
    
    @State var selectedModel: SheetModel = SheetModel(title: "START")
    @State var showSheet: Bool = false
    
    var body: some View {
        VStack(spacing: 20) {
            Button("Button 1") {
                selectedModel = SheetModel(title: "ONE")
                showSheet.toggle()
            }
            Button("Button 2") {
                selectedModel = SheetModel(title: "TWO")
                showSheet.toggle()
            }
        }
        .sheet(isPresented: $showSheet) {
            NextScreen(selectedModel: selectedModel)
        }
    }
}

struct NextScreen: View {
    
    let selectedModel: SheetModel
    
    var body: some View {
        Text(selectedModel.title)
            .font(.largeTitle)
    }
}

시트의 기초 단계에서 적용했던 방식으로 해보면 어떨까? isPresented를 토글하는 방식 말이다.

이렇게 하면 버튼 1을 누르면 "ONE"이, 버튼 2를 누르면 "TWO"가 나올것 같지만 "START"가 나온다.
Why?

이는 시트가 그려지는 특성 때문이다.
body가 호출되고 VStack이 그려질 때 거의 동시에 sheet도 그려진다.

이 때는 초기 selectedModel, 즉 "START"를 가지고 sheet가 그려진다.

VStack이 그려질 때 이미 "START"시트가 만들어져 있다는 얘기다.

그래서 버튼을 눌러도 이미 만들어진 "START" 시트가 보이는 것이다.

 

 

그러면 .sheet 내부에서 분기 처리를 하는 방식은 될까?

초기 selectedIndex를 0으로, 버튼을 누를 때마다 1, 2로 바꾼 뒤 
sheet에서 selectedIndex에 따라 다른 NextScreen객체를 만들어서 보여준다면?

struct MultipleSheetsPrac: View {
    
    @State var selectedModel: SheetModel = SheetModel(title: "START")
    @State var showSheet: Bool = false
    @State var selectedIndex: Int = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Button("Button 1") {
                selectedIndex = 1
                showSheet.toggle()
            }
            Button("Button 2") {
                selectedIndex = 2
                showSheet.toggle()
            }
        }
        .sheet(isPresented: $showSheet) {
            if selectedIndex == 1 {
                NextScreen(selectedModel: SheetModel(title: "ONE"))
            } else if selectedIndex == 2 {
                NextScreen(selectedModel: SheetModel(title: "TWO"))
            } else {
                NextScreen(selectedModel: SheetModel(title: "START"))
            }

        }
    }
}

첫번째 예시와 같은 이유로 제대로 동작하지 않는다. 여전히 어느 버튼을 눌러도 처음에는 "START"가 보일 것이다.

역시 VStack이 그려질 때 이미 "START"로 시트가 같이 그려져있기 때문이다.

 

이런 문제를 해결할 수 있는 방법이 크게 3가지가 있다.

1. 바인딩

2. 개별 시트

3. .sheet(item:content:)

1. 바인딩

        .sheet(isPresented: $showSheet) {
            NextScreen(selectedModel: $selectedModel)
        }
    }
}

struct NextScreen: View {
    
    @Binding var selectedModel: SheetModel
    
    var body: some View {
        Text(selectedModel.title)
            .font(.largeTitle)
    }
}

시트의 컨텐츠를 바인딩 처리해서 연결해준다.

간단한 방법이지만 시트 컨텐츠가 복잡하거나 바인딩이 어려운 경우에 쓰기 힘들다는 단점이 있다.

 

2. 개별 시트

 

여러가지 시트 모디파이어를 사용하는 방법이다. 버튼2 부분만 살펴보자.

버튼2 시트를 위한 @State변수 showSeet2를 따로 만들어 줬고, 이를 바인딩해서 시트를 따로 붙여줬다.

struct MultipleSheetsPrac: View {
    
    @State var selectedModel: SheetModel = SheetModel(title: "START")
    @State var showSheet: Bool = false
    @State var showSheet2: Bool = false

    
    var body: some View {
        VStack(spacing: 20) {
            Button("Button 1") {

                selectedModel = SheetModel(title: "ONE")
                showSheet.toggle()
            }
            Button("Button 2") {

//                selectedModel = SheetModel(title: "TWO")
                showSheet2.toggle()
            }
            //뷰 계층에서 시트는 하나만 있어야함
            .sheet(isPresented: $showSheet2) {
                NextScreen(selectedModel: SheetModel(title: "TWO"))
            }
        }
        .sheet(isPresented: $showSheet) {
            NextScreen(selectedModel: selectedModel)
        }
    }

원래는 위와 같은 방식이 안됐었다.

why?

뷰 계층 내에 sheet는 하나만 존재해야 했기 때문이다. 

여기서는 VStack에 붙어있는(마지막 sheet) 시트가 부모계층의 시트이고, Button2에 붙어있는 시트가 자식계층의 시트이다.

이 시트들의 계층 레벨이 다르고, 1개보다 많기 때문에 버튼 2를 눌러도 아무런 컨텐츠가 표시되지 않았었다.

 

하지만

SwiftUI가 업데이트 되면서 여러 시트를 동시에 표시하는 것이 가능해졌다.

sheet마다 고유한 식별자가 있는 경우 여러 개의 sheet를 동시에 사용할 수 있다는 말이다.
즉, isPresented 시트 개수에 맞게 변수를 여러개 만들어 주면 된다는 말.

 

하지만 위 코드는 어색하니 아래와 같은 형태로 사용하는것이 좋아 보인다.

버튼이 2개이니 총 2개의 시트를 만들었고, 시트가 2개이니 식별을 위해 isPresented 바인딩 프로퍼티를 2개 만들어서 사용한다.

쉽게 말하면 그냥 필요한 만큼 시트 만들어서 각각 붙여 줬다는 것

이렇게 하면 시트간 계층 레벨 차이로 인한 문제도 없어진다. (둘 다 자녀계층)

        VStack(spacing: 20) {
            Button("Button 1") {
                showSheet.toggle()
            }
            .sheet(isPresented: $showSheet) {
                NextScreen(selectedModel: SheetModel(title: "ONE"))
            }
            
            Button("Button 2") {

                showSheet2.toggle()
            }
            .sheet(isPresented: $showSheet2) {
                NextScreen(selectedModel: SheetModel(title: "TWO"))
            }
        }

 

3. .sheet(item:content:)

개인적으로 가장 깔끔하고 유연하며 활용도가 높은 방법이라 생각한다.

@State var selectedModel을 옵셔널로 선언하고
버튼을 누를 때 새로운 객체를 할당하는 방식이다.

 

.sheet 모디파이어는 VStack에 하나만 붙어있고,

selectedModel과 바인딩을 통해 model 객체를 통해 시트를 띄우는 것이다.

struct MultipleSheetsPrac: View {

    @State var selectedModel: SheetModel?

    var body: some View {
        VStack(spacing: 20) {
            Button("Button 1") {
                selectedModel = SheetModel(title: "ONE")
            }

            Button("Button 2") {
                selectedModel = SheetModel(title: "TWO")
            }
        }
        .sheet(item: $selectedModel) { model in
            NextScreen(selectedModel: model)
        }
    }
}

이 방식은 시트의 개수가 굉장히 많은 경우에도 유용하다

        ScrollView {
            VStack(spacing: 20) {
                ForEach(0..<30) { index in
                    Button("Button\(index)") {
                        selectedModel = SheetModel(title: "\(index)")
                    }
                    
                }
            }
            .sheet(item: $selectedModel) { model in
                NextScreen(selectedModel: model)
            }
        }