boilingsteam, to linux
@boilingsteam@mastodon.cloud avatar
nautilebleu, to sketch French
@nautilebleu@mamot.fr avatar
teissler, to SwiftUI
@teissler@hachyderm.io avatar

WWDC24 is right around the corner. Before the excitement and rush of what's to come, I want to share some navigation fun facts and tips. None of this is new, having been discussed previously in sessions, etc. From talking with developers, these are parts of the system that are often overlooked, or forgotten about #21DaysOfSwiftUINavigation #SwiftUI This is all running on macOS Sonoma 14.4, Xcode 15.3

teissler,
@teissler@hachyderm.io avatar
  1. List selection can drive navigation pushes. You can populate the selection binding with a value-destination NavigationLink, or, any other way you would normally populate it. #21DaysOfSwiftUINavigation #SwiftUI

A video showing a compact NavigationSplitView on iPhone Previews next to a code sample. The preview shows a list of with 4 elements, 3 navigation links, and 1 Text row. All 4 rows get a grey highlight when active. The non-navigation link row, the simple tagged Text, has no chevron, but still highlights gray in sync with the push progress. Code: import SwiftUI enum Sports: String, Hashable { case pickleball case dance case archery case volleyball } struct Day1: View { @State private var selection: Sports? var body: some View { NavigationSplitView { VStack { List(selection: $selection) { NavigationLink("Pickleball", value: Sports.pickleball) NavigationLink("Dance", value: Sports.dance) NavigationLink("Archery", value: Sports.archery) Text("Volleyball").tag(Sports.volleyball) } } } detail: { if let selection { VStack { Text(selection.rawValue.capitalized(with: .current)) Image(systemName: "figure.(selection.rawValue)") } } else { ContentUnavailableView( "Make a selection", systemImage: "volleyball") } } } } #Preview { Day1() }

teissler,
@teissler@hachyderm.io avatar
  1. The navigation APIs give different options for pushing a view or root replacing a column. In trivial examples, the API used seems arbitrary, but in larger apps, each version has tradeoffs. I could spell the example from yesterday like this instead:

#21DaysOfSwiftUINavigation #SwiftUI

A sample of SwiftUI code running macOS previews. The preview picks Feature 1 in the sidebar, changing the view shown in the detail. Then it pushes the red view with the “Push Red” button in the detail. Then, Feature 2 is selected in the sidebar, popping the path back to empty and displaying the original detail view for Feature 2. Green is pushed, then Feature 2 is command clicked, getting us back to the starting state. import SwiftUI struct Day3: View { @State var path = NavigationPath() @State var selection: Int? var body: some View { NavigationSplitView { List(selection: $selection) { NavigationLink("Feature 1", value: 1) NavigationLink("Feature 2", value: 2) } } detail: { NavigationStack(path: $path) { if let selection { feature(for: selection) } else { Text("Select a feature") } } } } @ViewBuilder func feature(for selection: Int) -> some View { switch selection { case 1: Feature1() case 2: Feature2() default: fatalError() } } } #Preview { Day3() }

teissler,
@teissler@hachyderm.io avatar
  1. Visiting first principles of NavigationStack for the next few tips. NavigationStack takes your app's data model, and transforms it into a stack of views. NavigationStack also supports fire-and-forget style navigation — these are standalone navigation actions that don't modify the client's state, i.e. the navigation path. The navigation system tracks that state internally for you. These are the simplest, so let's look at them first.

A video of a NavigationStack on watchOS. The stack pushes a view-destination navigation link. There is an onChange modifier for the navigation path, but it never prints anything because the client’s view of the path doesn’t change. Code: import SwiftUI struct Day6: View { @State private var path = NavigationPath() var body: some View { NavigationStack(path: $path) { NavigationLink( destination: { RainbowSymbol() }, label: { Label("Navigate", systemImage: "compass.drawing") .font(.headline) }) }.onChange(of: path) { oldValue, newValue in print("path changed from (oldValue) to (newValue)") } } } private struct RainbowSymbol: View { var body: some View { Image( systemName: "cloud.rainbow.half.fill") .symbolRenderingMode(.multicolor) .symbolEffect( .variableColor.hideInactiveLayers, options: .repeating) .font(.system(size: 90)) } } #Preview { Day6() }

teissler,
@teissler@hachyderm.io avatar
  1. NavigationLink(_ title:value:) is a "value-destination" link. Conceptually, it navigates to a value. From talking with developers, it's a misconception that this must be paired with a .navigationDestination(for:…) modifier, mapping directly to a view. As we saw in Day 3, it can drive List selection, mapping indirectly to a view.
    This link often uses data-driven navigation (the opposite of fire-and-forget) — causing changes in client state like appending to a path seen here. #SwiftUI

