Christopher‘s App Development Journal

Welcome to my journal here.

This will be cataloging my progress. I do plan to post here every day on the weekdays for the most part. As I’m commuting to college, I find time to code while on the train ride back, as odd as that might sound.

I’m picking up where I left off in my HealthKit App Challenge Journal where I’ve been working on an app called Liquidus. Although I won’t just be posting about my progress with that app.

Here’s a brief synopsis of it:

  • Developed with SwiftUI for iOS 15 that lets users log their intake throughout the day, see their consumption over various periods, and see how their consumption trends over time
  • Utilizes UserDefaults for data persistence and storage
  • Implement Optimizations for Smaller Screens, Dynamic Type, High-Contrast Colors, Differentiate with Color, Reduce Motion, VoiceOver, Audio Graphs
  • Implement Widgets, Siri Interactions, and Apple HealthKit Integration

For more details on all that check out the original journal linked above.

I decided to start a new journal since I’m expanding the purpose of it.

2 Likes

3/22/2022

I more recently finished work on a set of views that would let the user see how their intake trends over time and see their intake graphically.

I wanted to incorporate this into a widget.

I did make a widget but I ultimately decided to scrap some of my work.

Most of it comes down to one of the ways widgets work. Widgets are given a time limit for how long they take to gather information and put together views. With displaying 6-month and yearly data, I ran into this time limit.

I figured it might be best to repurpose this into the preexisting widget.

My original intent with this widget was to not copy code. So I had to rework my existing trends chart view.

This involves moving a bunch of functions into my ViewModel and ending up with a 10+ parameters for a view.

I stumbled to the Any and Any? types. As the name suggests this can be any data type (or nil).

The way the trends chart works is by generating a date, array, or 2D array of dates and using that to get the data for the chart. If I’m moving methods into the ViewModel I have to pass these in. I could have just passed in, but I did it for the sake of simplifying the function call and having parameters that may not be used.

Then you can do something like this:

let dates = Date.now

if let date = dates as? Date {
  // do whatever

} else if let dates = dates as? [Date] {
  // do whatever

} else if let dates = dates as? [[Date]] {
  // do whatever

}

In the parent view, I can dynamically change what to pass in as dates depending on user selection.

It was interesting to rework my functions where I need to send more parameters in compared to before I changed it and with not having direct access to these properties.

This also has the effect of reducing some of the file lengths.

Now I’m be starting work on updating the preexisting widget.

3/23/2022

Today I really just worked on modifiying the widget.

Adding the chart was straightforward enough. I did have to make some modification to the chart code to remove the axis labels, but it was simple enough to do by using an isWidget parameter.

I also discovered the .prefix() method for arrays. If I wanted the first 2 elements of an array I’d do .prefix(2). This returns an ArraySlice so all you have to do is cast to an Array. This is much better way of doing this than how I was doing it.

I did have to basically remove part of what the widget originally displayed and move it over.

There are some spacing things I have to work out. The alignment of the progress circle particularly bothers me. It’s been surprisngly difficult to get the progess circle aligned properly, using spacers and VStack alignments haven’t been working.

This widget originally utilized copying-and-pasting code from the app, while modifying parts of it. I’m thinking of just modifying the code in the app, like I did with the Trends Chart.

3/24/2022

Today I did rewrite my code for the progress circle so it’s more modular and I can remove duplicate code in the widget. Nothing particularly challenging came up with this.

I did resolve my spacing issue for the widget, though. The reson it’s not aligned with the text element is because of the frame. I used a GeometryReader to have it take up half of the space. All I did is change it to a fourth instead of a half.

I do want to play around with the text placement. I’m thinking of moving the percentage to below the progress circle and having the consumed amount above its respective chart.

3/28/2022

Yesterday I ended up with this:

I also learned something about the .prefix() method I mentioned in an earlier journal.

I assumed that it had an optional return type in the event you took the prefix of an empty array or of an array with fewer elements than you want the prefix of.

Say I have an array with two elements. If I want the .prefix(3) or greater I’ll just get those two elements. For an empty array, you’ll get ArraySlice([]).

I simply created my own method in the Widget view that would return nil for an empty array and the x-first elements in all other cases. This way I can use an if let statement.

For the medium widget all I have left to do is add the drink symbols, (maybe) mess with the spacing of the percentage, and test for smaller screens and Dynamic Type sizes.

3/29/2022

