1

I am doing an app that lists some photos. When the user taps a photo, a sheet appears with the selected photo.

The problem is: Sometimes I tap on the second photo, i. e., index = 1, and the first photo appears on the Sheet View. I don't know why, but the problem should also happen with other indices but until now only with the second photo. Here is my code:

import SwiftUI

struct SheetView: View {
   
   @Binding var photo: String
   
   var body: some View {
       Text("photo title: " + photo)
   }
}


struct ContentView: View {
   
   @State private var selectedIndex: Int = 0
   @State private var photoEdition: Bool = false
   
   @State var photos: [String] = ["gigio", "lulu", "lucky"]
   
   private var photoToEdit: Binding<String> {
       
       .init(get: { () -> String in
           print("get", photos[selectedIndex], selectedIndex)
           return photos[selectedIndex]
       },
             set: { (newValue) in
           photos[selectedIndex] = newValue
           print("set", photos[selectedIndex], selectedIndex)
       })
   }
   
   var body: some View {
       
       VStack {
           ForEach(photos.indices, id:\.self) { index in
               Button(action: {
                   photoTapHandler(selectedIndex: index)
               }) {
                   Text(photos[index]).padding(10)
               }
           }
           
       } .sheet(isPresented: $photoEdition, content: {
           SheetView(photo: photoToEdit)
       })
   }
   
   private func photoTapHandler(selectedIndex: Int) {
       print("updating selected index to ", selectedIndex)
       self.selectedIndex = selectedIndex
       self.photoEdition = true
   }
}

I think what is happening is that on the photoTapHandler function, the variable photoEdition is set to true before selectedIndex is changed. Any idea why? Here is the execution logs.

updating selected index to  1
get gigio 0
get gigio 0

By replacing the body with the following, I don't get the error.

var body: some View {
        
    VStack {
        ForEach(photos.indices, id:\.self) { index in
            Button(action: {
                photoTapHandler(selectedIndex: index)
            }) {
                Text(photos[index]).padding(10)
            }
        }
            
    }.onChange(of: selectedIndex) { selectedIndex in
        self.photoEdition = true
    }
    .sheet(isPresented: $photoEdition, content: {
            SheetView(photo: photoToEdit)
    })
}

private func photoTapHandler(selectedIndex: Int) {
    print("updating selected index to ", selectedIndex)
    self.selectedIndex = selectedIndex
    // self.photoEdition = true
}    
    

Thank you all in advance! :)

Laura Corssac
  • 1,217
  • 1
  • 13
  • 23

1 Answers1

1

SwiftUI is declarative so Order of Operations is a bit different. Just because an operation comes first doesn't mean it will be executed first.

In this case, when you tap a photo, you expect selectedIndex to be updated before photoEdition. But there's a possibility that the state changes (of which there are two) might execute asynchronously and in different orders. So when photoEdition == true Swift UI will re-render sheet view and photoToEdit could still still be in the middle of its operation of returing the old selectedIndex. Hence wrong photo.

Below photoEdition is only updated when selectedIndex has definitely been updated. The onChange modifier ensures that the photoEdition is only updated after selectedIndex has been updated. This isn't the cureall for race conditions but it works in this instance. Below this I provide a Swiftier answer.

var body: some View {
        
    VStack {
        ForEach(photos.indices, id:\.self) { index in
            Button(action: {
                photoTapHandler(selectedIndex: index)
            }) {
                Text(photos[index]).padding(10)
            }
        }
            
    }.onChange(of: selectedIndex) { selectedIndex in
        self.photoEdition = true
    }
    .sheet(isPresented: $photoEdition, content: {
            SheetView(photo: photoToEdit)
    })
}

private func photoTapHandler(selectedIndex: Int) {
    print("updating selected index to ", selectedIndex)
    self.selectedIndex = selectedIndex
    // self.photoEdition = true
}

This is a more "Swifty" way to do things IMO.

import SwiftUI

struct SheetView: View {
    @Binding var photo: String
    var body: some View {
        Text("photo title: " + photo)
    }
}

struct ContentView: View {
    struct Photo: Identifiable {
        var id: Int
        var name: String
    }
    
    @State var photos: [Photo] = [Photo(id: 0, name: "gigio"), Photo(id: 1, name: "lulu"), Photo(id: 2, name: "lucky")]
    @State var selectedPhoto: Photo? = nil
    
    var body: some View {
        VStack {
            ForEach(photos) { photo in
                Button(action: {
                    selectedPhoto = photo
                }) {
                    Text(photo.name).padding(10)
                }
            }
        }
        .sheet(item: $selectedPhoto) { photo in
            SheetView(photo: $selectedPhoto.name)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
KFDoom
  • 772
  • 1
  • 6
  • 19
  • 1
    Thanks! The first option solves my problem. The second, I agree that is more elegant, but it doesn't fit my needs. It I want to edit the photo on the sheet and update the list I would have to run over the photos array every time searching for the index and update the value of the array for example. – Laura Corssac Aug 02 '23 at 14:30
  • 1
    Understood! I like giving options so hopefully I didn't offend... – KFDoom Aug 02 '23 at 14:32
  • 1
    You didn't! Thank you! – Laura Corssac Aug 02 '23 at 14:33