0

So basically I'm wrapping a pretty simple UIKit component for it to be used within a SwiftUI view. Context goes as follows:

  • The component expects a UIImage as a parameter in its initializer but I wish to initialize it with a blank UIImage() and later update it with one downloaded async from the web with Kingfisher.

Wrapper code is the following:

struct WrappedView: UIViewRepresentable {
    var viewModel: Model
    
    func makeUIView(context: Context) -> HorizontalComponent {
        HorizontalComponent(image: UIImage(), title: viewModel.title, subtitle: viewModel.subtitle)
    }
    
    func updateUIView(_ uiView: HorizontalComponent, context: Context) {
            KingfisherManager.shared.retrieveImage(with: URL(string: "someImageUrl", completionHandler: { result in
                switch result {
                case .success(let value):
                    DispatchQueue.main.async {
                        //method from the UIView that simply updates the image of a UIImageview.
                        uiView.setImage(value.image)
                    }
                case .failure(let error):
                    print("Error fetching image")
                }
            })
        DispatchQueue.main.async {
            
            uiView.setTitle(viewModel.title)
            uiView.setSubtitle(viewModel.subtitle)
            // hides non needed views, though for whatever reason it seems Im not on a MainThread context so it needs to be explicitly run on Main Thread else it has no effect.
            uiView.roundedImageView.isHidden = true
            uiView.circularImageView.isHidden = true
        }
    }
}

The SwiftUI View that uses this representable is pretty simple and goes as follows:

struct LandView: View {

   private let models: [Model]
   private let isLoading: Bool

   init(models: [Model],
     isLoading: Bool) {
       self.models = models
       self.isLoading = isLoading
   }

var body: some View {
    VStack {
        if isLoading {
           ShimmerView()
        else { 
          WrappedView(viewModel: model[0])
        }
    }
 }

The view that injects the state variables is the following:

struct HomeView: View {

@ObservedObject var viewModel: ViewModel

var body: some View {
    VStack {
      LandView(models: viewModel.cards, isLoading: viewModel.isLoading)
    }
}

ViewModel:

class ViewModel: ObservableObject {
   @Published private(set) var cards: [Model] = []
   @Published private(set) var isLoading: Bool = true

   //more functionality that actually handles the value of this variables ofc
}

What happens is that it works fine the first time the View is shown on screen, but when I toggle some sort of SwiftUI state update from the VM and makeUIView is called as well as updateUIView, the view on screen resets to what is initialized on makeUIView and never actually does the changes from updateUiview(such as hiding the isHidden properties from the views that are not needed, for example). The updateUIView method is called, though.

It seems that for whatever reason my wrapped view is reset to its initial state every time a SwiftUI update is triggered.

stompy
  • 227
  • 1
  • 13
  • Your LandView use constants, so will never update. Tour WrappedView use a private var that will not update UI. You must redefine your properties with ObservableObject or Bindind to enable UI updates. – Ptit Xav Aug 14 '23 at 18:02

1 Answers1

0

updateUIView needs to check if the vars are different from last time the struct was init before doing anything to the UIView to prevent repeated actions.

DispatchQueue.main.async is a bad idea in SwiftUI, best move that into a view controller or coordinator.

In SwiftUI the View struct is the view model already you don't need an object for it. But seems to me your "VM" is actually your model store object which is fine.

malhal
  • 26,330
  • 7
  • 115
  • 133