← Back to list

Navigating SwiftUI in Pre-iOS 16, Part 2: Nested Navigation

Nikolai Nobadi · 2023-12-15 03:30 · 0 claps · 4.2 min read
#swiftui #navigation #ios-14 #navigation-patterns #swift
Open on Medium ↗
Wiki topics: 📱 · Mobile Development

Navigating SwiftUI in Pre-iOS 16, Part 2: Nested Navigation

Building on ‘Navigating SwiftUI in Pre-iOS 16 Projects’, this sequel dives deeper into implementing sophisticated nested navigation patterns, showcasing practical examples and innovative strategies for iOS developers.

Image generated by DALL-E

Image generated by DALL-E

While it is certainly useful to be able to perform basic navigation from a single view to another in using SwiftUI in iOS 14, most applications are much more complex. As such, a single level of navigation often isn’t enough.

We need nested navigation, and we need it now!

Lucky for us, iOS 16 provided the answers, so just increment your minimum deployment target and call it a day

Yes, I know. The boss still wants to cater to the 12 people that are still using iOS 14 with the company app. So let’s see if we can enable some nested navigation in pre-iOS 16 SwiftUI.

In a perfect world all navigation performed in an iOS app would be encapsulated in a single file. Unfortunately, pre-iOS 16 SwiftUI is a dystopia, so that concept might just have to remain a dream.

So if we can’t delegate navigation to a single file, would delegating the navigation to a certain type of file be an acceptable alternative?

struct FolderListCoordinatorView: View {
    @State private var selectedFolder: MyFolder?
    @State private var list: [MyFolder] = MyFolder.defaultFolderList

    var body: some View {
        NavigationView {
            GenericListView(list: list, onSelection: { selectedFolder = $0 })
                .navViewDestination(with: $selectedFolder) { folder in
                    ItemListCoordinatorView()
                        .navigationTitle(folder.name)
                }
                .navigationTitle("My folders")
        }
    }
}

See what I did there? It’s like a Coordinator, but in SwiftUI.

Genius, I know.

If you’re not familiar with .navViewDestination view modifier, check out my previous article.

FolderListCoordinatorView manages all things related to Folders. It uses default info, but it could easily be replaced with a viewModel that provides the actual data.

It presents its child, GenericListView, which is not only unaware of the type of item it is showing, but it also knows absolutely NOTHING about navigation.

protocol Named: Identifiable {
    var name: String { get }
}

struct GenericListView<Item: Named>: View {
    let list: [Item]
    let onSelection: (Item) -> Void

    var body: some View {
        List(list) { item in
            Text(item.name)
                .asTappableRow {
                    onSelection(item)
                }
        }
    }
}

Both MyFolder and MyItem (the data model for ItemListCoordinatorView) conform to the Named protocol and can thus both be used in GenericListView.

There’s no CRUD operations here because this article is about nested navigation, but their addition wouldn’t cause any problems. And yes, if more info needed to be displayed, an actual FolderListView and ItemListView would need to be created.

The navigation performed by FolderListCoordinatorView is on par with my previous article, so let’s take a look at ItemListCoordinatorView.

struct ItemListCoordinatorView: View {
    @StateObject private var dataModel: ItemListDataModel = .init()

    var body: some View {
        GenericListView(list: dataModel.items, onSelection: dataModel.showItem(_:))
            .navigationTitle("My List")
            .withNavButton(imageName: "plus", action: dataModel.addNewItem)
            .navViewDestination(with: $dataModel.itemToEdit) { itemToEdit in
                ItemDetailView(item: itemToEdit, save: dataModel.updateItem(_:))
            }
            .sheet(item: $dataModel.newItem) { newItem in
                NavigationView {
                    ItemDetailView(item: newItem, save: dataModel.updateItem(_:))
                        .navigationTitle("New Item")
                }
            }
    }
}

Similar to its parent, ItemListCoordinatorView handles all things in this section of the app, from navigation to composition of child views. It sounds like a lot of responsibility, but as you can see, the file is not even 20 lines of code.

That code will work fine…. Unless you’re expecting programatic dismissing when trying to save an item in ItemDetailView.

I haven’t been able to figure out the exact cause, but for some reason, Views don’t seem to react to changes in published values during nested navigation.

So setting the item to nil, as is done in ItemListDataModel.updateItem, is worthless. The view simply refuses to dismiss.

Fear not, DismissableViewModifier shall swoop in for the rescue.

import Combine
import SwiftUI

// 1
struct DismissableViewModifier<P: Publisher, Item: Equatable>: ViewModifier where P.Output == Item?, P.Failure == Never {
    // 2
    @Environment(\.presentationMode) private var presentationMode

    // 3
    let publisher: P

    func body(content: Content) -> some View {
        content
        // 4
            .onReceive(publisher) { output in
                if output == nil {
                    // 5
                    presentationMode.wrappedValue.dismiss()
                }
            }
    }
}

extension View {
    // 6
    func dismissable<P: Publisher, Item: Equatable>(publisher: P) -> some View where P.Output == Item?, P.Failure == Never {
        self.modifier(DismissableViewModifier(publisher: publisher))
    }
}

It looks a little weird, but here’s the breakdown.

  1. P and Item are declared as generics to allow any kind of publisher or Equatable item to be passed in.
  2. The ancient presentationMode makes an appearance to handle programatic view dismissal when the time comes.
  3. The publisher is stored.
  4. .onChange is worthless to us, but .onReceive guarantees that changes in the publisher will ellicit a response.
  5. if the output is nil, the view should be dismissed.
  6. the convenient modifier method to make things readable in the code.
struct ItemListCoordinatorView: View {
    @StateObject private var dataModel: ItemListDataModel = .init()

    var body: some View {
        GenericListView(list: dataModel.items, onSelection: dataModel.showItem(_:))
            .navigationTitle("My List")
            .withNavButton(imageName: "plus", action: dataModel.addNewItem)
            .navViewDestination(with: $dataModel.itemToEdit) { itemToEdit in
                ItemDetailView(item: itemToEdit, save: dataModel.updateItem(_:))
                    .dismissable(publisher: dataModel.$itemToEdit) // new
            }
            .sheet(item: $dataModel.newItem) { newItem in
                NavigationView {
                    ItemDetailView(item: newItem, save: dataModel.updateItem(_:))
                        .navigationTitle("New Item")
                }
            }
    }
}

And with that small change, we can perform programatic view dismissal during nested navigation to ItemDetailView.

The full project, as well as the previous single-level navigation iteration, can be found here at my GitHub.

If anyone finds value in this, please let me know with a clap or comment. And if you have ways to improve what I’ve created, feel free to share them.

There’s still plenty that could be done to expand the capabilities of this pre-iOS architecture.

From more view modifiers to clean up the code to adding CRUD operations and network calls, I’ve got ideas for how to fully utilize the power of SwiftUI in the dystopia that is iOS 14.

The quality of those ideas may be up for debate, but they’re there.

Image generated by DALL-E

Image generated by DALL-E


메타데이터
post_id
84d762d73f2f
slug
advancing-swiftui-navigation-mastering-nested-structures-in-pre-ios-16-apps-84d762d73f2f
url
https://medium.com/@nikolai.nobadi/advancing-swiftui-navigation-mastering-nested-structures-in-pre-ios-16-apps-84d762d73f2f
canonical_url
https://medium.com/@nikolai.nobadi/advancing-swiftui-navigation-mastering-nested-structures-in-pre-ios-16-apps-84d762d73f2f
author_url
https://medium.com/@nikolai.nobadi
status
ok
fetched_at
2026-07-24 18:22:41