SwiftUI와 UIKit 같이 사용하기

아직 레거시 코드가 많기 때문에, UIkit 과 SwiftUI 를 같이 사용하는 경우가 있다.

SwiftUI with UIKit

SwiftUI 프로젝트에 UIKit 를 같이 사용하는 방법에 대해서 알아보겠다.

예를들어 Pdf 파일을 볼 수 있는 PDFKit의 PDFView 는 UIKit으로 되어있는데, 이걸 SwiftUI 프로젝트에서 사용하려면, UIViewRepresentable 프로토콜을 사용하면 된다.

struct ViewMe: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: Context) -> PDFView {
        let pdfView = PDFView()
        pdfView.document = PDFDocument(url: url)
        return pdfView
    }
    
    func updateUIView(_ uiView: PDFView, context: Context) {
        
    }
}

그리고 그 ViewMe 를 SwiftUI 에 추가하면 끝이다.

struct ContentView: View {
    let fileURL = Bundle.main.url(forResource: "example", withExtension: "pdf")
    
    var body: some View {
        VStack {
            ViewMe(url: fileURL!)
        }
        .padding()
    }
}

이번에는 WebKit에 있는, WKWebView() 를 나타내기 위해서 UIViewRepresentable를 사용해보겠다.

struct WebView: UIViewRepresentable {
    let url: URL
    
    func makeUIView(context: UIViewRepresentableContext<WebView>) -> WKWebView {
        return WKWebView()
    }
    
    func updateUIView(_ webView: WKWebView, context: Context) {
        let request = URLRequest(url: url)
        webView.load(request)
    }
}

updateUIView 에서 load(URLRequest) 까지 다 하고나서, SwiftUI 에서는 WebView를 url과 함께 호출하면 된다.

struct ContentView: View {
    var body: some View {
        WebView(url: URL(string: "https://heroic-horse-c3d1d3.netlify.app")!)
        .ignoreSafeArea()
    }
}

WKWebView

UIViewRepresentableContext

Contextual information about the state of the system that you use to create and update your UIKit view.

UIViewRepresentableContext는 UIKit 뷰를 만들고 업데이트할 때 사용하는 state 관련한 정보를 담은 MainActor Structure 이다. 우리는 WKWebView 를 반환하고, 자기 자신 WebView 를 넣으면 된다.

@MainActor
struct UIViewRepresentableContext<Representable> where Representable : UIViewRepresentable

UIKit with SwiftUI

이번에는 UIKit 프로젝트에 SwiftUI View 를 호스팅하는 방법을 해보겠다.

Story board

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBSegueAction func openSwiftUIView(_ coder: NSCoder) -> UIViewController? {
        return UIHostingController(coder: coder, rootView: SwiftUIView(name: "Nancy"))
    }
    
}
  1. embeded in nav controller 를 한 스토리보드에서
  2. UIHostingController 를 만들고,
  3. Button 으로 show 연결을 하고나서,
  4. ViewController 에 @IBSegueAction 을 추가했다.

UIHostingController의 rootView 에는 SwiftUI View를 넣으면 된다. (Navigation으로 이동되기 때문에, back button이 보이게 됨.)

참고로, back button을 지우고 싶다면,


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        navigationController?.setNavigationBarHidden(true, animated: false)

    }

Code only

스토리보드로 보여주면, 설명이 쉽지 않기에 code only로 다시 한번 더 해보았다.

초기설정

  1. Main.storyboard 파일 삭제

  2. Info.plist 수정

set-1

set-1

위 두 key - value 를 삭제한다.


SceneDelegate 수정

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?


    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let windowScene = (scene as? UIWindowScene) else { return }
        
        window = UIWindow(windowScene: windowScene)
        
        let viewController = ViewController()
        let navController = UINavigationController(rootViewController: viewController)
        
        window?.rootViewController = navController
        window?.makeKeyAndVisible()
    }
import UIKit
import SwiftUI

class ViewController: UIViewController {

    let button: UIButton = {
        let button = UIButton(type: .system)
        var config = UIButton.Configuration.plain()
        config.title = "Button"
        button.configuration = config
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
//        navigationController?.setNavigationBarHidden(true, animated: false)

        title = "ViewController"
        view.backgroundColor = .white
        
        view.addSubview(button)
        
        button.addAction(UIAction { [weak self] action in
            let hostingController = UIHostingController(rootView: SwiftUIView(name: "Nancy"))
            self?.navigationController?.pushViewController(hostingController, animated: true)
        }, for: .touchUpInside)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }
}

이렇게 SwiftUI와 UIKit 을 통합하는 방법에 대해서 알아보았다.