SwiftUI NSManagedObject changes do not update the view - swift

In my view model, if I update an NSManagedObject property, then the view won't update anymore. I have attached the code and the view model.
I added a comment in front of the line that breaks the view update.
class StudySessionCounterViewModel: ObservableObject {
fileprivate var studySession: StudySession
init(_ studySession: StudySession) {
self.studySession = studySession
}
#Published var elapsedTime = 14 * 60
#Published var circleProgress: Double = 0
var timer: Timer?
var formattedTime: String {
get {
let timeToFormat = (Int(studySession.studyTime) * 60) - elapsedTime
let minutes = timeToFormat / 60 % 60
let seconds = timeToFormat % 60
return String(format:"%02i:%02i", minutes, seconds)
}
}
func startTimer() {
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerTicked), userInfo: nil, repeats: true)
studySession.isActive = true //Adding this stops my view from updating
}
#objc func timerTicked() {
elapsedTime += 1
circleProgress = (Double(elapsedTime) / Double(studySession.studyTime * 60))
}
func stop() {
timer?.invalidate()
}
}
This is the view that uses that view model. When adding that line, the text that represents the formatted time no longer changes and the progress circle's progress remain the same.
If I remove the line, everything updates and work as expected.
struct StudySessionCounterView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var studySessionCounterVM: StudySessionCounterViewModel
var studySession: StudySession
init(_ studySession: StudySession) {
studySessionCounterVM = StudySessionCounterViewModel(studySession)
self.studySession = studySession
}
#State var showAlert = false
#State var isCounting = false
var body: some View {
VStack {
ZStack {
Text(studySessionCounterVM.formattedTime)
.font(.largeTitle)
ProgressRingView(size: .large, progress: $studySessionCounterVM.circleProgress)
}
Spacer()
if isCounting {
Button(action: {
self.studySessionCounterVM.stop()
self.isCounting = false
}) {
Image(systemName: "stop.circle")
.resizable()
.frame(width: 64, height: 64, alignment: .center)
.foregroundColor(.orange)
}
} else {
Button(action: {
self.studySessionCounterVM.startTimer()
self.isCounting = true
}) {
Image(systemName: "play.circle")
.resizable()
.frame(width: 64, height: 64, alignment: .center)
.foregroundColor(.orange)
}
}
}.padding()
.navigationBarTitle("Matematica", displayMode: .inline)
}
}
UPDATE: Found out that each time the NSManagedObject changes a property, the view model gets reinitialised. Still no solution, unfortunately

Related

Conditional view modifier sometimes doesn't want to update

