Learn Courses My Dashboard

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.

1 Like

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.