11

The goal is to pass dynamic content into a single modal, as if it was a detailed view:

import SwiftUI

struct Item {
  let number: String
  let id = UUID()
}

class ItemSet: ObservableObject {
  @Published var collection: [Item]
  
  init() {
    self.collection = []
    for index in 1...100 {
      self.collection.append(Item(number: "\(index)"))
    }
  }
}

struct ContentView: View {
  @ObservedObject var items: ItemSet
  @State private var selectedItem: Item?
  @State private var showingFull = false
  
  init() {
    self.items = ItemSet()
    self.selectedItem = nil
  }
    
  var columns = [
    GridItem(.adaptive(minimum: 150), spacing: 5.0)
  ]
  
  var body: some View {
    ScrollView {
      LazyVGrid(columns: columns) {
        ForEach(items.collection, id: \.id) {item in
          Text(item.number)
            .frame(height: 100)
            .onTapGesture {
              self.selectedItem = item
              self.showingFull = true
            }
            .sheet(isPresented: $showingFull) {
              if let item = selectedItem {
                Text(item.number)
              }
            }
        }
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

For some reason, the first time you tap a cell, the modal is empty, as if the state was not updated before rendering the modal content, but any time after that works as intended. Am I missing something obvious or should I file a radar?

Megagator
  • 416
  • 6
  • 12
  • The only working solution for me was to move `selectedItem` from a `@State` in the view to a `@Published` in the model, and to move the `.sheet` out of the `ForEach`. – Eric Aya Jun 20 '21 at 22:18
  • 1
    Does this answer your question? [SwiftUI: show different sheet items conditionally](https://stackoverflow.com/questions/65567379/swiftui-show-different-sheet-items-conditionally) – lorem ipsum Jul 07 '21 at 22:32
  • @loremipsum This answers the question in a much better way than most answers here! I didn't know about sheet(item:content:), it's definitely the right way to present conditional modal content. – Clément Cardonnel May 27 '22 at 07:09

3 Answers3

5

One possible solution I found is moving the sheet and state to be their own structs:

struct ContentView: View {
  @ObservedObject var items: ItemSet
  
  init() {
    self.items = ItemSet()
  }
    
  var columns = [
    GridItem(.adaptive(minimum: 150), spacing: 5.0)
  ]
  
  var body: some View {
     ScrollView {
       LazyVGrid(columns: columns) {
         ForEach(items.collection, id: \.id) {item in
          TextItem(item: item)
         }
       }
     }
   }
}

struct TextItem: View {
  let item: Item
  @State private var showingFull = false

  var body: some View {
    Text(item.number)
      .frame(height: 100)
      .onTapGesture {
        self.showingFull = true
      }
      .sheet(isPresented: $showingFull) {
        Text(item.number)
      }
  }
}

I don't know how "correct" this solution is in terms of being proper SwiftUI, but I haven't noticed any performance issues when used inside a more complex and heavy layout.

Megagator
  • 416
  • 6
  • 12
3

This is multi-sheets conflict as you attached same sheet to every cell in ForEach with binding to single state, so on click they are all activated at once. The solution is to move sheet out of ForEach

Here is a corrected part. Tested with Xcode 12 / iOS 14

  var body: some View {
    ScrollView {
      LazyVGrid(columns: columns) {
        ForEach(items.collection, id: \.id) {item in
          Text(item.number)
            .frame(height: 100)
            .onTapGesture {
              self.selectedItem = item
              self.showingFull = true
            }
        }
      }
    }
    .sheet(isPresented: $showingFull) {      // << here !!
      if let item = selectedItem {
        Text(item.number)
      }
    }
  }
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    Ahh, yes that was a pretty silly mistake putting the sheet inside the ForEach. However, I just tried your exact body code and I get the same results: nothing inside the first modal, then everything after that works. I'm on Xcode beta 5, so perhaps this is just a regression. I'll file a radar now... – Megagator Aug 25 '20 at 13:50
  • 1
    Try on simulator or real device, preview might fail. – Asperi Aug 25 '20 at 13:53
  • 1
    Tried on a physical iPhone11,6, in the dynamic SwiftUI previews, and on a simulated iPhone 11; all produced the same, described result. – Megagator Aug 25 '20 at 14:01
  • 1
    Same results on beta 6; feedback filed as FB8531587 – Megagator Aug 25 '20 at 19:32
  • 1
    Any solution for this? – Urkman Sep 17 '20 at 10:01
  • 1
    Noticed the same issue. Did somebody report this? – Noim Oct 02 '20 at 15:05
  • This did not fix the issue for me either. Any solution? – vikzilla Dec 01 '20 at 01:46
  • 2
    @vikzilla, @Noim, @Urkman, probably you meet into another issue that can be solved by using `.sheet(item:` modifier, like in https://stackoverflow.com/a/63217450/12299030. – Asperi Dec 01 '20 at 05:05
0

encountered this same problem and solved it by switching to sheet(item) and removing the boolean check to show sheet

struct TextItem: View {
  @State let item: Item

  var body: some View {
    Text(item.number)
      .frame(height: 100)
      .onTapGesture {
        self.showingFull = true
      }
      .sheet($item) {
        Text($0.number)
      }
  }
}
meowmeowmeow
  • 713
  • 7
  • 17