전역적인 Window 객체를 이용해 LoadingView 띄우기
안녕하세요. 그린입니다🍏
이번 포스팅에서는 전역적으로 아예 뷰를 감싸버려 로딩뷰를 윈도우로 만들어 띄우는 방법을 포스팅 해보겠습니다!
왜 이런 구현이 필요하게 되었나요?
우선 각 피쳐에서 뷰를 구성함에 있어 바텀 영역이 존재할때도 있고 없을때도 있습니다.
즉, 각 다른 구성의 뷰에서 해당 로딩뷰를 종속으로 띄워준다면 보여지는 뷰의 패딩이 달라지기에 보여지는 위치가 달라질 수 있습니다.
이를 해결하기 위해 기존 뷰의 조합들이 이뤄진 윈도우 위에 새로운 윈도우를 만들어 덮어버리는 방식도 방법일것 같아 구현해봤어요!
이 구현을 위해서는 우선 띄워줄 로딩 뷰 자체를 만들어야 합니다.
그 다음으로는 로딩뷰를 담을 새롭게 공유할 윈도우 객체를 만들고 초기화 시 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 프로퍼티를 통해 숨겨줄지 보여줄지 결정해주게 바인딩 시켜줍니다.
그럼 구동 고고🏃🏻
위와 같이 기존 메인 뷰 위에 조건을 달성하면 로딩 윈도우가 올라가서 나타나는것을 볼 수 있습니다.
마무리
뷰를 컴포저블하게 가져갈 시 전역적으로 구현되어야할 로딩, 토스트, 바텀시트 등 되게 다양한 뷰의 구현이 필요한데 이럴때마다 사이즈를 분기 시켜 처리하거나 아니면 최상단으로 끌어들여가 의존성을 가지고 구현하기는 여간 귀찮고 유지보수가 힘든게 아니더라구요🥲
그럴때 이 방법도 하나의 방법이 될 수 있는것 같습니다🙌