Pass an observed object's projected value's property to #Binding - swift

I have a main screen with an #FetchRequest that returns a FetchResult<Item>. In that main screen I have a list of all of the items with navigation links that, when selected, pass an Item to an ItemDetail view. In this ItemDetail view, item is marked with #ObservedObject. A subview of ItemDetail is ItemPropertiesView which lists all of the item's properties. I pass the item properties directly to #Binding properties of ItemPropertiesView using $item.{insert property here}. In ItemPropertiesView, there's several LineItem where I pass the property using $ once again to an #Binding property called "value" which is passed into a text field, that can ultimately be changed.
My goal is to be able to edit this text field and once you're done editing, to be able to save these changes to my core data store.
Since this has been a little hard to read, here is a code recreation:
struct MainScreen: View {
#FetchRequest(entity: Item.entity(), sortDescriptors: [NSSortDescriptor(key: "itemName", ascending: true)]) var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach(items, id: \.self) { (item: Item) in
NavigationLink(destination: ItemDetail(item: item)) {
Text(item.itemName ?? "Unknown Item Name")
}
} // ForEach
}
}
} // body
} // MainScreen
struct ItemDetail: View {
#ObservedObject var item: Item
var body: some View {
ItemPropertiesView(itemCost: $item.itemCost)
}
}
struct ItemPropertiesView: View {
#Binding var itemCost: String?
var body: some View {
LineItem(identifier: "Item Cost", value: $itemCost)
}
}
struct LineItem: View {
let identifier: String
#Binding var value: String
var body: some View {
HStack {
Text(identifier).bold() + Text(": ")
TextField("Enter value",text: $value)
}
}
}
I am getting an error in ItemDetail: "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"
This is on the only error I'm getting.
I'm new to SwiftUI, so all feedback is appreciated.

Just by code reading I assume the problem is in optional property in ItemPropertiesView, just remove it
struct ItemPropertiesView: View {
#Binding var itemCost: String // << here !!
// .. other code
and update parent to have bridge to CoreData model optional property
struct ItemDetail: View {
#ObservedObject var item: Item
var body: some View {
let binding = Binding(
get: { self.item.itemCost ?? "" },
set: { self.item.itemCost = $0 }
)
return ItemPropertiesView(itemCost: binding)
}
}

Related

SwiftUI iterating through #State or #Published dictionary with ForEach

Here is a minimum reproducible code of my problem. I have a dictionary of categories and against each category I have different item array. I want to pass the item array from dictionary, as binding to ListRow so that I can observer the change in my ContentView. Xcode shows me this error which is very clear
Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Item' conform to 'Identifiable.
The solution shows in this question Iterating through set with ForEach in SwiftUI not using any #State or #Published variable. They are just using it for showing the data. Any work around for this issue ??
struct Item {
var id = UUID().uuidString
var name: String
}
struct ListRow {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
ForEach(testDictionary[category]) { item in
ListRow(item: item)
}
}
}.onAppear(
addDummyDateIntoDictonary()
)
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}
One problem is that you didn't make ListRow conform to View.
// add this
// ╭─┴──╮
struct ListRow: View {
#Binding var item: Item
var body: some View {
TextField("Place Holder", text: $item.name)
}
}
Now let's address your main problem.
A Binding is two-way: SwiftUI can use it to get a value, and SwiftUI can use it to modify a value. In your case, you need a Binding that updates an Item stored somewhere in testDictionary.
You can create such a Binding “by hand” using Binding.init(get:set:) inside the inner ForEach.
struct ContentView: View {
var categories = ["Bakery","Fruits & Vagetables", "Meat & poultry", "Dairy & Eggs", "Pantry", "Household"]
#State private var testDictionary: [String: [Item]] = [:]
var body: some View {
VStack(alignment: .leading) {
ForEach(categories, id: \.self) { category in
Text(category)
.font(.system(size: 30))
let items = testDictionary[category] ?? []
ForEach(items, id: \.id) { item in
let itemBinding = Binding<Item>(
get: { item },
set: {
if
let items = testDictionary[category],
let i = items.firstIndex(where: { $0.id == item.id })
{
testDictionary[category]?[i] = $0
}
}
)
ListRow(item: itemBinding)
}
}
}.onAppear {
addDummyDateIntoDictonary()
}
}
func addDummyDateIntoDictonary() {
for category in categories {
testDictionary[category] = [Item(name: category + "1"), Item(name: category + "2")]
}
}
}

