Generic SwiftUI Component cannot infer Hashable for CustomStringConvertible - swiftui

I want to create a generic type, that accepts anything that conforms to CustomStringConvertible and then iterates over those items.
Here is an example that distils down that problem:
public struct Test<ItemType: CustomStringConvertible, Hashable>: View {
var items: [ItemType]
public var body: some View {
ForEach(items, id: \.self) { item in
Text("test")
}
}
}
let items: [String] = ["a", "b"]
let viewController = UIHostingController(rootView: Test(items: items))
So I get an error
Generic struct 'ForEach' requires that 'ItemType' conform to 'Hashable'
and
Generic parameter 'Hashable' could not be inferred
So what am I doing wrong?

You have syntax issue:
public struct Test<ItemType: CustomStringConvertible & Hashable>: View { // <<: here!
var items: [ItemType]
public var body: some View {
ForEach(items, id: \.self) { item in
Text("test")
}
}
}

Related

SwiftUI: Cannot find type in scope

I'm having trouble coming up a good way to ask this question, so I'll instead show a simple example. I have a model, an #ObservableObject, that contains a struct:
class MyModel: ObservableObject {
#Published var allData: [TheData] = []
struct TheData: Hashable {
let thePosition: Int
let theChar: Character
}
func initState() {
let allChars = Array("abd")
for (index, element) in allChars.enumerated() {
allData.append(TheData(thePosition: index, theChar: element))
}
}
}
In my view, I'm attempting to reach the model from two different structs (as a result of an annoying The compiler is unable to type-check this expression in reasonable time..), but I get an error:
struct ContentView: View {
#StateObject var theModel = MyModel()
var body: some View {
VStack {
Image(systemName: "globe")
Text("Hello, world!")
HStack {
ForEach(theModel.allData, id: \.self) { theElement in
letterAcross(myLetter: theElement)
}
}
}
.onAppear {
theModel.initState()
}
.environmentObject(theModel)
}
}
struct letterAcross: View {
#EnvironmentObject var theModel: MyModel
var myLetter: TheData // <----- the ERROR is here
var body: some View {
HStack {
Text(String(myLetter.theChar))
}
}
}
The error is Cannot find type TheData in scope. It appears I am somehow messing up the #StateObject and #EnvironmentObject. What is the correct way to do this?
It's a nested type, so it's MyModel.TheData:
var myLetter: MyModel.TheData

How can I move List Items without having to toggle EditMode

I'm currently building a ToDo List App in SwiftUI. One feature that I'd really like to implement is the ability to sort your List manually, so I've integrated the functionality using a .onMove modifier on my ForEach loop populating my List, but I still had to toggle EditMode manually, so I set the EditMode of the list to be .active as follows:
import SwiftUI
struct ContentView: View {
#State private var items = ["1", "2", "3"]
#State var editMode: EditMode = .active
var body: some View {
List {
ForEach(items, id: \.self) { item in
Text("Item \(item)")
}
.onMove(perform: { _, _ in })
}
.environment(\.editMode, $editMode)
}
}
But I'm not happy with this Implementation, as I still have to use the grip from EditMode, and it also breaks SwipeActions as well as Button functionality.
So how can I move List Items without using EditMode?
Based on Asperi's answer on this question I implemented drag and drop Gestures to fix that problem as follows:
struct ContentView: View {
#State var items = [Item(id: 1), Item(id: 2), Item(id: 3), Item(id: 4)]
#State private var dragging: Item?
var body: some View{
List{
ForEach(items){ item in
Text("Item \(item.id)")
.onDrag {
self.dragging = item
return NSItemProvider(object: NSString())
}
.onDrop(of: [UTType.text], delegate: DragDelegate(current: $dragging))
}
.onMove(perform: {_, _ in })
}
}
}
Using a DropDelegate implementation:
struct DragDelegate<Item: Equatable>: DropDelegate {
#Binding var current: Item?
func dropUpdated(info: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info: DropInfo) -> Bool {
current = nil
return true
}
}
Note: the Items now have to conform to Identifiable & Equatable so the minimal Implementation is:
struct Item: Identifiable, Equatable{
let id: Int
}
and you also need to import:
import UniformTypeIdentifiers
in order to use drag and drop functionality

SwiftUI onDelete List with Toggle and NavigationLink

