使您现有的业务解决方案适应SwiftUI。第2部分

祝大家有美好的一天!有了你,我,安娜Zharkova,领先的移动开发者Usetech。
在这一部分中,我们将讨论已经如何适应一个完整的解决方案,以在SwiftUI一个项目的情况。如果您对这项技术不是特别熟悉,建议您熟悉该主题简要介绍

因此,让我们看一个简单的示例,说明如何在SwiftUI上的应用程序中为标准iOS应用程序使用现成的库。

让我们采用经典的解决方案:使用SDWebImage库异步上传图像。



为了方便起见,将库使用的图像封装在ImageManager中,该图像调用:

  • SDWebImageDownloader
  • SDImageCache

用于下载图像和缓存。

按照传统,与接收UIImageView结果的通信以两种方式实现:

  • 通过将弱链接传递到同一UIImageView;
  • 通过将闭包传递给ImageManager方法


通常,将对ImageManager的调用封装在UIImageView扩展中:

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

在后继类中:

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

现在,让我们尝试将此解决方案固定到SwiftUI。但是,在进行调整时,我们必须考虑框架的以下功能:

-视图-结构。不支持继承


-通常意义上的扩展是没有用的。当然,我们可以编写一些方法来扩展功能,但是我们需要以某种方式将其绑定到DataFlow

我们遇到了获取反馈的问题,并且需要使与UI交互的整个逻辑适应于DataDriven Flow。

对于解决方案,我们可以从“视图”端和“数据流”适配端进行操作。

让我们从View开始。

首先,请回想一下SwiftUI本身并不存在,而是作为UIKit的一个附加组件SwiftUI开发人员提供了一种在SwiftUI UIView中使用的机制,其类似物不在现成的控件中。对于这种情况,存在UIViewRepresentable和UIViewControllerRepresentable协议,分别用于调整UIView和UIViewController。

创建一个实现UIViewRepresentable的View结构,在其中我们重新定义方法:

  • makeUiView;
  • updateUIView

在其中,我们指示我们正在使用哪个UIView并设置其基本设置。并且不要忘记PropertyWrappers具有可变属性。

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

我们可以将生成的新控件嵌入到View SwiftUI中:



这种方法具有以下优点:

  • 无需更改现有库的操作
  • 逻辑封装在嵌入式UIView中。

但是有新的责任。首先,您需要在View-UIView捆绑软件中监视内存管理。由于采用了View结构,因此与它们一起进行的所有工作都是由框架本身在后台执行的。但是清洁新物体落在显影剂的肩膀上。

其次,需要其他步骤来进行自定义(尺寸,样式)。如果默认情况下启用了View选项,则必须将它们与UIView同步。

例如,要调整大小,我们可以使用GeometryReader,以便我们的图像占据屏幕的整个宽度和我们定义的高度:

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

原则上,对于这种情况,可以将嵌入式UIView的使用视为过度设计因此,现在让我们尝试通过DataFlow SwiftUI解决问题。

我们拥有的视图取决于状态变量或一组变量,即 来自某个模型,该模型本身可以是此状态变量。本质上,这种交互是基于MVVM模式的。

我们实现如下:

  • 创建一个自定义视图,在其中我们将使用SwiftUI控件;
  • 创建一个ViewModel,我们将使用Model(ImageManager)的逻辑转移到其中。


为了在View和ViewModel之间建立连接,ViewModel必须实现ObservableObject协议,并以ObservedObject的身份连接到View

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

在其生命周期的onAppear方法中的View调用ViewModel方法,并从其@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()
        }
    }
    
}

还有一个声明性的Combine API与DataFlow SwiftUI一起使用使用它与使用响应式框架(相同的RxSwift)非常相似:有主题,有订阅者,有类似的管理方法,有可取消的(而不是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)
 }

如果我们的ImageManager最初是使用Combine编写的,则解决方案将如下所示。

但是由于 ImageManager是用其他原理实现的,那么我们将尝试另一种方法。为了生成事件,我们将使用PasstroughSubject机制,该机制支持订阅的自动完成。

    var didChange = PassthroughSubject<UIImage, Never>()

在为模型的UIImage属性分配值时,我们将发送一个新值:

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

我们的View的最终值在onReceive方法中“侦听”:

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

因此,我们看了一个简单的示例,说明如何使现有代码适应SwiftUI。
还有什么要补充的。如果现有的iOS解决方案对UI部分的影响更大,则最好通过UIViewRepresentable使用适应功能。在其他情况下,需要从状态的视图模型进行调整。

在以下各部分中,我们将研究如何使现有项目的业务逻辑适应SwiftUI使用导航,然后深入适应以进行更深入的组合。

有关在SwiftUI下使用View的更多信息,请参见此处

All Articles