1

I have 3 views. Content View, TrainingView and TrainingList View. I want to list exercises from Core Data but also I want to make some changes without changing data.

In ContentView; I am trying to fetch data with CoreData

struct ContentView: View {
    // MARK: - PROPERTY
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Training.timestamp, ascending: false)],
        animation: .default)
    private var trainings: FetchedResults<Training>
    
    @State private var showingAddProgram: Bool = false
    
    // FETCHING DATA
    
    
    // MARK: - FUNCTION
    
    // MARK: - BODY
    
    var body: some View {
        NavigationView {
            Group {
                VStack {
                    HStack {
                        Text("Your Programs")
                        Spacer()
                        Button(action: {
                            self.showingAddProgram.toggle()
                        }) {
                            Image(systemName: "plus")
                        }
                        .sheet(isPresented: $showingAddProgram) {
                            AddProgramView()
                        }
                        
                    } //: HSTACK
                    .padding()
                    List {
                        ForEach(trainings) { training in
                            TrainingListView(training: training)
                        }
                    } //: LIST
                    Spacer()
                } //: VSTACK
            } //: GROUP
            .navigationTitle("Good Morning")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: {
                        print("test")
                    }) {
                        Image(systemName: "key")
                    }
                }
            } //: TOOLBAR
            .onAppear() {
                
            }
        } //: NAVIGATION
    }
    
    private func showId(training: Training) {
        guard let id = training.id else { return }
        print(id)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

In TrainingView; I am getting exercises as a array list and I am pushing into to TrainingListView.

import SwiftUI

struct TrainingView: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    
    @State var training: Training
    @State var exercises: [Exercise]
    @State var tempExercises: [Exercise] = [Exercise]()
    @State var timeRemaining = 0
    @State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    @State var isTimerOn = false

    var body: some View {
        VStack {
            HStack {
                Text("\(training.name ?? "")")
                Spacer()
                Button(action: {
                    presentationMode.wrappedValue.dismiss()
                }) {
                    Text("Finish")
                }
            }
            .padding()
            
            ZStack {
                Circle()
                    .fill(Color.blue)
                    .frame(width: 250, height: 250)
                Circle()
                    .fill(Color.white)
                    .frame(width: 240, height: 240)
                Text("\(timeRemaining)s")
                    .font(.system(size: 100))
                    .fontWeight(.ultraLight)
                    .onReceive(timer) { _ in
                        if isTimerOn {
                            if timeRemaining > 0 {
                                timeRemaining -= 1
                            } else {
                                isTimerOn.toggle()
                                stopTimer()
                                removeExercise()
                            }
                        }
                    }
            }
            Button(action: {
                startResting()
            }) {
                if isTimerOn {
                    Text("CANCEL")
                } else {
                    Text("GIVE A BREAK")
                }
            }
            Spacer()
            ExerciseListView(exercises: $tempExercises)
        }
        .navigationBarHidden(true)
        .onAppear() {
            updateBigTimer()
        }
    }
    
    private func startResting() {
        tempExercises = exercises
        
        if let currentExercise: Exercise = tempExercises.first {
            timeRemaining = Int(currentExercise.rest)
            startTimer()
            isTimerOn.toggle()
        }
    }
    
    private func removeExercise() {
        if let currentExercise: Exercise = tempExercises.first {
            if Int(currentExercise.rep) == 1 {
                let index = tempExercises.firstIndex(of: currentExercise) ?? 0
                tempExercises.remove(at: index)
            } else if Int(currentExercise.rep) > 1 {
                currentExercise.rep -= 1
                let index = tempExercises.firstIndex(of: currentExercise) ?? 0
                tempExercises.remove(at: index)
                tempExercises.insert(currentExercise, at: index)
            }
            updateBigTimer()
        }
    }
    
    private func updateBigTimer() {
        timeRemaining = Int(tempExercises.first?.rest ?? 0)
    }
    
    private func stopTimer() {
        timer.upstream.connect().cancel()
    }
    
    private func startTimer() {
        timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    }
}

struct TrainingView_Previews: PreviewProvider {
    static var previews: some View {
        TrainingView(training: Training(), exercises: [Exercise]())
    }
}

In TrainingListView; I am listing all exercises.

struct TrainingListView: View {
    
    @ObservedObject var training: Training
    @Environment(\.managedObjectContext) private var managedObjectContext
    
    var body: some View {
        NavigationLink(destination: TrainingView(training: training, exercises: training.exercises?.toArray() ?? [Exercise]())) {
            HStack {
                Text("\(training.name ?? "")")
                Text("\(training.exercises?.count ?? 0) exercises")
            }
        }
    }
}

Also, I am adding video: https://twitter.com/huseyiniyibas/status/1388571724346793986

What I want to do is, when user taps any Training Exercises List should refreshed. It should be x5 again like in the beginning.

2
  • 1
    What exactly is the issue here? Could you clarify your question? Commented May 1, 2021 at 19:49
  • @JoakimDanielson I added extra resources. Commented May 1, 2021 at 20:33

1 Answer 1

2

I had a hard time understanding your question but I guess I got the idea.

My understanding is this:

  1. You want to store the rep count in the Core Data. (Under Training > Exercises)
  2. You want to count down the reps one by one as the user completes the exercise.
  3. But you don't want to change the original rep count stored in the Core Data.

I didn't run your code since I didn't want to recreate all the models and Core Data files. I guess I've spotted the problem. Here I'll explain how you can solve it:

The Core Data models are classes (reference types). When you pass around the classes (as you do in your code) and change their properties, you change the original data. In your case, you don't want that.

(Btw, being a reference type is a very useful and powerful property of classes. Structs and enums are value types, i.e. they are copied when passed around. The original data is unchanged.)

You have several options to solve your problem:

  1. Just generate a different struct (something like ExerciseDisplay) from Exercise, and pass ExerciseDisplay to TrainingView.

  2. You can write an extension to Exercise and "copy" the model before passing it to TrainingView. For this you'll need to implement the NSCopying protocol.

extension Exercise: NSCopying {
  
  func copy(with zone: NSZone? = nil) -> Any {
    return Exercise(...)
  }
}

But before doing this I guess you'll need to change the Codegen to Manual/None of your entry in your .xcdatamodeld file. This is needed when you want to create the attributes manually. I'm not exactly sure how you can implement NSCopying for a CoreDate model, but it's certainly doable.


The first approach is easier but kinda ugly. The second is more versatile and elegant, but it's also more advanced. Just try the first approach first and move to the second once you feel confident.


Update: This is briefly how you can implement the 1st approach:

struct ExerciseDisplay: Identifiable, Equatable {
    public let id = UUID()
    public let name: String
    public var rep: Int
    public let rest: Int
}

struct TrainingView: View {

    // Other properties and states etc.

    let training: Training
    @State var exercises: [ExerciseDisplay] = []

    init(training: Training) {
        self.training = training
    }

    var body: some View {
        VStack {
        // Views
        }
        .onAppear() {
            let stored: [Exercise] = training.exercises?.toArray() ?? []
            self.exercises = stored.map { ExerciseDisplay(name: $0.name ?? "", rep: Int($0.rep), rest: Int($0.rest)) }
        }
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

@Hüseyin İyibaş could you please mark as an accepted if it is correct because of others benefits?
@emrcftci yes this is correct and I accepted.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.