So I ended up with this today. I landed on this as I thought this would be a bit easier to optimize for smaller screens and Dynamic Type.

However, I might revisit some of the design in the last post for the Large widget.

As you below can problem see I got some room for improvement on smaller screens.

(Top/Left: iPhone 13 Pro Max | Bottom/Right: iPod touch (7th gen))

Simulator Screen Shot - iPod touch (7th generation) - 2022-03-29 at 16.31.57

4/4/2022

Yesterday, I completed my work on the medium widget and started on the large one.

I did ultimately decide to restrict Dynamic Type sizes for the medium widget. Especially on smaller devices, the circular progress bar will continually shrink as the text below it gets bigger.

I do want to figure out a way to incorporate the amount consumed with the large widget. I don’t have an exact plan for this. All I’ve really decided on is that putting it next to the drink name definitely isn’t option considering Dynamic Type and user-added drink names.

4/13/2022

Well I landed on this:

The problem with this design (in both widgets) is for optimizing for Differentiate without Color. The amount, graph, and title are not next to each other. To create this differentiation, I used shapes.

In the large widget, I had to change the amount display a bit but it worked out.

This version of the widgets have now been pushed to my GitHub repo.

I want to do some changes unrelated to UI elements:

  • Not use strings for drink type names, make a class
  • Unit Testing
  • Replace UserDefaults with Core Data

I’ll probably do it in this order.

4/15/2022

Up to now, I have been relying on String arrays to store drink types names and dictionaries to store their properties.

I have created a new DrinkType model. This model stores the DrinkType's name, color, whether or not it is a deafult type, whether it’s enabled, and whether the color was changed.

It was fairly straight forward to implement the Encodable, Decodable, and Identifiable protocols. I also implemented the Equatable and Hashable protocols.

The Equatable protocol allows you to compare two custom objects (in this case DrinkTypes). As a property of a Drink is its type and I implemented Equtable for this class, I needed to do this. It was as simple as just comparing each of the above properties between two DrinkTypes.

Then the Hashable protocol. In the Log Drink view, I give the user a Picker to select a DrinkType. In order to use a class as a binding, you have to implement Hashable. Depending on the properties of a class, you may not have to do anything. I had to impelement hash(into hasher: inout Hasher) method. For each property I called hash.combine() to generate a unique hash. Just like UUIDs these are unique and each time the app is run a different hash will be generated.

Just like I had before, I store and save an array of DrinkTypes to keep track of all of them. For functions like, changing the name, changing the color, or enabling/disabling, I would update the particular instance in the array. For the case of name and color, I would update them in each drink as well. Since the app checks against this array for whether a type is enabled or not, I don’t need to change it for each individual drink.

However, there is a problem with this. When I would enable or disable a drink type the views wouldn’t immediately update. After some digging around online, I came across objectWillChange.send(). This essentially tells all your views there is some change in your observable or enviornment object and to refresh. You would just call this on the object, so in my case I did model.objectWillChange.send().

Well onto the next one: Unit Testing.

4/16/2022

Today I went through the Unit Test lessons.

I already had a basic understanding of Unit Testing going in, but not how it worked in Swift/SwiftUI. The general structure of units test methods and the assert methods are largely similar (or at least the ones I’m used to using).

While I wasn’t expecting it, I appreciate the setUp() and tearDown() methods. You don’t have to create a bunch of variables assigned to various instances of a class or a create seperate instances in each test method, you can just reuse a single variable.

I also really appreciate the Unit Testing UI in Xcode especially versus Eclipse. Having the option of having a description associated with each assert if it fails, seems like it’ll prove useful when it eventually happens.

It does seem like I have my work cut out for me with the sheer scope of Liquidus. I’d also wager a guess that I can create seperate Unit Tests from the main app for the widgets and intent.

4/24/2022

I’ve been chipping away at Unit Testing Liquidus.

I started with my models being Drink, DrinkType, DataItem, and DrinkData.

It was a good thing I already conformed Drink, DrinkType, and DataItem to the Equatable protocol so I can just do XCTAssertEqual(drinkA, drinkB) without having to go through each property every time. Interestingly, because DrinkData is a struct, not a class, I don’t have to do anything besides since it conforms to Equatable. Nor do I have to do any extra work when comparing [Drink], [DrinkType], and [DataItem] arrays.

