Timer within EnvironmentObject view model not updating the View - mvvm

I have a view model, that has multiple child view models. I am fairly new to watchOS, SwiftUI and Combine - taking this opportunity to learn.
I have a watchUI where it has
Play Button (View) - SetTimerPlayPauseButton
Text to show Time (View) - TimerText
View Model - that has 1 WatchDayProgramViewModel - N: ExerciseTestClass - N: SetInformationTestClass. For each ExerciseSets, there is a watchTimer & watchTimerSubscription and I have managed to run the timer to update remaining rest time.
ContentView - that has been injected the ViewModel as EnvironmentObject
If I tap SetTimerPlayPauseButton to start the timer, timer is running, working and changing the remainingRestTime(property within the child view model SetInformationTestClass) correctly, but the updates/changes are not being "published" to the TimerText View.
I have done most, if not all, the recommendation in other SO answers, I even made all my WatchDayProgramViewModel and ExerciseTestClass,SetInformationTestClass properties #Published, but they are still not updating the View, when the view model properties are updated as shown in the Xcode debugger below.
Please review my code and give me some advice on how to improve it.
ContentView
struct ContentView: View {
#State var selectedTab = 0
#StateObject var watchDayProgramVM = WatchDayProgramViewModel()
var body: some View {
TabView(selection: $selectedTab) {
SetRestDetailView().id(2)
}
.environmentObject(watchDayProgramVM)
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(.page(backgroundDisplayMode: .automatic))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
ContentView(watchDayProgramVM: WatchDayProgramViewModel())
}
}
}
SetRestDetailView
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
var setCurrentHeartRate: Int = 120
#State var showingLog = false
var body: some View {
HStack {
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
TimerText(elapsedRestTime: elapsedRestTime, totalRestTime: totalRestTime, rect: rect)
.border(Color.yellow)
}
HStack {
SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
playImage: "play.fill",
pauseImage: "pause.fill",
bgColor: Color.clear,
fgColor: Color.white.opacity(0.5),
rect: rect) {
print("playtimer button tapped")
self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
}
.border(Color.yellow)
}
}
}
TimerText
struct TimerText: View {
var elapsedRestTime: Int
var totalRestTime: Int
var rect: CGRect
var body: some View {
VStack {
Text(counterToMinutes())
.font(.system(size: 100, weight: .semibold, design: .rounded))
.kerning(0)
.fontWeight(.semibold)
.minimumScaleFactor(0.25)
.padding(-1)
}
}
func counterToMinutes() -> String {
let currentTime = totalRestTime - elapsedRestTime
let seconds = currentTime % 60
let minutes = Int(currentTime / 60)
if currentTime > 0 {
return String(format: "%02d:%02d", minutes, seconds)
}
else {
return ""
}
}
}
ViewModel
import Combine
final class WatchDayProgramViewModel: ObservableObject {
#Published var exerciseVMList: [ExerciseTestClass] = [
(static/hard-coded values for testing)
]
class ExerciseTestClass: ObservableObject {
init(exercise: String, sets: [SetInformationTestClass]) {
self.exercise = exercise
self.sets = sets
}
var exercise: String
#Published var sets: [SetInformationTestClass]
}
class SetInformationTestClass: ObservableObject {
init(totalRestTime: Int, elapsedRestTime: Int, remainingRestTime: Int, isTimerRunning: Bool) {
self.totalRestTime = totalRestTime
self.elapsedRestTime = elapsedRestTime
self.remainingRestTime = remainingRestTime
self.isTimerRunning = isTimerRunning
}
#Published var totalRestTime: Int
#Published var elapsedRestTime: Int
#Published var remainingRestTime: Int
#Published var isTimerRunning = false
#Published var watchTimer = Timer.publish(every: 1.0, on: .main, in: .default)
#Published var watchTimerSubscription: AnyCancellable? = nil
#Published private var startTime: Date? = nil
func startTimer() {
print("startTimer initiated")
self.watchTimerSubscription?.cancel()
if startTime == nil {
startTime = Date()
}
self.isTimerRunning = true
self.watchTimerSubscription = watchTimer
.autoconnect()
.sink(receiveValue: { [weak self] _ in
guard let self = self, let startTime = self.startTime else { return }
let now = Date()
let elapsedTime = now.timeIntervalSince(startTime)
self.remainingRestTime = self.totalRestTime - Int(elapsedTime)
self.elapsedRestTime = self.totalRestTime - self.remainingRestTime
guard self.remainingRestTime > 0 else {
self.pauseTimer()
return
}
self.objectWillChange.send()
print("printing elapsedRest Time \(self.elapsedRestTime) sec")
print("printing remaining Rest time\(self.remainingRestTime)sec ")
})
}
func pauseTimer() {
//stop timer and retain elapsed rest time
print("pauseTimer initiated")
self.watchTimerSubscription?.cancel()
self.watchTimerSubscription = nil
self.isTimerRunning = false
self.startTime = nil
}

Managed to resolve the issue with help of #lorem ipsum and his feedback. As per his comment, the problem lied with the fact that
it is more than likely not working because you are chaining ObservableObjects #Published will only detect a change when the object is changed as a whole now when variables change. One way to test is to wrap each SetInformationTestClass in an #ObservbleObject by using a subview that takes the object as a parameter.
After which, I managed to find similar SO answers on changes in nested view model (esp child), and made the child view model an ObservedObject. The changes in child view model got populated to the view. Please see the changed code below.
SetRestDetailView
import Foundation
import SwiftUI
import Combine
struct SetRestDetailView: View {
#EnvironmentObject var watchDayProgramVM: WatchDayProgramViewModel
var setCurrentHeartRate: Int = 120
#State var showingLog = false
var body: some View {
HStack {
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
let setInformatationVM = self.watchDayProgramVM.exerciseVMList[0].sets[2]
TimerText(setInformationVM: setInformatationVM, rect: rect)
.border(Color.yellow)
}
HStack {
SetTimerPlayPauseButton(isSetTimerRunningFlag: false,
playImage: "play.fill",
pauseImage: "pause.fill",
bgColor: Color.clear,
fgColor: Color.white.opacity(0.5),
rect: rect) {
print("playtimer button tapped")
self.watchDayProgramVM.exerciseVMList[0].sets[2].startTimer()
let elapsedRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].elapsedRestTime
let totalRestTime = watchDayProgramVM.exerciseVMList[0].sets[2].totalRestTime
print("printing elapsedRestTime from SetRestDetailView \(elapsedRestTime)")
print("printing elapsedRestTime from SetRestDetailView \(totalRestTime)")
}
.border(Color.yellow)
}
}
}
TimerText
struct TimerText: View {
#ObservedObject var setInformationVM: SetInformationTestClass
// #State var elapsedRestTime: Int
// #State var totalRestTime: Int
var rect: CGRect
var body: some View {
VStack {
Text(counterToMinutes())
.font(.system(size: 100, weight: .semibold, design: .rounded))
.kerning(0)
.fontWeight(.semibold)
.minimumScaleFactor(0.25)
.padding(-1)
}
}
func counterToMinutes() -> String {
let currentTime = setInformationVM.totalRestTime - setInformationVM.elapsedRestTime
let seconds = currentTime % 60
let minutes = Int(currentTime / 60)
if currentTime > 0 {
return String(format: "%02d:%02d", minutes, seconds)
}
else {
return ""
}
}
}

