SwiftUI: Best approach for fetching data and passing to SubViews? - ios

I have a parent view DetailView and I want to query for heart rates during a workout. After I get back the heart rates I want to pass them to a child View. The code below, the heart rates aren't getting passed to HeartRateRecoveryCard ?
import SwiftUI
import HealthKit
struct DetailView: View {
#State var heartRateSamples: [HKQuantitySample] = []
var body: some View {
HeartRateRecoveryCard(heartRateSamples: heartRateSamples)
.onAppear {
getHeartRatesFromWorkout()
}
}
private func getHeartRatesFromWorkout() {
guard let workout = SelectedWorkoutSingleton.sharedInstance.selectedWorkout else { return }
print("workout = \(workout.startDate)")
WorkoutManager.getHeartRateSamplesFrom(workout: workout) { (samples, error) in
guard let unwrappedSamples = samples else {
print("no HR samples")
return }
if let unwrappedError = error {
print("error attempting to get heart rates = \(unwrappedError)")
}
print("we have \(unwrappedSamples.count) heart rates")
DispatchQueue.main.async {
heartRateSamples = unwrappedSamples
}
}
}
}
struct HeartRateRecoveryCard: View {
//Don't download HR samples for this view the parent view should download HR samples and pass them into it's children views
#State var heartRateSamples: [HKQuantitySample]
#State var HRRAnchor = 0
#State var HRRTwoMinutesLater = 0
#State var heartRateRecoveryValue = 0
var body: some View {
VStack(alignment: .leading) {
Text("\(heartRateRecoveryValue) bpm")
Text("\(HRRAnchor) bpm - \(heartRateRecoveryValue) bpm")
SwiftUILineChart(chartLabelName: "Heart Rate Recovery", entries: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).0, xAxisFormatter: convertHeartRatesToLineChartDataAndSetFormatter(heartRates: heartRateSamples).1)
.frame(width: 350, height: 180, alignment: .center)
.onAppear {
print("heart rate count = \(heartRateSamples.count)")
if let unwrappedHRRObject = HeartRateRecoveryManager.calculateHeartRateRecoveryForHRRGraph(heartRateSamples: heartRateSamples) {
HRRAnchor = unwrappedHRRObject.anchorHR
HRRTwoMinutesLater = unwrappedHRRObject.twoMinLaterHR
heartRateRecoveryValue = unwrappedHRRObject.hrr
}
}
}
}

Related

How do I get a number from a counter to add onto a total and upload the total to the Firebase database, and retrieve that in another view?

