Reputation: 66262
I have a SummaryView
with a Report
as @State
.
A Report
is a protocol which includes some changes a user might want to make:
protocol Report {
var changeGroups: [ChangeGroup] { get set }
}
There are several kinds of reports; individual reports are implemented as a struct:
struct RealEstateReport: Report {
static let name = "Real Estate Report"
var changeGroups = [ChangeGroup]()
}
A ChangeGroup
is a struct with (among other stuff) a human-readable summary and a handful of proposed changes:
struct ChangeGroup: Identifiable {
var summary: String
var proposedChanges = [ProposedChange]()
}
A ProposedChange
is a class that represents one discrete change the app proposes to the user, which is enabled
by default:
class ProposedChange: ObservableObject, Identifiable {
@Published var enabled = true
let summary: String
(In a detail view, enabled
is bound to a Toggle
so a user can flip each proposed change on and off.)
So a Report
has many ChangeGroup
s which themselves have many ProposedChange
s.
I'm trying to include some high level details on the SummaryView
:
struct SummaryView: View {
@State var report: Report
var body: some View {
Text("Summary")
.foregroundColor(…) // ???
}
I want foregroundColor
to be red, yellow, or green:
enabled
is false
for all ProposedChange
s in this Report
enabled
is true
for all ProposedChange
s in this Report
enabled
is mixed for different ProposedChange
s in this Report
I've read a bit about Combine, and I think I need to create a new Combine subscription for each ChangeGroup
, and map that to a new Combine subscription for each ProposedChange
's enabled
property, flatten the values when one changes, and check if they're all the same.
I'm a little lost on the exact syntax I'd use. And also it seems like structs don't publish changes in the same way (I guess since the structs are value vs. reference types).
How can I set the foregroundColor of the Text view based on the above logic?
Upvotes: 1
Views: 782
Reputation: 66262
I ended up mapping all the enabled
flags to their publisher, combining them all using the CombineLatest
operator, and then recalculating when the value changes:
class ViewModel: ObservableObject {
enum BoolState {
case allTrue, allFalse, mixed
}
@Published var boolState: BoolState?
private var report: Report
init(report: Report) {
self.report = report
report
.changeGroups // [ChangeGroup]
.map { $0.proposedChanges } // [[ProposedChange]]
.flatMap { $0 } // [ProposedChange]
.map { $0.$enabled } // [AnyPublisher<Bool, Never>]
.combineLatest() // AnyPublisher<[Bool], Never>
.map { Set($0) } // AnyPublisher<Set<Bool>, Never>
.map { boolSet -> BoolState in
switch boolSet {
case [false]:
return .allFalse
case [true]:
return .allTrue
default:
return .mixed
}
} // AnyPublisher<BoolState, Never>
.assign(to: &$boolState)
}
}
Note:
.combineLatest()
is not part ofCombine
but it's just an extension I wrote that iterates each pair of publishers in the array and calls them iteratively, likefirst.combineLatest(second).combineLatest(third)
etc. If you need something more robust than this, it looks like the CombineExt project has aCombineLatestMany
extension with several options.
At this point my view just does a @ObservedObject var viewModel: ViewModel
and then uses viewModel.boolState
in the body
. Whenever any of the enabled
flags change for any reason, the view updates successfully!
Upvotes: 0
Reputation: 49590
Your issue is immediately solved if ProposedChange
is a struct and not a class. Unless its instances have their own life cycle, then they are just holders of value, so should be semantically a struct.
The reason your issue is solved is because mutating a property of a struct mutates the struct, so SwiftUI knows to recompute the view, whereas with a class you need to subscribe to changes.
Assuming ProposedChange
is a struct:
struct ProposedChange {
var enabled = true
var summary: String
}
the following should work:
struct SummaryView: View {
@State var report: Report
var body: some View {
Text("Summary")
.foregroundColor(summaryColor)
}
var summaryColor: Color {
let count = report.changeGroups.flatMap { $0.proposedChanges }
.map { ($0.enabled ? 1 : 0, 1) }
.reduce((0, 0), { ($0.0 + $1.0, $0.1 + $1.1) })
if count.0 == count.1 { return Color.green }
else if count.0 == 0 { return Color.red }
else { return Color.yellow }
}
}
Upvotes: 2