Xcode Previews showing a code sample and an iPhone. When the link in the iPhone is tapped, the stack pushes a pink view. Simultaneously, the console prints “New Path: [7]”. After popping the view back off, the console prints “New Path: []” Code: import SwiftUI struct Day7: View {

teissler,
@teissler@hachyderm.io avatar

11 cont) Let's examine a navigation cycle. This example shows a navigation cycle that is a client bug. It's hard to spot why immediately. The Day11_Bad_Cycle view has an environmental dependency on MyEnvironmentKey. The value of MyEnvironmentKey is a closure. Closures are not equatable or comparable in Swift, the graph always has to assume they have changed value. Down below, the navigationDestination captures self by means of localState! When I tap push, the app freezes up 😮 #SwiftUI

A video of Xcode previews. The phone has a single button “Push”. When tapped, the “Push” gets a pressed appearance, and the app freezes, never pushing. import SwiftUI struct Day11_Bad_Cycle: View { private var localState: String = "loop" @Environment(.myCustomValue) var functional var body: some View { NavigationLink("Push", value: 10) .navigationDestination(for: Int.self) { _ in OtherView(string: localState) // Captures

teissler,
@teissler@hachyderm.io avatar

11 final) In this version, I've wrapped the closure up in a Equatable Box. For a proof-of-concept, I use the @Namespace property wrapper to establish identity, given that I know that closure values identity is tied to the Namespace.The cycle is solved. The takeaway, make your environment values Equatable when possible, and when you can't, don't capture them as part of .navigationDestinations #SwiftUI

Shout out to a particular follower who knows where they are who filed quality FBs about this.

The same example as before, but conforming MyEnvironmentKey.Value to Equatable stops the cycle. The view pushes and pops successfully import SwiftUI struct Day11: View {

teissler,
@teissler@hachyderm.io avatar

12 final) Given the size classes and interactions supported, fixing the sidebar is a rare, and currently unsupported (FBs welcome). On iPad in certain size classes users can drag from the leading edge to reveal the sidebar. Lest we forget the iPad mini in portrait where a fixed sidebar would feel cramped. And lest we really not forget this peek interaction on macOS #SwiftUI #21DaysOfSwiftUINavigation

An Xcode Previews window showing a macOS preview of a NavigationSplitView. Hovering the mouse some 5-10 points from the leading edge of the window reveal a peek of the sidebar. The user can grab the peeking sidebar and drag it open from a collapsed state. import SwiftUI struct Day12: View { var body: some View { NavigationSplitView { List { ForEach(0...10, id: .self) { i in NavigationLink("Option (i)", value: i) } } } detail:{ ContentUnavailableView( "Make your selection", systemImage: "circle.hexagongrid.fill") .symbolRenderingMode(.multicolor) .symbolEffect(.pulse, options: .repeating) } } } #Preview { Day12() }

teissler,
@teissler@hachyderm.io avatar
  1. macOS supports Cmd-click deselect out of the box. This works with value-destination and view destination links. You'll notice the view-destination link’s (Settings) selection is cleared when the environmental action dismiss is called from the detail view #SwiftUI #21DaysOfSwiftUINavigation

macOS Xcode Previews showing a basic NavigationSplitView. The 3 options in the sidebar as successively clicked and Command-clicked, which dismisses the presented view. Additionally, a button that calls the dismiss action form the detail column will also dismiss the presented view and clear selection. import SwiftUI struct Day13: View { var body: some View { NavigationSplitView { List { NavigationLink(value: "House") { Label("Home", systemImage: "house") } NavigationLink(value: "Profile") { Label("Profile", systemImage: "person") } NavigationLink { DismissibleView() } label: { Label( "Settings", systemImage: "gear") } } .navigationDestination(for: String.self) { str in if str == "House" { Color.accentColor } else { Color.mint } } } detail: { Text("No selection") } } } struct DismissibleView: View { @Environment(.dismiss) var dismiss var body: some View { ZStack { Color.gray Button("Dismiss") { dismiss() } } } } #Preview { Day13() }

  • All
  • Subscribed
  • Moderated
  • Favorites
  • anitta
  • thenastyranch
  • rosin
  • GTA5RPClips
  • osvaldo12
  • love
  • Youngstown
  • slotface
  • khanakhh
  • everett
  • kavyap
  • mdbf
  • DreamBathrooms
  • ngwrru68w68
  • megavids
  • magazineikmin
  • InstantRegret
  • normalnudes
  • tacticalgear
  • cubers
  • ethstaker
  • modclub
  • cisconetworking
  • Durango
  • provamag3
  • tester
  • Leos
  • JUstTest
  • All magazines