iosswiftuiswiftui-navigationstackswiftui-sheet

Different navigationBarTitleDisplayMode behaviour between iOS 17 and iOS 18


I have this bottom sheet in my app I'm building for a tutorial (100 days of Swift UI), and I noticed that the exact same code for showing a bottom sheet with a title has different effects on iOS18 vs iOS17.

It seems like the .automatic option for .navigationBarTitleDisplayMode behaves differently across iOS versions and it's driving me crazy.

This is my ContentView:


import SwiftUI

@Observable
class Expenses {
    var items = [ExpenseItem]() {
        didSet {
            if let encoded = try? JSONEncoder().encode(items) {
                UserDefaults.standard.set(encoded, forKey: "items")
            }
        }
    }
    
    init() {
        if let encoded = UserDefaults.standard.data(forKey: "items") {
            if let decoded = try? JSONDecoder().decode([ExpenseItem].self, from: encoded) {
                items = decoded
                return
            }
        }
        items = []
    }
}

struct ContentView: View {
    @State private var expenses = Expenses()
    @State private var showingAddExpense = false

    var body: some View {
        NavigationStack {
            List {
                ForEach(expenses.items) { item in
                    Text(item.name)
                }.onDelete(perform: removeItems)
            }
            .navigationTitle("iExpense")
            .toolbar {
                Button("Add expense", systemImage: "plus") {
                    showingAddExpense = true
                }
            }
            .sheet(isPresented: $showingAddExpense) {
                AddView(expenses: expenses)
            }
        }
    }

    func removeItems(at offsets: IndexSet) {
        expenses.items.remove(atOffsets: offsets)
    }
}

#Preview {
    ContentView()
}

and here's my AddView:

import SwiftUI

struct AddView: View {
    @State private var name = ""
    @State private var type = "Personal"
    @State private var amount = 0.0

    var expenses: Expenses

    private let types = ["Personal", "Business", "Other"]

    var body: some View {
        NavigationStack(root:  {
            Form {
                TextField("Name", text: $name)
                
                Picker("Type", selection: $type) {
                    ForEach(types, id: \.self) {
                        Text($0)
                    }
                }
                
                TextField("Amount", value: $amount, format: .currency(code: Locale.current.currency?.identifier ?? "USD"))
                    .keyboardType(.decimalPad)
            }
            .navigationTitle("Add Expense")
            .navigationBarTitleDisplayMode(.automatic)
            .toolbar() {
                Button("Save") {
                    let item = ExpenseItem(name: name, type: type, amount: amount)
                    expenses.items.append(item)
                }
            }
        })
    }
}

#Preview {
    AddView(expenses: .init())
}

As you'll see on my screenshots, on iOS 18, the title of the AddView defaults to a large title, while on 17.4 it defaults to inline (which I'm trying to achieve in this case).

Even when manually changing the display mode to .inline, it is still has more padding than I would like to and looks significantly different from the iOS 17 version.

Has anyone experienced this or can anyone point me to a resource explaining what's changed? Or is this buggy behaviour in iOS 18?

ios18 automatic ios17 automatic ios18 inline


Solution

  • The size of the title and the gap above the Form can be resolved with two small changes:

    // AddView
    
    NavigationStack  {
        Form {
            // ...
        }
        .contentMargins(.top, 0, for: .automatic)
        .navigationTitle("Add Expense")
        .navigationBarTitleDisplayMode(.inline)
        // .toolbarTitleDisplayMode(.inline) // works the same
        .toolbar() {
            // ...
        }
    }
    

    Screenshot

    As mentioned in a comment, if you want more control over the styling then you might want to consider a custom header instead. See this answer for an example (it was my answer).