Home > Software engineering >  "xx" is get-only property - How to Assign decoded Json Data to class (nested view model) S
"xx" is get-only property - How to Assign decoded Json Data to class (nested view model) S

Time:02-14

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

  1. Watch App launches - loading empty Parent View Model (ReceivedDayProgViewModel) into the app and showing activity indicator in WatchLoadingView.
  2. WatchLoadingView set-up observer to receive actual json data/file from iPhone
  3. WatchLoadingView'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.

  1. Assign decoded object (let ReceivedDayProgViewModel = try! decoder.decode(ReceivedDayProgViewModel.self, from: ReceivedDayProgVMData)) (with values/data from iPhone) into the @StateObject in ContentView (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)
            }

        }
    }
}
  • Related