My Device Log

The core purpose of Retrospective Timelines is to help you keep track of the most important dates in life. I use it to keep track of my sobriety, places I have lived, and my professional life. I’ve also found it helpful as a tool to track less emotional data such as the computers and devices I’ve used over the years.

Over the last week or so I’ve build a Timeline in my copy of the app that can hold a list of all of my computers and devices. For the most recent devices all I had to do was look around my apartment to see what I’m using. For older devices I found that my email accounts help a treasure trove of data that I could use. I have receipts for the original orders of almost everything I own and I had a lot of emails containing info about when I sold or donated old devices. Another good source of data was my photos library, as I’m the kind of dork who takes photos of new computers when I get them.

Initially I started with a single Timeline called “Computers & Devices”. I started adding event records to this timeline for each device. I used date ranges to keep track of the date I received and got rid of each device. For things I still own, I used the Ongoing toggle on the End Date section. After working with the data for a couple of days I decided that I didn’t want to mix gaming/personal devices in with the list of computers. I ended up making three Timelines: Computers, Gaming Devices, and Virtual Reality Devices.

Timelines and my Computer Timeline

A couple of weeks ago I built a filtering feature for Timelines and the special report lists, but I had to remove the Timeline version of filters before shipping version 1.0 because a weird bug. As I was working with this data I realized that I wanted to add those filters back to the Timeline view. After working out the issues I released version 1.0.2 this week.

There are two new filter options that help me view my device history in different way. By default, the Computers timeline has a list of all dates for all events. This includes the start date for each item and the end date where applicable.

Filter: Hide End Date

The filtered called “Hide End Date” is a quick way to hide the end date rows for events with date ranges. In this case, it leaves me with a list of start dates/events for each computer on the list.

The other new filter is called “Only Show Ongoing Events” this does just that. It filters out all of the events except those that are marked with an Ongoing date range. This is a great way to show just the devices that I currently own and use.

Here is a short video describing how I use these lists and these new filters.

Retrospective Timelines 1.0.2

Retrospective Timelines version 1.0.2 is now available on the App Store. This update adds a new Filtering feature to the list of Events on your Timelines. Filter out End Date Records when working with date ranges, and also filter the list to only show Ongoing Events. This version also fixes a bug that could cause the iPad version to crash when exporting data as a CSV.

Reminder: Retrospective Timelines is on sale for $1.99 USD from now until Dec 31, 2019.

Retrospective Timelines 1.0.1

The first app update for Retrospective Timelines is hitting the App Store today.

Features

New color options when sharing an Event as an image.

  • Select from 10 colors
  • Colorful background with white
  • Light background with colorful text
  • Dark background with colorful text

Small changes

  • Added a “dismiss keyboard” button that show up when a text field is active. You can also dismiss the keyboard with the “Return” on the iOS keyboard.
  • Added links to the website “How To” and “Contact” pages
  • Minor bug fixes

Without a splash

Just over a week ago I launched Retrospective Timelines in the App Store. In fact, “launched” may be too strong of a word. I released the app on Sunday Dec 1, 2019 with little fanfare and even less of an idea of what I was doing. Over the months leading up to version 1.0 I had a lot of ideas about how the launch could go. I thought about trying to build up some excitement for the app and I considered reaching out to press in advance.

Every time I thought I was close to shipping the app something would change that would cause a delay. Sometimes a SwiftUI update would break something I had previously finished. Other times I would start to think about solving a problem in a new way. Knowing myself pretty well, I began to recognize that I could keep working on version 1.0 indefinitely without ever shipping a thing. I needed get something out and I needed to do it soon.

In early November I decided that I had to ship something by Dec 1 and I began to work backwards from that. I did everything I could to finish the features I was building, although I had to pull a few things at the last minute. By the time had the app in a suitable state for shipping I was running out of time on my deadline. I knew that I had no chance of getting attention on such short notice so I decided not to even try.

Retrospective Timelines hit the App Store and I mentioned it on Twitter. Aside from that, there was no launch.

Over the next few months I hope to get some attention on the app. I’m convinced that a lot of people could benefit from thinking accomplishments and I hope to get the app in their hands. As I add new features I’ll continue to talk about the app on Twitter and on my podcast Project Update.

In the future maybe I’ll be able to learn how to launch the “right way” but for now this will have to do. At least it’s out there and in some weird way that’s the most important thing right now.

SwiftUI Modal Badness

I’ve had a longstanding issue in Retrospective Timelines. I have a simple EditButton on the list of timelines in my app. This button toggles the list into edit mode where the user can perform several actions.

  1. Reorder timelines
  2. Delete a timeline
  3. Tap a button to open the timeline in a modal view so they can edit it.

Timeline List Edit Mode

The issue was that when I closed the modal view with a button, sometimes the EditButton on the list view would stop working. Looking at the view hierarchy in debug mode I could see that a containing object for the button was way out of position, and thus the button would stop working. I could sometimes get it snap back into position by swiping up and down on the list, but that is not really a viable solution. I needed a way to solve this.

This is a video I send to Apple as part of the feedback report (FB7265174). You can see the out-of-place container object.

