markusp
markusp

Reputation: 419

One of my @State vars won't update after running function within onAppear()

I am working with an image carousel that uses bullets for pagination. The total width of the carousel and the number of bullets depends on the number of images.

Here are my vars

struct UserView: View {
    @State var user_id : String = ""
    @State var profilePhotos : [Photo] = []
    @State var pages = 1
}

Here is my onAppear function

GetProfilePhotos().browseUserPhotos(user_id: self.user_id){(photos) in
    self.profilePhotos = photos
    self.pages = photos.count
}

Downstream, I use these variables to build the carousel.

struct Carousel : UIViewRepresentable {
    
   // .... 
    
    var width : CGFloat
    @Binding var page : Int
    @State var user_id : String
    @Binding var pages : Int
    @Binding var profilePhotos : [Photo]
    var height : CGFloat
    
    func makeUIView(context: Context) -> UIScrollView{
       
        let total = width * CGFloat(self.pages)
        let view = UIScrollView()
        view.isPagingEnabled = true
        view.contentSize = CGSize(width: total, height: 1.0)
        view.bounces = true
        view.showsVerticalScrollIndicator = false
        view.showsHorizontalScrollIndicator = false
        view.delegate = context.coordinator

        let view1 = UIHostingController(rootView: ListCards(page: self.$page, profilePhotos: self.$profilePhotos))
        view1.view.frame = CGRect(x: 0, y: 0, width: total, height: self.height)
        view1.view.backgroundColor = .clear
        
        view.addSubview(view1.view)

        return view
    }
    
    // ...
}

self.profilePhotos works JUST FINE. I can read out my images with no problem. self.pages however is always set to 1. It never changes even though I am using the same process to update both variables.

Can anyone explain what I'm missing?

Below is my ListCard

struct ListCards: View {
    @Binding var page : Int
    @Binding var profilePhotos : [Photo]
    
    var body: some View{
        HStack(spacing:0){
            ForEach(self.profilePhotos){photo in
                Card(page: self.$page, width: UIScreen.main.bounds.width, data: photo)
            }
        }.onAppear(){
            print(self.profilePhotos)
        }
   }
}

Upvotes: 1

Views: 78

Answers (1)

Asperi
Asperi

Reputation: 257693

In representable you have to move bindings-dependent code into updateUIView, `cause it is a method called whenever some of binding changed.

Here is a correct way (not tested due to absent dependent entities, so you might need to tune it)

struct Carousel : UIViewRepresentable {

   // ...

    var width : CGFloat
    @Binding var page : Int
    @State var user_id : String
    @Binding var pages : Int
    @Binding var profilePhotos : [Photo]
    var height : CGFloat

    func makeUIView(context: Context) -> UIScrollView{

        let view = UIScrollView()
        view.isPagingEnabled = true
        view.bounces = true
        view.showsVerticalScrollIndicator = false
        view.showsHorizontalScrollIndicator = false
        view.delegate = context.coordinator

        return view
    }

    func updateUIView(_ uiView: UIScrollView, context: Context) {
        uiView.subviews.last?.removeFromSuperview()

        let total = width * CGFloat(self.pages)
        uiView.contentSize = CGSize(width: total, height: 1.0)

        let view1 = UIHostingController(rootView: ListCards(page: self.$page, profilePhotos: self.$profilePhotos))
        view1.view.frame = CGRect(x: 0, y: 0, width: total, height: self.height)
        view1.view.backgroundColor = .clear

        uiView.addSubview(view1.view)
    }
}

also, any API callback might be executed on unknown background queue, but states are better to update on UI queue, so

GetProfilePhotos().browseUserPhotos(user_id: self.user_id){(photos) in
  DispatchQueue.main.async {    // redirect to main queue
    self.profilePhotos = photos
    self.pages = photos.count
  }
}

Upvotes: 1

Related Questions