carekit-apple / CareKit

CareKit is an open source software framework for creating apps that help people better understand and manage their health.
https://www.researchandcare.org
Other
2.42k stars 443 forks source link

Extending OCKSimpleLogView to support multiple log buttons #273

Closed pgallastegui closed 5 years ago

pgallastegui commented 5 years ago

I've been trying to create a View/Controller similar to the OCKSimpleLogTaskViewController and its view, but with a few different buttons to log an event with different values (for example, pain level 1 through 5)

I tried to subclass OCKEventViewController, looking at the source for OCKSimpleLogTaskViewController as a guide, but several key paths are marked as private or internal, not allowing me to do so.

Are there plans to extending this functionality so we can leverage synchronizable views without having to code them from scratch? Or is it already there and I am missing it?

Great work on 2.0. The WWDC session was awesome!

erik-apple commented 5 years ago

We’re pumped to hear that you’re excited about CareKit 2.0 and are already tinkering with it! GitHub is the right place to connect with us, so please continue to create issues like this as you come across new questions!

At the moment, a lot of the properties and methods used for synchronization are marked private or internal because CareKit 2.0 is still in beta and there is a possibility they may change. If there is enough interest from the community we may open those up down the road though.

Presently the options options available to you are…

  1. Subclass and try your best to squeeze everything you can out of the built in synchronization
  2. Build your view controller inside the CareKit framework where you have access to the internal properties and methods (and consider contributing your work back to the framework! :D )
  3. In your local copy of CareKit, mark the methods/properties you need as public or open

If you think you can get away with a simple sub class, that will be the easiest route forward. You could do it something like the snippet we’ve pasted below.

If (1) doesn’t suffice and you decide to try your hand at (2) or (3), then the three steps to follow are to create a naive view in CareKitUI, create a subclass of it in CareKit that conforms to OCKBindable, and then finally to subclass OCKEventViewController or OCKTaskViewController and hook it up to your bindable view. You can look at OCKSimpleLogTaskView, OCKBindableSimpleLogTaskView, and OCKSimpleLogTaskViewController for an example of how to do each of these. If done correctly you should be able to get very good code reuse out of what already exists.

import UIKit
import CareKit

class MyLogViewController<Store: OCKStoreProtocol>: OCKSimpleLogTaskViewController<Store> {

    let dizzinessButton = OCKButton(titleTextStyle: .body, titleWeight: .medium)
    let anxietyButton = OCKButton(titleTextStyle: .body, titleWeight: .medium)

    override func viewDidLoad() {
        super.viewDidLoad()

        dizzinessButton.setTitle("Log Dizziness", for: .normal)
        dizzinessButton.setBackgroundColor(view.tintColor, for: .normal)
        dizzinessButton.handlesSelectionStateAutomatically = false
        dizzinessButton.addTarget(self, action: #selector(logDizziness(sender:)), for: .touchUpInside)

        anxietyButton.setTitle("Log Anxiety", for: .normal)
        anxietyButton.setBackgroundColor(view.tintColor, for: .normal)
        anxietyButton.handlesSelectionStateAutomatically = false
        anxietyButton.addTarget(self, action: #selector(logAnxiety(sender:)), for: .touchUpInside)

        // The log outcomes stack is the last view in the content stack, so we insert above the last view in the content stack.
        taskView.contentStackView.insertArrangedSubview(dizzinessButton, at: taskView.contentStackView.arrangedSubviews.count - 1)
        taskView.contentStackView.insertArrangedSubview(anxietyButton, at: taskView.contentStackView.arrangedSubviews.count - 1)
        taskView.logButton.isHidden = true
    }

    @objc
    func logDizziness(sender: OCKButton) {
        guard
            let ockEvent = event?.convert(), // Convert the store's native event type to an OCKEvent
            let taskID = ockEvent.task.localDatabaseID
            else { return }

        let currentValues = ockEvent.outcome?.convert().values ?? []
        let ockOutcome = OCKOutcome(taskID: taskID,
                                    taskOccurenceIndex: ockEvent.scheduleEvent.occurence,
                                    values: currentValues + [OCKOutcomeValue("Dizziness")])
        let nativeOutcome = Store.Outcome(value: ockOutcome) // Convert back to the store's native outcome type

        if ockEvent.outcome == nil {
            storeManager.store.addOutcome(nativeOutcome)
        } else {
            storeManager.store.updateOutcome(nativeOutcome)
        }
    }

    @objc
    func logAnxiety(sender: OCKButton) {
        // Similar to above
    }
}
pgallastegui commented 5 years ago

Thank you so much for the quick reply!

The code does begin to get us there. We will use it to try to quickly test some scenarios and give our testers something to play with.

Nonetheless, I believe we will need to go the route of including controller and view in CareKit (and we'd be glad to do a pull request if it turns out well!). I will allow us some extra customization (and the use of OCKLabeledButton!), and stray away from hiding elements from the predefined views.

I'll keep an eye on the progress out of beta, too.

Again, thank you!

gavirawson-apple commented 5 years ago

@pgallastegui We have opened up a lot of functionality in the latest update that allows for creating custom views and view controllers. For example, if you would like to create a new view that displays and is synchronized with an event for a task, you can do something like this:

// Define your custom view which must be `OCKEventDisplayable`.
class CustomEventView: UIButton, OCKEventDisplayable {

    // Required by `OCKEventDisplayable`. Methods on this delegate notify the view controller when certain actions occur.
    weak var delegate: OCKEventViewDelegate?

    init() {
        super.init(frame: .zero)
        addTarget(self, action: #selector(tapped), for: .touchUpInside)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    @objc
    func tapped(_ sender: OCKButton) {
        delegate?.eventView(self, didCompleteEvent: sender.isSelected, sender: sender)
    }
}

// Define your custom view controller, specializing the view as a `CustomEventView` and the store as an `OCKStore`.
class CustomTaskViewController: OCKEventViewController<CustomEventView, OCKStore> {

    // Instantiate your custom view to be displayed by the view controller.
    override func makeView() -> CustomEventView {
        return CustomEventView()
    }

    // Update your custom view when the view model changes.
    override func updateView(_ view: CustomEventView, context: OCKSynchronizationContext<OCKStore.Event>) {
        // Update the view with the new data here.
    }
}
moritzdietsche commented 5 years ago

I just tried your proposed solution @gavirawson-apple. It fails to compile as the initializers are marked internal. Will they become public once stable or is there another, better way to instantiate my custom event view controller?

gavirawson-apple commented 5 years ago

That's a great point! See #321 for a fix.