Home > other >  Using SwiftUI Picker with Classes
Using SwiftUI Picker with Classes

Time:06-11

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