Anpassung Ihrer vorhandenen Geschäftslösung an SwiftUI. Teil 2

Guten Tag allerseits! Mit Ihnen habe ich, Anna Zharkova, eine führende mobile Entwicklerin von Usetech.
In diesem Teil werden wir bereits den Fall diskutieren, wie eine schlüsselfertige Lösung an ein Projekt auf SwiftUI angepasst werden kann. Wenn Sie mit dieser Technologie nicht besonders vertraut sind, empfehle ich Ihnen, sich mit einer kurzen Einführung in das Thema vertraut zu machen .

Schauen wir uns also ein einfaches Beispiel an, wie Sie eine vorgefertigte Bibliothek für eine Standard-iOS-Anwendung in einer Anwendung auf SwiftUI verwenden können.

Nehmen wir die klassische Lösung: Laden Sie Bilder mithilfe der SDWebImage-Bibliothek asynchron hoch.



Der Einfachheit halber ist die Arbeit mit der Bibliothek in ImageManager gekapselt, der Folgendes aufruft:

  • SDWebImageDownloader
  • SDImageCache

zum Herunterladen von Bildern und zum Zwischenspeichern.

Traditionell wird die Kommunikation mit dem empfangenden UIImageView-Ergebnis auf zwei Arten implementiert:

  • durch Übergeben schwacher Glieder an dieselbe UIImageView;
  • indem Sie den Schließungsblock an die ImageManager-Methode übergeben


Ein Aufruf von ImageManager ist normalerweise entweder in der UIImageView-Erweiterung gekapselt:

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

entweder in der Nachfolgeklasse:

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)
    }
}

Versuchen wir nun, diese Lösung mit SwiftUI zu verbinden. Bei der Anpassung müssen wir jedoch die folgenden Merkmale des Frameworks berücksichtigen:

- Ansicht - Struktur. Vererbung wird nicht unterstützt


- Eine Erweiterung im üblichen Sinne ist nutzlos. Natürlich können wir einige Methoden schreiben, um die Funktionalität zu erweitern, aber wir müssen dies irgendwie an DataFlow binden .

Wir haben das Problem, Feedback zu erhalten und die gesamte Logik der Interaktion mit der Benutzeroberfläche an DataDriven Flow anzupassen.

Für die Lösung können wir sowohl von der Ansichtsseite als auch von der Datenflussanpassungsseite gehen.

Beginnen wir mit Ansicht. Denken Sie zunächst

daran, dass SwiftUI nicht für sich allein existiert, sondern als Add-On zu UIKit. SwiftUI-Entwickler haben einen Mechanismus zur Verwendung in SwiftUI UIView bereitgestellt, dessen Analoga nicht zu den vorgefertigten Steuerelementen gehören. Für solche Fälle gibt es die Protokolle UIViewRepresentable und UIViewControllerRepresentable zum Anpassen von UIView bzw. UIViewController.

Erstellen Sie eine View-Struktur, die eine UIViewRepresentable implementiert, in der wir die Methoden neu definieren:

  • makeUiView;
  • updateUIView

in dem wir angeben, welches UIView wir verwenden, und deren Grundeinstellungen festlegen. Und vergessen Sie nicht PropertyWrappers für veränderbare Eigenschaften.

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
    }
}

Wir können das resultierende neue Steuerelement in View SwiftUI einbetten:



Dieser Ansatz hat folgende Vorteile:

  • Sie müssen den Betrieb einer vorhandenen Bibliothek nicht ändern
  • Die Logik ist in der eingebetteten UIView gekapselt.

Aber es gibt neue Verantwortlichkeiten. Zunächst müssen Sie die Speicherverwaltung im View-UIView-Bundle überwachen. Da die View-Struktur dann alle Arbeiten mit ihnen im Hintergrund vom Framework selbst ausgeführt werden. Die Reinigung neuer Objekte fällt jedoch auf die Schultern des Entwicklers.

Zweitens sind zusätzliche Schritte zur Anpassung erforderlich (Größen, Stile). Wenn für Ansicht diese Optionen standardmäßig aktiviert sind, müssen sie mit UIView synchronisiert werden.

Zum Anpassen der Größe können wir beispielsweise den GeometryReader verwenden, sodass unser Bild die gesamte Breite des Bildschirms und die von uns definierte Höhe einnimmt:

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

Grundsätzlich kann in solchen Fällen die Verwendung von eingebettetem UIView als Overengineering angesehen werden . Versuchen wir nun, über die DataFlow SwiftUI zu lösen.

Die Ansicht, die wir haben, hängt von einer Zustandsvariablen oder einer Gruppe von Variablen ab, d.h. von einem bestimmten Modell, das selbst diese Zustandsvariable sein kann. Im Wesentlichen basiert diese Interaktion auf dem MVVM-Muster.

Wir implementieren wie folgt:

  • Erstellen Sie eine benutzerdefinierte Ansicht, in der das SwiftUI-Steuerelement verwendet wird.
  • Erstellen Sie ein ViewModel, in das wir die Logik der Arbeit mit Model (ImageManager) übertragen.


Damit eine Verbindung zwischen View und ViewModel hergestellt werden kann, muss ViewModel das ObservableObject- Protokoll implementieren und eine Verbindung zur View als ObservedObject herstellen .

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 in der onAppear-Methode seines Lebenszyklus ruft die ViewModel-Methode auf und ruft das endgültige Bild von seiner @ Publishing-Eigenschaft ab:

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()
        }
    }
    
}

Es gibt auch eine deklarative Kombinations-API für die Arbeit mit DataFlow SwiftUI . Die Arbeit damit ist der Arbeit mit reaktiven Frameworks (dem gleichen RxSwift) sehr ähnlich: Es gibt Themen, es gibt Abonnenten, es gibt ähnliche Verwaltungsmethoden, es gibt stornierbare (anstelle von Disposable).

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)
 }

Wenn unser ImageManager ursprünglich mit Combine geschrieben wurde, sieht die Lösung folgendermaßen aus.

Aber seit ImageManager wird mit anderen Prinzipien implementiert, dann werden wir einen anderen Weg versuchen. Um ein Ereignis zu generieren, verwenden wir den PasstroughSubject-Mechanismus, der die automatische Vervollständigung von Abonnements unterstützt.

    var didChange = PassthroughSubject<UIImage, Never>()

Wir senden einen neuen Wert, wenn wir der UIImage-Eigenschaft unseres Modells einen Wert zuweisen:

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

Der endgültige Wert unserer Ansicht "hört" in der onReceive-Methode:

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

Wir haben uns ein einfaches Beispiel angesehen, wie Sie vorhandenen Code an SwiftUI anpassen können.
Was bleibt noch hinzuzufügen. Wenn die vorhandene iOS-Lösung den UI-Teil stärker beeinflusst, ist es besser, die Anpassung über UIViewRepresentable zu verwenden. In anderen Fällen ist eine Anpassung aus dem View-Modell des Staates erforderlich.

In den folgenden Abschnitten werden wir uns ansehen, wie Sie die Geschäftslogik eines vorhandenen Projekts an SwiftUI anpassen , mit der Navigation arbeiten und sich dann eingehender mit der Anpassung an Combine befassen .

Weitere Informationen zum Arbeiten mit View unter SwiftUI finden Sie hier .

All Articles