As I am working on a study app, I'm tring to build a set of cards, that a user can swipe each individual card, in this case a view, in a foreach loop and when flipped through all of them, it resets the cards to normal stack. The program works but sometimes the stack of cards doesn't reset. Each individual card updates a variable in a viewModel which my conditional view modifier looks at, to reset the stack of cards using offset and when condition is satisfied, the card view updates, while using ".onChange" to look for the change in the viewModel to then update the variable back to original state.
I've printed each variable at each step of the way and every variable updates and I can only assume that the way I'm updating my view, using conditional view modifier, may not be the correct way to go about. Any suggestions will be appreciated.
Here is my code:
The view that houses the card views with the conditional view modifier
extension View {
#ViewBuilder func `resetCards`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition == true {
transform(self).offset(x: 0, y: 0)
} else {
self
}
}
}
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count: currentStudySet.allSets[index].studyItem.count)
.resetCards(studyCards.isDone) { view in
view
}
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
}
}
HomeViewModel
class HomeViewModel: ObservableObject {
#Published var studySet: StudyModel = StudyModel()
#Published var allSets: [StudyModel] = [StudyModel()]
}
I initialize allSets with one StudyModel() to see it in the preview
StudyListViewModel
class StudyListViewModel: ObservableObject {
#Published var cardsSortedThrough: Int = 0
#Published var isDone: Bool = false
}
StudyModel
import SwiftUI
struct StudyModel: Hashable{
var title: String = ""
var days = ["One day", "Two days", "Three days", "Four days", "Five days", "Six days", "Seven days"]
var studyGoals = "One day"
var studyItem: [StudyItemModel] = []
}
Lastly, StudyItemModel
struct StudyItemModel: Hashable, Identifiable{
let id = UUID()
var itemTitle: String = ""
var itemDescription: String = ""
}
Once again, any help would be appreciated, thanks in advance!
I just found a fix and I put .onChange at the end for StudyCardItemView. Basically, the onChange helps the view scan for a change in currentCard.isDone variable every time it was called in the foreach loop and updates offset individuality. This made my conditional view modifier obsolete and just use the onChange to check for the condition.
I still used onChange outside the view with the foreach loop, just to set currentCard.isDone variable false because the variable will be set after all array elements are iterator through.
The updated code:
StudyCardItemView
struct StudyCardItemView: View {
#StateObject var currentCard: StudyListViewModel
#State var card: StudyItemModel
#State var count: Int
#State var offset = CGSize.zero
var body: some View {
VStack{
VStack{
ZStack(alignment: .center){
Text("\(card.itemTitle)")
}
}
}
.frame(width: 350, height: 200)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
.padding(5)
.rotationEffect(.degrees(Double(offset.width / 5)))
.offset(x: offset.width * 5, y: 0)
.gesture(
DragGesture()
.onChanged { gesture in
offset = gesture.translation
}
.onEnded{ _ in
if abs(offset.width) > 100 {
currentCard.cardsSortedThrough += 1
if (currentCard.cardsSortedThrough == count) {
currentCard.isDone = true
currentCard.cardsSortedThrough = 0
}
} else {
offset = .zero
}
}
)
.onChange(of: currentCard.isDone, perform: {_ in
offset = .zero
})
}
}
StudyListView
struct StudyListView: View {
#ObservedObject var currentStudySet: HomeViewModel
#ObservedObject var studyCards: StudyListViewModel = StudyListViewModel()
#State var studyItem: StudyModel
#State var index: Int
var body: some View {
ForEach(currentStudySet.allSets[index].studyItem.reversed()) { item in
StudyCardItemView(currentCard: studyCards, card: item, count:
currentStudySet.allSets[index].studyItem.count)
.onChange(of: studyCards.isDone, perform: { _ in
studyCards.isDone = false
})
}
}
}
Hope this helps anyone in the future!

SwiftUI View method called but output missing

