• Software
  • Blog
  • About
Menu

Dabbling Badger

Street Address
54455
Phone Number

Your Custom Text Here

Dabbling Badger

  • Software
  • Blog
  • About

Overlapping Navigation Titles In SwiftUI

December 12, 2020 Jonathan Badger
Photo by Brendan Church on Unsplash

Photo by Brendan Church on Unsplash

Navigation stacks are a fundamental user interface component in iOS. We use them everyday as we tap in and out of messages in Mail, search our contacts to make a phone call, and adjust the settings on our phones. Being so crucial to the user experience, I was a bit surprised to find a navigation related bug in SwiftUI. Navigation titles from dismissed views were piling up at the top of the navigation bar in an overlapping mess. What the heck?! Here, we will go over sample code that both recreates this behavior and demonstrates current fixes.

  • Based on XCode v12.3 and SwiftUI 2.0

  • Feel free to fire up XCode and start a new project or if you are feeling lazy download the completed demo here.

FoodGroups Demo

To recreate the overlapping title phenomenon we will code up a small demo app that lets users explore food groups. There will be two views in the app. Our primary ContentView will display a list with two categories, fruits and vegetables. Tapping a food group will navigate to a detailed list of example foods. We won’t code up additional user interaction, but let’s assume in our detailed view we expect the user to edit the content, so we will want to provide mechanisms to save or cancel changes.

FoodGroupsComposite.png

Here is the code for a simple data model and main ContentView:

struct FoodGroup {
    var name: String
    var examples: [String]
}

struct ContentView: View {
    var foodGroups: [FoodGroup] {
        let fruits = FoodGroup(name: "Fruit", examples: ["Apple", "Banana", "Pear", "Peach", "Mango", "Orange", "Strawberry", "Watermelon", "Pineapple", "Lemon", "Lime", "Cherry", "Date", "Plum", "Apricot", "Blueberry", "Blackberry", "Cranberry", "Kiwi", "Nectarine"])
        let vegetables = FoodGroup(name: "Vegetables", examples: ["Lettuce", "Carrot", "Beet", "Broccoli", "Corn", "Celery", "Chicory", "Kale", "Spinach", "Yarrow", "Brussels sprouts", "Arugula", "Cauliflower", "Turnip", "Sweet Potato"])
        return [fruits, vegetables]
    }

    @State var isDetailLink = true
    var body: some View {
        NavigationView {
            List {
                ForEach(foodGroups, id: \.name) { foodGroup in
                    NavigationLink(
                        destination: DetailView(foodGroup: foodGroup),
                        label: {
                            Text(foodGroup.name)
                        })
                        .isDetailLink(isDetailLink)
                }
                Button(action: {isDetailLink.toggle()}, label: {
                        Text("Detail link: \(String(isDetailLink))")
                })
                .buttonStyle(BorderlessButtonStyle())

            }
            .navigationTitle(Text("Food Groups"))
        }
    }
}

This code should look fairly straightforward. We have a simple struct FoodGroup for our data model, a List to display our food groups, and a NavigationLink on each item that will display examples of each food in DetailView. The only thing to note here is the addition of the “Detail link” button at the bottom of the list. Clicking detail link toggles whether the.isDetailLink modifier is true or false for the NavigationLink. We will revisit this button when we talk about fixes and what effect this has on the resulting UI.

The code for the second view of the app is as follows:

struct DetailView: View {
    @Environment(\.presentationMode) var presentationMode
    var foodGroup: FoodGroup
    @State var navDisplayMode: NavigationBarItem.TitleDisplayMode = .large
    var body: some View {
        Form {
            ForEach(foodGroup.examples, id: \.self) { foodName in
                Text(foodName)
            }
            Button(action: {
                if navDisplayMode == .large {
                    navDisplayMode = .inline
                } else {
                    navDisplayMode = .large
                }
            }, label: {
                Text("Nav Display Mode: \(navDisplayMode == .large ? "large" : "inline")")
            })
            HStack {
                Spacer()
                Button(action: {dismissView()}, label: {
                        Text("Save")
                    })
                Spacer()
            }
        }
        .navigationBarTitle(foodGroup.name, displayMode: navDisplayMode)
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: Button(action: {self.dismissView()}, label: {
            Text("Cancel")
        }))
    }

    func dismissView() {
        self.presentationMode.wrappedValue.dismiss()
    }
}

In DetailView we are using a Form to display the name of each food in the food group. The back button in the navigation bar has been hidden using .navigationBarBackButtonHidden(true) and in it’s place we have added a Cancel button. All that cancel does is dismiss the view in this example, but we might imagine pressing cancel calls an undo manager reverting some state in our application. Similarly, we have a save button at the bottom of the view that dismisses the view. Again, let’s imagine that we persist changes in state to our data model when pressing this button. The last thing to note is the ‘Nav Display Mode’ button. Tapping this button toggles the navigation title display mode between .large and .inline.

Our app is now complete, so let us explore the overlapping title phenomenon and fixes.

Demonstrating the behavior

To demonstrate the overlapping title behavior:

OverlappingNavigationTitles.jpg
  1. Start an interactive preview by pressing the play button in the Canvas window.

  2. Click the list row labeled Fruit.

  3. Scroll to the bottom of the list of fruits and click save.

If you don’t see anything on your first try keep moving back and forth between the main view and detail view a few times, being sure to scroll down in the view so that the navigation title shrinks and centers itself in the top of the navigation bar. Eventually you will start to see overlapping text in the navigation title.
As far as I can tell, this bug only shows up if you: 1) have the navigation title displayMode of a destination view set to .large and 2) have added items to the navigation bar using the .navigationBarItems modifier.

Fixes

1. Add the .isDetailLink modifier to your NavigationLink view and pass false.

If you aren’t familiar .isDetailLink dictates the view presentation behavior on iPads and Macs. When .isDetialLink is true (default) child views are presented next to the sidebar and when .isDetialLink is false the new view replaces the old view in the sidebar. So, if you are building a catalyst app or wish to support both iPhones and iPads this probably isn’t the best option.

2. Set the navigation display mode explicitly to be .inline in the detail view. To do this you can use the .navigationBarTitleDisplayMode or .navigationBarTitle(_, displayMode: _ ) modifiers.

You can test out both of these fixes in the Canvas preview using the extra buttons we coded in earlier. (You may need to start and stop your live preview to clear title text that is already stuck on the screen)

And there you have it. Another SwiftUI bug swept under the rug!

In Programming Tags iOS, SwiftUI, Bug
← Tips And Tricks For Making The Most Of TextFields In SwiftUIA Quick Fix For Misbehaving List Cells In SwiftUI →

E-mail: dabblingbadger@gmail.com

Copyright © 2023 Dabbling Badger LLC

POWERED BY SQUARESPACE