Aller au contenu
Blog de Kelvas
  1. Posts/

Utiliser une vue SwiftUI dans UIKit

·847 mots·4 mins

Ce n’est plus une nouveauté, SwiftUI fait partie de notre écosystème de développeur iOS. Nous avons de plus en plus d’opportunité avec cette technologie et il est grand temps de se projeter avec elle.

De ce fait lorsque l’on crée un nouveau composant graphique on se pose toujours la question : Est-ce que je le crée en SwiftUI pour qu’il soit compatible avec l’avenir ? Ou est-ce que je le fais avec mon bon vieux UIKit pour être sûr de bien tout maîtriser ? Je pense que beaucoup d’entre nous choisissent la seconde option pour plus de facilité, mais aussi parce que SwiftUI permet déjà d’incorporer des vues UIKit au sein de SwiftUI.

J’avais d’ailleurs déjà parlé d’un sous-ensemble de cette possibilité dans cet article : https://kelvas09.github.io/blog/posts/uikit-view-on-swiftui/

Il est fort à parier que SwiftUI deviendra un jour la principale manière de concevoir des applications pour iOS, iPadOS, macOS et tvOS. Même si UIKit ne disparaîtra jamais selon moi, il est très important d’avoir une bonne connaissance et une bonne stack SwiftUI. De ce fait, créer des composants en SwiftUI first semble pertinent.

Heureusement pour nous, il est assez simple de convertir une vue SwiftUI en vue UIKit.

Définir une vue SwiftUI #

Pour commencer, définissons une vue SwiftUI. Pour les besoins de la démonstration cette dernière sera très simple. Bien entendu, peu importe la complexité de votre vue, cela fonctionne pareil :

import SwiftUI

public struct MyCustomView: View {

    public let title: String
    public let tip: String

    public var body: some View {
        VStack {
            Text(title)
                .font(.title3)
            Text(tip)
                .font(.caption)
        }
    }

}

Deux textes l’un en dessous de l’autre avec une police différente. Très simple, mais largement suffisant pour notre cas.

Il nous faut maintenant convertir notre vue avec UIKit.

Wrapper la vue SwiftUI dans une vue UIKit #

Un peu de code #

À vrai dire convertir n’est pas le bon mot, embarqué serait le plus exact. Pour rappel, avec UIKit tout est UIView. Il nous faut donc créer une vue capable de contenir notre vue SwiftUI. Pour ce faire et afin d’éviter de dupliquer du code, nous allons créer notre propre class UIView capable de faire la conversion à volonté.

public class UIMyCustomView: UIView {

    public init()  {
        super.init(frame: .zero)
    }

    override public init(frame: CGRect) {
        super.init(frame: frame)
    }

    @available(*, unavailable)
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    public func setup(
        title: String,
        tip: String
    ) {
        backgroundColor = .clear
        subviews.forEach { $0.removeFromSuperview() }

        let myCustomView = MyCustomView(title: title, tip: tip)
        let viewController = UIHostingController(rootView: myCustomView)

        guard let swiftUIView = viewController.view else {
            return
        }
        swiftUIView.backgroundColor = .clear
        swiftUIView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(swiftUIView)

        let leading = swiftUIView.leadingAnchor.constraint(equalTo: leadingAnchor)
        leading.priority = .defaultHigh
        leading.isActive = true

        let trailing = swiftUIView.trailingAnchor.constraint(equalTo: trailingAnchor)
        trailing.priority = .defaultHigh
        trailing.isActive = true

        let top = swiftUIView.topAnchor.constraint(equalTo: topAnchor)
        top.priority = .defaultHigh
        top.isActive = true

        let bottom = swiftUIView.bottomAnchor.constraint(equalTo: bottomAnchor)
        bottom.priority = .defaultHigh
        bottom.isActive = true
    }

}

Pour des raisons évidentes nous rendons transparente notre nouvelle vue et nous supprimons d’éventuelles sous vues pré-existantes. Il va de soit également que la méthode setup pouvant être appelé plusieurs fois, il est important de bien toujours nettoyer notre vue pour éviter d’empiler les vue SwiftUI.

À partir de là la partie intéressante commence : Nous créons notre vue SwiftUI comme on le ferait dans du code standard. Nous en profitons pour passer les informations nécessaires au bon fonctionnement (ici le titre et le conseil).

Bien sûr il est possible d’utiliser des modifiers SwiftUI à ce moment là aussi.

Maintenant il nous faut créer un UIHostingController avec en rootView notre vue SwiftUI fraichement créée. Utiliser un UIHostingController permet justement d’embarqué notre vue SwiftUI dans un controller UIKit. Et comme vous le savez déjà probablement, chaque UIViewController possède une vue. C’est cette dernière que nous allons récupérer et ajouter à notre propre vue.

Il ne nous reste plus qu’à attacher cette vue aux quatre coins de la nôtre et le tour est joué !

La preuve en image #

struct MyCustomView_Previews: PreviewProvider {
    static var previews: some View {
        MyCustomView(
            title: "This is my title",
            tip: "This is my tips"
        )
    }
}

Avec le code ci-dessus nous optenons le résultat suivant. Il s’agit d’une vue 100% fait en SwiftUI.

SwiftUI preview
SwiftUI preview

Comme vous pouvez le voir, le rendu est tel qu’attendu. Maintenant regardons notre vue incorporée dans une vue UIKit. Le code suivant permet de générer la prochaine photo :

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let subview = UIMyCustomView()
        subview.setup(title: "This is my title", tip: "This is my tip")
        subview.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(subview)

        let leading = subview.leadingAnchor.constraint(equalTo: view.leadingAnchor)
        leading.priority = .defaultHigh
        leading.isActive = true

        let trailing = subview.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        trailing.priority = .defaultHigh
        trailing.isActive = true

        let top = subview.topAnchor.constraint(equalTo: view.topAnchor)
        top.priority = .defaultHigh
        top.isActive = true

        let bottom = subview.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        bottom.priority = .defaultHigh
        bottom.isActive = true
    }
}
UIKit display
UIKit display

Comme vous pouvez le constater le résultat est exactement le même.

J’espère que vous avez aimé cet article ! N’hésitez pas à réagir sur Twitter ou Mastodon ! À bientôt !