SwiftUI - Inherited Object in ListView (and binding to selection) - ios

I have a ListView that is a few levels deep in a NavigationView. The idea is when a user selects an item in the ListView, the item is stored and the UI pops back to the root NavigationView.
Specific Implementation (Working)
class SpecificListItem: ObservableObject, Codable, Hashable, Identifiable {
var id: String
var kind: String
var name: String
var mimeType: String
init(id: String, kind: String, name: String, mimeType: String) {
self.id = id
self.kind = kind
self.name = name
self.mimeType = mimeType
}
static func == (lhs: DriveListItem, rhs: DriveListItem) -> Bool {
return lhs.id == rhs.id &&
lhs.kind == rhs.kind &&
lhs.name == rhs.name &&
lhs.mimeType == rhs.mimeType
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
func processItem() {
// do backend stuff
}
}
...
struct SpecificListView: View {
#EnvironmentObject var selectedItem: SpecificListItem
// load excluded for brevity
#ObservedObject var item: MyResponse<SpecificListResponse>
var body: some View {
List(selection: $selectedItem, content: {
ForEach(item.data?.files ?? []) {
file in HStack {
Text(file.name)
}
.onTapGesture {
// pop to root view
}
}
})
}
}
This was good for a proof of concept. Now I need to add a layer of abstraction.
Attempt at Generic Implementation
The selected item in this view should be generic. I have the following protocol and class defined that the selected item should conform to:
protocol SelectableItem: ObservableObject, Identifiable, Hashable, Codable {
var id: String { get set }
func process()
}
class GenericListItem: SelectableItem {
var id: String = ""
var name: String = ""
init () { }
init(id: String) {
self.id = id
}
static func == (lhs: GenericListItem, rhs: GenericListItem) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
func process() {
// do backend stuff
}
}
class SpecificListItem: GenericListItem {
var type: String
init () { }
init(id: String) {
self.id = id
}
override func process() {
// do backend stuff
}
}
...
struct GenericListView: View {
#EnvironmentObject var selectedItem: GenericListItem
// load excluded for brevity
#ObservedObject var item: MyResponse<SpecificListItem>
var body: some View {
List(selection: $selectedItem, content: {
ForEach(item.data?.files ?? []) {
file in HStack {
Text(file.name)
}
.onTapGesture {
// pop to root view
}
}
})
}
}
The selected item can have it's own data elements, but I am at least expecting each item to have an id and a process() method that interacts with my backend.
However, when I try to bind the selectedItem in the ListView, I get the following error: Generic parameter 'SelectionValue' could not be inferred.
I am assuming this is because the types do not match. I have read there isn't really any dynamic or run-time type binding possible in Swift? Is this true?
I am a little more experienced in Java, and I know I could achieve this through generics and polymorphism.
SpecificListItem IS-A GenericListItem yet Swift doesn't seem to like the design. (And MyResponse has-a SpecificListItem).
How can I define a generic class that can be selected from many child views and bound at the root view, yet can be inherited to implement their own specific logic?
Edit
My abstraction layer explanation was unclear if not just wrong. So I tried to fix that.
Adding some more information surrounding the code structure and design. Maybe it will help give some more background and context on what I am trying to accomplish.
Here is a small representation of the object interactions:
The idea is I will have many detail views in the application. The backend will return various response types, all conforming to SelectableItem. In this case, the detail view item source is of type SpecificListItem.
And here is the interaction between the user, backend, and UI:
And the idea here being:
User navigates to Detail View
Backend call is made, returns a list of SpecificListItem
List View is populated with items of type SpecifcListItem.
When a user selects an item from Detail View, the application pops back to root, and the selectedItem is set in the root view which is of type GenericListItem.

Related

SwiftUI does not reliably propagate changes to child view

I am observing a peculiar behavior in my SwiftUI code and narrowed it down to the following minimal example.
Given this example storage holding an array of book model structs.
struct Book: Identifiable {
let id: UUID
var likes: Int
var unusedProperty: String = ""
}
extension Book: Equatable {
static func == (lhs: Book, rhs: Book) -> Bool {
return lhs.id == rhs.id
}
}
class MyStorage: ObservableObject {
#Published var books: [Book] = [
.init(id: .init(uuidString: "B2A44450-BC03-47E6-85BE-E89EA69AF5AD")!, likes: 0),
.init(id: .init(uuidString: "F5AB9D18-DF73-433E-BB48-1C757CB6F8A7")!, likes: 0)
]
func addLike(to book: Book) {
for i in books.indices where books[i].id == book.id {
books[i].likes += 1
}
}
}
And using it in this simple view hierarchy:
struct ReducedContentView: View {
#StateObject var storage: MyStorage = MyStorage()
var body: some View {
VStack(spacing: 8) {
ForEach(storage.books) { book in
HStack {
VStack(alignment: .leading) {
Text("Top-Level: \(book.likes)")
BookView(book: book)
}
Spacer()
Button("Add Like") {
storage.addLike(to: book)
}
}.padding(.horizontal)
}
}
}
}
struct BookView: View {
let book: Book
var body: some View {
Text("Nested: \(book.likes)")
.foregroundColor(.red)
}
}
Any changes to the likes property don't propagate to the BookView, only to the "top-level" Text.
Now, if I change one of the following, it works:
remove the unusedProperty (which is needed in production)
add && lhs.likes == rhs.likes to the Equatable conformance (which is not intended)
modify BookView to accept #Binding var book: Book instead of a let
The last option is something I could adopt in my production code - nevertheless, I would really like to understand what's happening here, so any hints would be greatly appreciated.
This is a result of your custom Equatable conformance:
extension Book: Equatable {
static func == (lhs: Book, rhs: Book) -> Bool {
return lhs.id == rhs.id
}
}
If you remove this, it'll work as expected.
In your current code, you're saying that if two Books have the same ID, they are equal. My suspicion is that you don't actually mean that they are truly equal -- you just mean that they are the same book.
Your second option ("add && lhs.likes == rhs.likes to the Equatable conformance (which is not intended)") essentially just uses the synthesized Equatable conformance that the system generates, since unusedProperty isn't used -- so, if you were to use the second option, you may as well just remove the custom == function altogether.
I think the decision to make here is whether you really want to tell the system an untruth (that two Books, no matter what their other properties, are equal if they share the same id) or if you should let the system do it's own work telling if the items are equal or not.

