End of Retrospective Timelines (and app development)

As of today, I’m giving up on my half-baked plans to make consumer software. I just took Retrospective Timelines off of the App Store and I don’t plan on renewing my Apple developer account. There are a number of reasons I’m making this decision. I’ll try to list a few here.

While I enjoyed the process of developing the app, it’s become clear to me that I will *never* actually do any of the marketing steps needed to promote a consumer product. I’ve made grand plans many times and at the end of the day I just refuse to do the work.

From the coding side, working with Apple APIs has been the most frustrating experience of my career. SwiftUI is a mess and I want nothing to do with UIKit. Core Data and CloudKit are needlessly complex for very little gain over using something else.

From a business aspect, it just doesn’t make sense for me to continue spending time on app ideas and other consumer products (games, VR apps, etc.). It is clear to me that my skills as a software and business consultant are far more valuable than any app I’ll ever make.

I may end up changing my mind someday when I have some hairbrained idea for an app (especially for AR headsets) but for now my time and energy are better spent working on projects that have a clear payoff, and a clear end.

On a personal note, my RSI is getting worse and worse. I can’t afford to do any “hobby” development like I used to. There is a limited time I can spend using a computer and I must spend that time on productive work.

Summer Planning for Retrospective Timelines

I am beginning the planning process for Retrospective Timelines 2.0. Below I listed some of the areas that I want to focus on this summer. My hope is that the latest round of updates to SwiftUI, iOS 14, and iPadOS 14 will enable me to make significant improvements to the app. I have only just started to learn about the new features, so I do not know which of these items are possible yet.  

Modal Windows / Views 

The current version of Retrospective Timelines uses sort of a hacky solution to be able to present the data entry screens in a modal mode. There were some weird bugs with SwiftUI sheets last year where onDismiss was not always called. Are these issues solved? Can I now present modal views where the user must use the Cancel and Done buttons to close the view? 

Alternatively, can I rethink my design entirely to omit this type of data flow? Currently I am using modal views to validate the data before the user saves the record. Perhaps I should rethink this process and save each change that the user makes on the data entry screen. 

Toolbars 

The current implementation of toolbars is a real mess. I found a lot of issues with buttons when placed in a navigation view (some of them having to do with opening and closing modals). Can I use the new toolbar features in SwiftUI 2 to replace all this stuff?  

Navigation Structure and List Views 

The current navigational structure of the app is less than ideal. It looks fine on an iPhone but on an iPad it is really quite bad. I would love to make a three-column interface for iPadOS. I think I might be able to use the new Sidebar APIs to accomplish this. 

I also want to use a new feature in List to group my main list view. The new child keypath feature could let me place all three top level elements in one list. Those are Reports, Active Timelines, and Archived Timelines. 

Something that annoyed me on iPad was the lack of a readable width setting for list views. Has this been added? It might not be as important if I can get the three-column interface working. 

Other stuff

  • Data Entry: TextField in SwiftUI 1.0 is bad. I mean really really bad. It has so many issues that I think nobody should be using it in a shipping app. Because of all of the bugs and missing features I ended up rewriting all my data entries screens in UIKit. I would love to write these in SwiftUI if I can. 
  • Grids: SwiftUI 2 has a new grid feature. I think I can use this to replace my hacky solution for the color and icon pickers for timelines.  
  • Accent Color: I did this the hard way last year. I am going to use this new method for the next version of my app. 
  • SwiftUI now has a feature for opening URLs. I can remove my hack for this as well. 
  • Core Data: I would love to learn some new ways to work with Core Data and CloudKit in SwiftUI. The approach I took last year has a lot of limitations and issues. 

As you can probably tell, I have no idea yet what is possible and what is not. I have a lot of work cut out for me over the next few weeks. I’m going to start a new branch in my repo and dive in. I’m also going to try a whole new code path in my project. Rather than try to update my current views, I’m going to start with a new top level view and work my way through reimplementing each core feature of the app, with an emphasis on learning the new APIs along the way.

Retrospective Timelines 1.0.4

A new version of Retrospective Timelines is rolling out now. This version adds a handful of new features

  • New Settings to control formatting for dates and durations. Select these settings to apply them across the app.
  • Redesigned share view with new formatting options for dates and durations.
  • Events can now have text notes. Enter notes on the Event Edit screen. Notes are viewable on Events Detail.