Is it possible to make SwiftUI ListMenu with different behaviors?

Is it possible to make a list menu with swiftUI where the List items have different behaviors(and added to the view with foreach)?
The list items would be models.
EG. the first would open a Profile view, the second would open another different view, the third would just simply log out.
And fill the List with a ForEach of the models.
I'm making a MoreMenu with SwiftUI list.
Code:
var body: some View {
NavigationView {
List(viewModel.menuItems) { item in
//Here should show another view or call a function depending on the item type
//EG. if its a profile Menu item, show profile
// if its a logout Menu item, logout
}
}
}
Inheritance makes it easy to share properties with similar items and then routing Views depending on the type
import SwiftUI
class MenuOption: ObservableObject, Identifiable{
var id: UUID = UUID()
#Published var title: String
init(title: String){
self.title = title
}
}
class MOToggle: MenuOption{
#Published var value: Bool
init(title: String, value: Bool = false){
self.value = value
super.init(title: title)
}
}
class MOOptions: MenuOption{
#Published var selection: Options
enum Options: String, CaseIterable{
case first
case second
case third
case unknown
}
init(title: String, selection: Options = .unknown){
self.selection = selection
super.init(title: title)
}
}
//You can have Views that use each type
struct MenuOptionToggleView: View {
#ObservedObject var option: MOToggle
var body: some View {
Toggle(isOn: $option.value, label: {
Text(option.title)
})
}
}
struct MenuOptionOptionsView: View {
#ObservedObject var option: MOOptions
var body: some View {
Picker(selection: $option.selection, label:
Text(option.title)
, content: {
ForEach(MOOptions.Options.allCases, id:\.rawValue, content: { item in
Text(item.rawValue).tag(item)
})
}).pickerStyle(MenuPickerStyle())
}
}
//And show them all in one View
struct MenuListView: View {
//When they share a type they can be put in an array together
#State var options: [MenuOption] = [MenuOption(title: "say hello"),MOToggle(title: "toggle the option"), MOOptions(title: "show the menu")]
var body: some View {
List(options, id: \.id){option in
//Then when you have the item determine what type it is
if option is MOToggle{
//When you pass it to its designated View
//You convert it to its specifc type
MenuOptionToggleView(option: option as! MOToggle)
} else if option is MOOptions{
//When you pass it to its designated View
//You convert it to its specifc type
MenuOptionOptionsView(option: option as! MOOptions)
} else{
//And since they are if the same type you can have a catch all
Button(action: {
print(option.title)
}, label: {
Text(option.title)
})
}
}
}
}
struct MenuListView_Previews: PreviewProvider {
static var previews: some View {
MenuListView()
}
}

Clear SwiftUI list in NavigationView does not properly go back to default

The simple navigation demo below demonstrate an example of my issue:
A SwiftUI list inside a NavigationView is filled up with data from the data model Model.
The list items can be selected and the NavigationView is linking another view on the right (demonstrated here with destination)
There is the possibility to clear the data from the model - the SwiftUI list gets empty
The model data can be filled with new data at a later point in time
import SwiftUI
// Data model
class Model: ObservableObject {
// Example data
#Published var example: [String] = ["Data 1", "Data 2", "Data 3"]
#Published var selected: String?
}
// View
struct ContentView: View {
#ObservedObject var data: Model = Model()
var body: some View {
VStack {
// button to empty data set
Button(action: {
data.selected = nil
data.example.removeAll()
}) {
Text("Empty Example data")
}
NavigationView {
// data list
List {
ForEach(data.example, id: \.self) { element in
// navigation to "destination"
NavigationLink(destination: destination(element: element), tag: element, selection: $data.selected) {
Text(element)
}
}
}
// default view when nothing is selected
Text("Nothing selected")
}
}
}
func destination(element: String) -> some View {
return Text("\(element) selected")
}
}
What happens when I click the "Empty Example data" button is that the list will be properly cleared up. However, the selection is somehow persistent and the NavigationView will not jump back to the default view when nothing is selected:
I would expect the view Text("Nothing selected") being loaded.
Do I overlook something important?
When you change the data.example
in the button, the left panel changes because the List has changed.
However the right side do not, because no change has occurred there, only the List has changed.
The ForEach does not re-paint the "destination" views.
You have an "ObservedObject" and its purpose is to keep track of the changes, so using that
you can achieve what you want, like this:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class Model: ObservableObject {
#Published var example: [String] = ["Data 1", "Data 2", "Data 3"]
#Published var selected: String?
}
struct ContentView: View {
#StateObject var data: Model = Model()
var body: some View {
VStack {
Button(action: {
data.selected = nil
data.example.removeAll()
}) {
Text("Empty Example data")
}
NavigationView {
List {
ForEach(data.example, id: \.self) { element in
NavigationLink(destination: DestinationView(),
tag: element,
selection: $data.selected) {
Text(element)
}
}
}
Text("Nothing selected")
}.environmentObject(data)
}
}
}
struct DestinationView: View {
#EnvironmentObject var data: Model
var body: some View {
Text("\(data.selected ?? "")")
}
}

