Tunko Development Diary

SwiftUI) @Environment와 dismiss를 사용하는 올바른 방법 본문

Development/SwiftUI

SwiftUI) @Environment와 dismiss를 사용하는 올바른 방법

Tunko 2022. 10. 3. 04:19

일반적으로 화면을 닫을때

if #available(iOS 15.0, *) {
    @Environment(\\.dismiss) private var dismiss
}
else {
    @Environment(\\.presentationMode) var presentationMode
}

을 사용하여 화면을 닫습니다.

iOS 15 이후 부터는

@Environment(\\.dismiss) private var dismiss

가 사용됩니다.

하지만 문제가 있습니다.

뷰를 열고 닫을 때마다 이러한 @Environment 변수를 사용하는 모든 뷰의 Body 내부 코드를 재실행합니다.

따라서 앱 성능과 메모리 및 전력소비에 부정적인 영향을 미칠 수 있습니다. 앱이 오랫동안 이 작업을 수행하고 실행하는동안 리소스 사용량이 급증하게 되면 퍼포먼스에 악영향을 미칩니다. 😭

또다른 문제는 최상위 뷰 수준에서 환경변수를 사용하는 경우 모든 하위 뷰의 본문을 다시 계산하여 Body내부 코드를 재실행합니다.

혹시라도 하위 View의 .onAppear에서 네트워크 요청이라도 한다면 뷰가 재실행됩니다. 이렇게 되면 다른 위치에서 표시된 View가 삭제될 때마다 하위 View가 강제로 재실행됩니다.

presentationMode나 dismiss 같은 환경변수를 사용하게 되면 뷰가 열리고 닫힐때마다 환경변수의 상태가 변경됩니다. 즉 환경변수가 선언된 뷰의 하위 View가 재실행됩니다.

요약! @Environment 와 같은 전역 환경 변수의 상태가 변경될때 이것을 사용하는 뷰에 영향을 미칠 수 있음을 인지하고 있어야 합니다.

따라서 실험을 해보았습니다.

뷰를 다시 그리는 횟수를 싱글톤 ViewModel을 만들어 기록하고 이를 카운트 했습니다.

import Foundation
 
enum Screen: String, CaseIterable {
    case loadingScreen
    case homeScreen
    case promotionsScreen
    case homeTab
    case searchTab
    case testTab
    case profileTab
    case settingTab
    case settingScreen
    case settingNextScreen
    case settingOptionScreen
}

class LoadCounterViewModel {
    
    static let loadCounterViewModel = LoadCounterViewModel()
    
    func increaseAndGetCount(for screen: Screen) -> Int {
        var count = UserDefaults.standard.integer(forKey: screen.rawValue)
        if count != 0 {
            count += 1
            UserDefaults.standard.set(count, forKey: screen.rawValue)
            return count
        } else {
            UserDefaults.standard.set(1, forKey: screen.rawValue)
            return 1
        }
    }
    
    func resetLoadCount() {
        for screen in Screen.allCases {
            UserDefaults.standard.set(0, forKey: screen.rawValue)
        }
    }
    
    private init() {
        resetLoadCount()
    }
}

그리고 각 뷰가 로딩될때 뷰의 카운트를 Text에 표시해 주었습니다.

let loadCount = LoadCounterViewModel.loadCounterViewModel.increaseAndGetCount(for: .homeScreen)
Text("Load count \\(loadCount)")

.sheet 나 fullScreenCover 를 통해서 다음 뷰를 보여주고

@Environment 를 통해서 dismiss를 할때마다 뷰를 다시 그리는 횟수가 마구 늘어납니다.

결론적으로 환경변수를 통해서 뷰를 닫는 일을 최대한 줄이고 @State/Binding을 통해서 닫아주어야 합니다.

예시)

HomeScreen 에서 .sheet를 사용해 PromotionsScreen 뷰를 열어줍니다. 근본적인 동작에 이상은 없지만 뷰를 재로딩하는 횟수가 2회씩 늘어나게 됩니다. 하위뷰가 더 많다면 더욱이 커집니다.

struct HomeScreen: View { 
    @State var isShowSheet = false  
    
    var body: some View { 
        VStack {
            HStack {
                let loadCount = LoadCounterViewModel.loadCounterViewModel.increaseAndGetCount(for: .homeScreen)
                Text("Load homeScreen Count : ")
                Text("\\(loadCount)")
                    .font(.largeTitle)
                    .foregroundColor(.red)
                Spacer()
            }

						Button {
                isShowSheet.toggle()
            } label: {
                Text("PromotionsScreen 열기")
            }
            Spacer()
        }   
        .sheet(isPresented: $isShowSheet) {
            PromotionsScreen()
        }
    }
}
struct PromotionsScreen: View {  
    @Environment(\\.dismiss) private var dismiss
      
    var body: some View {
        VStack(spacing: 16) {
            Button("Dismiss") { 
                presentationMode.wrappedValue.dismiss()
            }
            Text("Welcome to Promotions Screen")
            let loadCount = LoadCounterViewModel.loadCounterViewModel.increaseAndGetCount(for: .promotionsScreen)
            Text("Load count \\(loadCount)")
        }
    }
}

위 코드를 아래와 같이 변경하면 재로딩을 제어할 수 있게 됩니다.

...
.sheet(isPresented: $isShowSheet) {
		PromotionsScreen(isShowSheet: $isShowSheet)
}
struct PromotionsScreen: View {
      
    @Binding var isShowSheet : Bool
    
    var body: some View {
        VStack(spacing: 16) {
            Button("Dismiss") {
                isShowSheet.toggle() 
            }
            
            HStack {
                let loadCount = LoadCounterViewModel.loadCounterViewModel.increaseAndGetCount(for: .promotionsScreen)
                Text("Load promotionsScreen Count : ")
                Text("\\(loadCount)")
                    .font(.largeTitle)
                    .foregroundColor(.red)
            }
        }
    }
}

마지막으로…

.sheet 나 .fullScreenCover 등을 통해 뷰를 보일때는 @State/@Binding 을통해 뷰를 dismiss 합니다.

하지만

NavigationLink를 통한 새로운 네비게이션의 이동은

@Environment(\\.dismiss) private var dismiss

통해서 할 수 밖에 없습니다.

이상입니다.

예제 코드를 남깁니다.

https://github.com/LimHun/SwiftUIDismissTest

반응형
Comments