I have the View AlphabetLetterDetail:
import SwiftUI
struct AlphabetLetterDetail: View {
var alphabetLetter: AlphabetLetter
var letterAnimView : LetterAnimationView
var body: some View {
VStack {
Button(action:
animateLetter
) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
letterAnimView
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter(){
print("tapped")
letterAnimView.timerWrite()
}
}
containing the View letterAnimView of Type LetterAnimationView:
import SwiftUI
struct LetterAnimationView: View {
#State var Robot : String = ""
let LETTER =
["alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"]
var body: some View {
VStack(alignment:.center){
Image(Robot)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: timerWrite)
}
}
func timerWrite(){
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) {(Timer) in
Robot = LETTER[index]
print("one frame")
index += 1
if (index > LETTER.count - 1){
Timer.invalidate()
}
}
}
}
This gives me a fine animation, as coded in func timerWrite() and performed by .onAppear(perform: timerWrite).
After commenting //.onAppear(perform: timerWrite) I try animating by clicking
Button(action: animateLetter)
but nothing happens.
Maybe I got two different instances of letterAnimView, if so why?
Can anybody of you competent guys intentify my mistake?
Regards - Klaus
You don't want to store Views, they are structs so they are copied. Instead, create an ObservableObject to encapsulate this functionality.
I created RobotModel here with other minor changes:
class RobotModel: ObservableObject {
private static let atlas = [
"alpha_be_1_81",
"alpha_be_1_82",
"alpha_be_1_83",
"alpha_be_1_84",
"alpha_be_1_85",
"alpha_be_1_86",
"alpha_be_1_87",
"alpha_be_1_88",
"alpha_be_1_89",
"alpha_be_1_90",
"alpha_be_1"
]
#Published private(set) var imageName: String
init() {
imageName = Self.atlas.last!
}
func timerWrite() {
var index = 0
let _ = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.imageName = Self.atlas[index]
print("one frame")
index += 1
if index > Self.atlas.count - 1 {
timer.invalidate()
}
}
}
}
struct AlphabetLetterDetail: View {
#StateObject private var robot = RobotModel()
let alphabetLetter: AlphabetLetter
var body: some View {
VStack {
Button(action: animateLetter) {
Image(uiImage: UIImage(named: "alpha_be_1")!)
.resizable()
.scaledToFit()
.frame(width: 60.0, height: 120.0)
}
LetterAnimationView(robot: robot)
}.navigationBarTitle(Text(verbatim: alphabetLetter.name), displayMode: .inline)
}
func animateLetter() {
print("tapped")
robot.timerWrite()
}
}
struct LetterAnimationView: View {
#ObservedObject var robot: RobotModel
var body: some View {
VStack(alignment:.center){
Image(robot.imageName)
.resizable()
.frame(width: 80, height: 160, alignment: .center)
.onAppear(perform: robot.timerWrite)
}
}
}

Alarm Button interacts incorrectly with view - SwiftUI

