Lazy sorting

Yesterday I mentioned an issue I can across when sorting date records. Some of my date records could validly have a nil value in the date field (these are the “end” records for ongoing events). The problem was that Core Data was sorting these nil records to the wrong part of the list. When sorting descending, they should be at the top of the list, but they were at the bottom.

There are a ton of ways to tackle a problem like this: custom sort descriptor, sort by a derived value, omit the nil values, fetch the nil records in a different section, etc. I didn’t do any of these.

Instead I cheated. I added a new Boolean field called isOngoing and set it to false for all new records. When a user marks a record as “Ongoing” I toggle this to true. Then I modified my sorting to sort by this field, then the date field. This has an added bonus of letting me easily group these two types (true and false) into discrete sections in list view. I may even add a control to let the user hide the ongoing records and all I have to do is hide the section or drop in a predicate to filter out the true values.

This is a lazy way of solving this, but I’m happy with the results.

Schema changes and date sorting

I wrote about a sorting issue in Retrospective Timeline a while back. You can read the full post here. At the time I decided to go with Solution A, which involved wrapping my core data objects in a container using map and filter calls. That approach worked at first, but there were some issues when it came to refreshing the SwiftUI views. Basically, since I was moving the data outside of the fetched results controller, the views could no longer receive publisher updates when data was changed. I could work around this but it felt like reinventing the wheel.

