In iOS 16, aside from introducing the brand new NavigationStack
, Apple additionally launched a brand new view container named NavigationSplitView
for builders to create two or three column navigation interface. If you wish to construct UI much like the inventory Mail app, it’s best to take a look at this break up view part.
Whereas NavigationSplitView
is extra appropriate for iPadOS and macOS apps, it’s also possible to apply it to apps for iPhone. The view part mechanically adapts itself for iPhone. As an alternative of displaying a multi-column interface, it creates a single-column expertise.
The brand new NavigationSplitView
comes with numerous choices so that you can customise its look and operations. You may change the column width and programmatically present/disguise the columns.
On this tutorial, we’ll create a three-column navigation UI utilizing NavigationSplitView
.
Let’s get began.
The Primary Utilization of NavigationSplitView
The NavigationSplitView
helps each two-column and three-column navigation expertise. Their implementations are fairly comparable. To create a two-column navigation UI, you write the code like this:
NavigationSplitView { Â Â // Menu bar } element: { Â Â // Element view for every of the menu merchandise } |
For 3-column navigation interface, you add the content material
parameter in between:
NavigationSplitView { Â Â // Menu bar } content material: { Â Â // Sub menu } element: { Â Â // Element view for every of the sub-menu merchandise } |
We are going to begin with the two-column navigation UI and finally construct the three-column design.
Constructing the 2-Column Navigation Interface
If you happen to’ve learn my earlier tutorial on the expandable checklist view, it’s possible you’ll know that I’m an enormous fan of La Marzocco. In that tutorial, I confirmed you how you can create an expandable checklist view with inset group type.
Now let’s flip this expandable checklist right into a two stage navigation interface just like the screenshot proven beneath:
Earlier than we create the break up view, let’s start with the info mannequin. We create a struct to mannequin a menu merchandise:
struct MenuItem: Identifiable, Hashable {     var id = UUID()     var identify: String     var picture: String     var subMenuItems: [MenuItem]? } |
To make a nested checklist, the important thing right here is to incorporate a property that incorporates an optionally available array of youngsters (i.e. subMenuItems
). Word that the kids are of the identical kind of its guardian.
For the highest stage menu gadgets, we will create an array of MenuItem like this:
let topMenuItems = [ MenuItem(name: “Espresso Machines”, image: “linea-mini”, subMenuItems: espressoMachineMenuItems), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Grinders”, image: “swift-mini”, subMenuItems: grinderMenuItems), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Other Equipments”, image: “espresso-ep”, subMenuItems: otherMenuItems) Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â ] |
For every of the menu merchandise, we specify the array of the sub-menu gadgets. In case if there isn’t a sub-menu merchandise, you may omit the subMenuItems
parameter or cross it a nil
worth. For the sub-menu gadgets, we will outline them like this:
// Sub-menu gadgets for Grinder
let grinderMenuItems = [ MenuItem(name: “Swift”, image: “swift”),
MenuItem(name: “Vulcano”, image: “vulcano”),
MenuItem(name: “Swift Mini”, image: “swift-mini”),
MenuItem(name: “Lux D”, image: “lux-d”)
]
// Sub-menu gadgets for different tools
let otherMenuItems = [ MenuItem(name: “Espresso AV”, image: “espresso-av”),
MenuItem(name: “Espresso EP”, image: “espresso-ep”),
MenuItem(name: “Pour Over”, image: “pourover”),
MenuItem(name: “Steam”, image: “steam”)
]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Sub-menu gadgets for Espressco Machines let espressoMachineMenuItems = [ MenuItem(name: “Leva”, image: “leva-x”, subMenuItems: [ MenuItem(name: “Leva X”, image: “leva-x”), MenuItem(name: “Leva S”, image: “leva-s”) ]), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(identify: “Strada”, picture: “strada-ep”, subMenuItems: [ MenuItem(name: “Strada EP”, image: “strada-ep”), MenuItem(name: “Strada AV”, image: “strada-av”), MenuItem(name: “Strada MP”, image: “strada-mp”), MenuItem(name: “Strada EE”, image: “strada-ee”) ]), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(identify: “KB90”, picture: “kb90”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(identify: “Linea”, picture: “linea-pb-x”, subMenuItems: [ MenuItem(name: “Linea PB X”, image: “linea-pb-x”), MenuItem(name: “Linea PB”, image: “linea-pb”), MenuItem(name: “Linea Classic”, image: “linea-classic”) ]), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(identify: “GB5”, picture: “gb5”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(identify: “House”, picture: “gs3”, subMenuItems: [ MenuItem(name: “GS3”, image: “gs3”), MenuItem(name: “Linea Mini”, image: “linea-mini”) ]) Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â ] Â // Sub-menu gadgets for Grinder let grinderMenuItems = [ MenuItem(name: “Swift”, image: “swift”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Vulcano”, image: “vulcano”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Swift Mini”, image: “swift-mini”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Lux D”, image: “lux-d”) Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â ] Â // Sub-menu gadgets for different tools let otherMenuItems = [ MenuItem(name: “Espresso AV”, image: “espresso-av”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Espresso EP”, image: “espresso-ep”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Pour Over”, image: “pourover”), Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â MenuItem(name: “Steam”, image: “steam”) Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â ] |
To higher manage the info mannequin, we create a struct referred to as CoffeeEquipmentModel
like this:
// Sub-menu gadgets for Espresso Machines
let espressoMachineMenuItems = [ MenuItem(name: “Leva”, image: “leva-x”, subMenuItems: [ MenuItem(name: “Leva X”, image: “leva-x”), MenuItem(name: “Leva S”, image: “leva-s”) ]),
MenuItem(identify: “Strada”, picture: “strada-ep”, subMenuItems: [ MenuItem(name: “Strada EP”, image: “strada-ep”), MenuItem(name: “Strada AV”, image: “strada-av”), MenuItem(name: “Strada MP”, image: “strada-mp”), MenuItem(name: “Strada EE”, image: “strada-ee”) ]),
MenuItem(identify: “KB90”, picture: “kb90”),
MenuItem(identify: “Linea”, picture: “linea-pb-x”, subMenuItems: [ MenuItem(name: “Linea PB X”, image: “linea-pb-x”), MenuItem(name: “Linea PB”, image: “linea-pb”), MenuItem(name: “Linea Classic”, image: “linea-classic”) ]),
MenuItem(identify: “GB5”, picture: “gb5”),
MenuItem(identify: “House”, picture: “gs3”, subMenuItems: [ MenuItem(name: “GS3”, image: “gs3”), MenuItem(name: “Linea Mini”, image: “linea-mini”) ])
]
// Sub-menu gadgets for Grinder
let grinderMenuItems = [ MenuItem(name: “Swift”, image: “swift”),
MenuItem(name: “Vulcano”, image: “vulcano”),
MenuItem(name: “Swift Mini”, image: “swift-mini”),
MenuItem(name: “Lux D”, image: “lux-d”)
]
// Sub-menu gadgets for different tools
let otherMenuItems = [ MenuItem(name: “Espresso AV”, image: “espresso-av”),
MenuItem(name: “Espresso EP”, image: “espresso-ep”),
MenuItem(name: “Pour Over”, image: “pourover”),
MenuItem(name: “Steam”, image: “steam”)
]
return topMenuItems
}()
func subMenuItems(for id: MenuItem.ID) -> [MenuItem]? {
guard let menuItem = mainMenuItems.first(the place: { $0.id == id }) else {
return nil
}
return menuItem.subMenuItems
}
func menuItem(for categoryID: MenuItem.ID, itemID: MenuItem.ID) -> MenuItem? {
guard let subMenuItems = subMenuItems(for: categoryID) else {
return nil
}
guard let menuItem = subMenuItems.first(the place: { $0.id == itemID }) else {
return nil
}
return menuItem
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
struct CoffeeEquipmenModel {     let mainMenuItems = {         // Prime menu gadgets         let topMenuItems = [ MenuItem(name: “Espresso Machines”, image: “linea-mini”, subMenuItems: espressoMachineMenuItems),                                 MenuItem(name: “Grinders”, image: “swift-mini”, subMenuItems: grinderMenuItems),                                 MenuItem(name: “Other Equipments”, image: “espresso-ep”, subMenuItems: otherMenuItems)                             ]          // Sub-menu gadgets for Espresso Machines         let espressoMachineMenuItems = [ MenuItem(name: “Leva”, image: “leva-x”, subMenuItems: [ MenuItem(name: “Leva X”, image: “leva-x”), MenuItem(name: “Leva S”, image: “leva-s”) ]),                                         MenuItem(identify: “Strada”, picture: “strada-ep”, subMenuItems: [ MenuItem(name: “Strada EP”, image: “strada-ep”), MenuItem(name: “Strada AV”, image: “strada-av”), MenuItem(name: “Strada MP”, image: “strada-mp”), MenuItem(name: “Strada EE”, image: “strada-ee”) ]),                                         MenuItem(identify: “KB90”, picture: “kb90”),                                         MenuItem(identify: “Linea”, picture: “linea-pb-x”, subMenuItems: [ MenuItem(name: “Linea PB X”, image: “linea-pb-x”), MenuItem(name: “Linea PB”, image: “linea-pb”), MenuItem(name: “Linea Classic”, image: “linea-classic”) ]),                                         MenuItem(identify: “GB5”, picture: “gb5”),                                         MenuItem(identify: “House”, picture: “gs3”, subMenuItems: [ MenuItem(name: “GS3”, image: “gs3”), MenuItem(name: “Linea Mini”, image: “linea-mini”) ])                                         ]          // Sub-menu gadgets for Grinder         let grinderMenuItems = [ MenuItem(name: “Swift”, image: “swift”),                                 MenuItem(name: “Vulcano”, image: “vulcano”),                                 MenuItem(name: “Swift Mini”, image: “swift-mini”),                                 MenuItem(name: “Lux D”, image: “lux-d”)                                 ]          // Sub-menu gadgets for different tools         let otherMenuItems = [ MenuItem(name: “Espresso AV”, image: “espresso-av”),                                 MenuItem(name: “Espresso EP”, image: “espresso-ep”),                                 MenuItem(name: “Pour Over”, image: “pourover”),                                 MenuItem(name: “Steam”, image: “steam”)                                 ]          return topMenuItems     }()      func subMenuItems(for id: MenuItem.ID) –> [MenuItem]? {         guard let menuItem = mainMenuItems.first(the place: { $0.id == id }) else {             return nil         }          return menuItem.subMenuItems     }      func menuItem(for categoryID: MenuItem.ID, itemID: MenuItem.ID) –> MenuItem? {          guard let subMenuItems = subMenuItems(for: categoryID) else {             return nil         }          guard let menuItem = subMenuItems.first(the place: { $0.id == itemID }) else {             return nil         }          return menuItem     } } |
The mainMenuItems
array holds the pattern menu gadgets. Each subMenuItems
and menuItem
are helper strategies for wanting up a selected class or menu merchandise.
Now that we’ve ready the info mannequin, let’s transfer onto the implementation of the navigation break up view. Create a brand new file named TwoColumnSplitView.swift
utilizing the SwiftUI view template. Replace the TwoColumnSplitView
struct like this:
@State personal var selectedCategoryId: MenuItem.ID?
personal var dataModel = CoffeeEquipmenModel()
var physique: some View {
NavigationSplitView {
Checklist(dataModel.mainMenuItems, choice: $selectedCategoryId) { merchandise in
HStack {
Picture(merchandise.picture)
.resizable()
.scaledToFit()
.body(width: 50, peak: 50)
Textual content(merchandise.identify)
.font(.system(.title3, design: .rounded))
.daring()
}
}
.navigationTitle(“Espresso”)
} element: {
if let selectedCategoryId,
let categoryItems = dataModel.subMenuItems(for: selectedCategoryId) {
Checklist(categoryItems) { merchandise in
HStack {
Picture(merchandise.picture)
.resizable()
.scaledToFit()
.body(width: 50, peak: 50)
Textual content(merchandise.identify)
.font(.system(.title3, design: .rounded))
.daring()
}
}
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
} else {
Textual content(“Please choose a class”)
}
}
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
struct TwoColumnSplitView: View {      @State personal var selectedCategoryId: MenuItem.ID?      personal var dataModel = CoffeeEquipmenModel()      var physique: some View {         NavigationSplitView {             Checklist(dataModel.mainMenuItems, choice: $selectedCategoryId) { merchandise in                 HStack {                     Picture(merchandise.picture)                         .resizable()                         .scaledToFit()                         .body(width: 50, peak: 50)                     Textual content(merchandise.identify)                         .font(.system(.title3, design: .rounded))                         .daring()                 }             }              .navigationTitle(“Espresso”)          } element: {             if let selectedCategoryId,               let categoryItems = dataModel.subMenuItems(for: selectedCategoryId) {                  Checklist(categoryItems) { merchandise in                     HStack {                         Picture(merchandise.picture)                             .resizable()                             .scaledToFit()                             .body(width: 50, peak: 50)                         Textual content(merchandise.identify)                             .font(.system(.title3, design: .rounded))                             .daring()                     }                 }                 .listStyle(.plain)                 .navigationBarTitleDisplayMode(.inline)              } else {                 Textual content(“Please choose a class”)             }         }      } } |
The primary closure of NavigationSplitView
presents the principle menu merchandise. We use a Checklist
view to loop by all mainMenuItems
within the information mannequin and show every of the menu gadgets utilizing a HStack
view.
We even have a state variable named selectedCategoryId
, which is used to carry the chosen predominant menu merchandise.
For the element
closure, that is the place the submenu merchandise is rendered. If a class is chosen, we name the subMenuItems
methodology to get the submenu gadgets for that exact class. We then show the submenu gadgets utilizing Checklist
view. Conversely, if no class is chosen, we show a textual content message instructing the consumer to decide on a class.
When you made the adjustments, it’s best to see a two-column navigation UI within the Xcode preview.
Making a Three-Column Navigation Interface
Now that we’ve created a two-column navigation interface, let’s additional improve it to offer customers with a three-column navigation expertise. The additional column is used for displaying the photograph of the chosen tools.
To transform the two-column navigation interface to three-column, we have to implement a further parameter (i.e. content material
) for the NavigationSplitView
. Let’s create a brand new view named ThreeColumnSplitView
like this:
personal var dataModel = CoffeeEquipmenModel()
var physique: some View {
NavigationSplitView {
Checklist(dataModel.mainMenuItems, choice: $selectedCategoryId) { merchandise in
HStack {
Picture(merchandise.picture)
.resizable()
.scaledToFit()
.body(width: 50, peak: 50)
Textual content(merchandise.identify)
.font(.system(.title3, design: .rounded))
.daring()
}
}
.navigationTitle(“Espresso”)
} content material: {
if let selectedCategoryId,
let subMenuItems = dataModel.subMenuItems(for: selectedCategoryId) {
Checklist(subMenuItems, choice: $selectedItem) { merchandise in
NavigationLink(worth: merchandise) {
HStack {
Picture(merchandise.picture)
.resizable()
.scaledToFit()
.body(width: 50, peak: 50)
Textual content(merchandise.identify)
.font(.system(.title3, design: .rounded))
.daring()
}
}
}
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
} else {
Textual content(“Please choose a menu merchandise”)
}
} element: {
if let selectedItem {
Picture(selectedItem.picture)
.resizable()
.scaledToFit()
} else {
Textual content(“Please choose an merchandise”)
}
}
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
struct ThreeColumnSplitView: View {     @State personal var selectedCategoryId: MenuItem.ID?     @State personal var selectedItem: MenuItem?      personal var dataModel = CoffeeEquipmenModel()      var physique: some View {         NavigationSplitView {              Checklist(dataModel.mainMenuItems, choice: $selectedCategoryId) { merchandise in                 HStack {                     Picture(merchandise.picture)                         .resizable()                         .scaledToFit()                         .body(width: 50, peak: 50)                     Textual content(merchandise.identify)                         .font(.system(.title3, design: .rounded))                         .daring()                 }             }              .navigationTitle(“Espresso”)         } content material: {              if let selectedCategoryId,               let subMenuItems = dataModel.subMenuItems(for: selectedCategoryId) {                 Checklist(subMenuItems, choice: $selectedItem) { merchandise in                     NavigationLink(worth: merchandise) {                         HStack {                             Picture(merchandise.picture)                                 .resizable()                                 .scaledToFit()                                 .body(width: 50, peak: 50)                             Textual content(merchandise.identify)                                 .font(.system(.title3, design: .rounded))                                 .daring()                         }                     }                 }                 .listStyle(.plain)                 .navigationBarTitleDisplayMode(.inline)              } else {                 Textual content(“Please choose a menu merchandise”)             }          } element: {             if let selectedItem {                 Picture(selectedItem.picture)                     .resizable()                     .scaledToFit()             } else {                 Textual content(“Please choose an merchandise”)             }         }      } } |
Mainly, the code within the content material
closure needs to be similar to you. The content material
parameter is designed for displaying the submenu gadgets. Thus, we use the Checklist
view to point out the submenu gadgets for the chosen class.
When an merchandise is chosen within the submenu, the app exhibits the tools photograph. That is achieved by the code written within the element
closure.
After the code adjustments, the preview pane ought to present you a two-column format.
By default, the primary column is hidden. You want to faucet the menu button on the top-left nook to disclose it.
To regulate the visibility of the break up view, you may declare a state variable of the kind NavigationSplitViewVisibility
and set its worth to .all
:
@State personal var columnVisibility = NavigationSplitViewVisibility.all |
When instantiating the NavigationSplitView
, it has an choice parameter named columnVisibility
. You may merely cross the binding of columnVisibility
to manage the visibility of the columns.
The NavigationSplitViewVisibility.all
worth tells iPadOS to show all of the three columns. There are different choices together with:
.computerized
– Use the default main column visibility for the present gadget. That is the default setting..doubleColumn
– Present the content material column and element space of a three-column navigation break up view..detailOnly
– Conceal the main two columns of a three-column break up view. In different phrases, solely the element space exhibits.
Customizing the Model of Navigation Cut up Views
Have you ever examined the app in iPad Portrait? By default, the element space takes up the entire display when the iPad gadget is in portrait mode. So, if you carry up the principle menu and submenu areas, the element space is hidden behind these two main columns.
If you happen to don’t like this type, you may change it by attaching the .navigationSplitViewStyle
modifier to NavigationSplitView
:
NavigationSplitView(columnVisibility: $columnVisibility) { Â Â . Â Â . Â Â . } .navigationSplitViewStyle(.balanced) |
The default worth is ready to .computerized
. If you happen to set the worth to .balanced
, it reduces the element space such that the 2 main columns can seem on the similar time.
What’s Subsequent
This tutorial offers you an introduction to NavigationSplitView
in iOS 16. It’s very straightforward to create multi-column navigation expertise for iPad customers. Even should you develop apps for iPhone customers, NavigationSplitView
can adapt itself to suit the navigation expertise on narrower screens. For instance, when iPhone 13 Professional Max is in portrait mode, the break up view exhibits a one-column navigation. If you happen to rotate the gadget sideway, the break up view adjustments to a multi-column format.
Due to this fact, take a while to review this break up view part and apply it to your app every time this sort of UI is sensible.
For reference, if you wish to be taught extra about NavigationSplitView
, you may take a look at this WWDC video.
If you happen to get pleasure from this text and need to dive deeper into SwiftUI, it’s possible you’ll take a look at our Mastering SwiftUI ebook.