App Icon Drafts

I spent a little time this evening playing with app icon ideas. This is the one I like the most so far.

Update: 2019.10.30 I made my first round of revisions of these. I added some gradients and I replaced the white color with a light grey and I think looks pretty nice.

iPad Width issues

There are some issues with the NavigationView in SwiftUI that prevent the Master Detail version from showing the back button in portrait mode. To get around this I’m using StackNavigationViewStyle. This is OK on most iPads, but on the larger iPad Pro models in landscape it looks ridiculous.

This is my attempt to get around this issue. I added a frame to the list/form object on each of these layouts.

.frame(minWidth: nil, idealWidth: 600, maxWidth: 700, minHeight: nil, idealHeight: nil, maxHeight: nil, alignment: .leading)

This caps the maxWidth at 700pt. While this works as intended it does’t look great. On the layouts using GroupedListStyle I was able to match the background, but it would look much better if I could round the section corners. iOS 13 has a new grouped style for that but it has not made it to SwiftUI yet.

On the layout using DefaultListStyle this looks a little better. The only thing I don’t care for the section header. If I could remove the background color it would look much better.

If you know of a workaround for rounding the List Sections please let me know.

Event Detail and Edit combined

Up until now I’ve been using separate views for Event Detail (view only) and Event Edit. I decided to try to combine these into one view and work them into the main navigation stack. This way event data entry can be done without opening a modal. Adding a new event will still be done in a modal though.

I have two versions of this.

Option 1 is pretty much the old event edit view with some “time passed” calculations in the section footers below the dates.

Option 1

Option 2 is a version where I renamed the segmented control for End Date and changed the labels. Instead of asking the user to select what type of end date they want (none, closed, ongoing) I ask them what type of event they want (single date, date range, ongoing event). I think this helps clear up some confusion as it’s much easier to explain what an event type is then it was trying to explain the nuances of end dates.

Update: 10/22/2019

Dave and I discussed these options on Project Update episode 17 yesterday and he gave me some ideas. We both agreed that option two was the better choice out of the options above. Dave had a couple of suggestions.

  1. Change the Ongoing icon back to the empty circle to differentiate them from end dates.
  2. Change the Event Type control to omit the Ongoing option. Users can select either Single Date or Date Range
  3. If the user selects Date Range as the Event Type then show an additional control in the End Date section where they can mark an end date as Ongoing.

This is my first pass at implementing these suggestions. I added a toggle to the End Date section. If this is false (default) then the date picker will show. Otherwise the date picker will hide and the Ongoing symbol is shown with the label.

Event Detail modified Ongoing controls

Side note: the End Date section footer in this image has not been updated to omit the time passed string when an end date is set to Ongoing.

I’m not sure if I like this change or not. It might be a little easier to understand, which is the most important factor for this screen.

UI as of Oct 14

I’ve spend the last few weeks working on schema and core data. I’m ready to start working on some more advanced user interfaces. Before I begin I wanted to share some images of what the app looks like as of Oct. 14, 2019. Timelines will likely change very little, but I have a lot of work to do on the Events layouts.

Timelines
Timelines dark mode
Events
Events dark mode

Dynamic Sort Descriptors and Predicates

I based most of my core data code for Retrospective Timelines on an example project that you can check out here. The developer sent me the link to this in a comment on stack overflow a couple of months ago. Throughout the project I’ve made small changes to this to better suite my needs.

Today I made a huge set of changes. I wanted a way to build UI controls that can modify the sort descriptors and predicates that the fetch requests used to drive the FRC. In the sample project this is done by passing in some optional strings, then parsing them into the objects they need to be in the a function that prepares the fetch request. This approach did not scale for what I need, as some of the layouts need complex sorting and/or predicates. I made a new version of this that replaces the optional string properties with some alternatives.

First the sort descriptors. Core Data has a way to apply multiple sort descriptors by passing them as an array. Even if I only have one sort order (rare for this project) I can just pass a single descriptor in the array

private var sortDescriptors: [NSSortDescriptor]?

The predicates were a bit different. Core data accepts one predicate for a fetch request, not multiple… kinda. Predicates can be combined using compounds. I can make compound predicates on each layout as needed.

private var predicate: NSPredicate?

Then I needed a way to add these to the fetched request.

private func configureFetchRequest() -> NSFetchRequest<T> {
        let fetchRequest: NSFetchRequest<T> = T.fetchRequest() as! NSFetchRequest<T>
        fetchRequest.fetchBatchSize = 0
        
        if let sortDescriptors = self.sortDescriptors {
            fetchRequest.sortDescriptors = sortDescriptors
        }
        
        if let predicate = self.predicate {
            fetchRequest.predicate = predicate
        }
        
        return fetchRequest
    }

I need a way to call this publicly as well. This calls the private method after checking the optionals. If I no longer have a descriptor or predicate, I set the property back to nil so it’s no longer used in configureFetchRequest

public func loadDataSource(sort: [NSSortDescriptor]?, predicate: NSPredicate?) -> [T] {
        
        if let sort = sort {
            self.sortDescriptors = sort
        } else {
            self.sortDescriptors = nil
        }
        
        if let predicate = predicate {
            self.predicate = predicate
        } else {
            self.predicate = nil
        }
        
        self.fetchRequest = configureFetchRequest()
        self.frc = configureFetchedResultsController()
        
        return self.allInOrder
    }

Now for the cool part. I can make a View Model for each layout where I can place some properties to drive controls in the user interface. This View Model will also handle building the sort descriptors and predicates for the layout. They can then be used as a parameter when I call loadDataSource.

Here is a basic example of the View Model that drives the list of event dates. The sortToggle variable is bound to a UI toggle so when the user taps it the sort order changes. I’ll replace this with a better sort button, but the underlying concept will remain the same.

class EventListVM: ObservableObject {
    
    @Published var sortToggle = false

    public func getSort() -> [NSSortDescriptor] {
        return [NSSortDescriptor(key: "isOngoing", ascending: sortToggle), NSSortDescriptor(key: "date", ascending: sortToggle)]
    }
    
    public func getPredicate(timeline: Timeline?) -> NSPredicate? {
        if let timeline = timeline {
            
            let startString = String(format: "%@%@", "dateStartEvent.eventTimeline", " == %@")
            let startPredicate = NSPredicate(format: startString, timeline)
            
            let endString = String(format: "%@%@", "dateEndEvent.eventTimeline", " == %@")
            let endPredicate = NSPredicate(format: endString, timeline)
            
            let compoundPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [startPredicate, endPredicate])
            return compoundPredicate
        }
        return nil
    }
    
}

The only thing that is a little nuts is the way I get the records to use in the ForEach view. This calls a method on one ObservedObject while using return values from two functions on another ObservedObject. I feel like I’m getting away with something here.

@ObservedObject var dataSource = RADDataSource<RTDate>()
@ObservedObject var eventListVM = EventListVM()

...

ForEach(dataSource.loadDataSource(sort: self.eventListVM.getSort(), predicate: self.eventListVM.getPredicate(timeline: self.timeline))) { rtDate in
...
}

Togging the sort order.

This is just a simple example, but now that I have the foundation in place I can extend this to work on much more complex user interfaces.

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.

1 2 3 4 5