Adapter votre solution commerciale existante pour SwiftUI. Partie 2

Bonne journée à tous! Avec vous, Anna Zharkova, développeur mobile leader d' Usetech,
dans cette partie, nous allons déjà discuter du cas de l'adaptation d'une solution clé en main à un projet sur SwiftUI. Si vous n'êtes pas particulièrement familier avec cette technologie, je vous conseille de vous familiariser avec une brève introduction au sujet .

Alors, regardons un exemple simple de la façon dont vous pouvez utiliser une bibliothèque prête à l'emploi pour une application iOS standard dans une application sur SwiftUI.

Prenons la solution classique: télécharger des images de manière asynchrone à l'aide de la bibliothèque SDWebImage.



Pour plus de commodité, l'utilisation de la bibliothèque est encapsulée dans ImageManager, qui appelle:

  • SDWebImageDownloader
  • SDImageCache

pour télécharger des images et la mise en cache.

Par tradition, la communication avec le résultat UIImageView récepteur est implémentée de 2 manières:

  • en passant des liens faibles à ce même UIImageView;
  • en passant le bloc de fermeture à la méthode ImageManager


Un appel à ImageManager est généralement encapsulé dans l'extension UIImageView:

extension UIImageView {
 func setup(by key: String) {
        ImageManager.sharedInstance.setImage(toImageView: self, forKey: key)
    }
}

soit dans la classe successeur:

class CachedImageView : UIImageView {
    private var _imageUrl: String?
    var imageUrl: String?  {
        get {
            return _imageUrl
        }
        set {
            self._imageUrl = newValue
            if let url = newValue, !url.isEmpty {
                self.setup(by: url)
            }
        }
    }
    
    func setup(by key: String) {
        ImageManager.sharedInstance.setImage(toImageView: self, forKey: key)
    }
}

Essayons maintenant de visser cette solution à SwiftUI. Cependant, lors de l'adaptation, nous devons prendre en compte les caractéristiques suivantes du cadre:

- Vue - structure. L'héritage n'est pas pris en charge


- L'extension au sens habituel est inutile. Bien sûr, nous pouvons écrire quelques méthodes pour étendre la fonctionnalité, mais nous devons en quelque sorte le lier à DataFlow ;

Nous avons le problème d'obtenir des commentaires et la nécessité d'adapter toute la logique d'interaction avec l'interface utilisateur à DataDriven Flow.

Pour la solution, nous pouvons aller à la fois du côté de la vue et du côté de l'adaptation du flux de données.

Commençons par View.

Pour commencer, rappelez-vous que SwiftUI n'existe pas par lui-même, mais comme un complément à UIKit. Les développeurs de SwiftUI ont fourni un mécanisme à utiliser dans SwiftUI UIView, dont les analogues ne figurent pas parmi les contrôles prêts à l'emploi. Dans de tels cas, il existe des protocoles UIViewRepresentable et UIViewControllerRepresentable pour adapter respectivement UIView et UIViewController.

Créez une structure de vue qui implémente un UIViewRepresentable, dans lequel nous redéfinissons les méthodes:

  • makeUiView;
  • updateUIView

dans lequel nous indiquons quelle UIView nous utilisons et définissons leurs paramètres de base. Et n'oubliez pas PropertyWrappers pour les propriétés mutables.

struct WrappedCachedImage : UIViewRepresentable {
    let height: CGFloat
    @State var imageUrl: String
    
    func makeUIView(context: Context) -> CachedImageView {
        let frame = CGRect(x: 20, y: 0, width: UIScreen.main.bounds.size.width - 40, 
                                     height: height)
        return CachedImageView(frame: frame)
    }
    
    func updateUIView(_ uiView: CachedImageView, context: Context) {
        uiView.imageUrl = imageUrl
        uiView.contentMode = .scaleToFill
    }
}

Nous pouvons intégrer le nouveau contrôle résultant dans View SwiftUI:



Cette approche présente des avantages:

  • Pas besoin de changer le fonctionnement d'une bibliothèque existante
  • La logique est encapsulée dans l'UIView intégré.

Mais il y a de nouvelles responsabilités. Tout d'abord, vous devez surveiller la gestion de la mémoire dans le bundle View-UIView. Depuis la structure View, tous les travaux avec eux sont effectués en arrière-plan par le framework lui-même. Mais le nettoyage des nouveaux objets tombe sur les épaules du développeur.

