I've got an app that gets a list of vehicles from a REST backend server. It then uses that list to build a list of vehicles that can be tapped to show the details about one of them:
#State private var selectedVehicle: Vehicle?
#Binding var vehicles: [Vehicle]
List {
NavigationView {
ForEach( vehicles ) { vehicle in
NavigationLink( destination: VehicleDetailScreen( vehicle: vehicle ),
tag: vehicle,
selection: self.$selectedVehicle ) {
Text( vehicle.name )
}
}
}
}
struct VehicleDetailScreen: View {
var vehicle: Vehicle
var body: some View {
// Lots of rendering code omitted
}
}
So far, so good. This works nicely. The problem arises when we fetch updated information from the server. Updating the bound vehicles property works great for updating the list. But the detail screen is still showing data that's no longer relevant.
My first thought was just to pop the detail view off of the NavigationView. Unfortunately, SwiftUI doesn't provide any reliable way that I can find to do this in a two-column view on the iPad.
My next thought was that we needed to pass the vehicle in to VehicleDetailScreen as a #Binding too so that we can update it. But this is tough to do as well because we would need a reference to that binding so that we can cram updated values into it. The only way I can think of to do that would be to rework our network and model object code entirely so that it works like CoreData, keeping objects in memory and updating them with new values from the server, rather than generating new objects. This would be a good deal of effort, and obviously isn't something I'm keen to do if there's another option.
So I'm kind of stuck on this. Any thoughts/ideas/suggestions are very welcome!
Perhaps the concept of #Binding is somewhat confusing. From a #State var (parent view), to #Binding var (child view).
A struct Hashable to facilitate and reorder the elements of the array [Vehicle].
Something like this:
struct Vehicle: Hashable {
var name:String
//var otherItem: Any
}
struct ContentView: View {
#State var vehicle: Vehicle //the struct of your REST
#State var vehicles: [Vehicle] // the array of your REST
var body: some View {
List {
NavigationView {
ForEach(vehicles, id:\.self) { item in // loop the array to get every single item conform to the struct
NavigationLink( destination: VehicleDetailScreen(vehicle: self.$vehicle)) { // here to pass the binding
Text("\(self.vehicle.name)")
}
}
}
}
}
}
//detail view
struct VehicleDetailScreen: View {
#Binding var vehicle: Vehicle // here the binding
var body: some View {
Text("\(vehicle.name)")
}
}
If you want your detail views to update when data changes, you will have to make use of bindings.
As far as architecture goes, I would suggest to create so called Stores that hold data which can be used in multiple views. This, in combination with some static provider for Stores, makes it that you can easily access and modify data anywhere, and let your views update automatically.
When using UIKit, you would manually refresh data by calling reloadTable for instance. In SwiftUI this is not done. You could hypothetically manually trigger the view to update, but I would advice against this, as it is not the way SwiftUI was intended.
I've modified your code to show an example of this:
class StoreProvider {
static let carStore = CarStore()
}
class CarStore: ObservableObject {
#Published var vehicles: [Vehicle] = [Vehicle(id: "car01", name: "Porsche", year: 2016), Vehicle(id: "car02", name: "Lamborghini", year: 2002)]
}
struct Vehicle: Identifiable, Hashable {
let id: String
var name: String
var year: Int
}
struct CarOverview: View {
#ObservedObject var store = StoreProvider.carStore
#State var selectedVehicle: Vehicle?
var body: some View {
NavigationView {
List {
ForEach(store.vehicles.indices) { vehicleIndex in
NavigationLink(destination: VehicleDetailScreen(vehicle: self.$store.vehicles[vehicleIndex])) {
Text(self.store.vehicles[vehicleIndex].name)
}.onTapGesture {
self.selectedVehicle = self.store.vehicles[vehicleIndex]
}
}
}
}
}
}
struct VehicleDetailScreen: View {
#Binding var vehicle: Vehicle
func updateValues() {
vehicle.year = Int.random(in: 1990..<2020)
}
var body: some View {
VStack {
Text(vehicle.name)
Text("Year: ") + Text(vehicle.year.description)
}.onTapGesture(perform: updateValues)
}
}
Related
Consider the following code:
class Model: ObservableObject {
#Published var property1: Int = 0
#Published var property2: Int = 0
}
struct ObjectBindingTest: View {
#StateObject private var model = Model()
var body: some View {
print("——— top")
return VStack(spacing: 30) {
SomeSimpleComponent(property: $model.property1)
SomeSimpleComponent2(property: $model.property2)
}
.padding(50)
}
}
struct SomeSimpleComponent: View {
#Binding var property: Int
var body: some View {
print("component 1")
return HStack {
Text("\(property)")
Button("Increment", action: { property += 1 })
}
}
}
struct SomeSimpleComponent2: View {
#Binding var property: Int
var body: some View {
print("component 2")
return HStack {
Text("\(property)")
Button("Increment", action: { property += 1 })
}
}
}
Whenever you press on one of the buttons, you will see in console:
——— top
component 1
component 2
Meaning that all body blocks get evaluated.
I would expect that only the corresponding row gets updated: if I press the first button and therefore update property1, the second row shouldn't have to re-evaluate its body because it's only dependent on property2.
This is causing big performance issues in my app. I have a page to edit an object with many properties. I use an ObservableObject with many #Published properties. Every time a property changes (for instance while typing in a field), all the controls in the page get updated, which causes lags and freezes. The performance issues mostly happen in iOS 14; I'm not sure whether they're not happening in iOS 15 or if it's just that the device has more computing power.
How to prevent unnecessary updates coming from ObservableObject, and only update the views that actually watch the updated property?
The behavior you are seeing is expected
By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its #Published properties changes.
In other words all the wrappers trigger a single publisher so SwiftUI does not know which was updated.
https://developer.apple.com/documentation/combine/observableobject
You can get a partial performance upgrade by changing from a class to a struct and using #State
struct Model {
var property1: Int = 0
var property2: Int = 0
}
#State private var model = Model()
In certain cases such a ForEach you will get improvements by adding a few protocols.
struct Model: Equatable, Hashable, Identifiable {
let id: UUID = .init()
//More code
Check out Demystify SwiftUI from #wwdc21
https://developer.apple.com/wwdc21/10022 it will provide a greater insight into the why.
I'm new to Swift so I hope this isn't something really silly. I'm trying to build an array of Structs, and one of the parameters is another Array with another Struct in it. I'm not sure if there is a better way, but I thought I was making really good progress right up till I tried to edit the embedded Struct. In it's simplified form it looks like this ...
struct Group: Identifiable, Codable {
var id = UUID()
var name: String
var number: Int
var spaces: Bool
var businesses: [Business]
}
struct Business: Identifiable, Codable {
var id = UUID()
var name: String
var address: String
var space: Int
var enabled: Bool
}
These are used in a class with an Observable var that stored in User Defaults
class GroupSettings: ObservableObject {
#Published var groups = [Group]() {
didSet {
UserDefaults.standard.set(try? PropertyListEncoder().encode(groups), forKey: "groups")
}
}
init() {
if let configData = UserDefaults.standard.value(forKey: "groups") as? Data {
if let userDefaultConfig = try?
PropertyListDecoder().decode(Array<Group>.self, from: configData){
groups = userDefaultConfig
}
}
}
}
Its passed in to my initial view and then I'm wanting to make an "Edit Detail" screen. When it gets to the edit detail screen, I can display the Business information in a Text display but I can't get it to working a TextField, it complains about can't convert a to a Binding, but the name from the initial Struct works fine, similar issues with the Int ...
I pass a Group from the first view which has the array of Groups in to the detail screen with the #Binding property ...
#Binding var group: Group
var body: some View {
TextField("", text: $group.name) <---- WORKS
List {
ForEach(self.group.businesses){ business in
if business.enabled {
Text(business.name) <---- WORKS
TextField("", business.address) <---- FAILS
TextField("", value: business.space, formatter: NumberFormatter()) <---- FAILS
} else {
Text("\(business.name) is disabled"
}
}
}
}
Hopefully I've explained my self well enough, and someone can point out the error of my ways. I did try embedding the 2nd Struct inside the first but that didn't help.
Thanks in advance!
You could use indices inside the ForEach and then still use $group and accessing the index of the businesses via the index like that...
List {
ForEach(group.businesses.indices) { index in
TextField("", text: $group.businesses[index].address)
}
}
An alternative solution may be to use zip (or enumerated) to have both businesses and its indices:
struct TestView: View {
#Binding var group: Group
var body: some View {
TextField("", text: $group.name)
List {
let items = Array(zip(group.businesses.indices, group.businesses))
ForEach(items, id: \.1.id) { index, business in
if business.enabled {
Text(business.name)
TextField("", text: $group.businesses[index].address)
} else {
Text("\(business.name) is disabled")
}
}
}
}
}
I have a complex data structure which uses value types (structs and enums), and I'm facing major issues getting basic CRUD to work. Specifically:
How best to "Re-bind" a value in a ForEach for editing by a child view
How to remove/delete a value
Rebinding
If I have an array of items as #State or #Binding, why isn't there a simple way to bind each element to a view? For example:
import SwiftUI
struct Item: Identifiable {
var id = UUID()
var name: String
}
struct ContentView: View {
#State var items: [Item]
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $item) // 🛑 Cannot find '$item' in scope
}
}
}
}
Workaround
I've been able to work around this by introducing a helper function to find the correct index for the item within a loop:
struct ContentView: View {
#State var items: [Item]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $items[index(of: item)].name)
}
}
}
}
However, that feels clunky and possibly dangerous.
Deletion
A far bigger issue: how are you supposed to correctly delete an element? This sounds like such a basic question, but consider the following:
struct ContentView: View {
#State var items: [Item]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
TextField("name", text: $items[index(of: item)].name)
Button( action: {
items.remove(at: index(of: item))
}) {
Text("Delete")
}
}
}
}
}
Clicking the "Delete" button on the first few items works as expected, but trying to Delete the last item results in Fatal error: Index out of range...
My particular use case doesn't map to a List, so I can't use the deletion helper there.
Reference types
I know that reference types make much of this easier, especially if they can conform to #ObservableObject. However, I have a massive, nested, pre-existing value type which is not easily converted to classes.
Any help would be most appreciated!
Update: Suggested solutions
Deleting List Elements from SwiftUI's list: The accepted answer proposes a complex custom binding wrapper. Swift is powerful, so it's possible to solve many problems with elaborate workarounds, but I don't feel like an elaborate workaround should be necessary to have a list of editable items.
Mark Views as "deleted" using State or a private variable, then conditionally hide them, to avoid out-of-bounds errors. This can work, but feels like a hack, and something that should be handled by the framework.
I confirm that more appropriate approach for CRUD is to use ObservableObject class based view model. And an answer provided by #NewDev in comments is a good demo for that approach.
However if you already have a massive, nested, pre-existing value type which is not easily converted to classes., it can be solved by #State/#Binding, but you should think about what/when/and how update each view and in each order - that is the origin of all such index out of bounds on delete issues (and some more).
Here is demo of approach of how to break this update dependency to avoid crash and still use value types.
Tested based on your code with Xcode 11.4 / iOS 13.4 (SwiftUI 1.0+)
struct ContentView: View {
#State var items: [Item] = [Item(name: "Name1"), Item(name: "Name2"), Item(name: "Name3")]
func index(of item: Item) -> Int {
items.firstIndex { $0.id == item.id } ?? -1
}
var body: some View {
VStack {
ForEach(items, id: \.id) { item in
// separate dependent views as much as possible to make them as
// smaller/lighter as possible
ItemRowView(items: self.$items, index: self.index(of: item))
}
}
}
}
struct ItemRowView: View {
#Binding var items: [Item]
let index: Int
#State private var destroyed = false // internal state to validate self
var body: some View {
// proxy binding to have possibility for validation
let binding = Binding(
get: { self.destroyed ? "" : self.items[self.index].name },
set: { self.items[self.index].name = $0 }
)
return HStack {
if !destroyed { // safety check against extra update
TextField("name", text: binding)
Button( action: {
self.destroyed = true
self.$items.wrappedValue.remove(at: self.index)
}) {
Text("Delete")
}
}
}
}
}
Yes, it is not easy solution, but sometimes there are situations we need it.
I'm trying to remove the logic from the view, while keeping the benefits of SwiftUI. Idea 1 works but it makes use of an extra variable than I would want to. Idea 2 gives error: Property wrappers are not yet supported on local properties. The view should return "bar". What is the best way of making this work? Many thanks.
import Combine
import Foundation
import SwiftUI
// Model
enum Model: String, RawRepresentable {
case foo = "foo"
case bar = "bar"
}
// State
var data1: String = Model.foo.rawValue
class State: ObservableObject {
#Published internal var data2: String = data1
}
// Logic
func logic() {
// Idea 1: OK
//data1 = Model.bar.rawValue
//print(State().data2)
// Idea 2: Error Property wrappers are not yet supported on local properties
#EnvironmentObject private var state: State
state.data2 = Model.bar.rawValue
print(state.data2)
}
// View
struct bar: View {
#EnvironmentObject private var state: State
internal var body: some View {
logic()
return Text(verbatim: self.state.data2)
}
}
If you want a function to have access to a view's state, pass the state:
func logic(state: State) {
state.data2 = Model.bar.rawValue
print(state.data2)
}
But what you've done here is an infinite loop. Modifying a view's state causes the view to be re-rendered. So every time the view is rendered, it modifies its state and forces it to be rendered again. That will never resolve. What you may mean here is to change the state when the view first appears, in which case you'd call logic this way:
struct Bar: View {
#EnvironmentObject private var state: State
internal var body: some View {
Text(verbatim: state.data2)
.onAppear{ logic(state: self.state) }
}
}
I have just begun learning Swift (and even newer at Swift UI!) so apologies if this is a newbie error.
I am trying to write a very simple programme where a user chooses someone's name from a picker and then sees text below that displays a greeting for that person.
But, the bound var chosenPerson does not update when a new value is picked using the picker. This means that instead of showing a greeting like "Hello Harry", "Hello no-one" is shown even when I've picked a person.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson: String? = nil
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach ((0..<people.count), id: \.self) { person in
Text(self.people[person])
}
}
}
Section{
Text("Hello \(chosenPerson ?? "no-one")")
}
}
}
}
}
(I have included one or two pieces of the original formatting in case this is making a difference)
I've had a look at this question, it seemed like it might be a similar problem but adding .tag(person) to Text(self.people[person])did not solve my issue.
How can I get the greeting to show the picked person's name?
Bind to the index, not to the string. Using the picker, you are not doing anything that would ever change the string! What changes when a picker changes is the selected index.
struct ContentView: View {
var people = ["Harry", "Hermione", "Ron"]
#State var chosenPerson = 0
var body: some View {
NavigationView {
Form {
Section {
Picker("Choose your favourite", selection: $chosenPerson) {
ForEach(0..<people.count) { person in
Text(self.people[person])
}
}
}
Section {
Text("Hello \(people[chosenPerson])")
}
}
}
}
}
The accepted answer is right if you are using simple arrays, but It was not working for me because I was using an array of custom model structs with and id defined as string, and in this situation the selection must be of the same type as this id.
Example:
struct CustomModel: Codable, Identifiable, Hashable{
var id: String // <- ID of type string
var name: String
var imageUrl: String
And then, when you are going to use the picker:
struct UsingView: View {
#State private var chosenCustomModel: String = "" //<- String as ID
#State private var models: [CustomModel] = []
var body: some View {
VStack{
Picker("Picker", selection: $chosenCustomModel){
ForEach(models){ model in
Text(model.name)
.foregroundColor(.blue)
}
}
}
Hope it helps somebody.