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?
The size of the title and the gap above the Form
can be resolved with two small changes:
navigationBarTitleDisplayMode
(or toolbarTitleDisplayMode
) to .inline
instead of .automatic
.contentMargins(.top, 0, for: .automatic)
to the Form
to remove the gap.// AddView
NavigationStack {
Form {
// ...
}
.contentMargins(.top, 0, for: .automatic)
.navigationTitle("Add Expense")
.navigationBarTitleDisplayMode(.inline)
// .toolbarTitleDisplayMode(.inline) // works the same
.toolbar() {
// ...
}
}
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).