SwiftUI is it ok to use hashValue in Identifiable

For structs, Swift auto synthesizes the hashValue for us. So I am tempting to just use it to conform to the identifiable.
I understand that hashValue is many to one, but ID must be one to one. However, hash collision is very rare and I have faith in the math that it's most likely never going to happen to my app before I die.
I am wondering is there any other problems besides collision?
This is my code:
public protocol HashIdentifiable: Hashable & Identifiable {}
public extension HashIdentifiable {
var id: Int {
return hashValue
}
}
using hashValue for id is a bad idea !
for example you have 2 structs
struct HashID: HashIdentifiable {
var number: Int
}
struct NormalID: Identifiable {
var id = UUID()
var number: Int
}
when number is changed:
HashID's id will be changed as well makes SwiftUI thinks that this is completely new item and old one is gone
NormalID's id stays the same, so SwiftUI knows that item only modified its property
It's very important to let SwiftUI knows what's going on, because it will affect animations, performance, ... That's why using hashValue for id makes your code looks bad and you should stay away from it.
I am wondering is there any other problems besides collision?
Yes, many.
E.g., with this…
struct ContentView: View {
#State private var toggleData = (1...5).map(ToggleDatum.init)
var body: some View {
List($toggleData) { $datum in
Toggle(datum.display, isOn: $datum.value)
}
}
}
Collisions cause complete visual chaos.
struct ToggleDatum: Identifiable & Hashable {
var value: Bool = false
var id: Int { hashValue }
var display: String { "\(id)" }
init(id: Int) {
// Disregard for now.
}
}
No collisions, with unstable identifiers, breaks your contract of "identity" and kills animation. Performance will suffer too, but nobody will care about that when it's so ugly even before they notice any slowness or battery drain.
struct ToggleDatum: Identifiable & Hashable {
var value: Bool = false
var id: Int { hashValue }
let display: String
init(id: Int) {
self.display = "\(id)"
}
}
However, while it is not acceptable to use the hash value as an identifier, it is fine to do the opposite: use the identifier for hashing, as long as you know the IDs to be unique for the usage set.
/// An `Identifiable` instance that uses its `id` for hashability.
public protocol HashableViaID: Hashable, Identifiable { }
// MARK: - Hashable
public extension HashableViaID {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct ToggleDatum: HashableViaID {
var value: Bool = false
let id: Int
var display: String { "\(id)" }
init(id: Int) {
self.id = id
}
}
This protocol works perfectly for Equatable classes, as classes already have a default ID ready for use.
extension Identifiable where Self: AnyObject {
public var id: ObjectIdentifier {
return ObjectIdentifier(self)
}
}
Not that I at all recommend using a reference type for this example, but it would look like this:
public extension Equatable where Self: AnyObject {
static func == (class0: Self, class1: Self) -> Bool {
class0 === class1
}
}
class ToggleDatum: HashableViaID {

Perform action when user change Picker value

I have a piece of sample code that shows picker. It is a simplified version of project that I'm working on. I have a view model that can be updated externally (via bluetooth - in example it's simulated) and by user using Picker. I would like to perform an action (for example an update) when user changes the value. I used onChange event on binding that is set on Picker and it works but the problem is that it also is called when value is changed externally which I don't want. Does anyone knows how to make it work as I expect?
enum Type {
case Type1, Type2
}
class ViewModel: ObservableObject {
#Published var type = Type.Type1
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.type = Type.Type2
}
}
}
struct ContentView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
Row(type: $viewModel.type) {
self.update()
}
}
}
func update() {
// some update logic that should be called only when user changed value
print("update")
}
}
struct Row: View {
#Binding var type: Type
var action: () -> Void
var body: some View {
Picker("Type", selection: $type) {
Text("Type1").tag(Type.Type1)
Text("Type2").tag(Type.Type2)
}
.onChange(of: type, perform: { newType in
print("changed: \(newType)")
action()
})
}
}
EDIT:
I found a solution but I'm not sure if it's good one. I had to use custom binding like this:
struct Row: View {
#Binding var type: Type
var action: () -> Void
var body: some View {
let binding = Binding(
get: { self.type },
set: { self.type = $0
action()
}
)
return Picker("Type", selection: binding) {
Text("Type1").tag(Type.Type1)
Text("Type2").tag(Type.Type2)
}
}
}