I did forget how Dates work. I can’t just do dateA == dateB and expect it to evaluate to true assuming they are the same. You have to use Calendar.current.compare(). My best guess as to why would be similar to how Strings work in Java. If stringA and stringB are the same stringA == stringB will still return false because these two variables are stored in different places in memory. So you’d have to use stringA.equals(stringB). But that’s just my speculation with this.

In Java, the toString() method dictates how data types are converted to Strings. I wanted to see if something similar to this existed in Swift. I thought it could be useful to add more detailed test failure messages. If you make a class (probably a struct too but I didn’t try it) conform to CustomStringConvertible, you will be able to set the description property of your class as a computed property to return a specified String when the type is converted to a String.

In terms of the models, I tested the constructor by checking each property of the class. I also tested the Equatable code.

Then to DrinkModel. I did have to add a test parameter to it so it won’t read the stored data and just create a brand new DrinkData. So far I’ve written tests for the constructor where the test parameter is set to true, the addDrink() method, and the getDataItems methods for a day, week, and month.

For each getDataItems test they were split into two tests, one where all drinks regardless of types are used and one where one Water was filtered for. I had to create a method for creating and adding the drinks to the data store and one to get the expected results.

I got some 79 more methods to go in my DrinkModel with some work already started on the getDataItemsForHalfYear() method. Got a lot of work ahead of me.

May 16th, 2022

I’ve finally finished Unit Testing my ViewModel (DrinkModel).

Since I’ve worked with Drinks, DataItems, and AXDataPoint, I had to create sample arrays of these types. With a file for each type, I made a static function that would get a set of the data type across a given day, week, month, half-year, and year. All of these pulled from the same set of amounts stored as static properties in another file.

I couldn’t figure out a way to test functions that are within views that use an EnvironmentObject. You could do this EnvironmentObject<DrinkModel>(), but this doesn’t initialize the DrinkModel itself and will give you an error when testing.

But, all other functions within the main app have unit tests for them. For some more detail, since I’d have a lot to go through, check out the GitHub Repo.

Guess that means I’m onto CoreData implementation next.

May 29, 2022

I decided to divert from my original plan of jumping into CoreData.

Instead, I decided to revamp one of the main systems in the app, much as I did with Drink Types.

Liquidus allows viewing data across a day, week, month, half-year, and year. Prior I’ve used Date for a day and an hour, [Date] for a week and a month, and [[Date]] for half-years and years.

Each of these now has separate classes; Hour, Day, Week, Month, HalfYear, and Year. All of these classes conform to a protocol called DatesProtocol.

This protocol requires two initializers: one for the current day and one that takes in a Date. It also requires, a UUID, a description, and an accessibility description. Both descriptions are fairly similar with some variations. It also has a data property.

This property is assigned an associatedtype in the protocol. This means, that when creating a conforming class or struct, you can choose what this data type is. For instance, an Hour and Day uses a Date, a Week and a Month uses a [Day], a HalfYear uses a [Week], and a Year uses a [Month]. Yet, these classes all conform to DatesProtocol.

Additionally, HalfYear and Year have a superclass that supplies methods both classes use to generate their properties since that’s how I went about it in the old system.


With these created, I updated the DrinkModel, all the views, the widget, the intent, and the unit tests.

What was a little problematic was updating Day, Week, Month, etc. in the view and expecting the view to update. Just supplanting wouldn’t update the view with @Binding and @State.

After doing some looking around online, I found that I had to create a boolean trigger @State/@Binding The parent-most view would hold the @State variable, and the sub-views would get a @Binding. After some action occurred, I had to toggle the variable, from true to false or vice versa, or update another @State variable.


Another thing I did alongside this was updating the documentation throughout the app. All of the methods have blatant parameter and return methods. Classes, Structs, and their properties have documentation too, with the exception of views. More information about documentation can be found here.

You can also get your documentation to show in Xcode’s documentation view. All you have to do is, in the menu bar, go to Product > Build Documentation. It’ll create the documentation based on what you provide.

May 30, 2022

Before moving on to CoreData, I wanted to take on some minor things first.

The first of which was removing the Weekly viewing option in the Intake View. I believe this made more sense to have before the Trends view was implemented. This was fairly straightforward to do and wasn’t particularly complex. This included the main app, the widget, and the intent.

The next thing I tacked was some visual lag introduced during the work mentioned in the last entry. There were two sources: when logging a drink and when entering the Trends view.

By removing DispatchQueue.main.async in the drink adding method, the lag in the drink logging view was removed. I haven’t been able to pinpoint the source of the other visual lag and I’m at a bit of a loss of how to fix it.

