Tom
Tom

Reputation: 4033

SwiftUI passing data from view to modal won't update correctly

I'm trying to pass data from a ScrollView's component to a modal. In this case, the data I'm trying to pass is image. My approach was to update a state each time a ShelterView is clicked.

There is data being passed to ShelterViewDetailed, but somehow solely the ForEach-Loop's last item.background. In other words, no matter what ShelterView is clicked - always the last item.background is being displayed.

Any suggestions?

struct HomeList: View {
    @State var showContent = false
    @State var image = ""
    var shelters = SheltersData

    var body: some View {
        VStack() {
            HStack {
                HomeListTitle()
                Spacer()
            }
            .padding(.leading, 40)

            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 35) {
                    ForEach(shelters) { item in
                        ShelterView(title: item.title, background: item.background)
                            .onTapGesture {
                                self.showContent.toggle()
                                self.image = item.background
                        }
                        .sheet(isPresented: self.$showContent)
                        { ShelterDetailedView(image: self.image) }
                    }
                }
                .padding(.leading, 40)
                .padding(.trailing, 40)
                .padding(.bottom, 60)
                Spacer()
            }
        }
    }
}

struct ShelterDetailedView: View {
    var image: String

    var body: some View {
        ZStack {
            BlurView(style: .extraLight)
            VStack {
                Image(image)
                    .resizable()
                    .frame(width: UIScreen.main.bounds.width, height: 300)
                Spacer()
            }
        }
    }
}

Edit:

struct ShelterView: View {
    var title: String?
    var background: String?

    var body: some View {
        VStack {
            Text(title ?? "")
                .font(Font.custom("Helvetica Now Display Bold", size: 30))
                .foregroundColor(.white)
                .padding(15)
                .lineLimit(2)
            Spacer()
        }
        .background(
            Image(background ?? "")
                .brightness(-0.11)
                .frame(width: 255, height: 360)
        )
            .frame(width: 255, height: 360)
            .cornerRadius(30)
            .shadow(color: Color("shadow"), radius: 10, x: 0, y: 10)
    }
}

struct ShelterStruct: Identifiable {
    var id: Int
    var title: String
    var background: String
}

let SheltersData = [
    ShelterStruct(id: 1, title: "One", background: "pacific"),
    ShelterStruct(id: 2, title: "Two", background: "second"),
    ShelterStruct(id: 3, title: "Three", background: "ccc")
]

Upvotes: 5

Views: 2045

Answers (2)

David Monagle
David Monagle

Reputation: 1801

Update:

So the major problem here is that your images (I'm betting) are much wider than the 255 width frame you are putting them in. Normally not a problem as you are correctly using framing to restrict the visible size. However, as it turns out, onTapGesture will recognise taps even on the parts of the view that extend outside the visible frame.

In the example below I've refactored your view a bit and you can also see that I'm forcing the contentView of the ShelterView to a Rectangle() which should wrap nicely around the frame. The onTapGesture goes below this and therefore your taps are constrained to activating the view that lays within the bounds of the Rectangle.

Also notice that I moved the .sheet outside of the ForEach loop. You only need one modal.

My last thought, this was much harder than it had any right to be IMHO. Only by opening the view debugger did I realise that the problem lay in the touch targets and that it wasn't a glitch in SwiftUI or something more sinister. But that being said, I'm using it in what will be a production product and, these things aside, really loving it.

struct HomeList: View {
    @State var showContent = false
    @State var selectedShelter: ShelterStruct? = nil
    var shelters = SheltersData

    var body: some View {
        VStack() {
            HStack {
                Text("Home List Title")
                Spacer()
            }
            .padding(.leading, 40)

            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 35) {
                    ForEach(shelters, id: \.id) { item in
                        ShelterView(shelter: item)
                            .contentShape(Rectangle())
                            .onTapGesture {
                                print("Tap")
                                self.selectedShelter = item
                            }
                    }
                }
                .padding(.leading, 40)
                .padding(.trailing, 40)
                .padding(.bottom, 60)
                Spacer()
            }
            .sheet(item: self.$selectedShelter) { shelter in
                ShelterDetailedView(image: shelter.background)
            }
        }
    }
}

struct ShelterDetailedView: View {
    var image: String

    var body: some View {
        ZStack {
            VStack {
                Image(image)
                    .resizable()
                    .frame(width: UIScreen.main.bounds.width, height: 300)
                Spacer()
            }
        }
    }
}

struct ShelterView: View {
    var shelter: ShelterStruct

    var body: some View {
        VStack {
            Text(shelter.title)
                .font(Font.custom("Helvetica Now Display Bold", size: 30))
                .foregroundColor(.white)
                .padding(15)
                .lineLimit(2)
            Spacer()
        }
        .background(
            Image(shelter.background)
                .resizable()
                .aspectRatio(1, contentMode: .fill)
                .brightness(-0.11)
                .frame(width: 255, height: 360)
        )
        .frame(width: 255, height: 360)
        .cornerRadius(30)
        .shadow(color: .gray, radius: 10, x: 0, y: 10)
    }
}

struct ShelterStruct: Identifiable {
    var id: Int
    var title: String
    var background: String
}

let SheltersData = [
    ShelterStruct(id: 1, title: "One", background: "pacific"),
    ShelterStruct(id: 2, title: "Two", background: "second"),
    ShelterStruct(id: 3, title: "Three", background: "ccc")
]

It can take a while to get used to the way that SwiftUI works. What's happening here is that you are changing your @State image within your ForEach loop which is being evaluated when the body is instantiated. This means that your state will quickly loop through each of the available states and settle on the final image in your set of shelters.

Then when you tap any of the rows, you toggle your Bool and the sheet appears with the current image state, which will be the last one.

What you need to do is have an optional @State for the selectedImage:

@State private var selectedImage: String? = nil

Then you can use this form of sheet:

  .sheet(item: $selectedImage) { 

And to show it you have this:

  .onTapGesture {
    self.selectedImage = item.background
  }

Note that in this you are using the closure variable from the ForEach loop for item instead of using a @State.

When the sheet is dismissed, SwiftUI will update the bound selectedImage back to nil for you, thus keeping the state of your view correct.

Upvotes: 3

E.Coms
E.Coms

Reputation: 11531

I think I found the issue you have.

 //  ForEach(shelters) { item in 

I don't know what the value is here : SheltersData.

But usually, when people use some array values, they prefer using like the following:

ForEach(shelters, id: \.title) { item in

You are supposed to use id to identify the items.

  struct SheltersData{
  var title: String
  var background: String
  }

  var shelters : [SheltersData] = [SheltersData(title: "cool", background: "image2"),
SheltersData(title: "hot", background: "image1")]

If both titles are same, you will always see last background as you see before.

Therefore, I suggest you add different id to your ForEach structures.

Upvotes: 0

Related Questions