Adaptando su solución comercial existente para SwiftUI. Parte 2

¡Buen día a todos! Contigo yo, Anna Zharkova, un desarrollador móvil líder de Usetech.
En esta parte, ya discutiremos el caso de cómo adaptar una solución llave en mano a un proyecto en SwiftUI. Si no está particularmente familiarizado con esta tecnología, le aconsejo que se familiarice con una breve introducción al tema .

Entonces, veamos un ejemplo simple de cómo puede usar una biblioteca preparada para una aplicación iOS estándar en una aplicación en SwiftUI.

Tomemos la solución clásica: subir imágenes asincrónicamente usando la biblioteca SDWebImage.



Para mayor comodidad, trabajar con la biblioteca está encapsulado en ImageManager, que llama a:

  • SDWebImageDownloader
  • SDImageCache

para descargar imágenes y almacenar en caché.

Por tradición, la comunicación con el resultado UIImageView receptor se implementa de 2 maneras:

  • pasando enlaces débiles a este mismo UIImageView;
  • pasando el bloque de cierre al método ImageManager


Una llamada a ImageManager generalmente se encapsula en la extensión UIImageView:

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

ya sea en la clase sucesora:

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

Ahora intentemos atornillar esta solución a SwiftUI. Sin embargo, al adaptarnos, debemos tener en cuenta las siguientes características del marco:

- Ver - estructura. La herencia no es compatible


- La extensión en el sentido habitual es inútil. Por supuesto, podemos escribir algunos métodos para extender la funcionalidad, pero necesitamos vincular esto de alguna manera a DataFlow ;

Tenemos el problema de obtener comentarios y la necesidad de adaptar toda la lógica de interacción con la interfaz de usuario a DataDriven Flow.

Para la solución, podemos ir tanto desde el lado de Vista como desde el lado de adaptación de Flujo de datos.

Comencemos con Ver.

Para empezar, recuerde que SwiftUI no existe por sí solo, sino como un complemento de UIKit. Los desarrolladores de SwiftUI han proporcionado un mecanismo para su uso en SwiftUI UIView, cuyos análogos no se encuentran entre los controles listos para usar. Para tales casos, existen protocolos UIViewRepresentable y UIViewControllerRepresentable para adaptar UIView y UIViewController, respectivamente.

Cree una estructura de Vista que implemente un UIViewRepresentable, en el que redefinimos los métodos:

  • makeUiView;
  • updateUIView

en el que indicamos qué UIView estamos usando y establecemos sus configuraciones básicas. Y no olvide los PropertyWrappers para propiedades 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
    }
}

Podemos incorporar el nuevo control resultante en View SwiftUI:



este enfoque tiene ventajas:

  • No es necesario cambiar el funcionamiento de una biblioteca existente.
  • La lógica está encapsulada en la UIView incorporada.

Pero hay nuevas responsabilidades. Primero, debe monitorear la administración de memoria en el paquete View-UIView. Desde la estructura de Vista, todo el trabajo con ellos se realiza en segundo plano por el propio marco. Pero la limpieza de nuevos objetos recae sobre los hombros del desarrollador.

En segundo lugar, se requieren pasos adicionales para la personalización (tamaños, estilos). Si para Ver estas opciones están habilitadas de forma predeterminada, entonces deben estar sincronizadas con UIView.

Por ejemplo, para ajustar los tamaños, podemos usar el GeometryReader para que nuestra imagen ocupe todo el ancho de la pantalla y la altura que definimos:

 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 principio, para tales casos, el uso de UIView incorporado puede considerarse una ingeniería excesiva . Así que ahora intentemos resolver a través de DataFlow SwiftUI.

La vista que tenemos depende de una variable de estado o un grupo de variables, es decir de cierto modelo, que en sí mismo puede ser esta variable de estado. En esencia, esta interacción se basa en el patrón MVVM.

Implementamos lo siguiente:

  • cree una vista personalizada, dentro de la cual usaremos el control SwiftUI;
  • creamos un ViewModel al cual transferimos la lógica de trabajar con Model (ImageManager).


Para que haya una conexión entre View y ViewModel, ViewModel debe implementar el protocolo ObservableObject y conectarse a View como 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
            }
        }
    }
}

Ver en el método onAppear de su ciclo de vida llama al método ViewModel y obtiene la imagen final de su propiedad @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()
        }
    }
    
}

También hay una API combinada declarativa para trabajar con DataFlow SwiftUI . Trabajar con él es muy similar a trabajar con marcos reactivos (el mismo RxSwift): hay sujetos, hay suscriptores, hay métodos de gestión similares, hay cancelables (en lugar de desechables).

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 nuestro ImageManager se escribió originalmente usando Combinar, la solución se vería así.

Pero desde ImageManager se implementa con otros principios, luego lo intentaremos de otra manera. Para generar un evento, utilizaremos el mecanismo PasstroughSubject, que admite la finalización automática de suscripciones.

    var didChange = PassthroughSubject<UIImage, Never>()

Enviaremos un nuevo valor al asignar un valor a la propiedad UIImage de nuestro modelo:

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

El valor final de nuestra Vista "escucha" en el método onReceive:

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

Entonces, hemos visto un ejemplo simple de cómo puede adaptar el código existente a SwiftUI.
Lo que queda por agregar. Si la solución iOS existente afecta más a la parte de la interfaz de usuario, es mejor usar la adaptación a través de UIViewRepresentable. En otros casos, se necesita la adaptación del modelo de vista del estado.

En las siguientes partes, veremos cómo adaptar la lógica empresarial de un proyecto existente a SwiftUI , trabajar con la navegación y luego profundizar en la adaptación a Combinar un poco más.

Para obtener más información sobre cómo trabajar con View en SwiftUI, consulte aquí .

All Articles