Learn Courses My Dashboard

Christopher's HealthKit App Challenge Journal

I’ve decided to take on one last thing for this project: Onboarding.

Onboarding allows the user to set:

  • Units
  • Daily Goal
  • Disable Default Drink Type
  • Add Custom Drink Types
  • Authorize Apple Health




*The Custom Drink Type screen shows the same “Add Drink Type” dialogue as Settings does

If the user choose not to authorize access to Apple Health Data, the “Continue” button will display “Skip”. If it’s authorized during Onboarding the button will then display “Continue”.

Additionally, when Apple Health access is authorized, Settings will no longer show the “Sync with Apple Health” button. Of course, if the user hasn’t, the button will be there.

You may have noticed the “Daily Intake Recommendations” button. It gives a recommended daily intake based on biological gender. This view dynamically adjusts the recommended amount based on the user’s selected unit. This view is also present in Daily Goal Settings.

At this point, I’m very happy with the end product. I don’t foresee myself adding anything else to this in the near future.

If you want a closer look at Liquidus check out the GitHub repo linked in a previous post.

1 Like

Refinements

Despite my previous post, I’m back for more now that my first semester ended. However, these are really just refinements and nothing new.

Primarily, the changes come in the form of “custom” symbols. I haven’t created new symbols from scratch but modified preexisting SF Symbols to get what I want.

SF Symbols doesn’t contain any standalone drink symbols. While there is a coffee cup symbol, this doesn’t make too much sense in the context of the app.

However, the SF Symbol below (takeoutbag.and.cup.and.straw), has a drink cup that would fit and not require too much work on my end. This symbol was just added with SF Symbols 3.
takeoutbag.and.cup.and.straw@2x

So I modified this symbol, and ended up with this:
custom.drink@2x

I did this same process with the filled variant (takeoutbag.and.cup.and.straw.fill):
custom.drink.fill@2x

I made another modification to the outlined drink, adding a plus badge. I used the plus badge from rectangle.fill.badge.plus. This one in particular took a long time to get the way I wanted.
custom.drink.badge.plus@2x

My initial reasoning was to spruce up the onboarding screens with symbols, which lead me down the custom symbols path. Views with these symbols look slightly different in iOS 14 vs. iOS 15. I also added the Liquidus icon on the first onboarding screen.

For iOS 15, I used the hierarchical rendering mode for SF Symbols and my custom symbols. I also took the opportunity to use forms for all parts of the Onboarding views (except the welcome view) for greater consistency.



As the hierarchical rendering mode didn’t exist for custom symbols in iOS 14, all the symbols are solid blue:



In any previous versions of iOS, no symbols are displayed.

Since I had these symbols, I decided to replace some of the symbols within app with the custom drink symbols.



The tabs make use of the filled variant and all other instances make use of the palette rendering mode, with the outline changing based on Light/Dark Mode.

Likewise, the palette rendering mode didn’t exist for custom symbols in iOS 14 either. This also means there aren’t any noticeable differences in Light vs. Dark Mode.

Instead, I used the filled variant of the drink symbol:


For the non-onboarding screens, these symbols revert back to what they were prior. They appear in iOS 13 or earlier (not that I have tested the app in iOS 13 or earlier).

The last noticeable change I made besides some bug fixing, is updating the Drink Logging view to use a Form to be consistent with the rest of the app.

In order to display specific symbols for specific iOS versions I used this block of code:

if #available(iOS 14, *) {
  if #available(iOS 15, *) {
    // do something for iOS 15
  } else {
    // do something else iOS 14
} else {
  // do something for earlier versions
}

I didn’t do all three levels for everything since it depended on what I was doing. It also turns out you can replace iOS with macOS, watchOS, and tvOS, depending on what you’re doing.

Well, that was a lot to show, especially since there weren’t any new features. Something I may get around to is better UI optimization for smaller screens and experimenting with WidgetKit. Widgets are probably the closest I’ll get to adding a new feature.

