5

The Problem

The following example highlights my issue better than I can explain it. I explicitly give an optional variable a value before presenting a sheet. This sheet, which requires a non-optional variable to init, doesn't register the value and says it is nil. I can't understand why this would be if I only ever call the sheet after the optional has been given a value. Any help would be greatly appreciated. Thanks!

What I have tried

In the example I replaced:

.sheet(isPresented: $showModalView, content: {
     EditBookView(book: editingBook!) //Fatal error here
})

with:

.sheet(isPresented: $showModalView, content: {
     if let book = editingBook {
         EditBookView(book: book)
     }
})

However, this just shows an empty sheet (implying that editingBook is empty). But, interestingly when I close this empty sheet and select another item in the list, the view appears as intended.

Reproducible example

import SwiftUI

struct Book: Identifiable {
    var id: UUID
    var title: String
    
    init(title: String){
        self.title = title
        self.id = UUID()
    }
}

struct ContentView: View {
    
    @State var books = [Book]()
    @State var showModalView = false
    
    @State var editingBook: Book? = nil
    
    var body: some View {
        List{
            ForEach(books){ book in
                VStack(alignment: .leading){
                    Text(book.title)
                            .font(Font.title.bold())
                    Text("id: \(book.id.uuidString)")
                        .foregroundColor(.gray)
                    Button(action: {
                        editingBook = book
                        showModalView = true
                    }){
                        Text("Edit")
                            .foregroundColor(.accentColor)
                    }
                    .buttonStyle(PlainButtonStyle())
                    .padding(.top)
                }
            }
        }
        .padding()
        .onAppear{
            for i in 0...50 {
                books.append(Book(title: "Book #\(i)"))
            }
        }
        .sheet(isPresented: $showModalView, content: {
            EditBookView(book: editingBook!) //Fatal error here
        })
    }
    
}

struct EditBookView: View {
    var book: Book
    
    var body: some View {
        Text(book.title)
    }
}

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

Edit:

enum SheetChoice: Hashable, Identifiable {
    case addContentView
    case editContentView

    var id: SheetChoice { self }
}

...


.sheet(item: $sheetChoice){ item in
                        switch item {
                        case .addContentView:
                            AddContentView()
                                .environmentObject(model)
                        case .editContentView:
                            //if let selectedContent = selectedContent {
                                ContentEditorView(book: selectedContent!, editingFromDetailView: false)
                                    .environmentObject(model)
                            //}
                        }
}

santi.gs
  • 514
  • 3
  • 15
  • 2
    Does this answer your question https://stackoverflow.com/a/63948838/12299030? – Asperi Feb 18 '21 at 15:13
  • @Asperi for this example it would. However in my actual situation I already use .sheet(item: ...) to present different modal views depending on the situation. (Updated question to show this). Is there a way to use your solution in this situation? – santi.gs Feb 18 '21 at 15:24

3 Answers3

3

Make sure you also use editingBook inside your body (not only sheet building block). SwiftUI tracks which State variables are used in its body. When it’s not used, you might come into this weird situations when your body is called with ignored changes to that state variable.

So basically add this line at the beginning of your body:

var body: some View {
   _ = editingBook
   
   return <your view>
}

Alternatively, you can use this .sheet modifier version: https://developer.apple.com/documentation/swiftui/view/sheet(item:ondismiss:content:)

msmialko
  • 1,439
  • 2
  • 20
  • 35
  • Thanks, this helped me! What a head scratcher! – codeguy Nov 10 '22 at 16:36
  • The first solution feels soo wrong on so many levels, it works but it feels like we are fighting agains SwiftUI and might be a huge potential issue in the future. I like the alternative of using `sheet(item:onDismiss:content:)` , thanks. – Vladimir Amiorkov Jun 24 '23 at 14:32
2

Following the answer from @msmialko, I suspect this is a compiler problem.

_ = self.<your_variable>

inside body solves the problem.

Derrick
  • 21
  • 4
0

One possible workaround is moving out the sheet content into another View, and pass the Binding to the @Stete to it:

struct Book: Identifiable {
    var id: UUID
    var title: String
    
    init(title: String){
        self.title = title
        self.id = UUID()
    }
}

enum SheetChoice: Hashable, Identifiable {
    case addContentView
    case editContentView
    
    var id: SheetChoice { self }
}

class MyModel: ObservableObject {
    
}

struct ContentView: View {
    
    @State var books = [Book]()
    @State var selectedContent: Book? = nil
    
    @State var sheetChoice: SheetChoice? = nil
    @StateObject var model = MyModel()
    
    var body: some View {
        List{
            ForEach(books){ book in
                VStack(alignment: .leading){
                    Text(book.title)
                        .font(Font.title.bold())
                    Text("id: \(book.id.uuidString)")
                        .foregroundColor(.gray)
                    Button(action: {
                        selectedContent = book
                        sheetChoice = .editContentView
                    }){
                        Text("Edit")
                            .foregroundColor(.accentColor)
                    }
                    .buttonStyle(PlainButtonStyle())
                    .padding(.top)
                }
            }
        }
        .padding()
        .onAppear{
            for i in 0...50 {
                books.append(Book(title: "Book #\(i)"))
            }
        }
        .sheet(item: $sheetChoice){
            item in
            SheetContentView(item: item, selectedContent: $selectedContent)
                .environmentObject(model)
        }
    }
}
struct SheetContentView: View {
    var item: SheetChoice
    var selectedContent: Binding<Book?>
    var body: some View {
        switch item {
        case .addContentView:
            AddContentView()
        case .editContentView:
            ContentEditorView(book: selectedContent.wrappedValue!,
                              editingFromDetailView: false)
        }
    }
}

struct ContentEditorView: View {
    var book: Book
    var editingFromDetailView: Bool
    
    var body: some View {
        Text(book.title)
    }
}

struct AddContentView: View {
    var body: some View {
        Text("AddContentView")
    }
}
OOPer
  • 47,149
  • 6
  • 107
  • 142