Related

RealityKit removing old 3D objects from scene before adding new 3D objects

I'm trying to play with Apple's RealityKit and some usdz files I got from their website.
I made a small app where I can load each 3D model at a time.
It works but with one issue. If I add a single 3D object is fine but when I load the new models they all get stuck upon each other. I can't seem to figure out how to remove the old 3D object before adding a different one.
Here is my code
My Model object
import Foundation
import UIKit
import RealityKit
import Combine
class Model {
var modelName: String
var image: UIImage
var modelEntity: ModelEntity?
private var cancellable: AnyCancellable? = nil
var message: String = ""
init(modelName: String){
self.modelName = modelName
self.image = UIImage(named: modelName)!
let filename = modelName + ".usdz"
self.cancellable = ModelEntity.loadModelAsync(named: filename)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
print("Model.swift -> DEBUG: Succesfully loaded \(self.modelName)")
break
case .failure(let error):
print("Model.swift -> DEBUG: Unable to load : \(self.modelName)")
self.message = error.localizedDescription
}
}, receiveValue: { modelEntity in
//get model entity
self.modelEntity = modelEntity
})
}
}
RealityKit and SwiftUI View
import SwiftUI
import RealityKit
import ARKit
struct ContentView : View {
#State private var isPlacementEnabled = false
#State private var selectedModel: Model?
#State private var modelConfirmedForPlacement: Model?
private var models: [Model] {
//Dynamicaly get file names
let filemanager = FileManager.default
guard let path = Bundle.main.resourcePath,
let files = try? filemanager.contentsOfDirectory(atPath: path)
else {
return []
}
var availableModels: [Model] = []
for filename in files where filename.hasSuffix("usdz") {
let modelName = filename.replacingOccurrences(of: ".usdz", with: "")
let model = Model(modelName: modelName)
availableModels.append(model)
}
return availableModels
}
var body: some View {
ZStack(alignment: .bottom){
ARViewContainer(modelConfirmedForPlacement: self.$modelConfirmedForPlacement)
if self.isPlacementEnabled {
PlacementButtonsView(isPlacementEnabled: self.$isPlacementEnabled,
selectedModel: self.$selectedModel,
modelConfirmedForPlacement: self.$modelConfirmedForPlacement
)
}
else {
ModelPickerView(models: self.models,
isPlacementEnabled: self.$isPlacementEnabled,
selectedModel: self.$selectedModel)
}
}
}
}
struct ARViewContainer: UIViewRepresentable {
#Binding var modelConfirmedForPlacement: Model?
var anchorEntity = AnchorEntity(world: .init(x: 0, y: 0, z: 0))
func makeUIView(context: Context) -> ARView {
let arView = ARView(frame: .zero)
let config = ARWorldTrackingConfiguration()
config.planeDetection = [.horizontal, .vertical ]
config.environmentTexturing = .automatic
if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh){
config.sceneReconstruction = .mesh
}
arView.session.run(config)
return arView
}
func updateUIView(_ uiView: ARView, context: Context) {
//Here is where I try to remove the old 3D Model before loading the new one
if self.modelConfirmedForPlacement?.modelEntity != nil {
uiView.scene.anchors.first?.removeChild((self.modelConfirmedForPlacement?.modelEntity)!)
anchorEntity.scene?.removeAnchor(anchorEntity)
self.anchorEntity.removeChild((self.modelConfirmedForPlacement?.modelEntity)!)
}
if let model = self.modelConfirmedForPlacement{
if model.modelEntity != nil {
print("ARContainer -> DEBUG: adding model to scene - \(model.modelName)" )
}
else{
print("ARContainer -> DEBUG: Unable to load model entity for - \(model.modelName)" )
}
DispatchQueue.main.async {
let modelEntity = try? Entity.load( named: model.modelName + ".usdz")
anchorEntity.addChild(modelEntity!)
uiView.scene.addAnchor(anchorEntity)
}
}
}
}
struct ModelPickerView: View {
var models: [Model]
#Binding var isPlacementEnabled: Bool
#Binding var selectedModel: Model?
var body: some View {
ScrollView(.horizontal, showsIndicators: false){
HStack(spacing: 30){
ForEach(0 ..< self.models.count , id: \.self){ index in
Button {
// print("DEBUG : selected model with name: \(self.models[index].modelName)")
self.isPlacementEnabled = true
self.selectedModel = self.models[index]
} label: {
Image(uiImage: self.models[index].image)
.resizable()
.frame(height: 80 )
.aspectRatio(1/1, contentMode: .fit)
.background(Color.white)
.cornerRadius(12)
}
.buttonStyle(PlainButtonStyle())
}
}
}
.padding(20)
.background(Color.black.opacity(0.5))
}
}
struct PlacementButtonsView: View {
#Binding var isPlacementEnabled: Bool
#Binding var selectedModel: Model?
#Binding var modelConfirmedForPlacement: Model?
var body: some View {
HStack{
//Cancel button
Button {
// print("DEBUG: model ploacement cancelled")
self.resetPlacementParameters()
} label: {
Image(systemName: "xmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
//Confirm button
Button {
// print("DEBUG: model ploacement confirmed")
self.modelConfirmedForPlacement = self.selectedModel
self.resetPlacementParameters()
} label: {
Image(systemName: "checkmark")
.frame(width: 60, height: 60)
.font(.title)
.background(Color.white.opacity(0.75))
.cornerRadius(30)
.padding(20)
}
}
}
func resetPlacementParameters(){
self.isPlacementEnabled = false
self.selectedModel = nil
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif

SwiftUI makes UIViewRepresentable flicker while updating

I am rewriting the existing UIKit cell with SwiftUI. ChartView inside of it I wrapped with UIViewRepresentable because it is out of scope right now to rewrite it as well. The data for the chart I am getting async and update the view once I have it. The viewModel for the chart I marked as #Published so SwiftUI knows when to update the view. The problem is the view flickers once the update is there.
View:
struct DashboardWatchlistView: View {
#ObservedObject var viewModel: DashboardWatchlistViewModel
var body: some View {
VStack(alignment: .center) {
ZStack {
HStack(spacing: 4) {
HStack(spacing: 16) {
Image(systemName: "person")
VStack(alignment: .leading, spacing: .designSystem(8) {
HStack {
Text(viewModel.leftTitle).font(.subheadline).fontWeight(.semibold).lineLimit(1)
}
Text(viewModel.leftSubtitle).font(.caption2).foregroundColor(Color(ThemeManager.current.neutral50))
}
}
Spacer()
if let chartViewModel = viewModel.chartViewModel {
AssetLineChartViewRepresentable(viewModel: chartViewModel).frame(width: 65, height: 20)
}
VStack(alignment: .trailing, spacing: .designSystem(.extraSmall3)) {
percentChangeView(assetPercentChangeType: viewModel.assetPercentChangeType)
Text(viewModel.rightSubtitle).font(.caption2).foregroundColor(Color(ThemeManager.current.neutral50))
}.padding(.leading, .designSystem(.medium))
}
.padding([.leading, .trailing])
if !viewModel.hideSeparator {
VStack {
Spacer()
Divider().padding(.leading)
}
}
}
}
}
}
ViewModel:
final class DashboardWatchlistViewModel: ObservableObject {
#Published var logoURL: URL?
#Published var leftTitle = ""
#Published var leftSubtitle = ""
#Published var rightTitle = ""
#Published var rightSubtitle = ""
#Published var percentageChange: Decimal = .zero
#Published var placeholderColor = ""
#Published var corners: Corners = .none
#Published var assetPercentChangeType: AssetPercentChangeType = .zero
#Published var hideSeparator = false
#Published var isHighlighted = false
#Published var chartViewModel: AssetLineChartViewRepresentable.ViewModel?
let chartDataEvent: AnyPublisher<BPChartData?, Never>
init(tradable: Tradable,
priceString: String,
percentageChange: Decimal,
corners: Corners,
hideSeparator: Bool,
assetPercentChangeType: AssetPercentChangeType,
chartDataEvent: AnyPublisher<BPChartData?, Never>) {
self.logoURL = tradable.logoURL
self.leftTitle = tradable.name
self.rightTitle = PercentageFormatter.default.string(from: abs(percentageChange))
self.leftSubtitle = tradable.symbol
self.rightSubtitle = priceString
self.percentageChange = percentageChange
self.placeholderColor = tradable.placeholderColor ?? ""
self.corners = corners
self.chartDataEvent = chartDataEvent
self.hideSeparator = hideSeparator
self.assetPercentChangeType = assetPercentChangeType
}
}
Cell where I set the chartViewModel:
final class DashboardWatchlistCell: UICollectionViewCell, HostingCell {
var hostingController: UIHostingController<DashboardWatchlistView>?
private var dashboardWatchlistView: DashboardWatchlistView?
private var viewModel: DashboardWatchlistViewModel?
private var cancellables = Set<AnyCancellable>()
override var isHighlighted: Bool {
didSet {
viewModel?.isHighlighted = isHighlighted
}
}
func configure(with viewModel: DashboardWatchlistViewModel) {
self.dashboardWatchlistView = DashboardWatchlistView(viewModel: viewModel)
self.viewModel = viewModel
// HERE I SET THE VIEWMODEL
viewModel.chartDataEvent.compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { chartData in
viewModel.chartViewModel = AssetLineChartViewRepresentable.ViewModel(chartData: chartData, percentageChange: viewModel.percentageChange)
}
.store(in: &cancellables)
}
func addHostedView(to parent: UIViewController) {
guard let dashboardWatchlistView = dashboardWatchlistView else { return }
addHostedView(dashboardWatchlistView, to: parent)
backgroundColor = .clear
}
}

Display function output live without Button press

My Swift UI code currently calls a function to display calculations upon a button call. I'd like to display the function's output without the button call (in other words, the function is "live" and constantly calculating anytime a necessary variable is changed). Basically, I'm looking to get rid of the button that triggers this function call calculation, and always have the function's display shown. It has default values so it should have info even before the user inputs or something is changed.
The first screenshot shows the code currently, and the second shows where I'd like the time calculation string to always be. Note: this uses a Create ML file, so if you're inputting this code into your editor, it's not necessary to have the model use to calculate. Any use and output of the variables will do and I've left some commented code that might help.
I'm thinking there might be a calculate on change of X, Y, Z variable needed here. I'm not sure the best way to approach this and would love any ideas. Thanks!
import CoreML
import SwiftUI
struct ContentView: View {
#State var wakeUpTime = defaultWakeTime
#State var coffeeAmount = 1.0
#State var sleepAmount = 8.0
#State var alertTitle = ""
#State var alertMessage = ""
#State var showAlert = false
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date.now
}
var body: some View {
NavigationView {
Form {
Section {
DatePicker("Please enter a time", selection: $wakeUpTime, displayedComponents: .hourAndMinute)
.labelsHidden()
} header: {
Text("When do you want to wake up?")
.font(.headline)
}
VStack(alignment: .leading, spacing: 0) {
Text("Hours of sleep?")
.font(.headline)
Stepper(sleepAmount == 1 ? "1 hour" : "\(sleepAmount.formatted()) hours", value: $sleepAmount, in: 1...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0) {
Text("Cups of coffee?")
.font(.headline)
Stepper(coffeeAmount == 1 ? "1 cup" : "\(coffeeAmount.formatted()) cups", value: $coffeeAmount, in: 1...12, step: 0.25)
}
Section {
Text("Head to bed at: IDEAL TIME HERE")
}
}
.navigationTitle("BetterRest")
.toolbar {
Button("Calculate", action: calculateBedtime)
}
.alert(alertTitle, isPresented: $showAlert) {
Button("Ok") { }
} message: {
Text(alertMessage)
}
}
}
func calculateBedtime() {
do {
let config = MLModelConfiguration()
let model = try SleepCalculator(configuration: config)
let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUpTime)
let hour = (components.hour ?? 0) * 60 * 60
let minute = (components.minute ?? 0) * 60
let predicition = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
let sleepTime = wakeUpTime - predicition.actualSleep
alertTitle = "Your ideal bedtime is..."
alertMessage = sleepTime.formatted(date: .omitted, time: .shortened)
}
catch {
alertTitle = "Error"
alertMessage = "Sorry. There was a problem calculating your bedtime."
}
showAlert = true
// IF TRYING WITHOUT CREATE ML MODEL, comment out all of above^
// let alertTitle = "Showing calculated title"
// let alertMessage = "7:15 am"
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
you could try this approach, where you create a class BedTimeModel: ObservableObject to
monitor changes in the various variables that is used to calculate (dynamically)
your sleepTime using func calculateBedtime().
EDIT-1: using Optional sleepTime
class BedTimeModel: ObservableObject {
#Published var sleepTime: Date? = Date() // <-- here optional
#Published var wakeUpTime = defaultWakeTime {
didSet { calculateBedtime() }
}
#Published var coffeeAmount = 1.0 {
didSet { calculateBedtime() }
}
#Published var sleepAmount = 8.0 {
didSet { calculateBedtime() }
}
// can also change this to return the calculated value and use it to update the `sleepTime`
func calculateBedtime() {
// do {
// let config = MLModelConfiguration()
// let model = try SleepCalculator(configuration: config)
// let components = Calendar.current.dateComponents([.hour, .minute], from: wakeUpTime)
// let hour = (components.hour ?? 0) * 60 * 60
// let minute = (components.minute ?? 0) * 60
// let predicition = try model.prediction(wake: Double(hour + minute), estimatedSleep: sleepAmount, coffee: Double(coffeeAmount))
//
// sleepTime = wakeUpTime - predicition.actualSleep // <-- here
// }
// catch {
// sleepTime = nil // <-- here could not be calculated
// }
// for testing, adjust the real calculation to update sleepTime
sleepTime = wakeUpTime.addingTimeInterval(36000 * (sleepAmount + coffeeAmount))
}
static var defaultWakeTime: Date {
var components = DateComponents()
components.hour = 7
components.minute = 0
return Calendar.current.date(from: components) ?? Date.now
}
}
struct ContentView: View {
#StateObject private var vm = BedTimeModel() // <-- here
var body: some View {
NavigationView {
Form {
Section {
DatePicker("Please enter a time", selection: $vm.wakeUpTime, displayedComponents: .hourAndMinute)
.labelsHidden()
} header: {
Text("When do you want to wake up?").font(.headline)
}
VStack(alignment: .leading, spacing: 0) {
Text("Hours of sleep?").font(.headline)
Stepper(vm.sleepAmount == 1 ? "1 hour" : "\(vm.sleepAmount.formatted()) hours", value: $vm.sleepAmount, in: 1...12, step: 0.25)
}
VStack(alignment: .leading, spacing: 0) {
Text("Cups of coffee?").font(.headline)
Stepper(vm.coffeeAmount == 1 ? "1 cup" : "\(vm.coffeeAmount.formatted()) cups", value: $vm.coffeeAmount, in: 1...12, step: 0.25)
}
Section {
// -- here
if let stime = vm.sleepTime {
Text("Head to bed at: \(stime.formatted(date: .omitted, time: .shortened))")
} else {
Text("There was a problem calculating your bedtime.")
}
}
}
.navigationTitle("BetterRest")
}
}
}

Blinking symbol with didSet in SwiftUI

This is synthesized from a much larger app. I'm trying to blink an SF symbol in SwiftUI by activating a timer in a property's didSet. A print statement inside timer prints the expected value but the view doesn't update.
I'm using structs throughout my model data and am guessing this will have something to do with value vs. reference types. I'm trying to avoid converting from structs to classes.
import SwiftUI
import Combine
#main
struct TestBlinkApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class Model: ObservableObject {
#Published var items: [Item] = []
static var loadData: Model {
let model = Model()
model.items = [Item("Item1"), Item("Item2"), Item("Item3"), Item("Item4")]
return model
}
}
struct Item {
static let ledBlinkTimer: TimeInterval = 0.5
private let ledTimer = Timer.publish(every: ledBlinkTimer, tolerance: ledBlinkTimer * 0.1, on: .main, in: .default).autoconnect()
private var timerSubscription: AnyCancellable? = nil
var name: String
var isLEDon = false
var isLedBlinking = false {
didSet {
var result = self
print("in didSet: isLedBlinking: \(result.isLedBlinking) isLEDon: \(result.isLEDon)")
guard result.isLedBlinking else {
result.isLEDon = true
result.ledTimer.upstream.connect().cancel()
print("Cancelling timer.")
return
}
result.timerSubscription = result.ledTimer
.sink { _ in
result.isLEDon.toggle()
print("\(result.name) in ledTimer isLEDon: \(result.isLEDon)")
}
}
}
init(_ name: String) {
self.name = name
}
}
struct ContentView: View {
#StateObject var model = Model.loadData
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.foregroundColor(model.items[0].isLEDon ? .green : color)
Button("Toggle") {
model.items[0].isLedBlinking.toggle()
}
}
.foregroundColor(color)
}
}
Touching the "Toggle" button starts the timer that's suppose to blink the circle. The print statement shows the value changing but the view doesn't update. Why??
You can use animation to make it blink, instead of a timer.
The model of Item gets simplified, you just need a boolean variable, like this:
struct Item {
var name: String
// Just a toggle: blink/ no blink
var isLedBlinking = false
init(_ name: String) {
self.name = name
}
}
The "hard work" is done by the view: changing the variable triggers or stops the blinking. The animation does the magic:
struct ContentView: View {
#StateObject var model = Model.loadData
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
.padding()
// Change based on isLedBlinking
Image(systemName: model.items[0].isLedBlinking ? "circle.fill" : "circle")
.font(.largeTitle)
.foregroundColor(model.items[0].isLedBlinking ? .green : color)
// Animates the view based on isLedBlinking: when is blinking, blinks forever, otherwise does nothing
.animation(model.items[0].isLedBlinking ? .easeInOut.repeatForever() : .default, value: model.items[0].isLedBlinking)
.padding()
Button("Toggle: \(model.items[0].isLedBlinking ? "Blinking" : "Still")") {
model.items[0].isLedBlinking.toggle()
}
.padding()
}
.foregroundColor(color)
}
}
A different approach with a timer:
struct ContentView: View {
#StateObject var model = Model.loadData
let timer = Timer.publish(every: 0.25, tolerance: 0.1, on: .main, in: .common).autoconnect()
let color = Color(UIColor.label)
public var body: some View {
VStack {
Text(model.items[0].name)
if model.items[0].isLedBlinking {
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.onReceive(timer) { _ in
model.items[0].isLEDon.toggle()
}
.foregroundColor(model.items[0].isLEDon ? .green : color)
} else {
Image(systemName: model.items[0].isLEDon ? "circle.fill" : "circle")
.foregroundColor(model.items[0].isLEDon ? .green : color)
}
Button("Toggle: \(model.items[0].isLedBlinking ? "Blinking" : "Still")") {
model.items[0].isLedBlinking.toggle()
}
}
.foregroundColor(color)
}
}