Unless I happen upon the source and/or a solution, I’ll most likely just move on to CoreData.

May 31, 2022

I found the source of the lag!

I came down to how the Day, Week, Month, HalfYear, and Year classes relate to each other.

The data property of Year is a Month array and a Month is a Day array. Likewise, a HalfYear is a Week array and a Week is a Day array.

For all those days, descriptions have to be generated. This created that second or two of lag.

Now, this issue wasn’t present in the way I implemented the older system. So I based the change on the way I did it originally. I changed the typealias of Week and Month to a [Date].

The way I discovered this is by using the Instruments app included with Xcode, much like the Simulator and Accessibility Inspector.

Among the things you can test, is how long specific actions take in your app to complete. I was able to figure out this method from this article: How to use Instruments to profile your SwiftUI code and identify slow layouts - a free SwiftUI by Example tutorial

Now onto CoreData (for real this time).

June 5, 2022

I’ve started the process of implementing Core Data.

At first, I took some time to review the Core Data modules from the iOS Databases course.

I originally used UserDefaults for data persistence. I saved it using a DrinkData object, containing the drinks, drink types, daily goal, units, and other information.

I decided to move over my Drink and DrinkType models into CoreData. There are going to be multiple drinks and drink types. However, the other information, there should only be one daily goal, one unit set, etc. So this information is now UserInfo and still stored in UserDefaults.

After creating the CoreData Data Model, I spent a lot of time going through the whole app removing errors this introduced alone. The main error source was drinks and drink types not stored in DrinkData/UserInfo anymore. So this meant adding @FetchRequests, passing in FetchedResults into the DrinkModel, and rewriting functions.

Since a Drink is associated with a DrinkType, I created the opposing relation, so by having a DrinkType, you can access all the associated Drinks. So filtering data by date, creating DataItem for the Trends chart, and calculating amounts and percentages were moved to the DrinkType class.

However, my DrinkModel continues to handle Total figures. Such as the total amount consumed over some date(s), filtering all drinks by a time period, and other information where all the drinks are necessary.

So far, I’ve eliminated any Xcode Errors and the main app runs well. A source of error is retrieving from and adding to Apple Health. This comes from the processing of Apple Health data in the DrinkModel into a Drink and creating a new Drink which completely crashes the app.

You get nice and comforting errors, like, Serious application error. Exception was caught during Core Data change processing. This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification.

Well, that’s a thing I need to fix. In addition to things that probably don’t work properly in the Widget and Intent. Looking forward to WWDC Keynote tomorrow.

June 14, 2022

At last, I’m done with my CoreData implementation.

The first thing to start with is the Apple Health bug. The solution I landed on was moving the Apple Health data retrieval method to the specific views. While this does mean I three places with the same code, it does fix the bug.

The Widget and Intent were simple enough to rework for CoreData. I was actually able to simplify them a little bit since methods were directly tied to DrinkType.

Lastly, I tackled updating my Unit Tests.

First I looked into being able to access CoreData, without being able to save it to the store. This was achieved by modifying the PersistenceController initializer with a new parameter. If this parameter is true, it will use this code:

let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType

Otherwise, it uses this:

let description = NSPersistentStoreDescription()
description.type = NSSQLiteStoreType
description.url = storeURL

I also created a new static variable, inMemory, where this parameter is set to false.

Another problem I quickly ran into is passing in FetchedResults into methods. While the methods work fine, it is seemingly impossible to unit test for.

FetchRequests and FetchedResults can only be used in views. While we have NSFetchRequest, there doesn’t seem a way to convert to FetchedResults. So this parameter got changed to an array. By simplifying using .map { $0 }, on the FetchedResults we can get an array of the same type from the calling view.

I also had to get a little creative with methods that returned arrays of Drinks or DrinkTypes, as there isn’t just an initializer for all properties. So I created another class (called SampleDrinkTypes) that I contained static methods that generate the 4 default DrinkTypes: Water, Coffee, Soda, and Juice. Additionally, since DrinkType's now stored their associated Drinks, the SampleDrinksjust returns an array ofDrinkTypes with added Drink`s.

My work generally consisted of updating the Unit Tests to use the CoreData objects and UserInfo.

Now it’s finally done, with these changes now in the GitHub repo.

I haven’t exactly decided on my next step with Liquidus yet. As I might take some time to do some more of the CWC curriculum.