So I'm working on a fitness app using SwiftUI that uses vision to locate joints on a persons body and counts the number of repetitions done during a squat through the phones camera.
In the "CamView" I have a squat counter that goes up each time a squat is done properly. What I want to do is upload the number from the squat counter (when the user presses the finish button) to a new var maybe like "totalSquats" and upload this to the Firebase database. After this I want to retrieve the total squats for a user from the database to a view called "StatisticsView". I also want that when the user goes back to do the exercise again, the squat counter will start at 0 again and add onto the number in the "totalSquats". But I'm not really sure how to do it. Does any one know how to do this or does anyone have like a good example so I can follow?
Here is how my CamView looks Where my squat counter is on CamView
What I have done so far:
I used #AppStorage to save the counter and I was able to display it in another view but the issues with this were that the counter in the "CamView" would not start at 0 but at the number that it was last left out + it also stored the counter for each user instead of it being unique to a certain user.
To test out the database I have also created a page with a TextField where I can type in a number and that will save to the database UNIQUE to a user using a userID.
CamView - This is my camera view
struct CamView: View {
#StateObject var poseEstimate = PoseEstimate()
// var squatCount = 0
var body: some View {
VStack {
ZStack {
GeometryReader { geo in
CameraView(poseEstimate: poseEstimate)
StickFigureView(poseEstimate: poseEstimate, size: geo.size)
}
}.frame(width: UIScreen.main.bounds.size.width, height:
UIScreen.main.bounds.size.width * 1920 / 1080, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/)
HStack {
Text("Squat counter:")
.font(.title)
Text(String(poseEstimate.squatCount))
.font(.title)
Image(systemName: "exclamationmark.triangle.fill")
.font(.largeTitle)
.foregroundColor(Color.red)
.opacity(poseEstimate.isGoodPosture ? 0.0 : 1.0)
Button{
} label: {
Text("Finish Exercise")
}
}
}
}
}
#Here is my class where I estimate the pose
class PoseEstimate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, ObservableObject {
let sequenceHandler = VNSequenceRequestHandler()
#Published var bodyParts = [VNHumanBodyPoseObservation.JointName : VNRecognizedPoint]()
var wasInBottomPosition = false
// #AppStorage("NUMBER_KEY") var squatCount = 0
#Published var squatCount = 0
#Published var armRaiseCount = 0
#Published var isGoodPosture = true
var subscriptions = Set<AnyCancellable>()
}
#Here is a function where I get the "squatCount" to increase by one when a squat is done
func countSquats(bodyParts: [VNHumanBodyPoseObservation.JointName : VNRecognizedPoint]) {
let rightKnee = bodyParts[.rightKnee]!.location
let leftKnee = bodyParts[.leftKnee]!.location
let rightHip = bodyParts[.rightHip]!.location
let rightAnkle = bodyParts[.rightAnkle]!.location
let leftAnkle = bodyParts[.leftAnkle]!.location
let firstAngle = atan2(rightHip.y - rightKnee.y, rightHip.x - rightKnee.x)
let secondAngle = atan2(rightAnkle.y - rightKnee.y, rightAnkle.x - rightKnee.x)
var angleDiffRadians = firstAngle - secondAngle
while angleDiffRadians < 0 {
angleDiffRadians += CGFloat(2 * Double.pi)
}
let angleDiffDegrees = Int(angleDiffRadians * 180 / .pi)
if angleDiffDegrees > 150 && self.wasInBottomPosition {
self.squatCount += 1
self.wasInBottomPosition = false
}
let hipHeight = rightHip.y
let kneeHeight = rightKnee.y
if hipHeight < kneeHeight {
self.wasInBottomPosition = true
}
let kneeDistance = rightKnee.distance(to: leftKnee)
let ankleDistance = rightAnkle.distance(to: leftAnkle)
if ankleDistance > kneeDistance {
self.isGoodPosture = false
} else {
self.isGoodPosture = true
}
}
#StatisticsView - This is where I want the total Squats to display and this is where I can type in a number and save it to the database
struct StatisticsView: View{
#State private var squats = 0
#StateObject var poseEstimate = PoseEstimate()
#ObservedObject var viewModel = SaveStatisticsViewModel()
#Environment(\.presentationMode) var presentationMode
//test
var body: some View{
VStack{
HStack{
Button{
viewModel.uploadStatistics(sSquats: squats)
} label: {
Text("save")
}
}
TextField("Enter squat", value: $squats, formatter: NumberFormatter())
.keyboardType(.decimalPad)
// }.onReceive(viewModel.$didUploadStatistics){
// success in if success {
// presentationMode.wrappedValue.dismiss()
// } }
//HStack{
//Text(String(poseEstimate.squatCount))
// Text(String(poseEstimate.squatCount))
// }
}
}
}
struct StatisticsView_Previews: PreviewProvider{
static var previews: some View{
StatisticsView()
}
}

How to refresh Core Data array when user enters new view with SwiftUI?

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.
I had a hard time understanding your question but I guess I got the idea.
My understanding is this:
You want to store the rep count in the Core Data. (Under Training > Exercises)
You want to count down the reps one by one as the user completes the exercise.
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:
Just generate a different struct (something like ExerciseDisplay) from Exercise, and pass ExerciseDisplay to TrainingView.
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)) }
}
}
}

Updating a Progress Value in SwithUI

I am trying to update a progress bar using the DispatchQueue.main.async (based on #Asperi's reply at but this does not work! Update progress value from fileImporter modifier in SwiftUI). The problem I am having is the progress updates waits until end of the task and then updates the progress bar which is not the expected behavior.
I put the file processing code within DispatchQueue.global().async, which executes the code right away and updates the Progress bar immediately (goes to 100% instantly) but the file processing task is still not complete. How do I get this to work the right way. Below is the code I am executing. Appreciate your help
Below is the ContentView:
struct ContentView: View {
#ObservedObject var progress = ProgressItem()
#State var importData: Bool = false
var body: some View {
VStack {
Button(action: {importData = true}, label: {Text("Import Data")})
.fileImporter(isPresented: $importData, allowedContentTypes: [UTType.plainText], allowsMultipleSelection: false) { result in
do {
guard let selectedFile: URL = try result.get().first else { return }
let secured = selectedFile.startAccessingSecurityScopedResource()
DispatchQueue.global().async { // added this to import file asynchronously
DataManager(progressBar: progress).importData(fileURL: selectedFile)
}
if secured {selectedFile.stopAccessingSecurityScopedResource()}
} catch {
print(error.localizedDescription)
}
}
ProgressBar(value: $progress.progress, message: $progress.message).frame(height: 20)
}
}
}
ProgressBar View:
struct ProgressBar: View {
#Binding var value: Double
#Binding var message: String
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
Rectangle().frame(width: min(CGFloat(self.value)*geometry.size.width, geometry.size.width), height: geometry.size.height)
.foregroundColor(Color(UIColor.systemBlue))
.animation(.linear)
//Text("\(self.value)")
}.cornerRadius(45.0)
}
}
}
DataManager that processes the file. Right now I have a thread.sleep(0..001) to simulate reading of the file
class DataManager {
#Published var progressBar: ProgressItem? = nil
init(progressBar: ProgressItem? = nil) {
self.progressBar = progressBar
}
func importData(fileURL: URL, delimeter: String = ",") {
let lines = try! String(contentsOf: fileURL, encoding: .utf8).components(separatedBy: .newlines).filter({!$0.isEmpty})
let numlines = lines.count
for index in 0..<numlines {
let line = String(lines[index])
Thread.sleep(forTimeInterval: 0.001)
print("\(line)")
DispatchQueue.main.async { // updating the progress value here
self.progressBar?.progress = Double(index)/Double(numlines) * 100
self.progressBar?.message = line
}
}
}
}
ProgressItem:
class ProgressItem: ObservableObject {
#Published var progress: Double = 0
#Published var message: String = ""
}

Async call not getting updated using publisher in SwiftUI

I am trying to load HealthKit data and display a total distance and last workout date in a SwiftUI view (for a Widget). I am getting the data in the print statement but it's not getting updated in the HTWidgetView below:
class WidgetViewModel: ObservableObject {
#Published var workoutDate: Date = Date()
#Published var totalDistance: Double = 0.0
func getWorkoutDataForWidget() {
WorkoutManager.loadWorkouts { (workouts, error) in
DispatchQueue.main.async {
guard let unwrappedWorkouts = workouts else { return }
if let first = unwrappedWorkouts.first {
self.workoutDate = first.startDate
}
let distancesFromWorkouts = unwrappedWorkouts.compactMap {$0.totalDistance?.doubleValue(for: .foot())}
let total = distancesFromWorkouts.sum()
self.totalDistance = total
print("TOtal Distance = \(total)")
}
}
}
}
extension Sequence where Element: AdditiveArithmetic {
func sum() -> Element { reduce(.zero, +) }
}
struct HTWidgetView: View {
#ObservedObject var viewModel: WidgetViewModel
var body: some View {
VStack {
Text("Last Workout = \(viewModel.workoutDate)")
Text("Total Distance")
Text("\(viewModel.totalDistance)")
}
.onAppear {
viewModel.getWorkoutDataForWidget()
}
}
}

Swift access progressvalue in VNRecognizeTextRequest with completion handler

I'd like to capture the progress value in a VNRecognizeTextRequest session. So I inclueded it in a closure. The problem is it is passed when the closure is completed. I can capture the value and print it but not pass it to the main thread to update my progress bar. So I pass from 0% to 100% in the main thread. Can anybody give me a hand please? Thanks a lot.
Here's my code.
private func readImage(image:UIImage, completionHandler:#escaping(([VNRecognizedText]?,Error?)->Void), comp:#escaping((Double?,Error?)->())) {
var recognizedTexts = [VNRecognizedText]()
let requestHandler = VNImageRequestHandler(cgImage: (image.cgImage)!, options: [:])
let textRequest = VNRecognizeTextRequest { (request, error) in
guard let observations = request.results as? [VNRecognizedTextObservation] else { completionHandler(nil,error)
return
}
for currentObservation in observations {
let topCandidate = currentObservation.topCandidates(1)
if let recognizedText = topCandidate.first {
recognizedTexts.append(recognizedText)
}
}
completionHandler(recognizedTexts,nil)
}
textRequest.recognitionLevel = .accurate
textRequest.recognitionLanguages = ["es"]
textRequest.usesLanguageCorrection = true
textRequest.progressHandler = {(request, value, error) in
print(value)
comp(value,nil)
}
try? requestHandler.perform([textRequest])
}
This is how I call my function from the content view.
struct ContentView: View {
#State var ima = drawPDFfromURL(url: dalai)
#State private var stepperCounter = 0
#State private var observations = [VNRecognizedText]()
#State private var progressValue: Float = 0.0
private var originaImage = drawPDFfromURL(url: dalai)
var body: some View {
VStack { Button(action: {
//self.observations = readText(image: self.ima!)
DispatchQueue.main.async {
readImage(image: self.ima!, completionHandler: { (texts, error) in
self.observations = texts!
}) { (value, err) in
self.progressValue = Float(value!)
}
}
})
{
Text("Read invoice")
}
ProgressBar(value: $progressValue).frame(height: 20)
}.padding()
}
}
This is my ProgressBar object
struct ProgressBar: View {
#Binding var value: Float
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle().frame(width: geometry.size.width , height: geometry.size.height)
.opacity(0.3)
.foregroundColor(Color(UIColor.systemTeal))
Rectangle().frame(width: min(CGFloat(self.value)*geometry.size.width, geometry.size.width), height: geometry.size.height)
.foregroundColor(Color(UIColor.systemBlue))
.animation(.linear)
}.cornerRadius(45)
}
}
}
I believe that you need to put your request in a async Thread.
DispatchQueue.global(qos: .default).async {
// Run request
perform(...)
}

Resources