Home > front end >  How to dynamically update a SwiftUI View with an @Bindable value
How to dynamically update a SwiftUI View with an @Bindable value

Time:04-05

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) to Float(dayLeft) / Float(totalDays) as the first will allways produce 0
  • I changed @Bining var progress: Float to var progress: Float. You do not need a binding here. You only need one if the value of progress gets changed inside ProgressView and needs to get propagated to the parent ContentView
  • Related