TL;DR: I modified some SF Symbols and added them all throughout the app. I also did testing and bug fixing for iOS 15 and I updated the drink logging and onboarding views.

1 Like

Amazing, thanks for sharing :slight_smile:

1 Like

Glad to hear you like what I’ve done. I’m always happy to share my experience.

1 Like

Smaller Screen Optimizations

As I’ve been testing in the Simulator with an iPhone 13, this doesn’t show what the UI looks like on smaller screens. As such, I have to make modifications to accommodate this.

The Intake View is the only view without a ScrollView of some sort. The Data Logs View uses one, while the rest use forms. So for the Intake View, I added a ScrollView beneath the date selector and I made the percentage circle slightly smaller.

Additionally, while I was doing this, I gave the Intake view the same DatePicker button I did with the Data Logs View.

A little work also had to be done to the Onboarding Welcome view. To properly accommodate the text in the Welcome view I used a GeometryReder to properly size it.

However, there is a design problem in iOS 14 on iPhone 8/8+ or older.

On the top/left is an iPhone 13 and on the bottom/right is an iPod touch (7th gen), both on iOS 15. Both don’t have the .listRowBackground modifier.

Doesn’t look great. Hence the .listRowBackground modifier. Now, this is what it looks like with that modifier:

While the iPhone 13 looks fine, but the iPod touch has these separator lines. By using the .listSectionSeperator modifier resolves this issue:

However, the .listSectionSeperator modifier was introduced in iOS 15, so this doesn’t work in iOS 14.

While there are ways to remove the separator lines in iOS 14, it removes all of them in the view, so then the lists and pickers throughout the onboarding views wouldn’t look right.

I figured it was best to just not use the .listRowBackground the modifier in iOS 14 and just leave it:

While this isn’t exactly what I want, but at the same time, I don’t want to build a new view for each stage of Onboarding just for iOS 14.

I still want to look into Widgets, of course, but also into supporting Siri Shortcuts and accessibility features, like VoiceOver and Dynamic Type.

1 Like

Navigation Views and Toolbar Items

I wanted to figure out how to get an inline title in a sheet. Like this in the App Store:

This lead me to add Navigation Views to most of the views without.

Navigation Views have .navigationBarTitleDisplayMode modifier. If set to .navigationBarTitleDisplayMode(.inline) and a navigationTitle is used. You can achieve this look.

This started with the Daily Intake Recommendations sheet in Onboarding and Daily Goal Settings.

You can add Toolbar Items like this:

