본문 바로가기
프로그래밍

[SwiftUI] TabView와 UIViewController 같이 사용해보기

by hansoo.labs 한수댁 2020. 2. 20.

SwiftUI로 최근 앱을 만드는 동안 어려움은 있었지만, 빠르고 쉽게 UI를 만들고 테스트 할 수 있어서 좋았다. 여전히 잘 모르는 것들이 많이 있지만, Flutter와 함께 훌륭하다고 생각된다.

시간도 없고 아는 것도 없어서 어려웠는데, 특히 TabView의 한 Tab에 UIViewController를 붙이는 과정에서 이해해야 할 것들이 많았다.

앱의 기본 화면을 탭뷰로 구성했는데, 이런 식이다.

ContentView

struct ContentView: View {
    @State private var selection = 0
    var body: some View {
        TabView(selection: $selection) {
            BusView().tabItem {
                VStack {
                    Text("Tab1")
                }}.tag(0)

            ShopView().tabItem {
                VStack {
                    Text("Tab2")
                }}.tag(1)

            SettingView().tabItem {
                VStack {
                    Text("Tab3")
                }}.tag(2)
        }
    }
}

여기에서 ShopView를 웹뷰로 구성하고자 했다. 웹뷰가 SwiftUI에 없는 관계로 UIViewControllerRepresentable로 WebView를 만들어서 작업했다.

AppWebView

struct AppWebView: UIViewControllerRepresentable {
    var initialUrl: URL
    func makeUIViewController(context: Context) -> CommonWebVc {
        let vc = CommonWebVc(for: initialUrl)
        return vc
    }

    func updateUIViewController(
        _ vc: CommonWebVc,
        context: UIViewControllerRepresentableContext<AppWebView>) {
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject {
        var parent: AppWebView
        init(_ view: AppWebView) {
            self.parent = view
        }
    }
}

CommonWebVcWKWebView를 담고 있는 UIViewController이다.
이렇게 해서 웹뷰는 화면에 잘 표시되었지만, 탭을 왔다갔다하면 웹뷰의 이전 상태를 보전하지 못하는 문제가 생겼다. 이유는 탭에 보여질 때마다 CommonWebVc의 객체가 새로 만들어지기 때문이었다.

SwiftUI에서 @Status, @Published, @Binding 등에 값이 변할 경우, body가 새로 그려지게 된다. View 가 sturct 구조체임을 알고 보면, 탭의 상태가 변경될 때마다 ShopView가 다시 만들어지고 있음을 짐작할 수 있다.

결국, class인 CommonWebVc를 객체로 한번만 생성하도록 앱 환경에서 관리되어야 했다. Application 소스 어딘가에 메인 화면에서만 쓸 수 있는 CommonWebVc를 만들어 놓고 사용해도 되지만, CommonWebVc를 조금 손쉽게 재사용하기 위해 간단한 저장소를 만들어 봤다.

CommonWebVcStore

class CommonWebVcStore {
    private static var store: [CommonWebVc] = []
    // 해당 주소로 시작하는 Vc를 반환하거나 생성해준다. 
    static func getInstance(for url: URL) -> CommonWebVc {
        if let ins = store.first(where: { $0.initialUrl == url }) {
            return ins
        }
        let ins = CommonWebVc(url: url)
        store.append(ins)
        return ins
    }
    // 해당 Vc를 저장소에서 제거
    static func remove(_ vc: CommonWebVc) {
        store.removeAll { $0 === vc }
    }
}

이렇게 작성한 CommonWebVcStoreremove함수를 올바르게 호출함으로써 계속 쌓이는 문제도 해결해야 한다.
다행히 UIViewControllerRepresentable에서 UIViewController를 제거할 때 호출되는 함수가 하나 있다.

static func dismantleUIViewController(_ uiViewController: CommonWebVc, coordinator: AppWebView.Coordinator)

AppWebView 변경

AppWebView를 조금 수정해본다. AppWebView가 소멸되어도 대표하는 뷰 컨트롤러가 제거되지 않도록 속성을 하나 추가했다.

// 시작 URL (웹뷰 생성에 필요한 Key)
var initialUrl: URL
// 소멸시킬 수 있는지 여부
let isDismantlable: Bool

static func dismantleUIViewController(_ uiViewController: CommonWebVc, coordinator: AppWebView.Coordinator) {
    if coordinator.parent.isDismantlable {
        print(" ---- DISMANTLE ---->>>>>>>>>> ")
        CommonWebVcStore.remove(uiViewController) // <-- 스토어에서 제거
    }
}

func makeUIViewController(context: Context) -> CommonWebVc {
    let vc = CommonWebVcStore.getInstance(for: initialUrl) // <-- 스토어를 통해 생성
    vc.delegate = context.coordinator
    return vc
}

ShopView 완성!!

struct ShopView: View {
    var body: some View {
        AppWebView(initialUrl: URL(string: "https://6009.co.kr")!,
                   isDismantlable: false)
    }
}

정리하자면, UIKit을 SwiftUI와 사용할 때 class의 객체 생성에 주의해야 한다는 점이다.

광고지만, 이렇게 만든 공항버스 앱이 잘 되길 바란다ㅎ

댓글0