For simplicity I have modified my project and created a reproducible code.
Consider I have the following structure for Item
data:
struct Item: Identifiable {
let id = UUID()
let isChecked: AnyPublisher<Bool, Never>
let name: String
}
And I hae the following ItemCell
which should be updated when the isChecked
value publishes any new value.
struct ItemCell: View {
var item: Item
@State var imgState: Bool = false
var cancellables = Set<AnyCancellable>()
init(_ item: Item) {
self.item = item
item.isChecked
.print("State for item \(item.name): ") // --> Check this print
.assign(to: \.imgState, on: self)
.store(in: &cancellables)
}
var body: some View {
HStack {
HStack {
Image(systemName: imgState ? "checkmark.circle.fill" : "checkmark.circle")
.foregroundStyle(.blue)
Text(item.name)
}
}
}
}
I have checked in the debug console that the isChecked
always emiting values but the view is not updated.
Anyways, to test the code I have created this dummy code, you can just copy and paste to test:
Full code:
import SwiftUI
import Combine
struct Item: Identifiable {
let id = UUID()
let isChecked: AnyPublisher<Bool, Never>
let name: String
}
struct ItemCell: View {
var item: Item
@State var imgState: Bool = false
var cancellables = Set<AnyCancellable>()
init(_ item: Item) {
self.item = item
item.isChecked
.print("State for item \(item.name): ")
.assign(to: \.imgState, on: self)
.store(in: &cancellables)
}
var body: some View {
HStack {
HStack {
Image(systemName: imgState ? "checkmark.circle.fill" : "checkmark.circle")
.foregroundStyle(.blue)
Text(item.name)
}
}
}
}
struct ContentView: View {
var viewModel = ContentViewModel()
var body: some View {
List(viewModel.items) { item in
ItemCell(item)
}
}
}
class ContentViewModel: ObservableObject {
var itemNames = ["A", "B", "C", "D"]
@Published var checkList = [false, false, false, false]
init() {
updateChecklist()
}
// Dummy function to update data
func updateChecklist() {
Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] _ in
for index in 0...3 {
self?.checkList[index] = Bool.random()
}
}
}
// Dummy items whose isChecked data are continuously updating with publisher
var items: [Item] {
(0...3).map { index in
Item(
isChecked: $checkList
.map { $0[index] == true }
.eraseToAnyPublisher(),
name: itemNames[index]
)
}
}
}
#Preview {
ContentView()
}
I expect when the isChecked
publisher is emiting values the SwiftUI view ItemCell
should also be updated. But it is not updated.
In general, you should use onReceive
to listen for values published from a publisher:
struct ItemCell: View {
var item: Item
@State var imgState: Bool = false
init(_ item: Item) {
self.item = item
}
var body: some View {
HStack {
HStack {
Image(systemName: imgState ? "checkmark.circle.fill" : "checkmark.circle")
.foregroundStyle(.blue)
Text(item.name)
}
}
.onReceive(item.isChecked.print("State for item \(item.name): ")) {
imgState = $0
}
}
}
This works correctly for your toy example.
Going away from your contrived example, and suppose the published values come from some external source, instead of a @Published
property (which is very contrived). You should have a @Published
property of type [Item]
in your ContentViewModel
. sink
the publishers to assign to Item
s in this array.
Then ItemView
doesn't need any code to observe the changes. ContentView
observes ContentViewModel.items
, and automatically propagates the changes.
struct Item: Identifiable {
let name: String
var isChecked: Bool
// suppose Item is identified by its name
var id: String { name }
}
struct ItemCell: View {
let item: Item
init(_ item: Item) {
self.item = item
}
var body: some View {
HStack {
HStack {
Image(systemName: item.isChecked ? "checkmark.circle.fill" : "checkmark.circle")
.foregroundStyle(.blue)
Text(item.name)
}
}
}
}
struct ContentView: View {
@StateObject var viewModel = ContentViewModel()
var body: some View {
List(viewModel.items) { item in
ItemCell(item)
}
}
}
class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
private var cancellables: Set<AnyCancellable> = []
init() {
makeItem(name: "Foo")
makeItem(name: "Bar")
makeItem(name: "Baz")
}
func makeItem(name: String) {
items.append(Item(name: name, isChecked: false))
let someExternalPublisher = // get a publisher from somewhere...
// for example let's just say this is a timer publisher
Timer.publish(every: 2, on: .main, in: .default)
.autoconnect()
.map { _ in Bool.random() }
someExternalPublisher
.sink { bool in
if let index = self.items.firstIndex(where: { $0.name == name }) {
self.items[index].isChecked = bool
}
}
.store(in: &cancellables)
}
}
On iOS 17+, you can make Item
an @Observable class
, and have each item manage its own publisher:
@Observable
class Item: Identifiable {
let name: String
var isChecked: Bool = false
@ObservationIgnored
var cancellables = Set<AnyCancellable>()
init(name: String) {
self.name = name
let someExternalPublisher = // get a publisher from somewhere...
// for example let's just say this is a timer publisher
Timer.publish(every: 2, on: .main, in: .default)
.autoconnect()
.map { _ in Bool.random() }
someExternalPublisher.sink {
self.isChecked = $0
}
.store(in: &cancellables)
}
}
This doesn't really work with ObservableObject
s because SwiftUI can't observe changes on ObservableObject
s that have been put into an array.