dismiss

  • SwiftUI에서 화면 전환을 위해 사용할 수 있는 Environment가 등장했다. 익숙한 이름대로 sheet(isPresented:onDismiss:content:), fullScreenCover(isPresented:onDismiss:content:) 등의 올라온 화면을 닫는다.
private struct SheetContents: View {
    @Environment(\.dismiss) private var dismiss
 
    var body: some View {
        Button("Done") {
            dismiss()
        }
    }
}
  • 사실 실제로 함수는 아니고 DismissAction 타입인데 callAsFunction()을 가지고 있기 때문에 함수로 호출할 수 있다.

다른 동작

문제점

  • 그러면 NavigationStack을 가진 뷰를 modal로 띄우면 어떨까? 예측이 되지 않는다는 시점에서 이미 실패한 설계라고 볼 수 있을법하다.
import SwiftUI
 
struct ContentView: View {
    @State var isPresented: Bool = false
    var body: some View {
        VStack {
            Button {
                self.isPresented = true
            } label: {
                Text("Present")
            }
        }
        .sheet(isPresented: self.$isPresented) {
            NavigationSessionView()
        }
    }
}
 
struct NavigationSessionView: View {
    @State var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: self.$path) {
            Button {
                self.path.append("Push")
            } label: {
                Text("Push")
            }
            .navigationDestination(for: String.self) { text in
                NavigationSessionNextView()
            }
        }
    }
}
 
struct NavigationSessionNextView: View {
    @Environment(\.dismiss) var dismiss: DismissAction
    
    var body: some View {
        Button {
            self.dismiss()
        } label: {
            Text("Dismiss")
        }
    }
}
 

navigation pop

  • 결과는 보다시피 이름이 dismiss()임에도 불구하고 모달창 닫기 없이 NavigationPath만 한스택 빠진다.
  • 실제로 복잡한 앱을 개발하다 보면, present를 하고 navigation을 하는 것은 물건 결제나 회원가입 등 비즈니스적인 흐름이 시작된다는 것이고, 특별한 키인 dismiss()를 한다는 것은 모달창을 닫는것이 더 맥락상 맞는 것 같다고 생각하는데 아니었다.

해결

  • 결국 이 주어진 dismiss()는 안쓰는걸로 마음먹었다. 그리고 Environment로 closeSheet()를 하나 추가해서 넣어주었다. 처음 봤을부터 이름도 마음에 안들었다.
  • 내비게이션스택을 래핑하여 모디파이어를 달고 내비게이션스택을 내리도록 isPresent를 토글하면 된다.
  • 덕분에 모든 내비게이션이 isPresent를 가지게 되었는데, 루트쪽은 .constant(true) 넣으면 되니까…뭐가 됐든 iOS 화면전환은 2가지인데 기획에서 썩 잘 챙겨나오는 편이 아니라서 개발자가 신경써줘야 한다. 그런데 프레임워크부터 이런식이면 어떡하나
private struct CloseSheetEnvironmentKey: EnvironmentKey {
    static let defaultValue: () -> () = {}
}
 
public extension EnvironmentValues {
    var closeSheet: () -> () {
        get { self[CloseSheetEnvironmentKey.self] }
        set { self[CloseSheetEnvironmentKey.self] = newValue }
    }
}