I have a SwiftUI ProgressBar
View that displays the percentage progress based on a percentage-value you enter as a Bindable parameter. The progress percentage-value is calculated by a ReminderHelper
class, which takes two Ints as its parameters, totalDays
and daysLeft
, the values input to the ReminderHelper class come from a Reminder
object saved in Core Data.
I'm very confused as to how to structure my code to accomplish such of thing due to the poor understanding of how the SwiftUI/Combine, @Binding, @Published, @State, etc. work.
Based on the code below, what I'm expecting to see is two reminders, Cut the Grass
at 20% and Power Wash Siding
at 50%. Again, the two Ints
that determine the total percentage progress come from the Reminder
object saved in Core Data and the actual total percentage result comes from the RemindersHelper
class.
Any idea how to accomplish what I describe above?
Model:
This is saved in Core Data.
class Reminder:Identifiable{
var name = ""
var totalDays = 0
var daysLeft = 0
init(name:String, totalDays:Int, daysLeft:Int){
self.name = name
self.totalDays = totalDays
self.daysLeft = daysLeft
}
}
Helper class
This needs to be in charge of calculating the total percentage that will be passed to the ProgressBar View with the values coming from the Reminder object saved in Core Data.
class ReminderHelper:ObservableObject{
@Published var percentageLeft: Float = 0.80
func calculatePerentageLeft(daysLeft: Int, totalDays:Int)->Float{
percentageLeft = Float(daysLeft / totalDays)
return percentageLeft
}
}
Content View:
Here I'm calling the calculatePerentageLeft
method to prepare the percentageLeft
property before presenting the ProgressBar. Which of course is not working.
I see an error:
Static method 'buildBlock' requires that 'Float' conform to 'View'
struct ContentView: View {
var reminders = [Reminder(name: "Cut the Grass", totalDays: 50, daysLeft: 10),
Reminder(name: "Power Wash Siding", totalDays: 30, daysLeft: 15)]
@StateObject var reminderModel = ReminderHelper()
var body: some View {
List {
ForEach(reminders) { reminder in
HStack{
Text(reminder.name)
reminderModel.calculatePerentageLeft(daysLeft: reminder.daysLeft, totalDays: reminder.totalDays)
ProgressBar(progress: reminderModel.percentageLeft)
}
}
}
}
}
ProgressBar View
This is the view in charge of drawing and displaying the percentage value.
struct ProgressBar: View {
@Binding var progress: Float
var body: some View {
ZStack {
Circle()
.stroke(lineWidth:5.0)
.opacity(0.3)
.foregroundColor(Color.orange)
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(style: StrokeStyle(lineWidth: 5.0, lineCap: .round, lineJoin: .round))
.foregroundColor(Color.orange)
.rotationEffect(Angle(degrees: 270.0))
.animation(.linear, value: progress)
VStack{
Text(String(format: "%.0f %%", min(self.progress, 1.0)*100.0))
.font(.caption2)
}
}
}
}
CodePudding user response:
The error appears because your function returns a Float and every function that gets called in a buildblock like HStack should return a view.
You should change the design of your Model to acomplish what you want.
Create a calculated var in your Model (this won´t affect Coredata).
class Reminder:Identifiable{
var name = ""
var totalDays = 0
var daysLeft = 0
var percentageLeft: Float {
Float(daysLeft) / Float(totalDays)
}
init(name:String, totalDays:Int, daysLeft:Int){
self.name = name
self.totalDays = totalDays
self.daysLeft = daysLeft
}
}
Then use this var to display the progress:
struct ContentView: View {
var reminders = [Reminder(name: "Cut the Grass", totalDays: 50, daysLeft: 10),
Reminder(name: "Power Wash Siding", totalDays: 30, daysLeft: 15)]
var body: some View {
List {
ForEach(reminders) { reminder in
HStack{
Text(reminder.name)
ProgressBar(progress: reminder.percentageLeft)
}
}
}
}
}
Aditional changes:
- Depending on your overall design you may not need the Viewmodel here anymore.
- I changed
Float(daysLeft / totalDays)
toFloat(dayLeft) / Float(totalDays)
as the first will allways produce 0 - I changed
@Bining var progress: Float
tovar progress: Float
. You do not need a binding here. You only need one if the value of progress gets changed insideProgressView
and needs to get propagated to the parentContentView