There is also a new bonus feature on the Event Detail screen. Now that the app supports multiple format types for dates and durations, I decided it would be awesome to switch between these with a simple tap. When you are viewing an Event on the Event Detail screen, just tap a date or duration to cycle through the available format options. Doing so won’t effect the defaults that you can set on the settings screen.

Data Entry with SwiftUI and UIKit

Version 1.0 of Retrospective Timelines was written completely in SwiftUI and while I’m excited to keep working in SwiftUI, there were some issues and limitations that it imposed on my app.

You can’t really talk about SwiftUI without someone pointing out that it’s not “ready” to be used in production yet. I mostly disagree with that sentiment, but when it comes to data entry forms it rings true. Many of the common UI controls in SwiftUI have the most basic implementation possible, often with little to no ability to customize them.

SwiftUI TextField for example.

  • No way to dismiss the keyboard (you can press Return, but a lot of users don’t think to try that)
  • Text selection in the field is really flakey.
  • No SwiftUI equivalent of TextView, so multi-line text is not possible
  • Speech to Text fails. I’ve tried this on every device I have and it fails 100% of the time. It will insert the first character from the first word you speak, then quit listening. I reported this bug months ago.

These issues and limitations were reflecting poorly on the app and giving my users the wrong impression so I decided to do something about it.

My first attempt was to wrap the UIKit controls in SwiftUI wrappers, but I had little success. I cobbled together a sort-of-working version of UITextField, but it was buggy and unreliable. It was also super slow. Rather than keep spending time on it, I decided to just write the entire screens in UIKit.

In Retrospective Timelines all data entry is done in a modal view. There are only two data entry forms and a limited number of ways to open them. I had already disregarded SwiftUIs sheet presentation due to its limitations and instead was using an environmental variable to get the hosting View Controller to use for presentation. All had to do was change the View Controller that I was presenting from the SwiftUI hosting controller I was showing, to a new UIKt version.

I don’t know a ton about UIKit, so it took me a couple of days to make these forms. I used a storyboard for the view controllers and some XIBs for small elements like collection and table view cells. The forms are basically static UITableViewController objects with a ton of customization. Setting these up and accounting for all of the edge cases was a lot more work than the same thing in SwiftUI. I found myself missing the way SwiftUI handle the state of data.

The end result turned to better than I thought possible, so I can’t really complain. Not only did I work around the limitations of the SwiftUI controls, I also ended up with a far better version of the Color and Image pickers for Timelines.

Retrospective Timelines 1.0.3

A new update is rolling out now with improvements to the data entry screens for Timelines and Events. These screens have been completely rewritten to support user interface features common to iOS apps.

Time Tracking in 2020

While working on my goals and my annual theme for 2020, I decided that I’m going to give time tracking another try. When I was working full time as a consultant I used to track all of my time because the companies I worked for mostly billed by the hour. When I started Radical Application Development in 2015 I knew that I didn’t want to bill by the hour. Why? That’s for another post some time… or you can just read this to find out a lot more than I will ever explain.

Even without relying on billable time, I have at certain points used time tracking in my business. I find it helpful to set goals for myself such as:

  • “Work at least X hours on Project Y per day”
  • “Spend no more than X hours on product Z”

I’ve built my own time tracking apps in FileMaker and in 2016 I attempted to build one in Swift (it went badly…) I’ve also used a number of third-party apps and service. The problem is that I hate every single one of them, and all for the same reasons. The time part of time tracking. When I say I want to track my time, what I really mean is that I want to track the duration or amount of time I spent on an activity. I don’t want to track start and end times, I don’t want to punch a clock, and I really dislike the idea of having timers running while I work.

What I really want is a place where I can list out all of the activities that I would track time against, and then a super simple interface to enter a block of time for an activity and day. On Sunday I decided take another attempt at building my own time tracking app in Swift. This time I’m using SwiftUI and Core Data, along with all of the lessons that I learned building Retrospective Timelines. I talked through some of my ideas with my friend Dave and wrote out a short punch list. I decided to build something as fast as possible so I could start using it on Jan 1.

Yesterday I worked out some of the details around the schema. I setup Core Data CloudKit so I can use the app on multiple devices. I make a few simple placeholder views and started working on data entry. Today I kept working on the UI and got it far enough along that I can use it to log time against a list of activities. It’s really really basic, but it will serve my needs for at least a few weeks. I can work on adding small features to it in my spare time.

My time tracking app as of 2019.12.31