SwifUI ForEach List keeps modified values when reloading a #Published array inside ObservableObject

I have a SwiftUI List populated using a ForEach loop binded to a #Published array inside an ObservableObject
Elements in the list can be modified with an on/off flag. When the list is cleared out all items seems removed from the UI but when the data is refetched and loaded inside the #Published array the UI still shows the state of te previously modified elements. As you can see in the following video:
The code to reproduce it:
struct ListView: View {
#ObservedObject private var viewModel = ListViewModel()
var body: some View {
VStack {
List {
ForEach(viewModel.list, id: \.self) { element in
ElementRow(element: element)
}
}
HStack {
Button(action: { viewModel.fetch() }) { Text("Refresh") }
Button(action: { viewModel.remove() }) { Text("Remove") }
}
}.onAppear {
viewModel.fetch()
}
}
}
struct ElementRow: View {
#ObservedObject var element: ElementViewModel
var body: some View {
HStack {
Text(element.id)
Spacer()
Toggle("", isOn: $element.on )
}
}
}
class ListViewModel : ObservableObject {
#Published var list: [ElementViewModel] = []
func fetch() {
list.append(contentsOf: [ElementViewModel(id: "0", on: false), ElementViewModel(id: "1", on: false)])
}
func remove() {
list.removeAll()
}
}
class ElementViewModel : ObservableObject, Hashable {
var id: String = ""
#Published var on: Bool = false
init(id: String, on: Bool) {
self.id = id
self.on = on
}
static func == (lhs: ElementViewModel, rhs: ElementViewModel) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
What needs to be changed to make that all the elements in the list remains off when it's refreshed?
Your model objects is interpreted as equal, so views are not updated (List caches rows similarly to UITableView).
Find below a fix. Tested with Xcode 12.4 / iOS 14.4
class ElementViewModel : ObservableObject, Hashable {
// ... other code
static func == (lhs: ElementViewModel, rhs: ElementViewModel) -> Bool {
lhs.id == rhs.id && lhs.on == rhs.on // << here !!
}
// ... other code
}

How can I make my model conform to codable and save it locally on every change?

I have a parent class, that has children (Child Class), that have Puppets (Puppet Class).
The data is displayed in a list of parents that navigates to a list of children, that navigates to a list of puppets.
Within the views, the user is able to add parents, children and puppets or remove them.
I want to store the data locally so that every change is save. I think storing the Parent class is enough, because of parents.children.puppets.
Thus I need to conform to Codable anyhow and decode and encode my data anyhow.
The AppState should load local data or [] instead of the current dummy data. On every parent (or child or puppet) change, I want to store the parents locally in the most efficient way.
Same for receiving the data.
class Parent: ObservableObject, Hashable {
static func == (lhs: Parent, rhs: Parent) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
#Published var children: [Child]
init(name: String, children: [Child]? = nil) {
self.name = name
self.children = children ?? []
}
func remove(child: Child) {
self.children.remove(child)
}
func add(child: Child) {
self.children.append(child)
}
}
class Child: ObservableObject, Identifiable, Hashable {
static func == (lhs: Child, rhs: Child) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
let id = UUID()
let name: String
#Published var puppets: [Puppet]
init(name: String, puppets:[Puppet]? = nil) {
self.name = name
self.puppets = puppets ?? []
}
func remove(puppet: Puppet) {
self.puppets.remove(puppet)
}
func add(puppet: Puppet) {
self.puppets.append(puppet)
}
}
struct Puppet: Identifiable, Hashable {
let id = UUID()
let name: String
}
class AppState: ObservableObject {
#Published var parents: [Parent]
init() {
self.parents = [
Parent(name: "Foo", children: [Child(name: "bar", puppets: [Puppet(name: "Tom")])]),
Parent(name: "FooBar", children: [Child(name: "foo", puppets: nil)])
]
}
}
extension Array where Element: Identifiable {
mutating func remove(_ object: Element) {
if let index = self.firstIndex(where: { $0.id == object.id}) {
self.remove(at: index)
}
}
}
I like very much pawello2222's detailed response. If you continue to develop on iOS, you will eventually need to learn CoreData.
That said, if you don't have time to do that for your current project, personally, I would recommend using RealmSwift and creating Realm Parent, Child and Puppet objects. It would take you an hour to learn how to install and use RealmSwift.
Here's a start:
import RealmSwift
class Puppet: Object {
#objc dynamic var id = UUID()
#objc dynamic var name: String = ""
}
But if you want to save a small number of objects you could use UserDefaults - none of your objects are using unsupported types, so just declare them as codable and let the compiler do the conversion automatically.
Conforming class to codable protocol in swift4
It is quite straightforward. 🍀
If you're not bound to any database or format, I'd recommend using CoreData (as suggested in the comments).
From Apple documentation:
Use Core Data to save your application’s permanent data for offline
use, to cache temporary data, and to add undo functionality to your
app on a single device.
This way your model doesn't have to conform to Codable.
You can find more information in these links:
Core Data for beginners
Getting started with Core Data Tutorial
Getting started with Core Data

Resources