I'm continuing to develop a timer app for practice purposes and have the basic functions ready so far.
The timer works in such a way that if you click on "Start" the timer simply runs down to 0 and then selects the next player - here you can also the button in the upper middle whether an alarm is played after the timer or not - however, this also stops the timer, although this does not occur in the implementation (see also video below). I hope someone can help me.
For the timer I made a StopWatchManager class:
import Foundation
class StopWatchManager : ObservableObject {
#Published var secondsElapsed : Double = 0.00
#Published var mode : stopWatchMode = .stopped
var timer : Timer = Timer()
//Start the timer
func start() -> Void {
secondsElapsed = 0.00
mode = .running
timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true){ _ in
self.secondsElapsed += 0.01
}
}
//Pause the timer
func pause() -> Void {
timer.invalidate()
mode = .paused
}
//Stop the timer
func stop() -> Void {
timer.invalidate()
secondsElapsed = 0.00
self.mode = .stopped
}
}
enum stopWatchMode{
case running
case stopped
case paused
}
Then I have an overview TimerView which implements some little details and connects the buttons with TimerTextView:
import SwiftUI
struct TimerView: View {
#State private var alarmSoundOn : Bool = true
#State private var quitGame : Bool = false
//buttons variable for timer
#State private var startTimer : Bool = false
#State private var resetTimer : Bool = false
#State private var pauseTimer : Bool = false
#State private var quitTimer : Bool = false
var body: some View {
ZStack{
Color.blue.ignoresSafeArea()
TimerTextView(startTimer: $startTimer, resetTimer: $resetTimer, pauseTimer: $pauseTimer, quitTimer: $quitTimer, playAlarmSound: $alarmSoundOn)
if (!quitGame) {
VStack{
HStack(alignment: .top){
//TODO - find bug with Timer
Button(action: {
alarmSoundOn.toggle()
}, label: {
if (alarmSoundOn) {
Image(systemName: "speaker.circle")
.resizable().frame(width: 30, height: 30)
} else {
Image(systemName: "speaker.slash.circle")
.resizable().frame(width: 30, height: 30)
}
}).foregroundColor(.white)
}
Spacer()
}
}
}
}
}
And here is my TimerTextView where all the logic with the buttons and the circle happens:
import SwiftUI
import AVFoundation
struct TimerTextView: View {
//timer instance
#ObservedObject var playTimer : StopWatchManager = StopWatchManager()
//default buttons for the timer
#Binding var startTimer : Bool
#Binding var resetTimer : Bool
#Binding var pauseTimer : Bool
#Binding var quitTimer : Bool
//variables for circle
var timerSeconds : Int = 10
#State private var progress : Double = 1.00
#State private var endDate : Date? = nil
//seconds of timer as text in the middle of the timer
#State var textTimerSeconds : Double = 10
//sound for the alarm after the timer exceeded
#Binding var playAlarmSound : Bool
#State private var playAlarmAtTheEnd : Bool = true
let sound : SystemSoundID = 1304
var body: some View {
ZStack{
if (quitTimer) {
EmptyView()
} else {
//Circles---------------------------------------------------
VStack{
VStack{
VStack{
ZStack{
//Circle
ZStack{
Circle()
.stroke(lineWidth: 20)
.foregroundColor(.white.opacity(0.3))
Circle()
.trim(from: 0.0, to: CGFloat(Double(min(progress, 1.0))))
.stroke(style: StrokeStyle(lineWidth: 20.0, lineCap: .round, lineJoin: .round))
.rotationEffect(.degrees(270.0))
.foregroundColor(.white)
.animation(.linear, value: progress)
}.padding()
VStack{
//Timer in the middle
Text("\(Int(textTimerSeconds.rounded(.up)))")
.font(.system(size: 80))
.bold()
.multilineTextAlignment(.center)
.foregroundColor(.white)
}
}.padding()
//timer responds every milliseconds and calls intern function decrease timer
.onReceive(playTimer.$secondsElapsed, perform: { _ in
decreaseTimer()
})
//pauses the timer
.onChange(of: pauseTimer, perform: { change in
if (pauseTimer) {
playTimer.pause()
} else {
endDate = Date(timeIntervalSinceNow: TimeInterval(textTimerSeconds))
playTimer.start()
}
})
//resets the timer
.onChange(of: resetTimer, perform: { change in
if (resetTimer) {
resetTimerToBegin()
resetTimer = false
}
})
//play alarm sound at the end of the timer
.onChange(of: playAlarmSound, perform: { change in
if (playAlarmSound){
playAlarmAtTheEnd = true
} else {
playAlarmAtTheEnd = false
}
})
.onChange(of: startTimer, perform: { change in startTimerFromBegin()
})
}
}.foregroundColor(Color.black)
//----------------- Buttons
VStack(spacing: 50){
//if isStoped -> show play, reset & quit
//if !isStoped -> show pause
if(pauseTimer){
HStack(alignment: .bottom){
Spacer()
Spacer()
//Play Button
Button(action: {
pauseTimer = false
}, label: {
VStack(alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, spacing: 5){
Image(systemName: "play.fill")
Text("Play").font(.callout)
}
})
.font(.title2)
.frame(width: 60, height: 60, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.padding(3.0)
.buttonStyle(BorderlessButtonStyle())
Spacer()
//Reset Button
Button(action: {
resetTimer = true
}, label: {
VStack(alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, spacing: 5){
Image(systemName: "gobackward")
Text("Reset").font(.callout)
}
})
.font(.title2)
.frame(width: 60, height: 60, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.padding(3.0)
.buttonStyle(BorderlessButtonStyle())
Spacer()
//Quit Button
Button(action: {
quitTimer = true
}, label: {
VStack(alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, spacing: 5){
Image(systemName: "flag.fill")
Text("Exit").font(.callout)
}
})
.font(.title2)
.frame(width: 60, height: 60, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.padding(3.0)
.buttonStyle(BorderlessButtonStyle())
Spacer()
Spacer()
}
} else if (startTimer) {
//Pause Button
Button(action: {
pauseTimer = true
}, label: {
VStack(alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, spacing: 5){
Image(systemName: "pause.fill")
Text("Pause").font(.callout)
}
})
.font(.title2)
.frame(width: 60, height: 60, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
.padding(3.0)
.buttonStyle(BorderlessButtonStyle())
} else {
//Play Button
Button(action: {
pauseTimer = false
startTimer = true
}, label: {
VStack(alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/, spacing: 5){
Image(systemName: "play.fill")
Text("Start").font(.callout)
}
})
.font(.title2)
.frame(width: 60, height: 60, alignment: .center)
.padding(3.0)
.buttonStyle(BorderlessButtonStyle())
}
}.foregroundColor(.white)
}
}
}
}
//FUNCTIONS --------------------------------
private func startTimerFromBegin() -> Void{
endDate = Date(timeIntervalSinceNow: TimeInterval(timerSeconds))
playTimer.start()
}
private func resetTimerToBegin() -> Void {
endDate = Date(timeIntervalSinceNow: TimeInterval(timerSeconds))
progress = 1.0
textTimerSeconds = Double(timerSeconds)
}
private func decreaseTimer() -> Void{
guard let endDate = endDate else { print("decreaseTimer() was returned"); return }
progress = max(0, endDate.timeIntervalSinceNow / TimeInterval(timerSeconds))
textTimerSeconds -= 0.01
if endDate.timeIntervalSinceNow <= 0 {
if (playAlarmAtTheEnd) {
AudioServicesPlayAlertSound(sound)
}
}
}
}
You are instantiating your playTimer in the subview ... That will reset it when the view is redrawn. Instead you should do this in the parent view and pass it down.
Also you should use #StateObject to instantiate.
struct TimerView: View {
//timer instance HERE
#StateObject var playTimer : StopWatchManager = StopWatchManager()
...
pass down to subview
// pass timer here
TimerTextView(playTimer: playTimer, startTimer: $startTimer, resetTimer: $resetTimer, pauseTimer: $pauseTimer, quitTimer: $quitTimer, playAlarmSound: $alarmSoundOn)
Subview:
struct TimerTextView: View {
//passed timer
#ObservedObject var playTimer : StopWatchManager
...
One more thing: Firing the timer every 0.01 seconds is too much. You should go to 0.1

How to have ForEach in view updated when array values change with SwiftUI

I have this code here for updating an array and a dictionary based on a timer:
class applicationManager: ObservableObject {
static var shared = applicationManager()
#Published var applicationsDict: [NSRunningApplication : Int] = [:]
#Published var keysArray: [NSRunningApplication] = []
}
class TimeOpenManager {
var secondsElapsed = 0.0
var timer = Timer()
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.secondsElapsed += 1
let applicationName = ws.frontmostApplication?.localizedName!
let applicationTest = ws.frontmostApplication
let appMan = applicationManager()
if let appData = applicationManager.shared.applicationsDict[applicationTest!] {
applicationManager.shared.applicationsDict[applicationTest!] = appData + 1
} else {
applicationManager.shared.applicationsDict[applicationTest!] = 1
}
applicationManager.shared.keysArray = Array(applicationManager.shared.applicationsDict.keys)
}
}
}
It works fine like this and the keysArray is updated with the applicationsDict keys when the timer is running. But even though keysArray is updated, the HStack ForEach in WeekView does not change when values are added.
I also have this view where keysArray is being loaded:
struct WeekView: View {
static let shared = WeekView()
var body: some View {
VStack {
HStack(spacing: 20) {
ForEach(0..<8) { day in
if day == weekday {
Text("\(currentDay)")
.frame(width: 50, height: 50, alignment: .center)
.font(.system(size: 36))
.background(Color.red)
.onTapGesture {
print(applicationManager.shared.keysArray)
print(applicationManager.shared.applicationsDict)
}
} else {
Text("\(currentDay + day - weekday)")
.frame(width: 50, height: 50, alignment: .center)
.font(.system(size: 36))
}
}
}
HStack {
ForEach(applicationManager.shared.keysArray, id: \.self) {dictValue in
Image(nsImage: dictValue.icon!)
.resizable()
.frame(width: 64, height: 64, alignment: .center)
}
}
}
}
}
As mentioned in the comments, applicationManager should be an ObservableObject with #Published properties.
Then, you need to tell your view that it should look for updates from this object. You can do that with the #ObservedObject property wrapper:
struct WeekView: View {
#ObservedObject private var manager = applicationManager.shared
And later:
ForEach(manager.keysArray) {
Replace anywhere you had applicatoinManager.shared with manager within the WeekView
Additional reading: https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects

ProgressBar iOS 13

I've tried creating my own ProgressView to support iOS 13, but for some reason it appears to not work. I've tried #State, #Binding and the plain var progress: Progress, but it doesn't update at all.
struct ProgressBar: View {
#Binding var progress: Progress
var body: some View {
VStack(alignment: .leading) {
Text("\(Int(progress.fractionCompleted))% completed")
ZStack {
RoundedRectangle(cornerRadius: 2)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 4)
GeometryReader { metrics in
RoundedRectangle(cornerRadius: 2)
.foregroundColor(.blue)
.frame(width: metrics.size.width * CGFloat(progress.fractionCompleted))
}
}.frame(height: 4)
Text("\(progress.completedUnitCount) of \(progress.totalUnitCount)")
.font(.footnote)
.foregroundColor(.gray)
}
}
}
In my content view I added both the iOS 14 variant and the iOS 13 supporting one. They look the same, but the iOS 13 variant does not change anything.
struct ContentView: View {
#State private var progress = Progress(totalUnitCount: 10)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
ProgressBar(progress: $progress)
.padding()
.onReceive(timer) { timer in
progress.completedUnitCount += 1
if progress.isFinished {
self.timer.upstream.connect().cancel()
progress.totalUnitCount = 500
}
}
if #available(iOS 14, *) {
ProgressView(progress)
.padding()
.onReceive(timer) { timer in
progress.completedUnitCount += 1
if progress.isFinished {
self.timer.upstream.connect().cancel()
progress.totalUnitCount = 500
}
}
}
}
}
}
The iOS 14 variant works, but my iOS 13 implementation fails. Can somebody help me?
Progress is-a NSObject, it is not a struct, so state does not work for it. You have to use KVO to observe changes in Progress and redirect into SwiftUI view's source of truth.
Here is a simplified demo of possible solution. Tested with Xcode 12.1 / iOS 14.1
class ProgressBarViewModel: ObservableObject {
#Published var fractionCompleted: Double
let progress: Progress
private var observer: NSKeyValueObservation!
init(_ progress: Progress) {
self.progress = progress
self.fractionCompleted = progress.fractionCompleted
observer = progress.observe(\.completedUnitCount) { [weak self] (sender, _) in
self?.fractionCompleted = sender.fractionCompleted
}
}
}
struct ProgressBar: View {
#ObservedObject private var vm: ProgressBarViewModel
init(_ progress: Progress) {
self.vm = ProgressBarViewModel(progress)
}
var body: some View {
VStack(alignment: .leading) {
Text("\(Int(vm.fractionCompleted * 100))% completed")
ZStack {
RoundedRectangle(cornerRadius: 2)
.foregroundColor(Color(UIColor.systemGray5))
.frame(height: 4)
GeometryReader { metrics in
RoundedRectangle(cornerRadius: 2)
.foregroundColor(.blue)
.frame(width: metrics.size.width * CGFloat(vm.fractionCompleted))
}
}.frame(height: 4)
Text("\(vm.progress.completedUnitCount) of \(vm.progress.totalUnitCount)")
.font(.footnote)
.foregroundColor(.gray)
}
}
}
and updated call place to use same constructor
struct ContentView: View {
#State private var progress = Progress(totalUnitCount: 10)
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
ProgressBar(progress) // << here !!
// ... other code