Adapting your existing business solution for SwiftUI. Part 2

Good day to all! With you I, Anna Zharkova, a leading mobile developer of Usetech.
In this part, we will already discuss the case of how to adapt a turnkey solution to a project on SwiftUI. If you are not particularly familiar with this technology, I advise you to familiarize yourself with a brief introduction to the topic .

So, let's look at a simple example of how you can use a ready-made library for a standard iOS application in an application on SwiftUI.

Let's take the classic solution: asynchronously upload images using the SDWebImage library.



For convenience, working with the library is encapsulated in ImageManager, which calls:

  • SDWebImageDownloader
  • SDImageCache

for downloading images and caching.

By tradition, communication with the receiving UIImageView result is implemented in 2 ways:

  • by passing weak links to this same UIImageView;
  • by passing the closure block to the ImageManager method


A call to ImageManager is usually encapsulated either in the UIImageView extension:

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

either in the successor class:

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

Now let's try to screw this solution to SwiftUI. However, when adapting, we must take into account the following features of the framework:

- View - structure. Inheritance is not supported


- Extension in the usual sense is useless. Of course, we can write some methods to extend the functionality, but we need to somehow bind this to DataFlow ;

We get the problem of getting feedback and the need to adapt the entire logic of interaction with the UI to DataDriven Flow.

For the solution, we can go both from the View side and from the Data Flow adaptation side.

Let's start with View.

To begin with, recall that SwiftUI does not exist by itself, but as an add-on to UIKit. SwiftUI developers have provided a mechanism for use in SwiftUI UIView, analogues of which are not among ready-made controls. For such cases, there are UIViewRepresentable and UIViewControllerRepresentable protocols for adapting UIView and UIViewController respectively.

Create a View structure that implements a UIViewRepresentable, in which we redefine the methods:

  • makeUiView;
  • updateUIView

in which we indicate which UIView we are using and set their basic settings. And don't forget PropertyWrappers for mutable properties.

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

We can embed the resulting new control in View SwiftUI:



This approach has advantages:

  • No need to change the operation of an existing library
  • Logic is encapsulated in the embedded UIView.

But there are new responsibilities. First, you need to monitor memory management in the View-UIView bundle. Since the View structure, then all work with them is carried out in the background by the framework itself. But the cleaning of new objects falls on the shoulders of the developer.

Secondly, additional steps are required for customization (sizes, styles). If for View these options are enabled by default, then they must be synchronized with UIView.

For example, to adjust the sizes, we can use the GeometryReader so that our image occupies the entire width of the screen and the height defined by us:

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

In principle, for such cases, the use of embedded UIView can be regarded as overengineering . So now let's try to solve through the DataFlow SwiftUI.

The view we have depends on a state variable or a group of variables, i.e. from a certain model, which itself can be this state variable. In essence, this interaction is based on the MVVM pattern.

We implement as follows:

  • create a custom View, inside which we will use the SwiftUI control;
  • create a ViewModel into which we transfer the logic of working with Model (ImageManager).


In order for there to be a connection between the View and ViewModel, the ViewModel must implement the ObservableObject protocol and connect to the View as an 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 in the onAppear method of its life-cycle calls the ViewModel method and gets the final image from its @Published property:

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

There is also a declarative Combine API for working with DataFlow SwiftUI . Working with it is very similar to working with reactive frameworks (the same RxSwift): there are subjects, there are subscribers, there are similar management methods, there is cancellable (instead of 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)
 }

If our ImageManager was originally written using Combine, then the solution would look like this.

But since ImageManager is implemented with other principles, then we will try another way. To generate an event, we will use the PasstroughSubject mechanism, which supports auto-completion of subscriptions.

    var didChange = PassthroughSubject<UIImage, Never>()

We will send a new value when assigning a value to the UIImage property of our model:

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

The final value of our View "listens" in the onReceive method:

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

So, we've looked at a simple example of how you can adapt existing code to SwiftUI.
What remains to add. If the existing iOS solution affects the UI part more, it is better to use adaptation through the UIViewRepresentable. In other cases, adaptation from the View-model of the state is needed.

In the following parts, we will look at how to adapt the business logic of an existing project to SwiftUI , work with navigation, and then dig into adaptation to Combine a little deeper.

For more information about working with View under SwiftUI, see here .

All Articles