instantaphex
instantaphex

Reputation: 1001

SwiftUI ForEach not iterating when array changes. List is empty and ForEach does run

I'm trying to use MultiPeer Connectivity framework with swift ui and am having issues with using ForEach in my view. I have a singleton that I'm using to track connected users in an array:

class MPCManager: NSObject {
    static let instance = MPCManager()
    var devices: [Device] = []
...

And my device class:

class Device: NSObject {
    let peerID: MCPeerID
    var session: MCSession?
    var name: String
    var state = MCSessionState.notConnected
    var lastMessageReceived: Message?
...
}

When the MultiPeer connectivity frame finds new peers the MPCManager is appending new devices to the array. I have confirmed this in the debugger. The problem comes when I try to display the devices in a list. Here is the code that I'm using:

struct ContentView : View {
    var devices: [Device] = MPCManager.instance.devices
    var body: some View {
        List {
            ForEach(self.devices.identified(by: \.name)) { device in
                Text(device.name)
            }
        }
    }
}

When the app starts, the list is displayed but it is empty. When I put a breakpoint in the view code inside the ForEach execution never stops. When I change the array to a hardcoded list of values, it displays just fine. I have also tried referencing the array from the static instance directly in my view like this:

ForEach(self.devices.identified(by: \.name)) { device in
    Text(device.name)
}

Still nothing. I'm very new to swift so there may be something easy that I'm missing but I just don't see it. Any ideas?

Upvotes: 3

Views: 6703

Answers (1)

graycampbell
graycampbell

Reputation: 7800

There are a couple issues here as far as I can tell.

First, I would suggest you try this with your MPCManager:

import SwiftUI
import Combine

class MPCManager: NSObject, BindableObject {
    var didChange = PassthroughSubject<Void, Never>()

    var devices: [Device] = [] {
        didSet {
            self.didChange.send(())
        }
    }
}

Then, in your ContentView, do this:

struct ContentView : View {
    @ObjectBinding var manager: MPCManager = MPCManager()

    var body: some View {
        List {
            ForEach(self.manager.devices.identified(by: \.name)) { device in
                Text(device.name)
            }
        }
    }
}

The main difficulty with answering your question is that I can't run your code. Your question would be more useful to others (and much easier to answer) if you could distill your code down to something that people who might know the answer could just copy and paste into Xcode.

Update

As of Xcode Beta 4, identified(by:) has been replaced by specific initializers for List and ForEach, and as of Xcode Beta 5, BindableObject has been replaced by ObservableObject and @ObjectBinding has been replaced by @ObservedObject.

import SwiftUI
import Combine

class MPCManager: NSObject, ObservableObject {
    var objectWillChange = PassthroughSubject<Void, Never>()

    var devices: [Device] = [] {
        willSet {
            self.objectWillChange.send()
        }
    }
}
struct ContentView : View {
    @ObservedObject var manager: MPCManager = MPCManager()

    var body: some View {
        List {
            ForEach(self.manager.devices, id: \.name) { device in
                Text(device.name)
            }
        }
    }
}

Upvotes: 6

Related Questions