Last week I decided to modify the schema instead. I started with this

  • Timeline (Parent object)
  • Event (Child Object
    • Start date value
    • End date value

I added a new table called RTDate and moved the Event date fields here. The entities I have now are

  • Timeline
  • Event
  • RTDate (the “RT” prefix is just to avoid confusion with Date types)

As for the relationship from Event to RTDate, I decided to go with two “To One” relationships rather than a “To Many”. As a database developer this isn’t what I would normally do, but it is important to keep in mind that I really just wanted to relocate some properties in a new table that can drive the UI.

I finished up the schema changes yesterday, ripping out the old version as I went. Both Core Data and CloudKit now have the new schema. I considering doing some sort of migration but decided against it since I’m the only user for now. It’s easier to just re-add my data when I’m ready.

This week I’m going to focus on adapting my views for this new schema. Most of what I have now will be easy to modify, but the list of events (now list of dates) will take some extra work. There are four types of date rows that can show up in this list view.

  1. Single – the date record for an event with a single date
  2. Start – the start date record for an event that has an open or closed date range
  3. End – the end date record for an event that has a close date range
  4. Ongoing – the end date record for an event that has an open range.

The “Ongoing” row type will take some extra work. This is a valid RTDate record, but the date property is set to nil because no date is selected. I need to substitute the current date for these records. I think I can use Core Data Derived Attributes for this, but I’m having trouble learning how to use that feature.

Naming things is hard

One of the hardest parts of any project seems to be naming things. In Retrospective Timelines this problem surfaces when trying to communicate the purpose of event fields.

An event record consists just a handful of fields

  • Name: A string describing the event. All events must have a name.
  • Date / Start Date: All events must have a date in this field. It’s name changes though. If an event has an end date, then this needs to be displayed as “Start date”, otherwise it should just say “Date”
  • Include end date: This is a Boolean toggle that can show and hide an end date picker and field.
  • End Date: A second date that is only visible when the bool above is toggled to true.
  • Notes: An optional notes field. I may or may not include this in the shipping version.

This all seems straightforward to me, but I’m the one who made it so… It seems a bit inelegant.

Then there are the names of the entities themselves.

  • Timeline – this is a great name for the list item. Each timeline has minimal data and a list of related events.
  • Event – this one I’m not so sure of. At it’s core this app tracks important dates, but does the word “event” really convey this? Are all of my important dates really events? Sometimes I use the word “milestone” but that seems too descriptive of a subset of records.

Naming things is HARD.

Event row types (WIP)

This morning I made some change to the way that I get events data from Core Data. I outlined my thinking behind this yesterday. Today I put it into practice in the app.

There are two types of events in Retrospective Timeline

  1. An event with a single date (I refer to these as milestones)
  2. An event with a start date and an end date (I call these ranges)

I wanted a way to explode the second type of event into multiple list rows. I added some code at the View Model layer that does the heavy lifting here. The result is three event row types.

  1. Single date row: events with… a singe date!
  2. Start date row: A row that represents the starting date for a date range
  3. End date row: A row that represents the ending date for a date range

For the time being I just updated my list view to show some symbols and label strings depending on the event row type. I’m not happy with the half circle symbols. I wonder how else I could represent this data…

Something I’d like to add is a toggle to show and hide the end date rows. When those rows are hidden the list would just turn into a list of events sorted based on start date.

Follow up to event sorting

This is a short follow up article regarding the problems I described in this post.

I spent some time mocking this up in a Swift Playground on my iPad. First I added a new container object called EventContainer. This is an object that I can map my events to, while keeping a reference to the Core Data Event record.


class EventContainer {
    enum EventType {
        case startEvent
        case endEvent
    }
    var date: Date
    var eventType: EventType
    var event: Event
    
    init(date: Date, eventType: EventType, event: Event) {
        self.date = date
        self.eventType = eventType
        self.event = event
    }
}

I started by mapping the entire list of fetched events to an array of EventContainer

let startDateRows: [EventContainer] = fetchResults.compactMap ( { EventContainer(date: $0.dateStart!, eventType: .startEvent, event: $0 )} )

Next I filtered the fetched events to just those with end date data

let endDataData = fetchResults.filter({$0.includeEndDate == true && $0.dateEnd != nil})
let endDateRows: [EventContainer] = endDataData.compactMap( { EventContainer(date: $0.dateEnd!, eventType: .endEvent, event: $0 ) } )

Lastly I combined these arrays into one new array and sorted it based on the date property from the EventContainer

let fullList = startDateRows + endDateRows
fullList.sorted(by: {
    $0.date.compare($1.date) == .orderedDescending
})

Now I have a sorted array of EventContainers that I can use to build my list UI. Each list row can still access the Core Data Event entity to pass on to editing views. I think this is a reasonable approach, but I’m really just guessing. If you know a better way to do this please get in touch using the contact form or contact me on Twitter.

Thoughts on sorting events

Objective: 

I need a way to sort events in a list view that makes them easy to find. 

Constrains: 

  • All events have a start date
  • Some events have an end date
  • Some people may look for an event based on its start date, other may want to look for the end date. It could even vary depending on the type of data tracked on the timeline.

Options:

  1. Show only a list of events sorted by start date. Not at all what I want.
  2. Add a timeline setting to control sorting all related events. In this option an event would appear once in a list, either at the start date or end date position.
    1. Add a Bool to the Timeline entity to indicate preference to use end date if available.
    2. If an event has an end date, use it for the sorting
    3. Else, sort by start date
  3. Generate a list of all events with all start dates AND a list of events with end dates.
    1. Start date list – all events for a timeline
    2. End date list – only events with an end date for a timeline
    3. Combine these lists together and sort them. I don’t know how to sort both lists together like this…

I’d like to proceed with option three from the list above. Using this approach will place an event in the list twice if it has a start and end date. I can format the rows in the list with some indication of type such as single date, start date range, end date range. I can find some symbols to indicate what type the are. I could also place a toggle switch on the list layout to quickly show and hide the end date rows. I might even be able to make a cool animation for this.

I’m running into problems with how to actually pull this off. I wrote a simplified version of this in a playground but I can’t figure out how to sort the final resulting array, as the elements in the array have different sorting rules depending on rather they are a starting or ending record.

var fetchResults = [Event] // this is populated with an array of events

var endDateRows = fetchResults.filter({$0.includeEndDate == true && $0.dateEnd != nil})

var rows = fetchResults + endDateRows // combine the original array with the new array
// Now what do I sort by???

I’m considering some possible solutions but I’m not sure if these are a good way to proceed.

Solution A. 

Make a View Model that can fetch all of the events from Core Data, then process them into a new array of a different type. I could make a new containing object like this.

class EventContainer {
    var dateToSort: Date
    var eventRecord: Event // a reference to the Core Data record
}

My View Model could make two arrays of EventContainer, using map and filter calls to write the start and end dates to the dateToSort field, and writing the entire event to the eventRecord field. Then it could combine these two arrays into one and sort by the dateToSort field.

This approach seems reasonable but it puts a lot of processing into the hands of the View Model. Every time I navigate to a list I’ll have to run these processes. For individual Timelines that may not be a problem but when it comes to making list views that show events from all Timelines this could run into performance issues.

Solution B.

Modify the schema. Currently I have a simple Event entity in Core Data. In simplified form it looks like this.

class Event {
    var name: String
    var dateStart: Date
    var includeDateEnd: Bool = false
    var dateEnd: Date?
}

I could modify this by moving the date fields into a new table and making one to one relationships.

class Event {
    var name: String
    var dateStart: EventDate // Core Data To One relationship
    var includeDateEnd: Bool = false
    var dateEnd: EventDate //Core Data To One relationship
}

class EventDate {
    var date: Date
}

If I set this up correctly I could modify my fetch requests to record a list of EventDate records, then use the relationships to access the rest of the event data. This option would probably perform far better. It’s also a ton of work to update the app as it stands now. This might also complicate some of the calculations involving dates because the will be several database records involved instead of one Event record.

Out of these two options I’m leaning towards the first. This is not really an approach I’ve taken before and I think it could be valuable to try it. Coming from 8 years of FileMaker development, my first thought is to try to solve this like a database normalization problem, but I’ve learned the hard way before that Core Data is not a database. Sure, it uses a data and fills the same role as a database, but it’s an Object Modeling tool. It may be good to force myself to use a new type of thinking to solve this problem. If I run into performance issues then I can also take another crack at the second option. Either way, I need to spend some time thinking about this more before I decide how to proceed. Perhaps this is a good topics for Project Update.

The app I’ve been working on

The app I’ve been working on over the last few months is called Retrospective Timelines. I’m still trying to learn how to talk (and write) about the app in a way that makes sense and appeals to customers, but for now I’ll just write a bit about what it is and what it does. This will be a rambling post…

Retrospective Timelines is an app can track lists (Timelines) of personal milestones and important events. The main purpose of the app is to put these milestones and events into context. How long has it been since X Event? How long was I working on Project Y? What was happening in my life when I worked at Company Z?

An event can be a single point in time or a date range with a start date and end date. For example:

  • Jul 27, 2019: Decided to work with SwiftUI
  • June 25, 2015 – August 29, 2019: Employed at Company Z

I’ve been tracking this data for years in various spreadsheets and apps. I’ve never found one app that can hold all of it in a way that makes sense to me. Some examples of the type of data I want to track

  • Personal milestone – big picture things in my life such as my sobriety date
  • Addresses – when did I live at each address
  • Employers – when did I work for each company
  • Devices – when did I get a computer and how long did I keep using it

I’m still working on some views that will visualize this data. I’m going to start by making a graphical view for single timelines, but down the road I want to introduce the ability to combine timelines to cross reference data.

Database stuff

I’m using Core Data and CloudKit to build the backend of the app. It’s important to me that this data syncs to all of my iOS devices. The schema is simple, at least for now.

  • Timelines are lists that can have a name, icon, color, and multiple related Events.
  • Events are child records of a timeline and contain data such as event name, start date, optional end date, notes, etc.

User interface stuff

I started this project in UIKit and got pretty far before I decided to switch to SwiftUI instead. While SwiftUI is still new and buggy, there are just some things in UIKit that drive me nuts. I hope I never have to work with auto layout again.

For now the app is mostly using stock iOS controls with some styling added. I have a lot of work to do on each of the screens below, but this should give you an idea of what the app looks like for now.

Timeline views

The top of the navigation stack starts with Timeline List View. You can add, edit, reorder, and delete timelines. The four rows in the first section don’t do anything yet.

Event list and editing

I’m not really happy with the way that Events look right now, but I have data entry screens ready and working.

Event detail view

I’d like to replace this view with something more graphic. Perhaps some sort of card generated using the Timeline icon and color. The app calculates the time since a date in a readable format. For events with a date range it also calculates the curation of the range. Below you can see that I owned a MacBook Pro with an awful keyboard for 1 year, 10 months, and 7 days.

One week with SwiftUI

I just wrapped up a week of working with SwiftUI full time. It certainly has some rough edges and each new beta version brings a lot of changes. Here is a recap of what’s working for me and what I still need to figure out.

List views are really simple in SwiftUI. The first layout that loads in my app is a List view has two main sections. The first section has a few rows for some saved queries that I’ll implement later, the second section is dynamic data from Core Data. Xcode Beta 5 introduced some new property wrappers and environment variables for working with Core Data. You can read more about these additions here.

Data entry: I made some simple forms with a few fields and a custom control for picking colors. The only problem I’m having with data entry is actually with the modal presentation implementation of SwiftUI. I call the sheet modifier and pass it a view, but this has some issues with not loading data on repeated calls. I’m calling this modal sheet from the main list view for adding new records, and from a detail view for editing existing records. I have an issue where I can’t get the list view to update when I modify a record that is a few views away (List > Detail > Edit). I think this has something do do with the way that I’m loading the list with Core Data records, as there is nothing that is triggering a refresh when I save the modified record. I need to figure out a better way to setup a publisher with Core Data. A possible workaround for this is to implement record editing on the list rows, just like when a user swipes a table row to disclose an edit button. I just can’t seem to figure out how to do that yet. I may use the new ContextMenu instead if I can’t figure it out soon.

Button("Add List") {
    self.editModal = true
}
.padding(EdgeInsets(top: 0, leading: 0, bottom: 10, trailing: 20))
.accentColor(Color(Theme.shared.appMainTint))
                          
.sheet(isPresented: $editModal,
       onDismiss: {
        self.timelineEditModal = false
},
       content: {
        ListEditView()
            .environment(\.managedObjectContext, self.managedObjectContext)
})

Color Picker: The list based item in my app is composed of of a name, color, and icon. I wanted to try to mimic the new color picker in the new version of the Reminders app. While SwiftUI doesn’t have an alternative to the Collection View yet, I was able to get my color picker working by dynamically calculating a number of columns. I used Geometry Reader to get the width of the parent view, then just divided it by the size (including padding) that I wanted each color to take up. I passed that number to a child view that parses out a list of colors into rows and columns. I got the idea for the grid here. I’m planning on implementing something similar with the icon, using a subset of SF Symbols.

Dynamically sized color picker on iPad and iPhone.

I spent a large part of today refactoring a lot of my code into smaller views that are easier to reason about. Xcode can have a hard time handling complex views with lots of embedded views, so it can be helpful to break them into small components where possible.

Some issues I haven’t figured out:

  1. Master Detail by default – On iPad I get free Master Detail by default, but I’m not certain this is right for my app. I have not found a way to just fill the screen with my list view. There is also an annoying issue that iOS loads an empty detail view when in portrait mode. I can’t believe this is still the default behavior after years of iOS but it is… 
  2. Modal views seem to break when loaded from a List item. They will work correctly the first time, but when opened two or more times they do not load the data that is being passed to them. I was able to confirm that the data is being passed, but SwiftUI is not using it to update the views.
  3. iOS 13 has a new (old) style for rounding the corners of grouped tables/lists. I can’t find a way to do this yet. .listStyle(GroupedListStyle()) does not do the trick.
  4. List Edit Mode – I can toggle edit mode on, but have yet to figure out what to do next. I want to let users reorder rows, and I want to show an info button that will open a modal sheet for modifying a record.

SwiftUI is still new and can be frustrating at times but I think I can safely say that I’m going to make the production version of my app with it. In one week of work I caught up to the UIKit version of my app, which took nearly two months. Granted, a lot of that time was focused on planning and design, which benefited both versions. I think I’ll stick with SwiftUI just so I never have to deal with Auto Layout ever again.

Thinking about SwiftUI

Last week I decided to spend the weekend learning about SwiftUI. The weekend turned into an entire week but I was eventually able to learn everything I set out to learn. This course on Udemy was particularly helpful.

I’m going to take a long weekend to give myself time to reflect on what I have learned so far. One thing I want to think about is if I should use SwiftUI for my app. The app I’m working on is simple enough and SwiftUI can do almost everything I need. There are a few areas where I may have to drop out to UIKit but for the most part I think I can make it work. Importantly, I think I may be able to make a better user experience with SwiftUI than I can with UIKit. So many things in UIKit require more code and complexity and if I stay on that route it may be several months before my app meets the expectations that I have for it.

I guess I have a lot to think about this weekend.

Questions for myself about SwiftUI

This weekend I’m going to spend some time with SwiftUI. I tried to think of the most important questions that I want to answer. I have no idea how much progress I will make but if I come up with answers to these I’ll write about them.

  1. Can I connect a SwiftUI List to a Core Data FetchedResultsController?
    1. How do I set this up?
    2. Load data
    3. Edit data
    4. Delete data
  2. Does SwiftUI support advanced table features?
    1. Readable Size for cells
    2. Reorder controls
    3. Context actions
  3. How can I pass an object from a list view to a form view?
    1. Dependency injection?
  4. Can I make form views that do not save data until the done button is tapped? Pass back to the List view to save the changes. 
  5. Can I easily segue to UIKIT View Controllers? I need access to CollectionViews and Large TextViews, neither of which exist in SwiftUI.
1 2 3