I have no idea if I’m going to try to turn this into a product or not. I think I’d like so, but I’m not sure how many other people think about time tracking in the same way that I do, without the need to enter clock time or use timers.

If this is something you are interested in please get in touch. Use the contact form or reach out to me on Twitter.

Generating Shareable Text Images

One of the core features of Retrospective Timelines is the ability to share an Event as an image. I’m working in SwiftUI on this project so my first attempt was to see if I could render SwiftUI views as images. Last June Erica Sadun wrote a post about rendering SwiftUI on macOS Mojave that gave me some ideas to try. There is some sample code in that post that renders a UIView as an image, but the view is hosting SwiftUI views in the first place. I tried to adapt this to my needs in the app, but I quickly ran into issues.

First, I couldn’t find a way to make SwiftUI/iOS render view content that was not on screen. If I wanted to make a large resolution image I would need to scale it to fit on screen on an iOS device in order to save it as an image. Second, scaling the image in this manner introduced all sorts of scaling artifacts. The backgrounds look OK but the text was awful.

The next I thing I tried ended up being my “good enough” solution. I rendered a UIView offscreen with all of the text views manually positioned and added to this view.

Content View contains a simple UI. At the top is an Image view that is showing a live preview of the final image that we are rendering and sharing. There is a text field to enter a string of text and a simple color picker object in a scroll view. The function called `createViewToRender()` is doing most of the work here. I create some views, position them, and add them to a parent object.

struct ContentView: View {
    
    @Environment(\.viewController) private var viewControllerHolder: UIViewController?
    @State var selectedColor: String = "user_purple"
    @State var displayText: String = ""
    
    var body: some View {
        VStack(alignment: .center) {
            
            Image(uiImage: self.renderViewAsImage)
                .resizable()
                .mask(RoundedRectangle(cornerRadius: 10))
                .overlay(RoundedRectangle(cornerRadius: 10)
                    .stroke(Color.gray, lineWidth: 0.5)
            )
                .scaledToFit()
                .shadow(color: Color.gray, radius: 1, x: 0, y: 0)
                .padding()
            
            TextField("Enter a string", text: self.$displayText)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            
            ScrollView(.horizontal) {
                ColorPicker(selectedColor: self.$selectedColor)
            }
            
            Spacer()
            sharingActionButton
                .padding()
            Spacer()
            
        }
        .frame(minWidth: nil, idealWidth: nil, maxWidth: 800, minHeight: nil, idealHeight: nil, maxHeight: nil, alignment: .center)
        .navigationBarTitle(Text("Share"))
    }
    
    var sharingActionButton: some View {
        Button(action: {
            self.shareEvent()
        }, label: {
            VStack {
                Image(systemName: "square.and.arrow.up")
                    .font(Font.body.weight(.bold))
                    .padding()
                Text("Share as image and/or text")
                    .font(.footnote)
            }
        })
    }
    
    private func roundedFont(fontSize: CGFloat, weight: UIFont.Weight) -> UIFont {
        if let descriptor = UIFont.systemFont(ofSize: fontSize, weight: weight).fontDescriptor.withDesign(.rounded) {
            return UIFont(descriptor: descriptor, size: fontSize)
        } else {
            return UIFont.systemFont(ofSize: fontSize, weight: .regular)
        }
    }
    
    private var renderViewAsImage: UIImage {
        createViewToRender().renderAsImage()
    }
    
    private func createViewToRender() -> UIView {
        
        let labelPositionX: CGFloat = 40
        let labelWidth: CGFloat = 944
        
        let eventNamePositionY: CGFloat = 140
        let eventHeight: CGFloat = 300
        let eventFontSize: CGFloat = 60
        
        let background = UIView()
        background.backgroundColor = UIColor(named: self.selectedColor)!
        background.frame = CGRect(x: 0, y: 0, width: 1024, height: 576)
        
        
        let text = UILabel()
        text.frame = CGRect(x: labelPositionX, y: eventNamePositionY, width: labelWidth, height: eventHeight)
        text.text = self.displayText
        text.numberOfLines = 3
        text.textColor = UIColor.white
        text.font = roundedFont(fontSize: eventFontSize, weight: .bold)
        text.textAlignment = .center
        
        background.addSubview(text)

        return background
        
    }
    
