I have a watchapp that receives parent view model (nested view model) data from iPhone via json/file transfer. I am using "Communicator" library by @KaneCheshire.
The data flow is
- Watch App launches - loading empty Parent View Model (
ReceivedDayProgViewModel
) into the app and showing activity indicator inWatchLoadingView
. WatchLoadingView
set-up observer to receive actual json data/file from iPhoneWatchLoadingView
's observer receives actual json data, decodes them into ParentViewModel object (ReceivedDayProgViewModel
)
After step 3, I am having difficulty actually assigning the decoded object to the initially empty Parent View Model (ReceivedDayProgViewModel
). There is a error message saying "**Cannot assign to property: 'receivedDayProgramVM' is a get-only property**
"
So I did look up the error and have an idea that my parent View Model is actually a "computed-property" instead of stored property - How do I resolve this build issue - cannot assign to property: 'date' is a get only property But I am still struggling to add codes to ensure "set/write" function is embedded in the Parent View Model(ReceivedDayProgViewModel
).
Would appreciate any guidance/advice on how I can change my code to allow "set/write" operation in Parent View Model (ReceivedDayProgViewModel
) so below step can be fulfilled.
- Assign decoded object (
let ReceivedDayProgViewModel = try! decoder.decode(ReceivedDayProgViewModel.self, from: ReceivedDayProgVMData)
) (with values/data from iPhone) into the@StateObject
inContentView
(receivedDayProgramVM
)
Parent View Model
import Foundation
import Combine
class ReceivedDayProgViewModel: ObservableObject, Codable {
@Published var workoutModel = WorkoutModel()
@Published var watchDayProgramSequence: Int = 0
@Published var exerciseVM: [WatchExerciseViewModel] = []
enum CodingKeys: String, CodingKey {
case dayProgramSeq
case exerciseVM
}
init() {
//initiliazed empty view model via this function
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
watchDayProgramSequence = try container.decode(Int.self, forKey: .dayProgramSeq)
exerciseVM = try container.decode([WatchExerciseViewModel].self, forKey: .exerciseVM)
do {
watchDayProgramSequence = try container.decode(Int.self, forKey: .dayProgramSeq)
} catch DecodingError.typeMismatch {
do {
watchDayProgramSequence = try Int(container.decode(String.self, forKey: .dayProgramSeq)) ?? 0
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(watchDayProgramSequence, forKey: .dayProgramSeq)
try container.encode(exerciseVM, forKey: .exerciseVM)
}//encode function ends
}
WatchApp
import SwiftUI
import Communicator
@main
struct WatchApp: App {
@SceneBuilder var body: some Scene {
WindowGroup {
NavigationView {
ContentView(selectedTab: 0, receivedDayProgramVM: ReceivedDayProgViewModel())
}
}
WKNotificationScene(controller: NotificationController.self, category: "myCategory")
}
}
ContentView
import SwiftUI
import WatchKit
import Combine
import Communicator
struct ContentView: View {
@State var selectedTab = 0
@StateObject var receivedDayProgramVM = ReceivedDayProgViewModel()
var body: some View {
NavigationView {
//Show WatchLoadingView, if dayprogramVM is being fetched
if receivedDayProgramVM.exerciseVM.count == 0 {
WatchLoadingView()
.onAppear {
//Add Communicator functions if required
Blob.observe { Blob in
if Blob.identifier == "dayProgramData" {
let ReceivedDayProgVMData = Blob.content
print("WatchConnectivity blob setData printing \(ReceivedDayProgVMData)")
let decoder = JSONDecoder()
let ReceivedDayProgViewModel = try! decoder.decode(ReceivedDayProgViewModel.self, from: ReceivedDayProgVMData)
//Assign decoded class(parent view model) to environment object in contentView
//This is the line that causes error showing receivedDayProgramVM is get-only property
receivedDayProgramVM = ReceivedDayProgViewModel
} //Blob for "dayProgramData" code ends
} //Blob observation code ends
} else {
TabView(selection: $selectedTab) {
WatchControlView().id(0)
SetInformationView().id(1)
SetRestDetailView().id(2)
//Check if WatchControlView can be vertically scrollable
NowPlayingView().id(3)
}
.environmentObject(receivedDayProgramVM)
}
}
}
}
WatchLoadingView
import SwiftUI
import Communicator
struct WatchLoadingView: View {
var body: some View {
GeometryReader { geometry in
let rect = geometry.frame(in: .global)
VStack {
ProgressView()
Text("Fetching Data...")
.font(.body)
}
}
}
}
CodePudding user response:
I am posting my answer after getting the right clue from Joakim & Ptit's comments. As per their advice, I have separated (data) model and view model for the watch app.
It was a little time-consuming, but I managed to update the app design, and it enabled me to store transferred data into appropriate view model class.
See below code extract for future reference.
WatchDayProgram Struct (Data Model)
import Foundation
import Combine
struct WatchDayProgram: Codable {
var watchDayProgramSequence: Int = 0
var exerciseVM: [WatchExercise]
enum CodingKeys: String, CodingKey {
case dayProgramSeq
case exerciseVM
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
watchDayProgramSequence = try container.decode(Int.self, forKey: .dayProgramSeq)
exerciseVM = try container.decode([WatchExercise].self, forKey: .exerciseVM)
do {
watchDayProgramSequence = try container.decode(Int.self, forKey: .dayProgramSeq)
} catch DecodingError.typeMismatch {
do {
watchDayProgramSequence = try Int(container.decode(String.self, forKey: .dayProgramSeq)) ?? 0
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(watchDayProgramSequence, forKey: .dayProgramSeq)
try container.encode(exerciseVM, forKey: .exerciseVM)
}//encode function ends
}
WatchDayProgViewModel
import Foundation
import Combine
class ReceivedDayProgViewModel: ObservableObject {
@Published var workoutModel = WorkoutModel()
@Published var watchDayProgramSequence: Int = 0
@Published var exerciseVM: [WatchExerciseViewModel] = []
init() {
//initiliazed empty view model via this function, so empty view model class can be loaded onto Watch app when it starts while awaiting for the file transfer to be complete
}
}
ContentView
import SwiftUI
import WatchKit
import Combine
import Communicator
struct ContentView: View {
@State var selectedTab = 0
@StateObject var receivedDayProgramVM = ReceivedDayProgViewModel()
var body: some View {
NavigationView {
//Show WatchLoadingView, if dayprogramVM is being fetched
if receivedDayProgramVM.exerciseVM.isEmpty == true {
WatchLoadingView()
.onAppear {
Blob.observe { Blob in
if Blob.identifier == "dayProgramData" {
let ReceivedDayProgVMData = Blob.content
print("WatchConnectivity blob setData printing \(ReceivedDayProgVMData)")
let decoder = JSONDecoder()
let ReceivedDayProgViewModel = try! decoder.decode(WatchDayProgram.self, from: ReceivedDayProgVMData)
for (i, exercise) in ReceivedDayProgViewModel.exerciseVM.enumerated() {
DispatchQueue.main.async {
receivedDayProgramVM.exerciseVM.append(WatchExerciseViewModel(exercise: exercise))
}
} //Blob for "dayProgramData" code ends
} //Blob observation code ends
} else {
TabView(selection: $selectedTab) {
WatchControlView().id(0)
SetInformationView().id(1)
SetRestDetailView().id(2)
//Check if WatchControlView can be vertically scrollable
NowPlayingView().id(3)
}
.environmentObject(receivedDayProgramVM)
}
}
}
}