SwiftUI SceneDelegate - contentView Missing argument for parameter 'index' in call

I am trying to create a list using ForEach and NavigationLink of an array of data.
I believe my code (see the end of the post) is correct but my build fails due to
"Missing argument for parameter 'index' in call" and takes me to SceneDelegate.swift a place I haven't had to venture before.
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
I can get the code to run if I amend to;
let contentView = ContentView(habits: HabitsList(), index: 1)
but then all my links hold the same data, which makes sense since I am naming the index position.
I have tried, index: self.index (which is what I am using in my NavigationLink) and get a different error message - Cannot convert value of type '(Any) -> Int' to expected argument type 'Int'
Below are snippets of my code for reference;
struct HabitItem: Identifiable, Codable {
let id = UUID()
let name: String
let description: String
let amount: Int
}
class HabitsList: ObservableObject {
#Published var items = [HabitItem]()
}
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var index: Int
var body: some View {
NavigationView {
List {
ForEach(habits.items) { item in
NavigationLink(destination: HabitDetail(habits: self.habits, index: self.index)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}
}
}
}
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var habits: HabitsList
var index: Int
var body: some View {
NavigationView {
Form {
Text(self.habits.items[index].name)
}
}
}
}
You probably don't need to pass the whole ObservedObject to the HabitDetail.
Passing just a HabitItem should be enough:
struct HabitDetail: View {
#Environment(\.presentationMode) var presentationMode
let item: HabitItem
var body: some View {
// remove `NavigationView` form the detail view
Form {
Text(item.name)
}
}
}
Then you can modify your ContentView:
struct ContentView: View {
#ObservedObject var habits = HabitsList()
#State private var showingAddHabit = false
var body: some View {
NavigationView {
List {
// for every item in habits create a `linkView`
ForEach(habits.items, id:\.id) { item in
self.linkView(item: item)
}
}
}
}
// extract to another function for clarity
func linkView(item: HabitItem) -> some View {
// pass just a `HabitItem` to the `HabitDetail`
NavigationLink(destination: HabitDetail(item: item)) {
HStack {
VStack(alignment: .leading) {
Text(item.name)
.font(.headline)
Text(item.description)
}
}
}
}
}

SwiftUI ObservedObject causes undesirable visible view updates

