I've been struggling to get Picker to return an object from an array. I was of the impression that all I needed to do was include the object as a tag and the selection would end up as that object. Apparently not. Any help would be greatly appreciated.
My actual app uses information extracted from CoreData.
The expected result in this test app is to show the selection to the right of the item where the choice was made.
import SwiftUI
class Choice: Identifiable, Hashable
{
let id: UUID
var name = ""
init()
{
self.id = UUID()
}
init(_ name: String)
{
self.id = UUID()
self.name = name
}
static func == (lhs: Choice, rhs: Choice) -> Bool
{
return lhs.name < rhs.name
}
func hash(into hasher: inout Hasher)
{
hasher.combine(name)
}
}
class Stuff: Identifiable
{
let id: UUID
var title: String
var choice: Choice?
init(_ title: String)
{
self.id = UUID()
self.title = title
}
}
struct ContentView: View
{
let items = [Stuff("ABC"), Stuff("XYZ")]
let choices = [Choice("ONE"),Choice("Two"), Choice("Three)")]
@State var choice: Choice?
var body: some View
{
VStack
{
ForEach(items)
{ item in
HStack
{
Text(item.title)
Picker("Choice", selection: $choice)
{
ForEach(choices)
{
c in
Text(c.name).tag(c)
}
}
}
}
Spacer()
ForEach(items)
{
item in
HStack
{
Text(item.title)
Spacer()
if let c = item.choice
{
Text(c.name)
} else
{
Text("No choice")
}
}
}
}
}
}
CodePudding user response:
If you use a class
it has to conform to ObservableObject
class Stuff: Identifiable, ObservableObject
The property has to be wrapped
@Published var choice: Choice?
and it has to be wrapped in an observing wrapper
@StateObject var stuff: Stuff = Stuff()
or
@ObservedObject var stuff: Stuff
Then you add the optional tag to picker
.tag(Optional(c)
BUT your current setup where you are using a class
in a class
will be an uphill battle. There will be many other issues you will encounter where views don't refresh.
It is a much easier fix to change your classes to struct
then they become value type.
This like has a bit more info where this is explained a little differently.
SwiftUI picker .onChanged only firing on 2 selection changes
CodePudding user response:
Selection and tag should match by type, currently they are not, so fix is
// give initial value (anyway first is by default)
@State var choice: Choice = Choice("ONE") // << here !!
also correction needed in
static func == (lhs: Choice, rhs: Choice) -> Bool
{
return lhs.name == rhs.name // << here !!
}
Tested with Xcode 13.4 / iOS 15.5
CodePudding user response:
The below suggestions definitely helped but as pointed out, there were several errors/assumptions that made the original approach sketchy. I have incorporated the suggestions and changed strategy by creating a separate view to present each line/(list item) and to separate the pickers.
import SwiftUI
struct Choice: Identifiable, Hashable
{
let id: UUID
var name = ""
init()
{
self.id = UUID()
}
init(_ name: String)
{
self.id = UUID()
self.name = name
}
static func == (lhs: Choice, rhs: Choice) -> Bool
{
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher)
{
hasher.combine(name)
}
}
struct Stuff: Identifiable
{
let id: UUID
var title: String
var choice: Choice?
init(_ title: String)
{
self.id = UUID()
self.title = title
}
}
struct ContentView: View
{
@State var items = [Stuff("ABC"), Stuff("XYZ")]
@State var idx = 0
var body: some View
{
VStack
{
ForEach($items)
{ $item in
ItemLine(item: $item)
}
Spacer()
ForEach(items)
{
item in
HStack
{
Text(item.title)
Spacer()
if let c = item.choice
{
Text(c.name)
} else
{
Text("No choice")
}
}
}.id(idx)
Button("Refresh")
{
idx = idx 1
}
}.padding()
}
struct ItemLine: View
{
@Binding var item: Stuff
let choices = [Choice("One"),Choice("Two"), Choice("Three")]
var body: some View
{
HStack
{
Text(item.title)
Picker("Choice", selection: $item.choice)
{
ForEach(choices)
{
c in
Text(c.name).tag(Optional(c))
}
}
}
}
}
}