I’m trying to create a button that displays a different images/text depending on the current state of a task.
I’ve found the following doesn’t work:
Button {
taskViewModel.completeTask(id: task.id)
} label: {
if task.taskCompleted {
Image(systemName: "square")
.foregroundStyle(.black)
.padding(10)
.background(Color(.white), in: RoundedRectangle(cornerRadius: 10))
} else {
Image(systemName: "checkmark.square")
.foregroundStyle(.black)
.padding(10)
.background(Color(.white), in: RoundedRectangle(cornerRadius: 10))
}
}
But this does work:
if task.taskCompleted {
Button {
taskViewModel.completeTask(id: task.id)
} label: {
Image(systemName: "checkmark.square")
.foregroundStyle(.black)
.padding(10)
.background(Color(.white), in: RoundedRectangle(cornerRadius: 10))
}
} else {
Button {
taskViewModel.completeTask(id: task.id)
} label: {
Image(systemName: "square")
.foregroundStyle(.black)
.padding(10)
.background(Color(.white), in: RoundedRectangle(cornerRadius: 10))
}
}
The code that doesn’t work is far cleaner!. Surely there is a better way to do this than what I’ve done here?
The full code that isn’t working is:
func TasksView(completed: Bool)->some View {
LazyVStack(spacing: 20) {
if let tasks = taskViewModel.filteredTasks{
if tasks.isEmpty{
Text("No Tasks Today")
.offset(y: 100)
}else{
ForEach(tasks){ task in
if completed && task.taskCompleted || !task.taskCompleted {
HStack(alignment: .top, spacing: 20) {
VStack(spacing: 12) {
Circle()
.fill(taskViewModel.isCurrentHour(date: task.taskDate) ? .black : .clear)
.frame(width: 13, height: 13)
.background(
Circle()
.stroke(.black, lineWidth: 2)
.padding(-4)
)
.scaleEffect(taskViewModel.isCurrentHour(date: task.taskDate) ? 1.2 : 0.8)
Rectangle()
.fill(.black)
.frame(width: 3)
}
VStack {
HStack(alignment: .top, spacing: 10) {
VStack(alignment: .leading, spacing: 12) {
Text(task.taskTitle)
.font(.title2.bold())
Text(task.taskDescription)
.font(.callout)
.foregroundStyle(.secondary)
}
.headerLeading()
Text(task.taskDate.formatted(date: .omitted, time: .shortened))
}
HStack(spacing: 8) {
if taskViewModel.isCurrentHour(date: task.taskDate) {
HStack(spacing: 5) {
ForEach(1...3, id: .self) { user in
Image(systemName: "person")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 25, height: 25)
}
}
.headerLeading()
}
Button{
taskViewModel.completeTask(id: task.id)
} label: {
Image(systemName: task.taskCompleted ? "checkmark.square" : "square")
.foregroundStyle(.black)
.padding(10)
.background(Color(.white), in: RoundedRectangle(cornerRadius: 10))
}
Button{
taskViewModel.deleteTask(id: task.id)
} label: {
Image(systemName: "bin.xmark")
.foregroundStyle(.black)
.padding(10)
.background(Color(.white), in: RoundedRectangle(cornerRadius: 10))
}
}
.padding(.top)
.headerTrailing()
}
.padding(taskViewModel.isCurrentHour(date: task.taskDate) ? 15 : 0)
.padding(.bottom, taskViewModel.isCurrentHour(date: task.taskDate) ? 0 : 10)
.headerLeading()
.background(
Color(.black)
.cornerRadius(20)
.opacity(taskViewModel.isCurrentHour(date: task.taskDate) ? 1 : 0)
)
.foregroundColor(taskViewModel.isCurrentHour(date: task.taskDate) ? .white : .black)
}.headerLeading()
}
}
}
}else{
ProgressView()
.offset(y: 100)
}
}
.padding()
.padding(.top)
.onChange(of: taskViewModel.currentDay) { newDate in
taskViewModel.filterTodaysTasks()
}
}
When I try to run this the following error is thrown by Xcode:
The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions
3
Answers
To SwiftUI, the identity of the button hasn’t changed in the first example. You could fix this by adding an explicit
.id(…)
. For example:As soon as the task’s
completed
changes, and SwiftUI is re-evaluating the UI, it will detect that the button has a different ID and will then re-render it with your new image.In lieu of more details, generally the best way to adjust your view in the result of an object’s property is to make the smallest adjustment necessary. In this case, you can limit the change to the
systemName
of the image:What is more likely happening is that
task
isn’t signalling its change to the SwiftUI rendering system, so aftercompleteTask
has run there’s no indication that the view needs to be re-evaluated.There is a better and a cleaner way to do it, based on the state of the task completed.