I am working on an app that applies a filter to an image. The filter has a number of parameters that the user can modify. I have created an ObservableObject that contain said parameters. Whenever one of the parameters changes, there is a visible update for views, even if the view displays the same value as before. This does not happen when I model the parameters as individual #State variables.
If this is to be expected (after all the observed object does change, so each view depending on it will update), is an ObservedObject the right tool for the job? On the other hand it seems to be very inconvenient to model the parameters as individual #State/#Binding variables, especially if a large number of parameters (e.g. 10+) need to be passed to multiple subviews!
Hence my question:
Am I using ObservedObject correctly here? Are the visible updates unintended, but acceptable, or is there a better solution to handle this in swiftUI?
Example using #ObservedObject:
import SwiftUI
class Parameters: ObservableObject {
#Published var pill: String = "red"
#Published var hand: String = "left"
}
struct ContentView: View {
#ObservedObject var parameters = Parameters()
var body: some View {
VStack {
// Using the other Picker causes a visual effect here...
Picker(selection: self.$parameters.pill, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}.pickerStyle(SegmentedPickerStyle())
// Using the other Picker causes a visual effect here...
Picker(selection: self.$parameters.hand, label: Text("Which hand?")) {
Text("left").tag("left")
Text("right").tag("right")
}.pickerStyle(SegmentedPickerStyle())
}
}
}
Example using #State variables:
import SwiftUI
struct ContentView: View {
#State var pill: String = "red"
#State var hand: String = "left"
var body: some View {
VStack {
Picker(selection: self.$pill, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}.pickerStyle(SegmentedPickerStyle())
Picker(selection: self.$hand, label: Text("Which hand?")) {
Text("left").tag("left")
Text("right").tag("right")
}.pickerStyle(SegmentedPickerStyle())
}
}
}
Warning: This answer is less than ideal. If the properties of parameters will be updated in another view (e.g. an extra picker), the picker view will not be updated.
The ContentView should not 'observe' parameters; a change in parameters will cause it to update its content (which is visible in case of the Pickers). To prevent the need for the observed property wrapper, we can provide explicit bindings for parameter's properties instead. It is OK for a subview of ContentView to use #Observed on parameters.
import SwiftUI
class Parameters: ObservableObject {
#Published var pill: String = "red"
#Published var hand: String = "left"
}
struct ContentView: View {
var parameters = Parameters()
var handBinding: Binding<String> {
Binding<String>(
get: { self.parameters.hand },
set: { self.parameters.hand = $0 }
)
}
var pillBinding: Binding<String> {
Binding<String>(
get: { self.parameters.pill },
set: { self.parameters.pill = $0 }
)
}
var body: some View {
VStack {
InfoDisplay(parameters: parameters)
Picker(selection: self.pillBinding, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}.pickerStyle(SegmentedPickerStyle())
Picker(selection: self.handBinding, label: Text("Which hand?")) {
Text("left" ).tag("left")
Text("right").tag("right")
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct InfoDisplay: View {
#ObservedObject var parameters: Parameters
var body: some View {
Text("I took the \(parameters.pill) pill from your \(parameters.hand) hand!")
}
}
Second attempt
ContentView should not observe parameters (this causes the undesired visible update). The properties of parameters should be ObservableObjects as well to make sure views can update when a specific property changes.
Since Strings are structs they cannot conform to ObservableObject; a small wrapper 'ObservableValue' is necessary.
MyPicker is a small wrapper around Picker to make the view update on changes. The default Picker accepts a binding and thus relies on a view up the hierarchy to perform updates.
This approach feels scalable:
There is a single source of truth (parameters in ContentView)
Views only update when necessary (no undesired visual effects)
Disadvantages:
Seems a lot of boilerplate code for something that feels so trivial it should be provided by the platform (I feel I am missing something)
If you add a second MyPicker for the same property, the updates are not instantaneous.
import SwiftUI
import Combine
class ObservableValue<Value: Hashable>: ObservableObject {
#Published var value: Value
init(initialValue: Value) {
value = initialValue
}
}
struct MyPicker<Value: Hashable, Label: View, Content : View>: View {
#ObservedObject var object: ObservableValue<Value>
let content: () -> Content
let label: Label
init(object: ObservableValue<Value>,
label: Label,
#ViewBuilder _ content: #escaping () -> Content) {
self.object = object
self.label = label
self.content = content
}
var body: some View {
Picker(selection: $object.value, label: label, content: content)
.pickerStyle(SegmentedPickerStyle())
}
}
class Parameters: ObservableObject {
var pill = ObservableValue(initialValue: "red" )
var hand = ObservableValue(initialValue: "left")
private var subscriber: Any?
init() {
subscriber = pill.$value
.combineLatest(hand.$value)
.sink { _ in
self.objectWillChange.send()
}
}
}
struct ContentView: View {
var parameters = Parameters()
var body: some View {
VStack {
InfoDisplay(parameters: parameters)
MyPicker(object: parameters.pill, label: Text("Which pill?")) {
Text("red").tag("red")
Text("blue").tag("blue")
}
MyPicker(object: parameters.hand, label: Text("Which hand?")) {
Text("left").tag("left")
Text("right").tag("right")
}
}
}
}
struct InfoDisplay: View {
#ObservedObject var parameters: Parameters
var body: some View {
Text("I took the \(parameters.pill.value) pill from your \(parameters.hand.value) hand!")
}
}