.toolbar {
  ToolbarItem(placement: .primaryAction) {
    Button {
      // do something
   } label: {
     Text("Back")
  }
}

I continued this for my other sheet views.

The Drink Logging sheet:

The New Drink Type sheet:

And I decided to give the Calendar buttons to choose a date or week its own sheet view.

I wanted to use the graphical DatePicker style, but when used in a form the dates are misaligned.

I also added two new buttons as toolbar items.

First, the Today or This Week button. These buttons take you back to the current day or week from whatever day/week you are looking at.

Second is a Sorting button, only present in the Data Logs view. As the name suggests, this lets you sort between the drink types to show the data for all types or a specific one.


In the absense of a specific type, “There is no X data for this day” will be displayed instead (X could be Water, Coffee, etc.).

In actuality, this has been done for a while and I forgot to post about it. My current project for the app is Dynamic Type support, which is nearing its end. There are a massive amount of changes necessary to support all type sizes.

2 Likes

Dynamic Type Support

Happy New Year Code Crew!

This will most likely be a long entry as these are optimizations for the whole app. To give an idea of the scope, 26 files were modified so we have a lot to go through.

The general format of this journal will be showing the issues and going over the fixes.

Testing

First I wanted to go over how exactly I tested Dynamic Type.

I used the Accessibility Inspector included with Xcode that lets you test all sorts of accessibility options in iOS. In an Xcode Project, you can get to it if you go Xcode > Open Developer Tool > Accessibility Inspector.

This video goes into more detail for those unfamiliar: WWDC19 - Accessibility Inspector

The Accessibility Inspector, among other things, lets you easily change the Dynamic Type size, without going into Settings on the Simulator.

In most cases, I tested with the Extra Extra Extra Large Accessibility type size on iPhone 13 Pro Max and iPod touch.

Onboarding

I did save myself some work with the Onboarding as most of the views use a form and will adjust dynamically to text size.

However, five things need to be fixed.

The first is that the Onboarding Welcome Screen doesn’t have a ScrollView, so I simply added one.

Second, are the icons used throughout the Onboarding views. The issue is they don’t scale with the text in these views, so depending on the type size it can look too big or too small.

So I used @ScaledMetric. The way this works is you can scale a number according to the way Font Styles scale to Dynamic Type (read more here under the Dynamic Type Sizes and Larger Accessibility Type Sizes Apple Human Interface Guidelines - Typography).

The way you’d use it would be like this:
@ScaledMetric(relativeTo: .body) var symbolSize = 75

The result looks like this:

The third is the text field in the Daily Goal Onboarding View. I originally applied a frame to it so wouldn’t push to the edge of the form. But for bigger type sizes the frame is way too small. So I simply removed the frame.

Fourth, is the Daily Intake Recommendations in said view. It is a little cutoff. So I made it adapt to whether one of the accessibility Dynamic Type sizes are used. @Environment(\.sizeCategory) comes with sizeCategory.isAccessibilityCategory, so I can test for this and either use a label or VStack of the info symbol followed by the text.

However, as “Recommendation” is a long word, there is a limit to how well this works.

You might have noticed the last issue by now, the “Continue” button background card wasn’t big enough for the text anymore. In this case, I simply made the frame bigger.

Tab Bar

With the tab bar itself, I’m using a custom symbol for one of the Tab images. Apparently, this doesn’t scale properly with Dynamic Type.

There are two ways to fix this. The @ScaledMetric from before, or by using a font size. So I added .font(.system(size: 25)) beneath the custom tab symbols, so it now scales properly.

Intake View
Problems

There is a bunch of things to go through for the Intake View.

First, let’s see what it looks like at the biggest Dynamic Type size:

This is even worse on an iPod touch:

Essentially, everything below the Day/Week Picker needs to be changed.

Day/Week Data Selector

The first order of business is to add the Viewable Day or Week Selector into the ScrollView.

Then change the formatting of the date or week range using @Environment(\.sizeCategory) and isAccessibilityCategory.

For the Day Data Picker, when an Accessibility Dynamic Type Size is used a date will be represented in the format of Jan. 1, 2020. This similarily works for the Week Data Picker, except it isn’t dependent on an Accessibility Dynamic Type size being selected.

On iPhone 13 Pro Max:

And iPod touch:

Simulator Screen Shot - iPod touch (7th generation) - 2022-01-01 at 19.44.09

Circular Progress Bar

First, I removed the daily/weekly goal in the middle of the Progress Circle.

Second, I removed the frame on the Circular Progress Bar and applied the .scaleToFit modifier.

Third, I created functions to return a font style depending on the Dynamic Type size using @Environment(\.sizeCategory).

A lot of experimentation went into this. I tried to give it a width of the text, which didn’t work. I tried to use a GeometryReader to set the frame. This seemed like the only view that worked.

I ended up with a solution that looks good at large and small Dynamic Type Sizes and on different devices.

iPhone 13 Pro Max: Large and Accessibility xxxLarge

iPod touch: Large and Accessibility xxxLarge

Simulator Screen Shot - iPod touch (7th generation) - 2022-01-01 at 20.01.48

Breakup Statistics

This information no longer uses a LazyVGrid, but a VStack. In addition, the data is formatted differently depending on isAccessibilityCategory.

I also decided to swap out the symbols used to display the drink type. The exact reasoning for this will be discussed later.

iPhone 13 Pro Max: Large vs. Accessibility xxxLarge

iPod touch: Large vs. Accessibility xxxLarge

Logs View
Problems

The issues here are fairly obvious. While the text for each day fits at the Large Dynamic Type size, this is not the case at accessibility xxxLarge. Additionally, the amount and logging time are squeezed.

Fixes

My first thought for this was to dynamically change the width and height of the background card. Even after I did this, I discovered a problem. There would be no consistency in the width and height of the card.

So, I simply made it a form instead. The header of each section would be the date of each day.

In terms of the content, the drink symbol was scaled using a @ScaledMetric. The elements are still in a HStack and the symbol and text have a Spacer between them.

When an Accessibility Dynamic Type size is used, this becomes a VStack instead. Additionally, the month names are now abbreviated, like in the Intake View.

iPhone 13 Pro Max: Large and Accessibility xxxLarge:

iPod touch: Large and Accessibility xxxLarge:

Settings

The whole of the Settings view uses forms, so I saved myself some time here.

However, there is one thing that needs refinement.

First off, was the list of drink types.

The drink symbols, once again, weren’t scaled properly so I used a @ScaledMetric. However, this alone presents an issue when editing custom drink types.

So this means I need to display it with the symbol followed by the type name. While I’m at it I also added a vertical display for the accessibility type sizes.

Calendar Sheet (Intake & Logs)

At the largest accessibility type size this view doesn’t look great.

I solved my problem with the graphical DatePicker, so I kinda bypassed these issues. I just wrapped the DatePicker in a VStack. (The same thing was done with the Drink Log sheet).

Daily Intake Recommendations (Onboarding & Settings)

On an iPod touch it looks like this:

Very simple solution: add a ScrollView

Congrats you made it to the end, or you just skipped everything. I did my best to organize things so it’s a bit more digestible.

After I finished this, I also worked on better supporting the Increased Contrast accessibility option. However, considering the length of this journal, I think it is better suited for another post.

High Contrast

As you may know, system colors (i.e. .systemRed, .systemOrange, etc.) offer slight variations based on Light and Dark modes. When the High Contrast settings are enabled these colors are even darker or lighter (depending on Light/Dark Mode).

You can see this under System Colors here: Color - Visual Design - iOS - Human Interface Guidelines - Apple Developer

Liquidus has two custom colors. A green that shows when the user’s daily or weekly goal is met. And a brown which is used for Coffee in iOS 14 or earlier.

If we look at the Color Contrast Inspector, part of the Accessibility Inspector, we see the contrast of text against a background.

Note: A white background refers to Light Mode and a black background refers to Dark Mode.

If we perform this test with the green color on a white background, we get the following:

If we make this color darker, this check is passed:

Back in Assets, we can set a high contrast variation of a color set. Based on the above tests, I made the following change:

Likewise, this had to be done with brown color as well. The same test results in this:

After making these changes I quickly discovered a problem.

In order to store colors for each drink type, I used a custom struct to save the colors. Say I start with the light-mode variant of .systemRed. The light-variant of .systemRed will be saved. If dark mode is enabled, the dark-mode variant of .systemRed will not be displayed. This is the same case for the high contrast variants.

I can’t exactly remove this struct, as I still need to save colors if/when they are changed and when a drink type is created.

In order to resolve this, I need to keep track of if/when a drink type’s color is changed and create a function to return the right color.

Default drink types are initially set to not changed. In this state, it will return a system color, with the exception of iOS 14 returning that custom brown color. If changed the saved color is defaulted to. Custom drink types automatically default to the saved color.

Then it became a matter of replacing color calls to this function.

With High Contrast enabled we see something like this:

All other colors are system colors, so no other changes are really necessary.

Next, I’m going to work on better supporting another accessibility setting, Differentiate with Color.

Differentiate Without Color

This accessibility feature is setting your preference to make visual elements differentiable from each other with means other than color. This can be helpful for those with colorblindness.

In most cases, information isn’t displayed by just color, but there are two cases.

First is when the user’s daily or weekly goal is met the circle outline fills green.

Using @Environment(\.accessibilityDifferentiateWithoutColor) we can tell if Differentiate without Color is enabled or not. When the goal is met a flag will be displayed, which is just a simple if-statement.

Second, is in the logs. The sole means of differentiating between drink types is color. So, I created an alternate display that replaces the drink symbol with text.

In order for this to display well with accessibility Dynamic Type sizes I had to have each text element vertically aligned for those type sizes.

I also decided to change the sorting symbol and layout. It took a little trial and error to figure out, but I had to use a Menu and put my Picker in it.

I want to also make modifications for the Grayscale Color Filters.

Grayscale Color Filter

While there’s probably less of a point to optimizing for the Grayscale Color Filter, I kinda already did it so…

Although there is a grayscale option in the Accessibility Inspector, it doesn’t work so I had to test on my iPhone.

Unlike the other accessibility features I’ve written about, there isn’t an @Environment property for it.

But there is UIAccessibility.isGrayscaleEnabled. Unlike an @Environment property, it won’t update if/when changed while the app is open.

There is, however, UIAccessibility.grayscaleStatusDidChangeNotification. Using .onRecieve, we can update a property when grayscale is disabled/enabled.

So, it would be something like this:

.onReceive(NotificationCenter.default.publisher(for: UIAccessibility.grayscaleStatusDidChangeNotification)) { _ in
  model.grayscaleEnabled.toggle()
}

model.grayscaleEnabled is a @Published boolean property in my model.

To make sure the model.grayscaleEnabled is set properly when opened, I used:

.onAppear {
  model.grayscaleEnabled = UIAccessibility.isGrayscaleEnabled
}

The drink symbols throughout app, are set to use Color.primary instead of the drink type’s color.

For the Intake View, all drinks in the circular progress bar use Color.primary. When the goal is met the flag is displayed, like with Differentiate without Color.

This is also the case for the symbols in Onboarding.

I also used the alternate Drink Logs layout for grayscale as well for better differentiation.

I also disabled the color picker when editing a drink type’s color.

When a new drink type is created the color picker is also disabled. However, a random color is assigned to the drink type if/when Grayscale is disabled.

I also made some other UI changes independent of Grayscale.

As you may have noticed I move the “Save” button in the editing drink type views to the toolbar. This is also the case for editing the daily goal.

My next project with this is going to be Reduce Motion / Perfer Cross-Fade Transitions. I’m not expecting this to be too much work as there’s only a few places where withAnimation is used.

Reduce Motion / Perfer Cross-Fade Transitions

There are two animations in Liquidus that are custom, using withAnimation or .animation.

To read reduce motion we can use @Environment(\.accessibilityReduceMotion)

The first (and the easiest to change) is the animation for the circular progress bar as drinks are added and day/week are changed.

I simply used a inline boolean statement:
.animation(reduceMotion ? .none : .linear)

The other place was with onboarding, for when the Continue button is pressed and the tab changes.

This was much more complicated to figure out.

For whatever reason when using withAnimation the only animation I could get is the swipe. While this is fine for Reduce Motion, this isn’t the case for Perfer Cross-Fade Transitions.

I eventually decided to use NavigationViews and NavigationLinks and move the Continue/Skip button into the toolbar. Now each individual onboarding view takes care of its’ own logic.



Another things I decided to change, independent of Reduce Motion and Perfer Cross-Fade Transition, was to replace the Edit button in the Settings Drink Type View with a symbol. In addition with moving the “Add Drink Type” button into the toolbar.

The next project is optimizing for VoiceOver. I’m expecting it to take a good chunk of time.

VoiceOver

Much like Dynamic Type, I went through every inch of UI for the app.

A great way to test VoiceOver is with the Accessibility Inspector’s Inspector Tab.

You can go element-by-element to see how VoiceOver reads it.


There are a bunch of different accessibiltiy modifiers so you can tailor the experince. Here are some:

.accessibilityLabel()
.accessibilityValue()
.accessibilityAddTraits()
.accessibilityRemoveTraits()
.accessibilityIdentifier()
.accessibilityInputLabels()

Some of them correspond to the basic properties in the Inspector.

Symbols and Images usually aren’t great for VoiceOver by default, as VoiceOver will read the symbol or image name. One way to resolve this is to use .accessibilityHidden(true), which will skip over an element in VoiceOver. Depending on the symbol it might be okay.

This is what I did for most of the symbols throughout the app, with the exception of the Logs View.


Another strange thing are ColorPickers. Despite having a title, VoiceOver doesn’t read it and only recognizes the button that shows the color picker sheet. There is no way to modify this acccessibility element (as far as I can tell).

So I used a combination of 3 accessibility modifiers:

ColorPicker("Choose a color", selection: $color, supportsOpacity: false)
  .accessibilityElement()
  .accessibilityLabel("Choose a color")
  .accessibilityAddTraits(.isButton)

So now it reads like this:


If we look at this element, on default it will read each element individually left-to-right.

This will read as:

  • Back, Button
  • January 14th, 2022
  • Forward, Button, Not Enabled

VoiceOver now reads as “January 14th, 2022, Adjustable, Go forward or back a day”. This is a bit more simple and is only one element, while communicating the date or week range can be changed.

First we need to use .accessibilityHidden(true) on the buttons. Then use .accessibilityElement(children: .combine) on the enclosing HStack so VoiceOver considers these three elements as one. Then the last line is a .accessibilityHint().

To get “Adjustable”, I used .accessibilityAdjustableAction(). With this an element can be incremented or decremented, so to go forward a day you increment and back you decrement. Of course, you still can’t go to a future date. This also applies to the Week variant of this.


I also created Accessibility Rotors. In the case of Liquidus, I made 2 rotors. Only only goes through drink breakups with a non-zero amount. The other only goes through dates in the Log View that have an amount.

Another thing I did is .accessibilityActions. For the most part I put the toolbar items here, so you don’t have to go to the top of the view.


There is the @AccessibilityFocusState property wrapper and the .accessibilityFocused modifier.

Using these we can redirect the targeted element by VoiceOver.

I used them in the following instances:

  • Redirect to the Date or Week range when the date or week is changed
  • Redirect to the daily/weekly percentage and amount when a drink is added
  • Redirect to a field when it is empty

Unrelated to VoiceOver, I decided to make some UI tweaks.

I revamped the main Settings view:

I also decided to remove support of iOS 14. So all of the if #available checks are removed and the 2.0 custom symbols are removed.

I also had to rework the code for the keyboard dismissal for TextFields. Like it’s accessibility counterpart, there exists the @FocusState property wrapper and .focused modifier. I redirect the focus away from the TextFields when a “Done” button is pressed.


These are the resources I used to implement for VoiceOver:


While I might work on optimizing for other accessibility features, I’d like to work on a Widget, which’ll be inspired by the Fitness Widget.

1 Like

Wow, you’ve put a lot of work into this app! So cool to see it grow over time. I love scrolling through to see the screenshots/ how it progresses. Interesting to see you handle Gray Scale/ all of these accessibility features as well!

So cool, how you set out to implement these new features, then we can watch as they come into fruition. I want to get as good at coding in Swift as you! #goals

Keep up the good work, and love how much you share/ document this stuff along the way. Perhaps, consider summarizing it all, or even writing a Medium article on how you created this!

Cheers,
Andrew

2 Likes

Thanks. I’m glad to hear you like what I’ve done.

Sometimes I know where to start to implement a feature or optimization, but a lot of my progress comes from researching how to do certain things or how to solve any problems I run into. Sometimes this means CWC+ lessons, looking at videos from WWDC, or just doing an internet search. This is especially the case when it came to the accessibility features I’ve optimized for, as I didn’t find anything for them in CWC+ to start with.

Even if I never release Liquidus on the App Store, I can use Liquidus as a way to learn. How to optimize for accessibility features, how to add and use custom symbols, how to make a widget, and so on.

You’re right, this journal could probably do with a good summary. When one will exactly be made is hard to tell as I’m not sure where my end goal is for Liquidus.

2 Likes

Wigets

Well, I made a widget. Two, to be exact; a medium and large widget.


The CWC+ WidgetKit course got me through most of what I wanted to do with the widgets. However, there were afew things I had to figure out on my own.

The data source used in the course is pulling data from an API, which is unlike Liquidus that uses an API.

I landed on passing my View Model to the Widget, since on creation it pulls data from UserDefaults. Plus I had the benefit of being able to easily use all the functions in my View Model.

In hindsight, I’m not sure how necessary this was or not, but I created an App Group. These let you share data between your app and a widget (or other extensions).

This was my orginal code:

if let data = UserDefaults.standard.data(forKey: Constants.savedKey) {
  if let decoded = try? JSONDecoder().decode(DrinkData.self, from: data) {
    self.drinkData = decoded
    return
  }
}

With an App Group it changes to this, with the suite name being defined by the developer:

if let userDefaults = UserDefaults(suiteName: "group.com.cengelbart.Liquidus.shared") {
  if let data = userDefaults.data(forKey: Constants.savedKey) {
    if let decoded = try? JSONDecoder().decode(DrinkData.self, from: data) {
      self.drinkData = decoded
      return
    }
  }
}

Say you want to update your widgets after a change occurs in the app. You can use this line of code in your main app: WidgetCenter.shared.reloadAllTimelines(). You’ll have to import WidgetCenter though.

There’s also WidgetCenter.shared.reloadTimelines(ofKind:), for if you have multiple widgets types.


The other thing that came up was a configurable widget. In Liquidus, you can view your intake data by the day or by the week. With the widgets, I wanted this same option.

To figure this out I used these resources:

For any configuration you need a SiriKit Intent Definition File. Since the time period can either be by day or by week and only these values, I can create an Enum in the definition file as my list of options is not dynamic. Then I just have to tell the definition file to take in a value from Enum.

First, since I started from a Static Configuration, I changed my widget to be a Intent Configuration

Then for my SimpleEntry, I added these properties:

  • relevance: TimelineEntryRelevance
  • timePeriod: String

From here we need to update the Provider struct, since I started with a Static, non-configurable, Provider. The provider has to conform to the IntentTimelineProvider Protocol. The getSnapshot() and getTimeline() now have an additional for configuration: _ parameter.

In the Provider struct we also have to add a function that converts the value of the Intent to a value to use in the entry, since I used an Enum. Which for me looked like this:

private func timePeriod(for configuration: TimePeriodSelectionIntent) -> String {
  switch configuration.timePeriod {
  case .day:
    return Constants.selectDay
  case .week:
    return Constants.selectWeek
  default:
    return Constants.selectDay
  }
}

In the getTimeline() function of Provider, we now have to pass in a relevance to the entry. This dictates when its important to show your widget, if it’s in a Smart Stack, to the user. To get the relevance we need this: TimelineEntryRelevance(score: _). score is the numerical value of the relevance. The higher the number, the more important it is. I decided to dictate this based on how close the user is too their daily or weekly goal. The bigger the difference, the higher relevance.

The above resources are great for at least figuring out how to fill a Intent Definition file and what changes you need to make. From what I can tell this process is a bit more complicated if you have a dynamic list of options. For instance, if I wanted to let the user sort between drink types on the widget. This list won’t be static since the user can add and remove from it.

Now, we can choose and see daily/weekly data:

Since (I think) I have some of the groundwork for Siri Shortcuts, I will take this on next. The functionality of the shortcut will be like the widgets, to view daily/weekly intake.

2 Likes

Wow this is awesome! Nicely done - so cool to see you build out major features this fast :slight_smile:

Interesting solution to deal with no API as well by using the UserDefaults and making everything reload.

The Siri interactions seem next level, so can’t wait to see how they turn out.

Cheers,
Andrew

1 Like

Siri Shortcuts

For a shortcut, the idea is to tell you how much progress you’ve made to your daily or weekly goal.

For this purpose, I can reuse the SiriKit Intent Definition File I used for the Widget. Of course, I had to configure it some more for use with Siri and Shortcuts.

An Intent Definition File essentially tells Siri what it should expect as input to perform an action, what to expect returned from the action, and how to display information.

For a Widget, this file dictates the input, not the output, which is your Widget view.

Something this file doesn’t do for you is process your input and gather the information for your response.

Just like a Widget, Xcode generates some files for you when you add an Intents Extension to your project. One of these files is the IntentHandler, which you have to code.

This alone is enough for Siri to return information if you create a shortcut in the Shortcuts app.

Depending on the configuration of the Shortcut, Siri will also ask for the parameters of your intent.

I also set up Shortcut donation, or if you do an action, it will appear as a Suggested Shortcut from your app. When a drink exists, “View Daily Intake” and “View Weekly Intake” will be donated.

Now you can create a custom UI for your shortcut, an IntentsUI, but you have to use UIKit for this. As I’m not as experienced in UIKit I skipped this, but it’s something you can do.

This is the resource I used if you’re interested in doing this yourself:

I want to try to display data in the form of a chart or graph. So, over some period of time, you could see your intake graphically. This includes overall and for specific drink types. I do want to expand this past the Daily and Weekly Intake the app already displays. I also think this could be incorporated into another Widget and Shortcut. This might be a new tab in the app altogether or just a rework of the current Logs tab.

1 Like

Trends View

Since my last entry, I’ve been slowly chipping away at this new view while keeping up with my college classes.

The goal of this view is to show your consumption data graphically and how it changes over time.

The Trends View does replace the Data Logs View from the earlier versions of the app.

For each drink type, you will see an up or down arrow or a line. This represents if your consumption is trending up or down over the last 3 months. For this to display, 3 months have to pass from the first entry of the selected drink type. So you’ll eventually see something like in the image to the right.

From there, you’ll see a graph with your data along with the average or total. As you might have noticed this view was very much inspired by the Apple Health app. Now data can also be viewed by month, 6-months, and year.

When you tap any of the bars, you’ll get more specific information. If you tap on the background or the same bar, the highlight will go away.

You might be wondering how you change the day/week/month/6-month/year. That’s where swipe gestures come in. Just like in the Intake View, you can’t go to a future date/week/month.

To do this I made numerous new functions in my View Model and a new DataItem model. I also unintentionally made two loops that never end, so that’s cool.

Then there’s the “View All Data” button. As the name suggests, you will be able to see each data point for each day. This replaces the function of the Data Logs View.

Like all the other views throughout the app, optimizations have been made for Smaller Screens, Dynamic Type, High Contrast Colors, Grayscale, and VoiceOver. Here’s the chart on an iPod touch.

For VoiceOver, how exactly do you represent a visual aid auditorily? I decided to use iOS 15’s Audio Graphs. For an understanding of what this is, I’d recommend watching this from 1:31 for a demo: Bring accessibility to charts in your app - WWDC21 - Videos - Apple Developer

While I have implemented this using this tutorial, I don’t know if it works since I get this when trying to activate it through the Simulator: [AXRuntimeCommon] Unknown client: Liquidus. I don’t know if this is a limitation of the Simulator or if I messed something up.

I do want to implement this as a Widget (in some form or another) so that’s my next goal for this. I’m expecting this widget to be a bit more challenging as I don’t have a static list of options like I did before.

Jumped over to a new journal.