SwiftUI

전역적인 Window 객체를 이용해 LoadingView 띄우기

GREEN.1229 2022. 11. 29. 16:40

안녕하세요. 그린입니다🍏

이번 포스팅에서는 전역적으로 아예 뷰를 감싸버려 로딩뷰를 윈도우로 만들어 띄우는 방법을 포스팅 해보겠습니다!

 

왜 이런 구현이 필요하게 되었나요?

우선 각 피쳐에서 뷰를 구성함에 있어 바텀 영역이 존재할때도 있고 없을때도 있습니다.

즉, 각 다른 구성의 뷰에서 해당 로딩뷰를 종속으로 띄워준다면 보여지는 뷰의 패딩이 달라지기에 보여지는 위치가 달라질 수 있습니다.

이를 해결하기 위해 기존 뷰의 조합들이 이뤄진 윈도우 위에 새로운 윈도우를 만들어 덮어버리는 방식도 방법일것 같아 구현해봤어요!

이 구현을 위해서는 우선 띄워줄 로딩 뷰 자체를 만들어야 합니다.

그 다음으로는 로딩뷰를 담을 새롭게 공유할 윈도우 객체를 만들고 초기화 시 UIHostingController를 이용해 해당 로딩뷰를 지정해줍니다.

마지막으로 SwiftUI에서 각 뷰에서 조금 더 간편하게 사용할 수 있도록 View Modifier를 만들어주고 사용하면 끄으으으읕!!🔚

 

그럼 바로 구현해보도록 하죠 🙌

구현 START⭐️

로딩 뷰 부터 아주 간단히 구현해보죠!

import SwiftUI

public struct LoadingView: View {
  public init() { }
  
  public var body: some View {
    VStack(spacing: 10) {
      ProgressView()
      Text("Loading")
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .background(DesignSystem.Colors.white100.opacity(0.5))
  }
}

SwiftUI의 기본 API인 ProgressView를 가져옵니다.

이 뷰는 프로그래스바를 도는 그런 뷰에요!

그 다음 프레임을 무한정으로 잡아주고 배경색은 로딩뷰이니만큼 불투명하게 0.5를 주어 해당 로딩이 되는 사이 배경은 딤드 처리 되어 보일 수 있도록 해줍니다.

 

그 다음으로 해당 로딩 뷰를 얹어줄 Window를 만들어주겠습니다~

import SwiftUI
import UIKit

public class LoadingWindow: UIWindow {
  public static let shared = LoadingWindow(frame: UIScreen.main.bounds)

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented: please use LoadingWindow.shared")
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    let loadingViewController = UIHostingController(rootView: LoadingView())
    loadingViewController.view?.backgroundColor = .clear
    self.rootViewController = loadingViewController
    self.isHidden = true
  }

  func show() {
    self.isHidden = false
  }

  func hide() {
    self.isHidden = true
  }

  func toggle() {
    if self.isHidden {
      show()
    } else {
      hide()
    }
  }
}

전체에서 사용될 것으로 싱글턴으로 해당 윈도우 class 객체를 만들어줍니다.

초기화 시 UIHostingController를 통해 rootView를 위에서 만들어준 LoadingView로 지정해줘요.

그 다음 우선 숨겨놓기 위해 isHidden 처리해줍니다.

해당 객체에서 메서드는 해당 로딩뷰를 보여주고 숨겨주는 처리를 위한 각각의 역할을 담은 메서드를 구현해줘요.

 

자 이제 ViewModifier를 만들어볼께요!

import SwiftUI

public extension View {
  func loading(
    _ isLoading: Bool
  ) -> Self {
    if isLoading {
      LoadingWindow.shared.show()
    } else {
      LoadingWindow.shared.hide()
    }
    return self
  }
}

어떠한 뷰에서든 이 loading 메서드를 통해 위에서 만들어준 공유된 LoadingWindow를 보여주고 숨겨줄 수 있습니다.

 

그럼 마지막으로 한가지 빼먹은게 있어요ㅎㅎ

바로 LoadingWindow를 앱 실행 시 등록하는거에요🙋🏻

그걸 위해 앱 컨트롤을 해주는 AppDelegate로 가줍니다.

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  var window: UIWindow?

  func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    self.window = UIWindow(frame: UIScreen.main.bounds)
    
    // 메인 뷰
    
    window?.rootViewController = UIHostingController(rootView: 메인 뷰)
    window?.makeKeyAndVisible()
    
    // 로딩윈도우
    setupLoadingWindow()
    return true
  }
  
  func setupLoadingWindow() {
    _ = LoadingWindow.shared
  }

해당 구현을 통해 앱이 실행되었을때 로딩 윈도우를 추가로 등록해줍니다.

 

이제 지인짜아 다끝!!!!!

이제 적용해보겠습니다.

public struct MainView: View {
  private var store: Store<MainState, MainAction>
  
  public var body: some View {
    WithViewStore(store) { viewStore in
    // 메인 뷰 구현
      .loading(viewStore.state.isLoading)
    ...
    }
  }
}

위에서 만든 View Modifier를 적용해서 원하는 뷰에서 구현해줍니다.

저는 TCA를 기반으로 사용하여서 viewStore State의 isLoading Bool 프로퍼티를 통해 숨겨줄지 보여줄지 결정해주게 바인딩 시켜줍니다.

 

그럼 구동 고고🏃🏻

위와 같이 기존 메인 뷰 위에 조건을 달성하면 로딩 윈도우가 올라가서 나타나는것을 볼 수 있습니다.

 

마무리

뷰를 컴포저블하게 가져갈 시 전역적으로 구현되어야할 로딩, 토스트, 바텀시트 등 되게 다양한 뷰의 구현이 필요한데 이럴때마다 사이즈를 분기 시켜 처리하거나 아니면 최상단으로 끌어들여가 의존성을 가지고 구현하기는 여간 귀찮고 유지보수가 힘든게 아니더라구요🥲

그럴때 이 방법도 하나의 방법이 될 수 있는것 같습니다🙌