    private func shareEvent() {
        let postText: String = self.displayText
        let postImage: UIImage = self.renderViewAsImage
        let activityItems = [postText, postImage] as [Any]
        
        let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
        activityController.excludedActivityTypes = [.postToTencentWeibo, .postToVimeo, .assignToContact, .markupAsPDF, .openInIBooks, .postToWeibo]
        
        if (UIDevice.current.userInterfaceIdiom == .pad) {
            
            activityController.popoverPresentationController?.sourceView = viewControllerHolder?.view
            activityController.popoverPresentationController?.sourceRect = CGRect(x: (viewControllerHolder?.view?.bounds)!.maxX, y: (viewControllerHolder?.view?.bounds)!.minY, width: 0, height: 0)
            activityController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0) //.Down
            viewControllerHolder?.present(activityController, animated: true, completion: nil)
            
        } else {
            
            viewControllerHolder?.present(activityController, animated: true, completion: nil)
        }
        
    }
}

There are a couple utility code snippets that make help out the content view.

This help me access the hosting view controller so I can present the action sheet. I could have wrapped action sheet in SwiftUI but this was way faster to get up and running. I saw this somewhere on Stack Overflow, but I can’t remember where.

struct ViewControllerHolder {
    weak var value: UIViewController?
}

struct ViewControllerKey: EnvironmentKey {
    static var defaultValue: ViewControllerHolder { return ViewControllerHolder(value: UIApplication.shared.windows.first?.rootViewController ) }
}

extension EnvironmentValues {
    var viewController: UIViewController? {
        get { return self[ViewControllerKey.self].value }
        set { self[ViewControllerKey.self].value = newValue }
    }
}

This is what allows me to render the UIView as an image.

extension UIView {

    func renderAsImage() -> UIImage {
        let imageRenderer = UIGraphicsImageRenderer(bounds: bounds)
        if let format = imageRenderer.format as? UIGraphicsImageRendererFormat {
            format.opaque = true
        }
        
        
        let image = imageRenderer.image { context in
            context.cgContext.setAllowsFontSmoothing(true)
            context.cgContext.setShouldSmoothFonts(true)
            context.cgContext.setAllowsAntialiasing(true)
            context.cgContext.setShouldAntialias(true)
            context.cgContext.setAllowsFontSubpixelQuantization(true)
            context.cgContext.setShouldSubpixelQuantizeFonts(true)
            return layer.render(in: context.cgContext)
        }
        return image
    }
    

}

The color view as a flattened version of the one I’m using in the Timeline picker. This control just using a @Binding string variable where the string is the name of a color asset.

struct Colors : Identifiable {
    var id = UUID()
    var colorName: String = "user_purple"
}

extension Colors {
    static func all() -> [Colors] {
        
        // These strings reference named colors in the asset catelog
        return [
            Colors(colorName: "user_purple"),
            Colors(colorName: "user_light_blue"),
            Colors(colorName: "user_cyan"),
            Colors(colorName: "user_green"),
            Colors(colorName: "user_blue"),
            
            Colors(colorName: "user_red"),
            Colors(colorName: "user_orange"),
            Colors(colorName: "user_yellow"),
            Colors(colorName: "user_brown"),
            Colors(colorName: "user_grey"),
        ]
    }
}

struct ColorPicker: View {
    
    @Binding var selectedColor : String
    
    let colors = Colors.all()
    
    var body: some View {
            
        return
            
            VStack(alignment: .leading) {
                    
                HStack(alignment: .top) {
                    
                    ForEach(self.colors) { radColor in
                        
                        ZStack {
                            
                            // Add a selection circle to the current color
                            
                            if (radColor.colorName == self.selectedColor) {
                                Circle()
                                    .stroke(Color.gray, lineWidth: 3)
                                    .frame(width: 40, height: 40, alignment: .center)
                            }
                            
                            Button(action: {self.selectedColor = radColor.colorName}) {
                                Circle()
                                    .foregroundColor(Color(radColor.colorName))
                                    .frame(width: 32, height: 32, alignment: .center)
                                    .padding(4)
                            }
                            
                        } // END ZStack
                        
                    } // END ForEach
                    
                } // END HStack
                    
            } // END VStack
                
        .padding()
             
    }
}

I’m sure there are better ways to handle a feature like this, but this approach is fine for now. There are a number of changes I’d like to make in future versions.

  • Dynamic text / auto-layout. Currently the text views are manually positioned and sized around a simple layout, but this does not allow for different fonts and sizes.
  • It might be interesting to offer some gradients in addition to the colors
  • I’d like to add some decorations such as patterns or simple images that can make the images more interesting.

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
1 2 3 4