How To Make a Custom Picker with Multi-Selection in SwiftUI

Kyra

My name is Kyra and I’m a computer programmer that decided to stay at home with my two beautiful daughters: Ada and Zoey. I created this website to share with you anything I come across in my day to day life that I think you may enjoy.

You may also like...

18 Responses

  1. Patrick says:

    (Trying to send my comment in several posts, as in one big, I got Internal Server Error – also tried to send you an email, but not sure you got it earlier this week. Feel free to delete this one if you got the email, no worries :-))

    New question, about the code itself, and cannot figure out how to fix it. Everything works fine now with the Add View, where I can get the list, and display it and save it to Core Data.

    • Patrick says:

      Ok, so even breaking down the comment to multiple part, still get an Internal Server Error, not sure what text I’m using is causing the issue though. Did you get any emails from me with title: “Additional question about MultiSelectPickerView”?

    • Patrick says:

      Figured it out. now onto the next issue 🙂 – not related to that one, it’s about refreshing views when sheet is dismissed. Minor thing though.

      • Kyra says:

        Glad you got it working. I did see your email. That said, I had a busy week and hadn’t gotten a chance to look at it or these comments until now. Hope you had a good weekend.

        • Patrick says:

          No worries 🙂

          I had a good wee-end. Coded a little too, always have ideas to improve what I’ve done already, it’s never ending lol.

          Since you are around: for iOS, instead of a List item you tap on to show the MultiSelectPickerView, would it be possible, with minor adjustments, to display the full selectable list on the same view, with no tap needed on “Select Items:”? I’m not sure if I will like the look, but it’s one tap less to get there and that might be a nice user experience.

          If I’m not clear, let me know 🙂

  2. Patrick says:

    Forgot to mention how I declare hostServices and services in my previous message. Here they are:
    @FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \ServiceEntity.servicePort, ascending: true)],
    animation: .default)
    private var services: FetchedResults

    @State var hostServices: ServiceEntity

  3. Patrick says:

    Hi Kyra,

    Thank you for the tutorial, it’s really great. I tried and works great if I’m using static/pre-defined arrays, but in my case, I would like to use a mutli-picker with an array/list coming from Core Data. I’ve tried several tweaks to make it work, but it’s always complaining about conversion from/to String to Entity and things like that.

    Would you have some examples with using Core Data as source to populate the picker?

    Thank you 🙂

    • Kyra says:

      I’m really glad this is helpful. I’m currently doing this with CoreData in my current project but it’s too large to show right now. I don’t have time right now to make an example project but can show you the pertinent code in case it helps.
      ——
      In my view that calls the multipicker I use the Entity “Category” and have
      *Declared:
      @State private var selectedCategories = [Category]()
      @State private var allCategories = [Category]()
      @State private var showingSelectedCategories = false
      *Use in the view itself:
      // Categories
      HStack {
      Text(“Select Categories:”).foregroundColor(.primary)
      Button(action: {
      showingSelectedCategories.toggle()
      }) {
      HStack {
      Spacer()
      Image(systemName: “\($selectedCategories.count).circle”)
      .foregroundColor(.secondary)
      .font(.title2)
      Image(systemName: “chevron.right”)
      .foregroundColor(.secondary)
      .font(.caption)
      }
      }
      .popover(isPresented: $showingSelectedCategories) {
      CategoriesMultiSelectPickerView(allCategories: allCategories, selectedCategories: $selectedCategories)
      .frame(minWidth: 300, alignment: .center)
      }
      }
      *In the View’s Load
      allCategories = user.categories?.allObjects as! [Category] // Shows ALL the Categories based on my user entity
      selectedCategories = anotherEntity!.categories?.allObjects as! [Category] // shows the subset of categories based on another entity

      Then on save I add/remove them from the “anotherEntity” based on what’s in the selectedCategories.

      Not sure how this will format but figure this way you get an answer right away. Let me know if it helps and whether you solve your issue or not. If not let me know your errors and I can try to help. That said I don’t have a lot of time this week.

      • Patrick says:

        Thank you very much for the swift reply. Will look into it.

        Right now, I have 2 Entities: HostEntity and ServiceEntity. ServiceEntity contains 2 attributes: servicePort and serviceName, these are the 2 information I want to display in there. It might be a little it more complicate, as I need to retrieve 2 information out of the Entity, not just one.

        My current code just uses a standard Picker, as follow:
        Picker(selection: $hostServices) {
        ForEach(services, id: \.self) { (service: ServiceEntity) in
        Text(“\(service.serviceName!) – \(String(service.servicePort))”).tag(service as ServiceEntity?)
        }
        } label: {
        Text(“Select a port*”)
        }

        With your code, I want to be able to display the same information, but able to select multiple “Services” at once.

        Eventually, I will need the code in 2 different places with 2 different slightly different behaviors. The first place is when adding a host. By definition, it’s a new entry, so there is no selected service yet. And the second place will be when editing an existing host, where there is 1 or more selected services already.

        I may be able to make it work, but when you have time, will appreciate your take on it, since I’m pretty new to swiftui, I don’t have all the best practices and usually end up with 20 lines of code when 2 should be enough 😀

        • Kyra says:

          I’ve just been working on it for less than a year. Unless you give me a specific error etc I can’t specifically help. For me I have connections from User to Category (all categories for the user) and then another relationship between an in between element (from the users categories the other entity stores the selected ones). So in your case services would be my “allCategories” and your hostServices would need to become an array to store the selected ones (and saved somehow). I saved the connections with the relationships. Good luck

          • Patrick says:

            I thought as much you had setup a relationship between these entities. I wanted to at first, but eventually didn’t see the need for it, as I just want to store the service names and service ports in one Entity, so I don’t have to store the same information again and again, and then the code just does the rest for me. It might not be the best practice, but it works. I might need to revisit that. Right now, on the Edit view, I do this when the form appears:
            services.forEach({ ServiceEntity in
            if ServiceEntity.servicePort == host.hostPort {
            hostServices = ServiceEntity
            hostPortOldValue = String(host.hostPort)
            }
            })

            So yeah, not the best in class for sure 🙂

            I’ll keep trying 🙂

          • Patrick says:

            Hi Kyra,

            Based on your last response, I’ve added the relationships. Now I have 3 Entities:
            HostEntity => contains information about the host, like hostName, hostStatus, hostPort, etc..
            ServiceEntity => contains information about the services: serviceName and servicePort
            HostServiceEntity => only 1 attribute in there: hostServicePort and will save the hosts multiple ports.

            As for the Relationships, I have these:
            From HostEntity to HostServiceEntity: myPorts (one-to-many, since one host can have multiple ports)
            From HostServiceEntity to HostEntity: myHost (one-to-one, since the port in that Entity can only have one host at a time).

            For the code, your struct now looks like that:
            // The struct that the custom picker (button) opens which
            // is minorly adapted from:
            // https://gist.github.com/dippnerd/5841898c2cf945994ba85871446329fa
            struct MultiSelectPickerView: View {
            @FetchRequest(
            sortDescriptors: [NSSortDescriptor(keyPath: \ServiceEntity.servicePort, ascending: true)],
            animation: .default)
            private var services: FetchedResults

            // The list of items we want to show
            @State var allItems: [String]

            // Binding to the selected items we want to track
            @Binding var selectedItems: [String]

            var body: some View {
            Form {
            List {
            ForEach(allItems, id: \.self) { item in
            Button(action: {
            withAnimation {
            if self.selectedItems.contains(item) {
            // Previous comment: you may need to adapt this piece
            self.selectedItems.removeAll(where: { $0 == item })
            } else {
            self.selectedItems.append(item)
            }
            }
            }) {
            HStack {
            Image(systemName: “checkmark”)
            .opacity(self.selectedItems.contains(item) ? 1.0 : 0.0)
            Text(item)
            }
            }
            .foregroundColor(.primary)
            }
            }
            }
            .onAppear {
            services.forEach { service in
            allItems.append(“\(service.serviceName!) – \(String(service.servicePort))”)
            }
            }
            }
            }

            And in my Save function, I have this code (didn’t put everything, just the part that is causing me an issue):
            let newHost = HostEntity(context: viewContext)
            hostPortValues.forEach { portValue in
            newHost.addToMyPorts([Int32(portValue as! String)!])
            }

            The .addToMyPorts has 2 options:
            – addToMyPorts(_ value: HostServiceEntity)
            – addToMyPorts(_ values: NSSet)

            I’ve have not set the Codegen to anything else in Core Data, it’s still set to “Class Definition”.

            Now when I run the app, I’m greeted with that error:
            Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
            on that line:
            newHost.addToMyPorts([Int32(portValue as! String)!])

            I’ve searched in many places, and either I’m doing something obviously wrong or I do not understand how to use the addToMyPorts feature.

            If you have any idea what I’m doing wrong, I’ll appreciate the input 🙂

            Thank you

          • Kyra says:

            From what I can see, in your save, you create a new host: let newHost = HostEntity(context: viewContext)
            then go through the values hostPortValues.forEach { portValue in
            before adding that host like: newHost.addToMyPorts([Int32(portValue as! String)!])

            You said there’s two addToMyPorts. One HostServiceEntity and one NSet but here you’re adding an Int32. Don’t have enough code to demo but are there already ports created with that portValue or are these new? Assuming you have a list of ports already created (allPorts) and if it’s not there create a new one. So maybe something like (psuedocode not looking up)
            // Is there a matching one
            matchingPort = allPorts.filter( $0.portNumber == portValue).first // you only need one
            if matchingPort != nil {
            // exists already so add it
            newHost.addToMyPorts(matchingPort)
            } else {
            // doesn’t exist so create it
            let newPort = TableOfPorts(context: viewContext)
            // set value
            newPort.portValue = portValue
            // add the relationship between them
            newHost.addToMyPorts(newPort)

            }

          • Patrick says:

            Hi Kyra,

            I think I need to take a step back, I seem to have confused you with what I’m trying to do.

            For starters, I’m about a month old into SwiftUI, but have been coding on vb.net for years now. I’m familiar with coding, but SwiftUI is something different, so I’m learning as I go 🙂

            Now that is established, let’s share a little bit more about what I’ve done and what I’m trying to do, shall we? 😉

            I’m creating an iOS app that will let you create hosts to check if their port is opened or closed, which may be due to either the port being really closed, or if you are behind a firewall, then it’s blocked, but in any cases, gives you information based on the current network you are on.

            As of before finding you code, I was able to make that all work and everything works fine. The only thing I was not happy with is that if I want to check for port 443 and 80 and 21, all at the same time, I need to create 3 host entries in HostEntity. While it works, it’s not nice looking to have a list with multiple entries with the same hostnames on the screen, needs more scrolling, and more activities to add the same host again and again, selecting a different port each time.

            The other Entity is ServiceEntity, which is just an Entity that stores the port name and the port description, to make it nice on screen. This Entity is filled in by the user and can contain only one service (which is a combination of a port number and a port name) or as many as the user wants to have. There is no relationship between these 2 Entities. As mentioned in another reply, I’m using that piece of code to add all the values from that Entity to the variable that is used by the MultiSelectPickerView:
            .onAppear {
            services.forEach { service in
            allItems.append(“\(service.serviceName!) – \(String(service.servicePort))”)
            }

            I probably could do that differently, but I’ve not found a better way yet that works.

            Now that I’ve found your code, what I would like to do is what I just mentioned before: on the Add Host view, I would like the user to be able to pick multiple services at once and save that in Core Data, so I’m able to read these values and then run the code to check if the associated ports are opened.

            I’ve thought of using a Transformable, but there are too many pieces involved that I do not understand yet. like the piece of code to actually do the transformation for instance, so using a third Entity where I would store these could be the answer, using the appropriate relationship.

            It can be stored as String if needed, I can always convert to Int32 before checking if the port is opened, I just find is better to store the data as it’s intended to be used if I can.

            I hope this clarifies what I’m trying to do, if not, please let me know 🙂

            And feel free to tell me to research more on my own, no worries, I don’t like being a bother to people, that’s the last thing I want.

            Appreciate your feedback and responsiveness so far 🙂

            Thank you!

          • Kyra says:

            Hi Patrick. Thanks for the clarification. I’ve only been working in SwiftUI for about a year so I’m no expert either 😊 Just mentioned the fix before because it looked like your addToPorts expected an Entity and not an int or string. It’s a bit hard to wrap my head around your problem through textual comments. Im a bit tired tonight but maybe you could make a stackoverflow question as then you’d be able to include code snippets, images, and maybe ER diagrams to better explain it. If you send me the link I can take a better look at it in the morning.

            Either way I wish you luck. I’m bashing my head against a different SwiftUI issue right now too. 😊

          • Patrick says:

            Hi Kyra,

            Thank you for your time on this. I ended up learning more about Transformable and managed to get what I wanted out of it. Is it perfect? No!. Does it work? Yes 🙂 One step at a time 🙂

          • Kyra says:

            That sounds perfect 👍 definitely one step at a time. Glad you got it working.

Leave a Reply

Your email address will not be published.

CommentLuv badge

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: