Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API/public #24

Open
wants to merge 11 commits into
base: HealthChart/main
Choose a base branch
from
Open

API/public #24

wants to merge 11 commits into from

Conversation

nriedman
Copy link

Health Chart Public Interface

♻️ Current situation & Problem

This PR defines the public API for the Health Chart module.

⚙️ Release Notes

  • Initializer for HealthChart takes a MeasurementType, a DateRange, and a DataProvider (defaults to HealthKitDataProvider)
  • User interactions (e.g. swipe to change date range, tap on data points, etc.) default to enabled, and can be disabled with a HealthChart.disable(interactions:) modifier.
  • Custom Chart Styling can be injected into the HealthChart via a .style(HealthChartStyle) modifier.

📚 Documentation

See inline documentation.

✅ Testing

NA

📝 Code of Conduct & Contributing Guidelines

By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

@nriedman nriedman changed the title public-interface API/public Oct 30, 2024
Still need to work on binding initializer and reducing view modifiers.
Copy link
Member

@PSchmiedmayer PSchmiedmayer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this @nriedman!

@Supereg I had the chance to sync with @nriedman on this in our last meeting and added some first suggestions. Would love to get your input in the API surface and how we should structure this before we take over elements from ENGAGE and put them in here.

A special focus would be on configuration and an easy way to also load date from data sources outside of HealthKit if necessary.

Copy link

codecov bot commented Nov 6, 2024

Codecov Report

Attention: Patch coverage is 5.80645% with 146 lines in your changes missing coverage. Please review.

Project coverage is 55.33%. Comparing base (a9c4daf) to head (5bf0d21).

Files with missing lines Patch % Lines
...s/MetricChart/ChartViews/InternalHealthChart.swift 0.00% 42 Missing ⚠️
...thCharts/MetricChart/Models/MeasurementCache.swift 0.00% 41 Missing ⚠️
...lthCharts/MetricChart/ChartViews/HealthChart.swift 0.00% 27 Missing ⚠️
...ziHealthCharts/MetricChart/Models/ChartRange.swift 32.15% 19 Missing ⚠️
...ws/Modifiers/HealthChart+DisableInteractions.swift 0.00% 4 Missing ⚠️
.../HealthKitDataProvider/HealthKitDataProvider.swift 0.00% 4 Missing ⚠️
...Chart/ChartViews/Modifiers/HealthChart+Style.swift 0.00% 3 Missing ⚠️
...s/MetricChart/Models/HealthChartInteractions.swift 0.00% 3 Missing ⚠️
...thCharts/MetricChart/Models/HealthChartStyle.swift 0.00% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                  Coverage Diff                  @@
##           HealthChart/main      #24       +/-   ##
=====================================================
- Coverage             71.83%   55.33%   -16.50%     
=====================================================
  Files                    11       20        +9     
  Lines                   465      620      +155     
=====================================================
+ Hits                    334      343        +9     
- Misses                  131      277      +146     
Files with missing lines Coverage Δ
...Chart/ChartViews/Modifiers/HealthChart+Style.swift 0.00% <0.00%> (ø)
...s/MetricChart/Models/HealthChartInteractions.swift 0.00% <0.00%> (ø)
...thCharts/MetricChart/Models/HealthChartStyle.swift 0.00% <0.00%> (ø)
...ws/Modifiers/HealthChart+DisableInteractions.swift 0.00% <0.00%> (ø)
.../HealthKitDataProvider/HealthKitDataProvider.swift 0.00% <0.00%> (ø)
...ziHealthCharts/MetricChart/Models/ChartRange.swift 32.15% <32.15%> (ø)
...lthCharts/MetricChart/ChartViews/HealthChart.swift 0.00% <0.00%> (ø)
...thCharts/MetricChart/Models/MeasurementCache.swift 0.00% <0.00%> (ø)
...s/MetricChart/ChartViews/InternalHealthChart.swift 0.00% <0.00%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a9c4daf...5bf0d21. Read the comment docs.

Copy link
Member

@Supereg Supereg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @nriedman,
I’ve shared a few thoughts and suggestions throughout—hope they’re helpful! 🎉 I'm happy to dive deeper into any of them if you’d like to discuss.
I’m excited to see how things shape up, especially as you move forward with the Chart implementation. 🚀 I think seeing some of the use cases in action will really help shape some of the current interfaces. Just let me know when you’d like another round of feedback!

Package.swift Show resolved Hide resolved

public struct HealthChart: View {
@State private var privateRange: ChartRange
private var privateRangeBinding: Binding<ChartRange>?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this works in the end, as this will not be used for view re-render by SwiftUI. Not sure if it is needed though. Just a little note.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Supereg! Would you mind explaining a little more about this? When I tested the UI with this setup, I saw that when I change the date range in the lowest view in the hierarchy, this binding propagated that change up to the top level as expected, and vice versa when I change the range at the highest level. That is, the views seem to re-render correctly when I change the state? Let me know if I'm misunderstanding what you mean!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tldr: There is a slight technical different, though, it is not 100% clear if it makes any difference. I think it shouldn't impact your development work right now as the interfaces might not be fully there where they will be. Chances are it might be refactored anyways. And if it works it works.

Long answer:

You are right, it seems that from a view-update perspective everything works fine. Either it's just because the parent view refreshes or the Bindings transaction actually changes from a Equatable perspective.

What I head in the back of my head: Binding conforms to DynamicProperty (basically every SwiftUI property does to know when to prepare for the next view update). SwiftUI would then just inspect every DynamicProperty conforming property of the type and call update() accordingly. However, Optional does not conform to DynamicProperty, even if its Wrapped type does. I made a small example below. If foo is declared as optional, update() is never called, if you declare it as non-optional it will be called.

There is two questions here: a) does Binding contain any important logic in its update() method and how would it influence behavior (e.g., properties like @Environment or @State make sure to have a consistent view on the data while body is getting called) and b) does SwiftUI workaround this internally and has custom handling for optional types of e.g. Binding. So honestly not sure if it actually makes a difference at all.

struct Foo: DynamicProperty {
    func update() {
        print("UPDATE")
    }
}

struct MyView: View {
    var a: Foo? = Foo()

    var body: some View {
        Text("Hello World")
    }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After discussing with @PSchmiedmayer, we had the following thoughts:

Potential workaround that addresses the fact that Optional does not conform to DynamicProperty:

Make Optional conform to DynamicProperty when it's wrapped value conforms to DynamicProperty. The update() function will then just call the wrapped values venation if it's not nil, and do nothing otherwise.

Note: Apple/SwiftUI may already have native support to work around this, as we are not the first to encounter this issue.

public init(
_ type: HKQuantityType,
in initialRange: ChartRange = .month,
provider: any DataProvider = HealthKitDataProvider()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What was the reason to have the concept of the DataProvider be part of the public interface here? Would it make sense to just to just pass a collection of (HK) samples to the chart to be as reusable as possible. For example, if I just have a few recordings form my Bluetooth device that I turned into HKQuantitySamples, I can just pass the collection to the Chart.
I think the concept of the HealthKitDataProvider API is great, especially to make it easier to query samples from the HealthKitStore. But can these two APIs be separate? Or have a chart view that works with a DataProvider be a separate component that sits on top of a simple chart implementation that just takes a collection of samples?
Let me know what your thoughts are here 👍

Comment on lines 17 to 18
extension HealthChart {
public func disable(interactions newValue: HealthChartInteractions) -> some View {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having an extension on View would provide the most flexibility. Though, to not collide with the existing disabled(_:) modifier we might want to find a more unique name like healthChartInteractions(disabled:) maybe? Some of the accessibility modifiers have a similar naming scheme, where the external parameter name express the semantic effect of the modifier.

/// Default implementation fetches `HealthKitDataProvider` fetches data from a HealthKit `HealthStore`.
public protocol DataProvider: Sendable {
// TODO: Pass `ChartRange` instead of interval, query all health data at once aggregated by `DateRange` granularity.
func fetchData(for measurementType: HKQuantityType, in interval: ChartRange) async throws -> [HKQuantitySample]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of interest, what would be other DataProvider implementations except for a HealthKit-based one?

import Foundation


public struct HealthChartStyle: Sendable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eager to see how the health chart style turn out. What's the general idea, just having some parameters that control how the chart is rendered?

@PSchmiedmayer
Copy link
Member

Thank you for all the input @Supereg!

I think we might want to stick with HKSample types for now as the majority of the package here deals with them (and their challenges within Swift + SwiftUI). I am wondering if small and targeted extensions could help here. Or alternatively a wrapper type that could still allow access to the underlying types.

I think for now we could stick with HKSample types, similar to how we enable their usage in SpeziDevices and other elements. It would be amazing to see if Apple adopts some more modern types here in the future.

@PSchmiedmayer PSchmiedmayer added the enhancement New feature or request label Jan 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

3 participants