I was once again banging my head on this issue today, trying to see if I could come up with a solution. While working through some options, I noticed that this only happed to SwiftUI Buttons, not to other types of objects in the navigation bar. I made a simple Text view and gave it an onTagGuesture modifier with a call to toggle the edit mode.

Text(self.listEditMode.isEditing ? "Done" : "Edit")
    .onTapGesture {
        self.listEditMode.isEditing ? .inactive : .active
    }

This worked, but I lost the nice animations that SwiftUI was taking care of for me. I came across this post on StackOverflow and modified my view to use this bool version of Edit Mode instead of the the default one that SwiftUI provides. Here is a rough approximation

struct ContentView: View {

    @State var isEditing = false

    var names = ["Item 1", "Item 2", "Item 3"]

    var body: some View {
        NavigationView {
            VStack {
                List(names, id: \.self) { name in
                    Text(name)
                }
                .environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
                
            }
            .navigationBarItems(trailing: trailingButton)
        }
    }
    
    private var trailingButton: some View {
        
        Text(self.isEditing == true ? "Done" : "Edit")
            .contentShape(Rectangle())
            .frame(height: 44, alignment: .trailing)
            .onTapGesture {
                self.$isEditing.wrappedValue.toggle()
        }

    }
}

This sample code strips away all of the additional logic from my Timeline List view, but it shows how to implement the alternative version of the edit button as a Text view. Finally I have a reliable way to toggle edit mode in SwiftUI.

User Interface Update

I’m making progress on the user interface for the app. Most of my time has been spent on making the Event List views look the way I want. I’ve also updated the Event Edit view with a new timeline picker and a long form text view for editing notes.

Timeline and Event List views side by side. I Added a new top level report called Ongoing that will show all ongoing records from active timelines.


Report list views. These contain an extra timeline element. These list views show data from all active timelines, active meaning “not archived”.


Timeline to Event List to Event Edit.


Event Edit View now contains a long form text view. This uses a UITextView wrapped in SwiftUI. It’s far from perfect but it’s good enough, at least until Apple ships a SwiftUI multi-line text field.


Event Edit View now contains a custom timeline picker.

SwiftUI – A note about onAppear()

This morning I made a custom version of a picker for the events form. I needed a way for events to select a different timeline. The default picker in SwiftUI had some limitations so I set out to make my own. The only main difference is that it uses a sectioned list with timelines sorted into non-archived and archived sections. I made a binding variable to pass in the selected timeline. This allowed me to update the selected timeline from the picker view, then just close it when done.

struct TimelinePicker: View {

    @Binding var selectedTimeline: Timeline?
    @ObservedObject var dataSource = RADDataSource<Timeline>()
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    var body : some View {
        List {

            ForEach(dataSource.loadSectionedDataSource(sort: self.getSort(), predicate: nil, sectionKeyPathName: "isArchived"), id: \.name) { listSection in

                Section(header:

                    HStack {
                        Text(listSection.name == "1" ? "Archived Timelines" : "Timelines")
                    }
                    .font(.system(.headline, design: .rounded))

                ) {
                     ForEach(self.dataSource.objects(inSection: listSection)) { timeline in
                        
                        Button(action: ({
                            self.selectedTimeline = timeline
                            self.presentationMode.wrappedValue.dismiss()
                        })) {
                            
                            HStack {
                                TimelineListRowView(timeline: timeline)
                                .foregroundColor(.primary)
                                Spacer()
                                if(self.selectedTimeline == timeline) {
                                    Image(systemName: "checkmark")
                                }
                            }
                            
                        }
                    }
                }
            }
        }
    }

    public func getSort() -> [NSSortDescriptor] {
        ...
    }
}

It just would not work though. I tried several ways of passing in the timeline, binding, observed object, environment object, etc. Nothing would work. Every time I closed the picker the initial value remained set on the events form.

Turns out that’s because I’m an idiot. I was using a call to onAppear to load data from the event record into a View Model class. This was running every time I closed the timeline picker. I would set the initial value, go to the picker, select a new value, and then close the picker which would call onAppear to set the initial value again.

The fix for this was super simple. I just added a new state variable called onAppearCalled and initialized it to false. Then I could check this condition in the function that populates my form when onAppear is called, setting it to true when it is called.

func populateOnAppear() {
    if(self.onAppearCalled == false) {
        ...
        self.onAppearCalled = true
    }
}

My Timeline Picker

Event Date List Row

I’ve been spending a lot of time today thinking about the row for the list of event dates. This has been something I’ve been sort of stuck on for the last couple of weeks. This list is a bit tricky because the data on it is not strictly events. The rows represent date records related to events. All events have at least one date (the single date or start date field) but they can optionals have an end date to form a date range. The ending date can be set to a specific date or set to an “ongoing” status where the event will show up as something that hasn’t ended yet.

For a couple of weeks I was including the ongoing date rows in this list, sorted by the current date. I decided to omit them for now. They always looked a bit out of place to me.

Here are several versions of a new design for these rows. I think I’m going to use the 6th and last option. I removed the circle indicators from the row entirely. The capsule around one of the dates indicates that the date is the one for the row. For example: You can see two rows for the event called “A date range”. It shows up in the list of sorted dates for both its start and end dates. I also decided to make everything in the list content region use the selected color for the timeline, in this case red.

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.

1 2