I refer to two questions that I already asked and have been answered very well by Asperi: SwiftUI ForEach with .indices() does not update after onDelete,
SwiftUI onDelete List with Toggle
Now I tried to modify the closure in ForEach with a NavigationLink and suddenly the App crashes again with
Thread 1: Fatal error: Index out of range
when I try to swipe-delete.
Code:
class Model: ObservableObject {
#Published var name: String
#Published var items: [Item]
init(name: String, items: [Item]) {
self.name = name
self.items = items
}
}
struct Item: Identifiable {
var id = UUID()
var isOn: Bool
}
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
List {
ForEach(model.items) {item in
NavigationLink(destination: DetailView(item: self.makeBinding(id: item.id))) {
Toggle(isOn: self.makeBinding(id: item.id).isOn)
{Text("Toggle-Text")}
}
}.onDelete(perform: delete)
}
}
}
func delete(at offsets: IndexSet) {
self.model.items.remove(atOffsets: offsets)
}
func makeBinding(id: UUID) -> Binding<Item> {
guard let index = self.model.items.firstIndex(where: {$0.id == id}) else {
fatalError("This person does not exist")
}
return Binding(get: {self.model.items[index]}, set: {self.model.items[index] = $0})
}
}
struct DetailView: View {
#Binding var item: Item
var body: some View {
Toggle(isOn: $item.isOn) {
Text("Toggle-Text")
}
}
}
It works without NavigationLink OR without the Toggle. So it seems for me that I only can use the makeBinding-Function once in this closure.
Thanks for help
Your code was crashing for me with and even without Navigation Link. Sometimes only if I deleted the last object in the Array. It looks like it was still trying to access an index out of the array. The difference to your example you linked above, is that they didn't used EnvironmentObject to access the array. The stored the array directly in the #State.
I came up with a little different approach, by declaring Item as ObservedObject and then simply pass it to the subview where you can use their values as Binding, without any function.
I changed Item to..
class Item: ObservableObject {
var id = UUID()
var isOn: Bool
init(id: UUID, isOn: Bool)
{
self.id = id
self.isOn = isOn
}
}
Change the ContentView to this..
struct ContentView: View {
#EnvironmentObject var model: Model
var body: some View {
NavigationView {
List {
ForEach(model.items, id:\.id) {item in
NavigationLink(destination: DetailView(item: item)) {
Toggler(item: item)
}
}.onDelete(perform: delete)
}
}
}
I outsourced the Toggle to a different view, where we pass the ObservedObject to, same for the DetailView.
struct Toggler: View {
#ObservedObject var item : Item
var body : some View
{
Toggle(isOn: $item.isOn)
{Text("Toggle-Text")}
}
}
struct DetailView: View {
#ObservedObject var item: Item
var body: some View {
Toggle(isOn: $item.isOn) {
Text("Toggle-Text")
}
}
}
They both take an Item as ObservedObject and use it as Binding for the Toggle.

How to edit an item in a list using NavigationLink?

I am looking for some guidance with SwiftUI please.
I have a view showing a simple list with each row displaying a "name" string. You can add items to the array/list by clicking on the trailing navigation bar button. This works fine. I would now like to use NavigationLink to present a new "DetailView" in which I can edit the row's "name" string. I'm struggling with how to use a binding in the detailview to update the name.
I've found plenty of tutorials online on how to present data in the new view, but nothing on how to edit the data.
Thanks in advance.
ContentView:
struct ListItem: Identifiable {
let id = UUID()
let name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct ContentView: View {
#ObservedObject var myList = MyListClass()
var body: some View {
NavigationView {
List {
ForEach(myList.items) { item in
NavigationLink(destination: DetailView(item: item)) {
Text(item.name)
}
}
}
.navigationBarItems(trailing:
Button(action: {
let item = ListItem(name: "Test")
self.myList.items.append(item)
}) {
Image(systemName: "plus")
}
)
}
}
}
DetailView
struct DetailView: View {
var item: ListItem
var body: some View {
TextField("", text: item.name)
}
}
The main idea that you pass in DetailsView not item, which is copied, because it is a value, but binding to the corresponding item in your view model.
Here is a demo with your code snapshot modified to fulfil the requested behavior:
struct ListItem: Identifiable, Equatable {
var id = UUID()
var name: String
}
class MyListClass: ObservableObject {
#Published var items = [ListItem]()
}
struct ContentView: View {
#ObservedObject var myList = MyListClass()
var body: some View {
NavigationView {
List {
ForEach(myList.items) { item in
// Pass binding to item into DetailsView
NavigationLink(destination: DetailView(item: self.$myList.items[self.myList.items.firstIndex(of: item)!])) {
Text(item.name)
}
}
}
.navigationBarItems(trailing:
Button(action: {
let item = ListItem(name: "Test")
self.myList.items.append(item)
}) {
Image(systemName: "plus")
}
)
}
}
}
struct DetailView: View {
#Binding var item: ListItem
var body: some View {
TextField("", text: self.$item.name)
}
}

How make work a Picker with an ObservedObject in SwiftUI?

I'm trying to get a list of Datacenters from a Rest API and show them in a Picker, so the user can choose one. When I do it with a static list it works fine. However, retrieving the Datacenters dinamically seems not to work fine.
I'm using Xcode 11 (GM)
This is the Datacenter Object
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
This is the ObservedObject (it has the property datacenters that is an array of Datacenter objects)
#ObservedObject var datacenters_controller : DatacentersController
#State private var selectedDatacenter = 0
This was my first attempt:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(0 ..< datacenters_controller.datacenters.count) {
Text(self.datacenters_controller.datacenters[$0].location)
}
}
Swift complained with the following error:
ForEach<Range<Int>, Int, Text> count (4) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
Then I switched to:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
It "works" (no error), but the result is not the expected because although I can select a datacenter, it is not "stored", not shown in the Picker as selected.
Actual result
Expected result
Any idea? What I'm doing wrong?
Here's a working example. The key is that selectedDatacenter needs to be the same type as Datacenter.id (in this case, String).
struct ContentView: View {
#ObservedObject var datacenters_controller = DatacentersController()
#State private var selectedDatacenter = ""
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
// Just here for demonstration
Text("selectedDatacenter (id): \(selectedDatacenter.isEmpty ? "Nothing yet" : selectedDatacenter)")
}
}
}
}
Here's the supporting code
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
class DatacentersController: ObservableObject {
#Published var datacenters: [Datacenter] = []
init() {
datacenters = [
Datacenter(id: "ABQ", location: "Albuquerque"),
Datacenter(id: "BOS", location: "Boston"),
Datacenter(id: "COS", location: "Colorado Springs")
]
}
}
I think you are missing tag on your picker:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
Apple docs on tag:
Sets the tag of the view, used for selecting from a list of View
options.
In your second attempt, you need to use the tag modifier (as described by LuLuGaGa). You also need to change the type of selectedDatacenter to match. For example:
struct ContentView: View {
init(_ controller: DatacentersController) {
self.datacenters_controller = controller
self._selectedDatacenter = State(initialValue: controller.datacenters[0].id)
}
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
}
}
}
#ObservedObject private var datacenters_controller: DatacentersController
#State private var selectedDatacenter: String
}