Simple SwiftUI CRUD using structs rather than classes? - swift

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.

Related

I'm struggling to get a Core Data list to sort from a button press

Right now I'm trying to get a Core Data list in SwiftUI to be sortable. I've followed along with a Ray Wenderlich tutorial, and did well with it. I then tried to incorporate what I learned there into my own project. Currently, everything builds and runs with my project. The list to select sorting from appears, and its list of options appear when that button is clicked. However, clicking a new sort does nothing.
I am under the impression from using the tutorial, that I'd need to swap the Core Data object in lieu of the ListSortAccount struct/object. As well, I recognize that this would be easier to do using the ToolBarItemGroup, however there are parts of my projects design that need this sort to be away from the title bar.
I'll put my minimum code below. Any feedback is appreciated. Thank you.
This creates the array that the user can pick from after the button press:
struct ListSorterAccount: Hashable, Identifiable {
var id: Int
var name: String
var descriptors: [SortDescriptor<AccountCoreData>]
}
extension ListSorterAccount {
static let sorts: [ListSorterAccount] = [
ListSorterAccount(
id: 0,
name: "Account A-Z",
descriptors: [
SortDescriptor(\AccountCoreData.accountName, order: .forward),
SortDescriptor(\AccountCoreData.accountDateAdded, order: .forward)]),
ListSorterAccount(
id: 1,
name: "Account Z-A",
descriptors: [
SortDescriptor(\AccountCoreData.accountName, order: .reverse),
SortDescriptor(\AccountCoreData.accountDateAdded, order: .reverse)])]
static var `default`: ListSorterAccount {
sorts[0]
}
}
This is the view for the button:
struct ListSorterButton: View {
#Binding var selectedSort: ListSorterAccount
let sorts: [ListSorterAccount]
var body: some View {
Menu {
Picker("Sort By", selection: $selectedSort) {
ForEach(sorts, id:\.self) { sort in
Text(sort.name)
}
}
} label: {
Label("", systemImage: "line.horizontal.3.decrease.circle")
}
}
}
This is the view that holds that button (irrelevant code removed for clarity):
struct SearchBarView: View {
#FetchRequest(entity: AccountCoreData.entity(), sortDescriptors: []) var accounts: FetchedResults<AccountCoreData>
#State private var selectedSort = ListSorterAccount.default
var body: some View {
HStack {
ListSorterButton(
selectedSort: $selectedSort,
sorts: ListSorterAccount.sorts)
.onChange(of: selectedSort) { _ in
accounts.sortDescriptors = selectedSort.descriptors
}
}
}
}
And this is the View that contains my list I'm trying to sort (irrelevant code removed for clarity):
struct AccountList: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(sortDescriptors: ListSorterAccount.default.descriptors, animation: .default) var accountFetch: FetchedResults<AccountCoreData>
var body: some View {
VStack {
List {
ForEach(accountFetch.filter {
self.searchClass.searchText.isEmpty
? true :
$0.wrappedAccountName.localizedCaseInsensitiveContains(self.searchClass.searchText) ||
$0.wrappedAccountCity.localizedCaseInsensitiveContains(self.searchClass.searchText)
}){ account in
NavigationLink(value: Route.mainAccountView(account), label: {
AccountListCell(accountCoreData: account)
})
}
}
}
}
}

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

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

Why does picker binding not update when using SwiftUI?

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.

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