IOS - Swift/SwiftUI

[iOS/SwiftUI] SwiftUI에서 특정부분만 UIkit으로 추출해서 사용하기

게게겍 2024. 12. 3. 14:56

이번에 스유 공부를 하면서 UIKit을 사용해야하는 경우를 많이 만나보았는데 UIHostingController 과 UIViewRepresentable 을 통해서 사용을 했었는데 말이죠

근데 굳이 뷰컨을 새로 만들어서 기능을 끌고오는거보다 부분적으로 바로 사용하면 좋겟다는 생각이 들엇어요!

그래서 한번 끄적여봣는데 잘못된 부분이나 질문 환영입니다

 

viewExtractor하기

//
//  ContentView.swift
//  extract
//
//  Created by 이지훈 on 12/3/24.
//

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            // 시스템 이미지 설정
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            
            // NavigationStack 예제
            NavigationStack {
                List {
                }
                .navigationTitle("Home")
                .viewExtractor { view in
                    if let navController = view.next as? UINavigationController {
                        print(navController)
                    }
                }
            }
            
            // TextField 예제
            TextField("Hello World", text: .constant(""))
                .viewExtractor { view in
                    if let textField = view as? UITextField {
                        print(textField)
                    }
                }
            
            // Slider 예제
            Slider(value: .constant(0.2))
                .viewExtractor { view in
                    if let slider = view as? UISlider {
                        slider.tintColor = .red
                        slider.thumbTintColor = .systemBlue
                    }
                }
        }
        .padding()
    }
}

// UIKit 뷰 추출을 위한 extension
extension View {
    @ViewBuilder
    func viewExtractor(result: @escaping (UIView) -> ()) -> some View {
        self
            .background(ViewExtractHelper(result: result))
            .compositingGroup()
    }
}

// UIKit 뷰 추출을 돕는 헬퍼 구조체
fileprivate struct ViewExtractHelper: UIViewRepresentable {
    var result: (UIView) -> ()
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView(frame: .zero)
        view.backgroundColor = .clear
        view.isUserInteractionEnabled = false
        
        DispatchQueue.main.async {
            if let uiKitview = view.superview?.superview?.subviews.last?.subviews.first {
                result(uiKitview)
            }
        }
        
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
    }
}

코드설명

Composite group을 통해서 모든 것을 하나로 묶어주는 컨테이너라고 생각하고이 그룹을 통해서 SwiftUI는가 "Wrapped UIView"라는 것을 만들게 됩니다!

  1. 배경 뷰 (extractor view라고 함)
  2. 작업 중인 SwiftUI 뷰

이미지에 보이는 계층 구조를 자세히 뜯어보면

  • 최상단에는 "Grouped View" (superView.superView로 접근)
  • 그 안에 배경으로 사용되는 "Extractor View"
  • 그 다음 SwiftUI View가 있습니다 (subviews.last로 접근)
  • SwiftUI View 안에는 "Hosting Kind a View"가 있습니다
  • 마지막으로 맨 아래에 UIKit View가 있습니다 (subviews.first로 접근)

만약 뷰가 순수하게 SwiftUI로만 만들어졌고 UIKit 컴포넌트를 전혀 사용하지 않는다면, 이 계층 구조의 맨 아래에 UIKit 뷰가 존재하지 않으니, 이런 경우에는 UIKit 뷰를 찾으려고 하면nil로 반환됩니다.

UIHostingController의 차이점

먼저, UIHostingController의 특징을 살펴보면:

let swiftUIView = MySwiftUIView()
let hostingController = UIHostingController(rootView: swiftUIView)

UIHostingController는 SwiftUI 뷰를 UIKit 환경에서 사용할 때 주로 사용합니다. 즉, UIKit → SwiftUI 방향으로의 통합을 위한 도구입니다.

하지만 이런식으로 ViewExtractHelper를 사용해서 접근한다면

  • SwiftUI → UIKit 방향으로의 접근이 가능합니다 (SwiftUI 환경을 유지하면서 UIKit의 세밀한 컨트롤이 필요할 때 사용가능)
  • 전체 ViewController를 생성하지 않고도 UIKit 뷰에 접근할 수 있다

(TextField, Slider 같은 기본 컴포넌트들의 UIKit 속성에 접근할 때 매우 유용합니다. 전체 ViewController를 만들지 않고도 필요한 부분만 Custom이 가능해집니다!)