Streaming is available in most browsers,
and in the WWDC app.
What's new in CareKit
Build feature-rich research and care apps with CareKit: Learn about the latest advancements to our health framework, including new views for its modular architecture, improvements to the data store, and tighter integration with other frameworks on iOS. And discover how the open-source community continues to leverage CareKit to allow developers to push the boundaries of digital health — all while preserving privacy.
- CareKit Repository
- Have a question? Ask with tag wwdc20-10151
- Research and Care Website
- Search the forums for tag wwdc20-10151
Hello and welcome to WWDC.
Hello, hello. My name is Gavi Rawson, and I'm a software engineer on the Apple Health team working on the CareKit framework. Later we'll be joined by my colleague Erik Hornberger, who works on the framework as well. We're here to talk to you today about the updates that we've added to CareKit this year. So, what is CareKit? CareKit is an open-source framework that helps you build beautiful care apps. The framework is split up into three parts. We have CareKit, CareKitUI and CareKitStore. Each part is a framework in and of itself and can be imported using Swift Package Manager. CareKitStore provides health flavored data models and a core data layer for persistence. CareKitUI provides static views that are perfect for displaying that data. And CareKit ties the UI and store layers together by providing synchronization between the two. When data in the store changes, the views are automatically updated. Last year, we rearchitected the framework using Swift. We made sure to design the framework in such a way that it's easy to use for beginners but also provides enough customization hooks for more advanced developers. This year, we focused on strengthening the framework and building out even more new tools that make it easier for you to create your care apps. Instead of giving you a boring old agenda, we've created this sticker pack for all the new CareKit features, because who doesn't love stickers? By the end of the talk today, we'll collect all the stickers and tack them onto our MacBook.
So, let's kick things off today in CareKitUI, which provides different types of views for visualizing tasks, charts and contacts. This year, we're adding even more new views to the framework. Let's look at these new views in the context of a wellness application built using CareKit. Here we have my personal feed, showing a list of cards relevant to me. Each card here is a new view that we've added to the framework.
The first is the SimpleTaskView, reminding me to do my daily stretches. If you've worked with CareKit before, this view might look familiar to you. We already have a UIKit API for the SimpleTaskView, and now we've added a SwiftUI API as well. Let's look at that SwiftUI API in detail. We start by importing CareKitUI and SwiftUI and we write some boilerplate SwiftUI code. Then we add the SimpleTaskView to the body. We initialized the view with a title, a detail and a flag that determines if the task is complete or not.
But this view is just a starting point. So, let's say you want to customize it just a bit. We'll take a closer look at the title and detail parameters that accepts SwiftUI texts rather than a string.
This allows you to decorate the title and detail using custom view modifiers. Notice how we changed the title to a thin fontWeight to provide less emphasis in the header.
We'll jump back to our default fontWeight and check out another customization point available to us.
SwiftUI encourages you to build small view components then compose them together to create more rich and functional views. This SimpleTaskView is built from small view components as well. We have a detail disclosure and a header.
If you'd like, you have the option of injecting your own custom header or detail disclosure into the view in place of the default ones we provide for you. Let's try creating a custom header for this view.
We'll go back to our code and use another initializer available to us. This one provides us a closure where we can build our custom header, and CareKitUI provides a few small view components to help us along the way.
In particular, we can use a HeaderView to help us match the style of other CareKitUI cards. We pass a title and a detail to the header and place it beside a custom accent bar in an HStack.
With that, the view is already starting to look custom, but let's go even deeper using another customization point.
We can attach custom views to any side of the content here, which allows us to extend the card in any direction. Let's look at some code to attach content to the bottom of the view.
We'll start by creating a new view just so that we have more space to work. Then we wrap our SimpleTaskView inside of a CardView and place it beside a divider and instructions text. The CardView is another component from CareKitUI, and when wrapping one card inside of another, only the outermost card will be displayed. This allows us to put all of our content into a single card. And with that, we're all done building our custom view, and you can see it looks very different from when we started. We started with the SimpleTaskView that had just a header and a completion button. And now we have a custom accent bar and detailed instructions for the task.
Let's go back to our wellness application and look at some of the other new views in CareKitUI.
The next new view is the labeled value view. This one is great for displaying a value and its associated units. Here we're showing my heart rate measurement is 62 beats per minute, which seems a bit generous.
We can create this view by providing title and detail text just like in the previous view. We also provide the completion state. If you'd like to customize the view even further, we provide identical hooks into the view as the ones we saw for the SimpleTaskView.
The next new addition to CareKitUI is the NumericProgressTaskView, which is helpful for displaying my cumulative progress towards a goal. Here you can see that I've exercised for 22 minutes in pursuit of my 30-minute goal. Just a gentle reminder to get back out there.
To create the view, we again pass a title and a detail to display in the header. We also provide instructions for the task and the text for the progress and goal. Lastly, we pass in a flag that determines if the task is complete.
The next new view is the FeaturedContentView, and this one is great for highlighting important information, such as an article for me to read. Here we have a great article on easy and healthy recipes that I can cook at home.
Creating the FeaturedContentView is a bit different from before. We start by importing CareKitUI and UIKit. We then define the view, then set the large image in the background and the text on the bottom of the view.
When the FeaturedContentView is tapped, we can display a new DetailView. The DetailView supports HTML and CSS in the content under the image, allowing you to put virtually anything in here.
Creating the view is similar to the last one. We start by importing CareKitUI and UIKit. Then we define styledHTML, which is a combination of HTML and the associated CSS for styling.
We then instantiate the view by passing in the styledHTML and a flag that determines whether or not to show the close button in the top right corner.
Finally, we set the image, just like we did before.
The last new view is the LinkView, which is great for displaying buttons that present new views directly inside or outside of the application. Here I have links to help me schedule a physical therapist appointment.
We create the view by supplying a title, instructions and the links to display. Here we create a link button that opens up a website inside of the app.
These link options are just a few of the many that we provide. We also provide options like navigating to the App Store. But if we don't address your use case, there's an option to provide a custom URL as well. So, we've reached the end of today's feed in the app. And that finishes up with all of the new views that we're adding to the framework. These are just a few new views, but we're essentially laying a road map for a UIKit and SwiftUI APIs. We now look to all of you in the community to build on top of what we have and improve the framework with new views for new use cases. With that, we can get our first sticker for CareKitUI views, and we can tack that on right next to our WW sticker.
So, we just saw how we can create static views in CareKitUI, but CareKit goes one step further and wraps the static views in a synchronization layer, so that when data in the store is updated, the UI will reflect the changes. So now that we have new SwiftUI views in CareKitUI, we're adding synchronized SwiftUI views in CareKit.
Let's look at how we can create one of these views. We start by importing CareKit, CareKitUI and SwiftUI.
Then in the body we can create the SimpleTaskView from CareKit by providing a taskID, an eventQuery and a SynchronizedStoreManager that holds a reference to the CareKitStore. The view will use the taskID and the eventQuery to locate the task data in that store.
Once it's located, it'll be automatically mapped to the view. And after that, the SynchronizedStoreManager will ensure that the view is updated when task data changes.
But you may want to customize the way that the task data is mapped to the view. To do that, we can use another initializer. In this one, we provide the same parameters as before, but we also provide a closure where we can create the underlying view from CareKitUI to display. The closure will be called each time SwiftUI recomputes the body of this view.
Inside of the closure we have access to a controller that holds a reference to the task data and a view model. The view model is a convenient struct to help us instantiate the underlying view. So, let's use that view model to instantiate the SimpleTaskView from CareKitUI. Since we're creating the view from CareKitUI, we have access to all of the customization points that we saw earlier for CareKitUI views.
So with the help of the view model, we've essentially created the default SimpleTaskView. To make things a little bit more interesting, let's try modifying this view to display a ResearchKit survey when it's tapped.
To do that, we first add a state property that determines whether or not the survey is showing.
Then we change the displayed task to the researchKitSurveyTask.
After that, we can modify the action to set the isShowingSurvey flag to true when the view is tapped.
And finally when that flag is true, we can present a popover with the ResearchKitSurvey. We've seen a lot of you out there using CareKit cards to present ResearchKitSurveys, so this should be a really good starting point for you all. All right. That finishes up our section. Let's get our new sticker for synchronized views and build up our collection even more.
We've clearly embraced SwiftUI in our API, and it's brought many advantages to the framework, including a simplified API and many customization points. But one of the biggest advantages is that SwiftUI has allowed us to bring CareKit to the Apple Watch. You can now build CareKit, CareKitUI and CareKitStore all for watchOS.
On the UI side, we currently support the simple and instructions task views, and each have been fine-tuned for the Apple Watch screen. Like we said before, these are just a few new views, and we're super excited to see you all take them one step further and build new views for the Apple Watch.
That finishes up our really short section, so let's get a shiny new watch sticker.
Now, let's move on to some updates to the CareKitStore, which is perfect for storing health data in your app. But while the CareKitStore is useful, we already have a store on our devices today that's storing an immense amount of health data. That store is HealthKit. And now you can use data in HealthKit alongside CareKit data to create HealthKit-driven tasks. The tasks can be stored in a CareKitStore and can be auto-completed based on data that comes out of HealthKit.
Before diving into the new HealthKit architecture, let's look at the current architecture around the CareKitStore. We have an OCKStore, which is a core data implementation. The store can be wrapped by a StoreManager that powers synchronization in the views by emitting notifications when data in the store changes.
The StoreManager can be used to create synchronized views in CareKit, like the ones we saw earlier.
Now let's look at how HealthKit integration fits into the picture.
We've created a new HealthKitPassthroughStore that sits beside the CareKitStore. While the CareKitStore uses core data as its source of truth, the HealthKitPassthroughStore uses HealthKit as its source of truth.
The two stores can be wrapped with a new StoreCoordinator. We can interact with the StoreCoordinator in the same way that we interact with the individual stores, by calling create, update and delete methods for CareKit entities.
So, let's dive deeper into the StoreCoordinator to see how it communicates with its internal stores.
When we ask the StoreCoordinator to fetch data, it aggregates results from its internal stores. But if we ask the StoreCoordinator to write data, it only writes to a single store at a time, which helps ensure that writing data is a transactional operation.
Let's take a look at some code that actually sets up this new HealthKit integration. We'll start by importing CareKit and CareKitStore.
Next, create both the CareKitStore and the HealthKitPassthroughStore, making sure to give each a name that's unique for your application.
Then create a StoreCoordinator and attach the two stores that we've just created.
Finally, create a StoreManager using the StoreCoordinator, and this StoreManager can now be used to create synchronized CareKit views.
Now that we have a store set up, let's actually create an exercise task to add to it.
We create a schedule for the task, which specifies that the task occurs at 8:00 a.m. every day. It also sets a target value of 30 exercise minutes, which will be used by the views to display the exercise goal.
Next we create a HealthKit linkage to help us link the task to a HealthKit quantity. We can use the quantity identifier, type and unit that corresponds to the HealthKit data type.
Now that we have a schedule and a HealthKit linkage, we can create the new task and add it to the store.
To display the task, we can use some of the new views that we've introduced in CareKitUI. In this case, the NumericProgressTaskView is a perfect fit because it shows a progress and a goal value. The LabeledValueTaskView above it is better for displaying tasks that don't have a particular goal.
All right. That finishes up another section. We already have a lot of stickers, but let's make room for one more for HealthKit-driven tasks.
In addition to HealthKit, there are many other storage systems out there today in the health care industry. The vast majority of them store their data in a format called FHIR. And FHIR is used so that data can be easily exchanged and parsed. And to make it easier for you to interact with these databases in your apps, we're introducing FHIR compatibility.
To understand FHIR a little bit better, let's look at a snippet of JSON that's structured in the FHIR format.
This JSON represents data for a medication order of Tylenol. The JSON is formed in a certain release of FHIR. In CareKit, we support compatibility with a few releases, including DSTU2 and R4.
To enable FHIR compatibility, we're introducing coders that can map between CareKit entities and FHIR data. To accomplish this mapping, we make use of a new open-source Apple framework, called FHIRModels.
For more information on the framework, check out the session, "Handling FHIR Without Getting Burned," which is one of the best-named WW sessions out there. When we're doing this mapping of FHIR data to CareKit entities, oftentimes a single FHIR resource maps to a single CareKit entity. A good example is a FHIR patient. But sometimes FHIR resources are more granular than CareKit entities, and in those cases, a few FHIR resources might map to a single CareKit entity. It's important to take this into account when doing the mapping yourselves.
Now that we understand the coders at a high level, let's actually create them. We'll start by importing CareKitStore and CareKitFHIR, which is a new SPM package that contains the coders.
We can then initialize the coder that's responsible for mapping the data, and here we have a coder that will map JSON in the R4 release to CareKit entities.
Using that coder, we can take a CareKit patient and convert it to FHIR data.
In the reverse direction, we can take FHIR data and map it to a CareKit patient. Notice that in the process, we first create a FHIR resource data. This helps ensure that the binary data is JSON in the R4 format, so that it can be safely passed to our R4 coder. But the mapping between FHIR data and CareKit entities isn't always so perfect. For example, it's possible that a property on a CareKit entity cannot be perfectly represented by a FHIR resource. In those cases, it's important to define the mapping yourself, so that data isn't lost in translation. Here you can define the way that a patient name is mapped to the FHIR data.
In this closure, we're given a name and a patient from the FHIRModel's framework. Our job is to actually map the name to the patient.
In the reverse direction, you can define the way that a patient name is mapped from the FHIR data. Here we're given a patient from the FHIRModel's framework, but this time, our job is to extract the name components for the CareKit entity.
Notice that these closures expose types from FHIRModels. Like we mentioned before, the framework is completely open-source, and you can find all of the source code on the CareKit GitHub page.
That finishes up the FHIR updates. Let's see what our new sticker looks like. Awesome. And now, I'll pass it off to my colleague, Erik, who's going to go through some more exciting updates to the CareKitStore. On to you, Erik. Thanks, Gavi. And hello, everybody. My name's Erik. I'm also an engineer on the CareKit team, and today I have the privilege of introducing and demoing an exciting new feature in CareKit. Gavi's just talked to you a bit about how CareKit helps keep your views in sync with the data in your store. What I'm going to talk with you about is a different kind of synchronization: synchronization with a server. One of the most common questions we've received since CareKit was open-sourced has been, "How do I synchronize the data in my CareKit app with a server?" We've put a lot of time and thought into considering how to do that and how to do it well. And today, I'm happy to introduce a new set of CareKit remote synchronization APIs. The new remote sync API defines a protocol for communicating with CareKit. Any server that observes these rules can be used as a synchronization back end for CareKit apps.
When a CareKit app enables remote synchronization, changes made locally, such as completing a task, will be synchronized to a remote server.
Other devices can interact with the data on the server as well. Here the other device adds a new task for this user. The next time our patient synchronizes with the server, the store receives the updates, and it can be displayed to them. There are two sides to the new APIs that enable these interactions. The first lives on an extension to OCKStore and is for iOS developers. The second is a new protocol for adding support for CareKit to servers. Let's start with the app developer facing bits of the API.
We've added a new remote parameter to OCKStore's initializer. If you pass an argument into this parameter, CareKit will enable remote synchronization with the object you provided. If you leave the remote nil, then CareKit will continue to function entirely off-line, as it did previously. We've also added a new synchronize method, and as its name suggests, calling this method will prompt CareKit to synchronize its local store with its remote store. The policy parameter's default value is generally the one that you'll want, but there are other options that allow you to completely overwrite the data on either the local or the remote with the data from its partner.
Let's take a look at how you'll use these in practice. First you'll need to import CareKitStore plus a VendorSDK. We'll talk more about these packages in a moment, but let's assume that we have one for now. The next step is to instantiate a class that conforms to OCKRemoteSynchronizable.
You'll need to pass that instance into OCKStore's initializer. And once you've done that, just use the store as you normally would. Many remotes support automatic synchronization, which means that CareKit will call the synchronized method for you as needed.
Of course, you're always welcome to call synchronize manually to kick it off on your own. So, we've just had a look at the app developer facing side of the API. The other side of the coin is the new OCKRemoteSynchronizable protocol that back end engineers and cloud providers can use to add support for CareKit to their servers. This protocol has five requirements.
You'll need to equip your class with a delegate, and it'll be your responsibility to alert the delegate when changes happen on the server that CareKit needs to be aware of.
You'll also need to tell CareKit if you want automatic synchronization or not. It can be helpful to turn this off for deterministic unit tests, but typically, you'll want this to be true.
You'll also need to provide a method for fetching changes from the server. CareKit will give you a knowledgeVector, also known as a vector clock in other contexts. This is a specialized data structure that allows the server to know exactly what data already exists on your device and what doesn't.
Your job will be to send this vector to the server, exchange it for a RevisionRecord, pass the RevisionRecord into the merger vision closure and indicate when you're done by calling the completion block.
The next requirement is pushRevisions. CareKit will provide you with a deviceRevision, which documents all of the changes that have happened on the device since the last time it checked in with the server. It's your job to pass this RevisionRecord to your server and tell CareKit when you're done by calling the completion closure.
Finally, chooseConflictResolutionPolicy will be invoked when conflicting edits are made on the device and on the server. The conflict description contains a copy of both versions of the conflicted entity. And your job will be to inspect both and decide which one to keep.
By implementing this protocol, plus the server side logic to support it, it's possible to create new integrations with CareKit.
Now, Apple does not provide a bespoke server implementation, but we have made it as easy as we can for others to do so. And we're thrilled to announce that we already have one partner lined up.
IBM has become the very first to add support for CareKit to their IBM Cloud Hyper Protect offering. In the spirit of CareKit, IBM has even open-sourced their work as part of the CareKit organization on GitHub.
If you'd like to learn more about their SDK, you can follow their self-guided lab to find out how to get started. All right. We've done a lot of talking now, and I'm really excited to get to the showing. Today we're gonna go through a demonstration that uses the new sync API we've just introduced to do something that we think is pretty clever. Now, the remote sync API is primarily intended for synchronizing an iOS CareKit app with a server, but I'm gonna show you how we can use the very same API to synchronize data between an iOS app and its companion watchOS app. The app we're gonna be creating today will remind our user to stretch daily, prompt them to report any muscle cramps and show them a chart to illustrate the relationship between the two. Our goal is to get to a point where the same tasks are displayed on both devices, and completing them on one device should automatically update them on the other.
Now the iOS application is actually just a pruned-down version of the very same sample app that we built out in our talk from last year. If you haven't seen that presentation yet, I'd highly recommend going back and watching it to learn how we got to where we're going to be jumping in today. All right, let's hop into Xcode. As I mentioned a moment ago, the iOS application is actually complete already. That means that we'll be able to focus on creating a stellar watchOS experience.
Let's begin by setting up the synchronization between our Apple Watch app and our iOS app. We'll do that by leveraging a new class: the OCKWatchConnectivityPeer. This class conforms to the remote synchronizable protocol, and it makes it possible for the Apple Watch and the iPhone to act as remote stores for one another.
We'll need to pass this remote into the store when we instantiate it.
Now, in today's demo, we're going to be working with WatchConnectivity. And when using WatchConnectivity, all of the messages passed back and forth between iOS and watchOS get funneled through the WatchConnectivity session delegate, which is a class that's typically owned and controlled by you, the app developer. What that means is that CareKit is going to require a little bit of cooperation on your part in order to get its messages back and forth.
I'll walk you through what we need to do.
We'll need to set up and activate our WatchConnectivity session. That'll entail setting the delegate and calling the activate method.
For our purposes, we're gonna define a little helper class to function as our delegate.
The WatchConnectivity session delegate has two required methods. The first, activationDidComplete, seems like the perfect place to kick off our very first synchronization.
The second method, didReceiveMessage, will be triggered each time a message from the iPhone lands on the Apple Watch. And this is where we're gonna give CareKit a bit of help.
Each time we receive a message, we're going to show that message to our CareKit remote and give it an opportunity to furnish a response.
We'll then take that response and forward it back to the iPhone on CareKit's behalf.
Now that that's out of the way, we'll just need to create an instance of our new class and remember to set it as the session delegate.
Now, typically, we would need to perform this exact same setup on the iOS side as well. But because it really is the exact same setup, we've gone ahead and done some demo magic, and we've taken care of that already. We have our remote setup on watchOS. We have our remote setup on iOS. And our synchronization story is complete.
We're going to move on and tackle the views next.
In order to keep the views up-to-date with the latest data in the store, we'll need to provide them with a reference to a SynchronizedStoreManager. Let's create one right here.
We'll be building out our views in SwiftUI today. And when using SwiftUI, EnvironmentValues can be a great way to provide a reference to your StoreManager to your views.
Here we're defining a new EnvironmentKey that provides a SynchronizedStoreManager. For the default value, let's use the instance owned by our extensionDelegate.
We'll also extend EnvironmentValues to define a new property: storeManager.
And we'll use this property as an environment variable inside of our TodaysTasksView.
Now that we have a reference to the storeManager, we can begin fleshing out our view. We want to display two task cards and we'll wrap both of them inside of a ScrollView. We'll also go ahead and tint that ScrollView red to match the theme of our iOS application.
Now on iOS we chose to use the InstructionsTaskView for the stretch task. So we'll do the exact same thing here.
Similarly, we'll use the SimpleTaskView for the cramps card.
And because reporting cramps is sufficiently self-explanatory, we'll demonstrate the use of a secondary initializer to hide away the detail label and create a nice crisp view.
With our view complete, our app is now finished.
Let's build, run and see how it looks.
Note that the very first time we run our app, we won't see any tasks appear immediately on the Apple Watch.
This is because it will take a moment for synchronization between watchOS and iOS to complete.
But once it does, we should see our tasks appear on the Apple Watch. And we do. Let's go ahead and try reporting muscle cramps on the iPhone to see if it makes it across to our Apple Watch.
It does. This is looking really good.
Remember, all of the views in CareKit are synchronized with the store.
What that means is that, in a moment, when I check off our stretch task on the Apple Watch, we should see all the subscribed views on the iPhone update simultaneously.
Keep an eye on the adherence ring, the stretch task card and the chart as I complete our task.
There is just something about that that is really satisfying.
There's a bit more here that we can tinker around with, but I think that you all get the gist. So I'm gonna go ahead and wind down our demo right here.
We've taken a look at the new APIs in CareKit. We've also looked at how you can add support for CareKit to existing servers or clouds. Then we demonstrated how these new APIs can be used to synchronize an Apple Watch with an iPhone.
We've worked really hard for this one, so let's claim our remote synchronization sticker.
I'm gonna hand things back to Gavi now. And he's gonna cover our very last topic for today: community updates. Take it away, Gavi. Thanks, Erik. We're really excited about the new remote synchronization API, and we can't wait to see how all of you will use it in your apps. Like Erik mentioned, next we'll go through some community updates. And each year, there's amazing work that's done by all of you in the community. And I'm super excited to share those updates with you today.
Erik just talked all about our new remote synchronization API. As a member of our community, IBM has been one of the first adopters of the API. You can find all of their source code on the CareKit GitHub page.
Setting remote synchronization aside, each year we love to highlight amazing apps that are on the App Store today, making great use of CareKit and ResearchKit.
To call out a few, the OYM Athlete app helps support athletes' nutrition goals by providing daily meal tracking and trends using our chart views. The Health Connected app makes it easy for you to share health data with your doctors and family members and makes use of contact views and charts as well.
We've also seen apps leverage CareKit and ResearchKit to quickly respond to COVID-19. The Stanford First Responder app is helping responders assess their risk for COVID-19 and quickly get them tested.
The University of Nebraska's 1-Check COVID app is providing investigators with greater awareness of the disease.
These are just a few of the apps released this year, but we'd love to hear how you've been leveraging the frameworks, and we'll show you how to do that in just a bit.
Last year, we announced that we'd be releasing a brand-new website. It's out there now at researchandcare.org and acts as a beautiful landing page for the frameworks. On our overview page, you can get a snapshot of the features in both CareKit and ResearchKit and get insight into how the frameworks can help you and your app.
To get more specific information on CareKit, you can navigate to the CareKit tab where you can browse through CareKit features, and as you scroll more, you can read case studies from programs that are using the frameworks to help people on a daily basis.
You can also check out our Investigator Support Program, which aims to support researchers with Apple Watches for their studies.
And if you're interested in applying for this program, please reach out to us using the e-mail address on this page.
And finally, there are a lot of you out there using ResearchKit and CareKit in your apps. Like I mentioned before, we'd love to hear all about your use case and how you've been leveraging the frameworks. So, please reach out to us using this submission form.
Now comes a bittersweet moment where we get our last new sticker for community updates. It's been a long journey and we've talked about everything from a beautiful new UI to enhancements to the CareKitStore. I hope you're all as excited as I am about all of the new updates.
I think it's about time we earned our badge for this year's new CareKit features.
To get started with the framework, make sure to check out our GitHub page and newly designed website.
And as you all know, we're an open-source framework, and we're made better by all of you out there in the community that put in the hard work to build new features and contribute them back. So don't be afraid to open up your first PR on GitHub whether it's big or small. And to finish things off today, the team just wants to take a moment to thank all of you for being amazing supporters of the framework and for improving health in the world. We can't wait to see what you all build next. Thank you.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.