Deuxièmement, des étapes supplémentaires sont nécessaires pour la personnalisation (tailles, styles). Si pour Afficher ces options sont activées par défaut, elles doivent être synchronisées avec UIView.

Par exemple, pour ajuster les tailles, nous pouvons utiliser le GeometryReader afin que notre image occupe toute la largeur de l'écran et la hauteur que nous définissons:

 var body: some View {
      GeometryReader { geometry in 
        VStack {
           WrappedCachedImage(height:300, imageUrl: imageUrl) 
           .frame(minWidth: 0, maxWidth: geometry.size.width,
                     minHeight: 0, maxHeight: 300)
        }
    }
}

En principe, dans de tels cas, l'utilisation d'UIView intégré peut être considérée comme une ingénierie excessive . Alors maintenant, essayons de résoudre à travers le DataFlow SwiftUI.

La vue que nous avons dépend d'une variable d'état ou d'un groupe de variables, c'est-à-dire à partir d'un certain modèle, qui peut lui-même être cette variable d'état. En substance, cette interaction est basée sur le modèle MVVM.

Nous mettons en œuvre comme suit:

  • créer une vue personnalisée, dans laquelle nous utiliserons le contrôle SwiftUI;
  • créer un ViewModel dans lequel nous transférons la logique de travail avec Model (ImageManager).


Pour qu'il y ait une connexion entre la vue et le ViewModel, le ViewModel doit implémenter le protocole ObservableObject et se connecter à la vue en tant qu'observedObject .

class CachedImageModel : ObservableObject {
    @Published var image: UIImage = UIImage()
    
    private var urlString: String = ""
    
    init(urlString:String) {
        self.urlString = urlString
    }
    
    func loadImage() {
        ImageManager.sharedInstance
        .receiveImage(forKey: urlString) {[weak self] (im) in
            guard let self = self else {return}
            DispatchQueue.main.async {
                self.image = im
            }
        }
    }
}

View dans la méthode onAppear de son cycle de vie appelle la méthode ViewModel et obtient l'image finale de sa propriété @Published:

struct CachedLoaderImage : View {
    @ObservedObject var  model:CachedImageModel
    
    init(withURL url:String) {
        self.model = CachedImageModel(urlString: url)
    }
    
    var body: some View {
        Image(uiImage: model.image)
            .resizable()
            .onAppear{
                self.model.loadImage()
        }
    }
    
}

Il existe également une API Combine déclarative pour travailler avec DataFlow SwiftUI . Travailler avec est très similaire à travailler avec des frameworks réactifs (le même RxSwift): il y a des sujets, il y a des abonnés, il y a des méthodes de gestion similaires, il est annulable (au lieu de jetable).

class ImageLoader: ObservableObject {
 @Published var image: UIImage?
 private var cancellable: AnyCancellable?

 func load(url: String) {
     cancellable = ImageManager.sharedInstance.publisher(for: url)
         .map { UIImage(data: $0.data) }
         .replaceError(with: nil)
         .receive(on: DispatchQueue.main)
         .assign(to: \.image, on: self)
 }

Si notre ImageManager a été écrit à l'origine à l'aide de Combine, la solution ressemblerait à ceci.

Mais depuis ImageManager est implémenté avec d'autres principes, alors nous allons essayer une autre façon. Pour générer un événement, nous utiliserons le mécanisme PasstroughSubject, qui prend en charge l'auto-complétion des abonnements.

    var didChange = PassthroughSubject<UIImage, Never>()

Nous enverrons une nouvelle valeur lors de l'attribution d'une valeur à la propriété UIImage de notre modèle:

var data = UIImage() {
        didSet {
            didChange.send(data)
        }
    }
 ,    .

La valeur finale de notre vue "écoute" dans la méthode onReceive:

 var body: some View {
       Image(uiImage: image)
            .onReceive(imageLoader.didChange) { im in
            self.image = im
           //-   
        }
    }

Nous avons donc examiné un exemple simple de la façon dont vous pouvez adapter le code existant à SwiftUI.
Ce qui reste à ajouter. Si la solution iOS existante affecte davantage la partie UI, il est préférable d'utiliser l'adaptation via UIViewRepresentable. Dans d'autres cas, une adaptation du View-model de l'état est nécessaire.

Dans les parties suivantes, nous verrons comment adapter la logique métier d'un projet existant à SwiftUI , travailler avec la navigation, puis approfondir l'adaptation pour combiner un peu plus profondément.

Pour plus d'informations sur l'utilisation de View sous SwiftUI, cliquez ici .

All Articles