SwiftUI 4.0 - Passing a Binding via .navigationDestination(for: , destination: ) - swift

How do I pass a Binding via the new .navigationDestination(for: , destination: )?
import SwiftUI
enum TestEnum: String, Hashable, CaseIterable {
case first, second, third
}
struct ContentView: View {
#State private var test: TestEnum = .first
var body: some View {
NavigationStack {
VStack {
NavigationLink(value: test, label: {
Text(test.rawValue)
})
}
// This does not work, as it won't allow me to use $caze
.navigationDestination(for: TestEnum.self, destination: { caze in
SecondView(test: $caze)
})
}
}
}
struct SecondView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var test: TestEnum
var body: some View {
ForEach(TestEnum.allCases, id: \.self) { caze in
Button(action: {
test = caze
presentationMode.wrappedValue.dismiss()
}, label: {
Text(caze.rawValue)
})
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
In SwiftUI 3.0 I'd simply use:
NavigationLink(destination: SecondView(test: $test), label: {
Text(test.rawValue)
})
Is this still the correct approach, as we cannot pass a Binding yet?
Not really interested in complex workarounds like using an EnvironmentObject and passing an index, as the SwiftUI 3.0 approach works fine.
However, if there is a proper way of passing a Binding via .navigationDestination(for: , destination: ) I'll happily use it.

According to this doc:
https://developer.apple.com/documentation/charts/chart/navigationdestination(for:destination:)/
[WRONG: Where destination get passed from NavigationLink, but in the doc they use the NavigationLink("Mint", value: Color.mint) version of the NavigationLink. I don't know if this make any difference.
You are using the NavigationLink(value:label:)]
EDIT: I insist, after further investigation, that you are using the ViewModifier wrong.
Read the doc that I pointed above. The viewModifier signature is:
func navigationDestination<D, C>(
for data: D.Type,
destination: #escaping (D) -> C
) -> some View where D : Hashable, C : View
Note D is Hashable, not State
So, when you pass $caze you are not passing the #State var above, but the NavigationLink(value: test... that is not a #State wrapper. Is the generic D ... a single value, so $caze is not a Binding to the State.
You can pass normally $test, from that is a Binding to the State test. But the parameter inside the closure .navigationDestination( value in ... is not.

Related

Broken Binding and ObservedObject in SwiftUI

I have a structure where at the end of AddTaskView_Preview: ..... there is { task in } in AddTaskView, exactly like this:
struct AddTaskView_Previews: PreviewProvider {
static var previews: some View {
AddTaskView{task in }, formVM: ToDoFormViewModel())
.environmentObject(DataStore())
}
}
This type does not allow the use of #Binding or #ObservedObject - formVM: ToDoFormViewModel() in my application, because the two values Binding and ObservedObject work for me in other views only with parentheses of type () and not { }!
What about that please? I normally have this code in my views:
ContentView(viewRouter: ViewRouter(), formVM: ToDoFormViewModel()) , but it doesn't fit inside the square brackets { } with { task in } in the above example.
How should I consume my ObservedObject in formVM: ToDoFormViewModel() with those { } ?

Binding Array of Structs with Strings in an Array of Structs

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

Simple SwiftUI CRUD using structs rather than classes?

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.

SwiftUI: How to Update NavigationView Detail Screens?

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

Error: Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'

I am getting the above error and couldn't figure out how to solve it. I have an array of objects that contain a boolean value, and need to show a toggle for each of these boolean.
Below is the code.
class Item: Identifiable {
var id: String
var label: String
var isOn: Bool
}
class Service: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
var items: [Item] {
didSet {
didChange.send(())
}
}
}
struct MyView: View {
#ObservedObject var service: Service
var body: some View {
List {
ForEach(service.items, id: \.self) { (item: Binding<Item>) in
Section(header: Text(item.label)) { // Error: Initializer 'init(_:)' requires that 'Binding<String>' conform to 'StringProtocol'
Toggle(isOn: item.isOn) {
Text("isOn")
}
}
}
}
.listStyle(GroupedListStyle())
}
}
Use the #Published property wrapper in your Service class, rather than didChange, and iterate over the indices of service.items like so:
struct Item: Identifiable {
var id: String
var label: String
var isOn: Bool {
didSet {
// Added to show that state is being modified
print("\(label) just toggled")
}
}
}
class Service: ObservableObject {
#Published var items: [Item]
init() {
self.items = [
Item(id: "0", label: "Zero", isOn: false),
Item(id: "1", label: "One", isOn: true),
Item(id: "2", label: "Two", isOn: false)
]
}
}
struct MyView: View {
#ObservedObject var service: Service
var body: some View {
List {
ForEach(service.items.indices, id: \.self) { index in
Section(header: Text(self.service.items[index].label)) {
Toggle(isOn: self.$service.items[index].isOn) {
Text("isOn")
}
}
}
}
.listStyle(GroupedListStyle())
}
}
Update: Why use indices?
In this example, we need to get two things from each Item in the model:
The String value of the label property, to use in a Text view.
A Binding<Bool> from the isOn property, to use in a Toggle view.
(See this answer where I explain Binding.)
We could get the label value by iterating over the items directly:
ForEach(service.items) { (item: Item) in
Section(header: Text(item.label)) {
...
}
But the Item struct does not contain a binding. If you tried to reference Toggle(isOn: item.$isOn), you'd get an error: "Value of type 'Item' has no member '$isOn'."
Instead, the Binding is provided at the top level by the #ObservedObject property wrapper, meaning the $ has to come before service. But if we're starting from service, we'll need an index (and we cannot declare intermediate variables inside the ForEach struct, so we'll have to compute it inline):
ForEach(service.items) { (item: Item) in
Section(header: Text(item.label)) {
Toggle(isOn: self.$service.items[self.service.items.firstIndex(of: item)!].isOn) {
// This computes the index ^--------------------------------------^
Text("isOn")
}
}
}
Oh, and that comparison to find the index would mean Item has to conform to Equatable. And, most importantly, because we are looping over all items in the ForEach, and then again in the .firstIndex(of:), we have transformed our code from O(n) complexity to O(n^2), meaning it will run much more slowly when we have a large number of Items in the array.
So we just use the indices. Just for good measure,
ForEach(service.items.indices, id: \.self) { index in
is equivalent to
ForEach(0..<service.items.count, id: \.self) { index in