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.

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.

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.