Megagator
Megagator

Reputation: 424

SwiftUI: Opening a modal sheet with dynamic content fails only on first attempt

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?

Upvotes: 11

Views: 3361

Answers (3)

meowmeowmeow
meowmeowmeow

Reputation: 762

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)
      }
  }
}

Upvotes: 1

Megagator
Megagator

Reputation: 424

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.

Upvotes: 5

Asperi
Asperi

Reputation: 257653

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)
      }
    }
  }

Upvotes: 3

Related Questions