I create an instance of 3 different view models and assign each to a state object. How do I place this into a dispatch queue?

I have a Swift program that works as desired. I have 3 view models that each call a separate model. Each model calls a function that reads a separate large CSV file, performs some manipulation and returns a data frame. This takes some time and I would like to speed things up.
Swift offers a DispatchQueue that allows one to place code into an asynchronous global queue with QOS and I believe if I ran the creation of the view models in this fashion, I would display the initial view sooner.
The problem is: I have no idea how to incorporate it. Any help to point me in the right direction will be appreciated.
Below is my content view, one view model, and one model. The test dispatch queue code at the end runs successfully in a playground.
struct ContentView: View {
#StateObject var vooVM: VOOViewModel = VOOViewModel()
#StateObject var vfiaxVM: VFIAXViewModel = VFIAXViewModel()
#StateObject var principalVM: PrincipalViewModel = PrincipalViewModel()
#State private var selectedItemId: Int?
var body: some View {
NavigationView {
VStack (alignment: .leading) {
List {
Spacer()
.frame(height: 20)
Group {
Divider()
NavigationLink(destination: Summary(vooVM: vooVM, vfiaxVM: vfiaxVM, prinVM: principalVM), tag: 1, selection: $selectedItemId, label: {
HStack {
Image(systemName: "house")
.padding(.leading, 10)
.padding(.trailing, 0)
.padding(.bottom, 5)
Text("Summary")
.bold()
.padding(.bottom, 2)
} // end h stack
})
} // end group
NavigationLinks(listText: "VOO", dataFrame1: vooVM.timeSeriesDailyDF1, dataFrame5: vooVM.timeSeriesDailyDF5)
NavigationLinks(listText: "VFIAX", dataFrame1: vfiaxVM.timeSeriesDailyDF1, dataFrame5: vfiaxVM.timeSeriesDailyDF5)
NavigationLinks(listText: "Principal", dataFrame1: principalVM.timeSeriesDailyDF1, dataFrame5: principalVM.timeSeriesDailyDF5)
Divider()
Spacer()
} // end list
} // end v stack
} // end navigation view
.onAppear {self.selectedItemId = 1}
.navigationTitle("Stock Data")
.frame(width: 1200, height: 900, alignment: .center)
} // end body view
} // end content view
View Model
class VOOViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
var timeSeriesDailyDF1: DataFrame {
return vooModel.vooDF.0
}
var timeSeriesDailyDF5: DataFrame {
return vooModel.vooDF.1
}
var symbol: String {
return vooModel.symbol
}
var currentShares: Double {
return vooModel.currentShares
}
var currentSharePrice: Double {
let lastRowIndex: Int = vooModel.vooDF.0.shape.rows - 1
let currentPrice: Double = (vooModel.vooDF.0[row: lastRowIndex])[1] as! Double
return currentPrice
}
var percentGain: Double {
let pastValue: Double = (vooModel.vooDF.0[row: 0])[1] as! Double
let numRows: Int = vooModel.vooDF.0.shape.rows - 1
let curValue: Double = (vooModel.vooDF.0[row: numRows])[1] as! Double
let oneYearGain: Double = (100 * (curValue - pastValue)) / pastValue
return oneYearGain
}
}
Model
struct VOOModel {
var vooDF = GetDF(fileName: "FormattedVOO")
let symbol: String = "VOO"
let currentShares: Double = 1
}
Playground Code
let myQue = DispatchQueue.global()
let myGroup = DispatchGroup()
myQue.async(group: myGroup) {
sleep(5)
print("Task 1 complete")
}
myQue.async(group: myGroup) {
sleep(3)
print("Task 2 complete")
}
myGroup.wait()
print("All tasks completed")
I was able to solve my problem by using only 1 viewmodel instead of 3. The viewmodel calls all three models which were modified such that their function call to read a CSV file and place it into a dataframe is contained in a function. This function is in turn called within a function in the viewmodel which is called in the viewmodels init. Below is the updated code. Note that the ContentView was simplified to make testing easy.
New Content View:
struct ContentView: View {
#StateObject var viewModel: ViewModel = ViewModel()
var body: some View {
let printValue1 = (viewModel.dataFrames.0.0[row: 0])[0]
let tempValue = (viewModel.dataFrames.0.0[row: 0])[1] as! Double
let tempValueFormatted: String = String(format: "$%.2f", tempValue)
Text("\(dateToStringFormatter.string(from: printValue1 as! Date))" + " " + tempValueFormatted )
.frame(width: 1200, height: 900, alignment: .center)
}
}
New ViewModel:
class ViewModel: ObservableObject {
#Published private var vooModel: VOOModel = VOOModel()
#Published private var vfiaxModel: VFIAXModel = VFIAXModel()
#Published private var principalModel: PrincipalModel = PrincipalModel()
var dataFrames = ((DataFrame(), DataFrame()), (DataFrame(), DataFrame()), (DataFrame(), DataFrame()))
init() {
self.dataFrames = GetDataFrames()
}
func GetDataFrames() -> ((DataFrame, DataFrame), (DataFrame, DataFrame), (DataFrame, DataFrame)) {
let myQue: DispatchQueue = DispatchQueue.global()
let myGroup: DispatchGroup = DispatchGroup()
var vooDF = (DataFrame(), DataFrame())
var vfiaxDF = (DataFrame(), DataFrame())
var principalDF = (DataFrame(), DataFrame())
myQue.async(group: myGroup) {
vfiaxDF = self.vfiaxModel.GetData()
}
myQue.async(group: myGroup) {
principalDF = self.principalModel.GetData()
}
myQue.async(group: myGroup) {
vooDF = self.vooModel.GetData()
}
myGroup.wait()
return (vooDF, vfiaxDF, principalDF)
}
}
One of the new models. The other 2 are identical except for the CSV file they read.
struct VOOModel {
let symbol: String = "VOO"
let currentShares: Double = 1
func GetData() -> (DataFrame, DataFrame) {
let vooDF = GetDF(fileName: "FormattedVOO")
return vooDF
}
}