SwiftUI List only using first item of given array

Viewed 1769

I have a simple view below to display all of the contacts for the user:

struct AllContactsView: View {
    static let withState: some View = AllContactsView().environmentObject(AllContactsProvider())
    @EnvironmentObject var contactsProvider: AllContactsProvider
    let title: UserListType = .selectInvitees

    var body: some View {
        List(self.contactsProvider.invitees) { invite in
            self.row(for: invite)
        }
        .navigationBarTitle(Text(self.title.rawValue))
        .onAppear(perform: self.contactsProvider.fetch)
    }

    func row(for invite: Invitee) -> some View {
        // everything below is only printed out once!
        print(self.contactsProvider.invitees.prettyPrinted) // shows correct array of contacts
        print(type(of: self.contactsProvider.invitees)) // this is indeed an array
        print(invite) // prints out the first item in the array (which is expected on first pass)
        return UserRow(invitee: invite)
    }
}

I am manipulating the array of CNContacts I get like this to an array of Invitees, which is what I am attempting to display in my list:

self?.invitees = contacts.asSimpleContacts.map({ $0.asUser.asInvitee })

Using the supporting functions and extensions below:

// Contact Value simplified so we can pass it around as a value type.
public struct SimpleContact: Hashable, Codable {
    let firstName: String
    let lastName: String
    let emails: [String]
    let phoneNumbers: [PhoneNumber]

    var fullName: String { "\(self.firstName) \(self.lastName)" }

    var asUser: User {
        User(
            id: Constants.unsavedID,
            username: self.fullName,
            picURL: "al",
            email: self.emails.first ?? "",
            phone: self.phoneNumbers.first ?? "",
            created: Date().timeIntervalSince1970
        )
    }
}


extension CNContact {
    /// Returns the `SimpleContact` representation of `self`
    var asSimpleContact: SimpleContact {
        SimpleContact(
            firstName: self.givenName,
            lastName: self.familyName,
            emails: self.emailAddresses.map({ String($0.value) }),
            phoneNumbers: self.phoneNumbers.map({ Authentication.sanitize(phoneNo: $0.value.stringValue) })
        )
    }
}

extension Array where Element == CNContact {
    /// Returns the `SimpleContact` mapping of `self`
    var asSimpleContacts: [SimpleContact] { self.map({ $0.asSimpleContact }) }
}

public struct User: Hashable, Codable, Identifiable {
    public let id: String
    let username: String
    let picURL: String
    let email: String
    let phone: String
    let created: Double

    var asInvitee: Invitee { Invitee(user: self, isGoing: false) }
}

The contacts are populated into self.contactsProvider.invitees as expected following self.contactsProvider.fetch(). However, SwiftUI is displaying self.contactsProvider.invitees.count instances of self.contactsProvider.invitees.first, rather than each contact. I have compared my approach below to other examples online and can't seem to find where I went wrong. I have determined that the issue lies somewhere with the contacts manipulation - when I supply a mocked array of invitees, everything works as expected, despite things compiling and running as expected without the mocks, and printing and debugging not revealing anything.

Any help would be appreciated.

2 Answers

I just ran into this issue, to answer a bit more clearly:

Each row in a SwiftUI list should be generated with a unique ID.

If you are using the List() function to create the view, make sure you are specifying an id

struct MenuView: View {
var body: some View {
    VStack {
        Section(header: MenuSectionHeader()) {
            //item.sku is the unique identifier for the row
            List(Menu.allItems, id:\.sku) { item in 
                MenuItemRow(item)
            }
        }
    }
}

For anyone who runs across something similar, the problem was that the ID I was instantiating the objects with was not unique. Curious that this would be the expected behavior if that error is made, but thats what it was.

Related