I know this has been asked before, but I just can't figure out why it isn't working for me. I'm pretty new to coding, so any help would be appreciated.
Trying to have BookSheetView open as a sheet after selecting a cell in my lazyvgrid.
struct LibraryView: View {
#State private var showingSheet = false
let book: Book
let spacing: CGFloat = 10
var gridItems: [GridItem] {
[GridItem(.adaptive(minimum: 180, maximum: 180))]
}
var body: some View {
Text("Text to come")
.multilineTextAlignment(.leading)
.navigationTitle("Library")
ScrollView {
LazyVGrid(columns: gridItems,
spacing: spacing
)
{ ForEach(books, id: \.self) { book in
Button {
showingSheet = true
} label: {
BookTileModel(book: book)
}
// NavigationLink(destination: BookSheetView(book:book),
// label: {BookTileModel(book: book)})
}
}
}
// Start of sheet
.sheet(isPresented: $showingSheet) {
BookSheetView(book: book)
}
// End of sheet
}
}
struct LibraryView_Previews: PreviewProvider {
static var previews: some View {
LibraryView(book: books[1])
}
}
If I use a button, I can get the sheet to open to the right view, but it's not passing the information to the sheet, and if I use a NavigationLink, I get the right information, but as a full page, not a sheet.
I've looked at a bunch of similar posts and watched some tutorials, but I just can't quite figure it out :(
UPDATED CODE
import Foundation
import SwiftUI
extension String: Identifiable {
public var id: String { self }
}
struct GridView: View {
#State private var selected: String? = nil
let book: Book
var gridItems: [GridItem] {
[GridItem(.adaptive(minimum: 180, maximum: 180))]
}
var body: some View {
ScrollView (showsIndicators: false) {
LazyVGrid(columns: gridItems) {
ForEach(book, id: \.self) { item in //ERROR Generic struct 'ForEach' requires that 'Book' conform to 'RandomAccessCollection'
Button(action: {
selected = item
}) {
BookTileModel(book: book)
}
}
}
}.sheet(item: $selected) { item in
BookSheetView(book: item) // ERROR Cannot convert the value of type 'String' to expected argument type 'Book'
}
}
}
Here is a working example, notice the comments:
struct ContentView: View {
let books: [Book] = [
Book(title: "Anna Karenina", author: "Tolstoi"),
Book(title: "To Kill a Mockingbird", author: "Harper Lee"),
Book(title: "The Great Gatsby", author: "F. Scott Fitzgerald"),
Book(title: "One Hundred Years of Solitude", author: "Gabriel García Márquez "),
Book(title: "Mrs. Dalloway", author: "Virginia Woolf’"),
Book(title: "Jane Eyre", author: "Charlotte Brontë’"),
]
#State private var selected: Book? = nil // selection has to be an optional of the Type you use in the ForEach, here Book?
var gridItems: [GridItem] {
[GridItem(.adaptive(minimum: 180, maximum: 180))]
}
var body: some View {
ScrollView (showsIndicators: false) {
LazyVGrid(columns: gridItems) {
ForEach(books) { item in // you cant ForEach over one book, it has to be an array of books = [Book]
Button(action: {
selected = item
}) {
BookTileModel(book: item)
}
}
}
}.sheet(item: $selected) { item in
BookSheetView(book: item) // now that selected is of Type Book, this should work
}
}
}
Related
I'm trying pass optional #State in View with non optional #Binding for edit it there. I faced with problem Xcode is crushing with Fatal error: Unexpectedly found nil while unwrapping an Optional value. But when I check optional value before call that view it is not nil: Editing car is set: Optional("Audi A8"). I checked other SO advices how to solve that problem but nothing helps me to understand what is going wrong... How to pass #State correctly for edit it in SheetView?
:
import SwiftUI
struct Car: Identifiable {
let id = UUID().uuidString
var model: String
var color: String
}
class CarModelView: ObservableObject {
#Published var cars: [Car] = [
.init(model: "Audi A8", color: "Red"),
.init(model: "Honda Civic", color: "Blue"),
.init(model: "BMW M3", color: "Black"),
.init(model: "Toyota Supra", color: "Orange")
]
}
struct CarListView: View {
#StateObject var vm = CarModelView()
#State var editingCar: Car?
var body: some View {
List {
ForEach(vm.cars) { car in
VStack(alignment: .leading) {
Text(car.model)
.font(.headline)
Text(car.color)
.font(.callout)
}
.swipeActions(edge: .trailing) {
Button {
setEditing(car: car)
} label: {
Label("Edit", systemImage: "pencil.circle")
}
}
}
.sheet(item: $editingCar) {
resetEditingCar()
} content: { _ in
SheetView(editingCar: Binding($editingCar)!) // Crash!
}
}
.environmentObject(vm)
}
func setEditing(car: Car) {
editingCar = car
print("Editing car is set: \(String(describing: editingCar?.model))")
}
func resetEditingCar() {
editingCar = nil
print("Editing car should be nil: \(String(describing: editingCar?.model))")
}
}
struct SheetView: View {
#Binding var editingCar: Car
var body: some View {
VStack {
Text("Edit Car Data")
TextField("Model", text: $editingCar.model)
TextField("Color", text: $editingCar.color)
}
}
}
Actually we don't need binding to state here, because it will edit nothing (after sheet close - state will be dropped, so all changes will be lost). Instead we need to transfer a binding to view model item into sheet.
A possible solution is to iterate over view model bindings and use state of binding as activator to inject it as sheet's item into content.
Tested with Xcode 13.4 / iOS 15.5
Here is main part:
#State var editingCar: Binding<Car>? // << here !!
var body: some View {
List {
ForEach($vm.cars) { car in // << binding !!
VStack(alignment: .leading) {
Text(car.wrappedValue.model)
.font(.headline)
Text(car.wrappedValue.color)
.font(.callout)
}
.swipeActions(edge: .trailing) {
Button {
setEditing(car: car) // << binding !!
} label: {
Label("Edit", systemImage: "pencil.circle")
}
}
}
}
.sheet(item: $editingCar) { // << sheet is here !!
resetEditingCar()
} content: {
SheetView(editingCar: $0) // non-optional binding !!!
}
Test module on GitHub
For some reason I don't understand, when I add/remove items from a #State var in MainView, the OutterViews are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView, OutterView, and InnerView.
I've tried using State vars instead of the computed property in OutterView, but it doesn't work. Also, I tried using a var instead of the computed property in OutterViewand initialized it in init() but also doesn't work.
Hope you can help me to find what I am doing wrong.
Thanks!
struct MainView: View {
#State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
#State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: #escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
struct Item : Identifiable {
var id = UUID()
var flagged = false
var title : String
}
class StateManager : ObservableObject {
#Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.items[index].flagged
} set: { (newValue) in
self.items = self.items.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.flagged = newValue
} else {
//not the same index
if newValue {
itemCopy.flagged = false
}
}
return itemCopy
}
}
}
func reset() {
items = items.map { item in
var itemCopy = item
itemCopy.flagged = false
return itemCopy
}
}
}
struct MainView: View {
#ObservedObject var stateManager = StateManager()
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
}
}
Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
Button(action: {
stateManager.reset()
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
var text: String
#Binding var flag: Bool
private var color: Color { flag ? Color.green : Color.gray }
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
}
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
What's happening:
There's a Item that has an ID for each item, the flagged state of that item, and the title
StateManager keeps an array of those items. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.
The ForEach now gets an enumeration of the items. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using .1.id for the id parameter -- .1 is the item while .0 is the index.
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
Using this strategy of an ObservableObject that contains all of your state and passes it on via #Published properties and #Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with your update function. This ends up being a pretty idiomatic way of doing things in SwiftUI.
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)
//}
}
}
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:)
Following the answer from #msmialko, I suspect this is a compiler problem.
_ = self.<your_variable>
inside body solves the problem.
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")
}
}
I have an issue using a sheet inside a ForEach. Basically I have a List that shows many items in my array and an image that trigger the sheet. The problem is that when my sheet is presented it only shows the first item of my array which is "Harry Potter" in this case.
Here's the code
struct ContentView: View {
#State private var showingSheet = false
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.showingSheet = true
}
.sheet(isPresented: self.$showingSheet) {
Text(self.movies[movie])
}
}
}
}
}
}
There should be only one sheet, so here is possible approach - use another sheet modifier and activate it by selection
Tested with Xcode 12 / iOS 14 (iOS 13 compatible)
extension Int: Identifiable {
public var id: Int { self }
}
struct ContentView: View {
#State private var selectedMovie: Int? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
}
}
}
}
I changed your code to have only one sheet and have the selected movie in one variable.
extension String: Identifiable {
public var id: String { self }
}
struct ContentView: View {
#State private var selectedMovie: String? = nil
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(movies) { movie in
HStack {
Text(movie)
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie, content: { selectedMovie in
Text(selectedMovie)
})
}
}
}
Wanted to give my 2 cents on the matter.
I was encountering the same problem and Asperi's solution worked for me.
BUT - I also wanted to have a button on the sheet that dismisses the modal.
When you call a sheet with isPresented you pass a binding Bool and so you change it to false in order to dismiss.
What I did in the item case is I passed the item as a Binding. And in the sheet, I change that binding item to nil and that dismissed the sheet.
So for example in this case the code would be:
var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
NavigationView {
List {
ForEach(0 ..< movies.count) { movie in
HStack {
Text(self.movies[movie])
Image(systemName: "heart")
}
.onTapGesture {
self.selectedMovie = movie
}
}
}
.sheet(item: self.$selectedMovie) {
Text(self.movies[$0])
// My addition here: a "Done" button that dismisses the sheet
Button {
selectedMovie = nil
} label: {
Text("Done")
}
}
}
}
I want to change another unrelated #State variable when a Picker gets changed, but there is no onChanged and it's not possible to put a didSet on the pickers #State. Is there another way to solve this?
Deployment target of iOS 14 or newer
Apple has provided a built in onChange extension to View, which can be used like this:
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor, label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.onChange(of: favoriteColor) { tag in print("Color tag: \(tag)") }
}
}
Deployment target of iOS 13 or older
struct MyPicker: View {
#State private var favoriteColor = 0
var body: some View {
Picker(selection: $favoriteColor.onChange(colorChange), label: Text("Color")) {
Text("Red").tag(0)
Text("Green").tag(1)
}
}
func colorChange(_ tag: Int) {
print("Color tag: \(tag)")
}
}
Using this helper
extension Binding {
func onChange(_ handler: #escaping (Value) -> Void) -> Binding<Value> {
return Binding(
get: { self.wrappedValue },
set: { selection in
self.wrappedValue = selection
handler(selection)
})
}
}
First of all, full credit to ccwasden for the best answer. I had to modify it slightly to make it work for me, so I'm answering this question hoping someone else will find it useful as well.
Here's what I ended up with (tested on iOS 14 GM with Xcode 12 GM)
struct SwiftUIView: View {
#State private var selection = 0
var body: some View {
Picker(selection: $selection, label: Text("Some Label")) {
ForEach(0 ..< 5) {
Text("Number \($0)") }
}.onChange(of: selection) { _ in
print(selection)
}
}
}
The inclusion of the "_ in" was what I needed. Without it, I got the error "Cannot convert value of type 'Int' to expected argument type '()'"
I think this is simpler solution:
#State private var pickerIndex = 0
var yourData = ["Item 1", "Item 2", "Item 3"]
// USE this if needed to notify parent
#Binding var notifyParentOnChangeIndex: Int
var body: some View {
let pi = Binding<Int>(get: {
return self.pickerIndex
}, set: {
self.pickerIndex = $0
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// TODO: DO YOUR STUFF HERE
// USE this if needed to notify parent
self.notifyParentOnChangeIndex = $0
})
return VStack{
Picker(selection: pi, label: Text("Yolo")) {
ForEach(self.yourData.indices) {
Text(self.yourData[$0])
}
}
.pickerStyle(WheelPickerStyle())
.padding()
}
}
I know this is a year old post, but I thought this solution might help others that stop by for a visit in need of a solution. Hope it helps someone else.
import Foundation
import SwiftUI
struct MeasurementUnitView: View {
#State var selectedIndex = unitTypes.firstIndex(of: UserDefaults.standard.string(forKey: "Unit")!)!
var userSettings: UserSettings
var body: some View {
VStack {
Spacer(minLength: 15)
Form {
Section {
Picker(selection: self.$selectedIndex, label: Text("Current UnitType")) {
ForEach(0..<unitTypes.count, id: \.self) {
Text(unitTypes[$0])
}
}.onReceive([self.selectedIndex].publisher.first()) { (value) in
self.savePick()
}
.navigationBarTitle("Change Unit Type", displayMode: .inline)
}
}
}
}
func savePick() {
if (userSettings.unit != unitTypes[selectedIndex]) {
userSettings.unit = unitTypes[selectedIndex]
}
}
}
I use a segmented picker and had a similar requirement. After trying a few things I just used an object that had both an ObservableObjectPublisher and a PassthroughSubject publisher as the selection. That let me satisfy SwiftUI and with an onReceive() I could do other stuff as well.
// Selector for the base and radix
Picker("Radix", selection: $base.value) {
Text("Dec").tag(10)
Text("Hex").tag(16)
Text("Oct").tag(8)
}
.pickerStyle(SegmentedPickerStyle())
// receiver for changes in base
.onReceive(base.publisher, perform: { self.setRadices(base: $0) })
base has both an objectWillChange and a PassthroughSubject<Int, Never> publisher imaginatively called publisher.
class Observable<T>: ObservableObject, Identifiable {
let id = UUID()
let objectWillChange = ObservableObjectPublisher()
let publisher = PassthroughSubject<T, Never>()
var value: T {
willSet { objectWillChange.send() }
didSet { publisher.send(value) }
}
init(_ initValue: T) { self.value = initValue }
}
typealias ObservableInt = Observable<Int>
Defining objectWillChange isn't strictly necessary but when I wrote that I liked to remind myself that it was there.
For people that have to support both iOS 13 and 14, I added an extension which works for both. Don't forget to import Combine.
Extension View {
#ViewBuilder func onChangeBackwardsCompatible<T: Equatable>(of value: T, perform completion: #escaping (T) -> Void) -> some View {
if #available(iOS 14.0, *) {
self.onChange(of: value, perform: completion)
} else {
self.onReceive([value].publisher.first()) { (value) in
completion(value)
}
}
}
}
Usage:
Picker(selection: $selectedIndex, label: Text("Color")) {
Text("Red").tag(0)
Text("Blue").tag(1)
}.onChangeBackwardsCompatible(of: selectedIndex) { (newIndex) in
print("Do something with \(newIndex)")
}
Important note: If you are changing a published property inside an observed object within your completion block, this solution will cause an infinite loop in iOS 13. However, it is easily fixed by adding a check, something like this:
.onChangeBackwardsCompatible(of: showSheet, perform: { (shouldShowSheet) in
if shouldShowSheet {
self.router.currentSheet = .chosenSheet
showSheet = false
}
})
SwiftUI 1 & 2
Use onReceive and Just:
import Combine
import SwiftUI
struct ContentView: View {
#State private var selection = 0
var body: some View {
Picker("Some Label", selection: $selection) {
ForEach(0 ..< 5, id: \.self) {
Text("Number \($0)")
}
}
.onReceive(Just(selection)) {
print("Selected: \($0)")
}
}
}
iOS 14 and CoreData entities with relationships
I ran into this issue while trying to bind to a CoreData entity and found that the following works:
Picker("Level", selection: $contact.level) {
ForEach(levels) { (level: Level?) in
HStack {
Circle().fill(Color.green)
.frame(width: 8, height: 8)
Text("\(level?.name ?? "Unassigned")")
}
.tag(level)
}
}
.onChange(of: contact.level) { _ in savecontact() }
Where "contact" is an entity with a relationship to "level".
The Contact class is an #ObservedObject var contact: Contact
saveContact is a do-catch function to try viewContext.save()...
The very important issue : we must pass something to "tag" modifier of Picker item view (inside ForEach) to let it "identify" items and trigger selection change event. And the value we passed will return to Binding variable with "selection" of Picker.
For example :
Picker(selection: $selected, label: Text("")){
ForEach(data){item in //data's item type must conform Identifiable
HStack{
//item view
}
.tag(item.property)
}
}
.onChange(of: selected, perform: { value in
//handle value of selected here (selected = item.property when user change selection)
})