Skip to content

Instantly share code, notes, and snippets.

@JadenGeller
Last active September 17, 2025 05:12
Show Gist options
  • Select an option

  • Save JadenGeller/a20e1b2cd6434d7755a50d3fe8f6c752 to your computer and use it in GitHub Desktop.

Select an option

Save JadenGeller/a20e1b2cd6434d7755a50d3fe8f6c752 to your computer and use it in GitHub Desktop.

Revisions

  1. JadenGeller revised this gist Jul 13, 2025. 1 changed file with 8 additions and 0 deletions.
    8 changes: 8 additions & 0 deletions ZoomNavigator.swift
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,11 @@
    /// A view that displays a collection of items in a scrollable list that can "zoom" to focus on a single selected item.
    ///
    /// `ZoomNavigator` provides a navigation pattern similar to the iOS lock screen wallpaper picker,
    /// where items can be viewed together in a scrollable list or individually in full screen.
    /// The same view instance is maintained during transitions, preserving state and identity.
    ///
    /// When no item is selected (`selection` is `nil`), all items are displayed in a scrollable list.
    /// When an item is selected, the view filters to show only that item, expanded to fill the available space.
    struct ZoomNavigator<Data: RandomAccessCollection, ID: Hashable, Content: View, Background: View, Modifier: ViewModifier>: View {
    var data: Data
    var id: KeyPath<Data.Element, ID>
  2. JadenGeller created this gist Jul 13, 2025.
    62 changes: 62 additions & 0 deletions ZoomNavigator.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,62 @@
    struct ZoomNavigator<Data: RandomAccessCollection, ID: Hashable, Content: View, Background: View, Modifier: ViewModifier>: View {
    var data: Data
    var id: KeyPath<Data.Element, ID>
    var selection: ID?

    var alignment: Alignment = .center
    var axes: Axis.Set = .vertical // btw: for horizontal, collapsed behavior isn't identical
    var showsIndicators: Bool = true

    @ViewBuilder var content: (Data.Element) -> Content
    @ViewBuilder var background: (Data.Element) -> Background
    var modifier: (Data.Element) -> Modifier

    var isZoomedOut: Bool {
    selection == nil
    }

    var body: some View {
    GeometryReader { geometry in
    ScrollView(isZoomedOut ? axes : .vertical, showsIndicators: true) {
    let data = data.filter { item in
    if let selection {
    item[keyPath: id] == selection
    } else {
    true
    }
    }
    ForEach(data, id: id) { item in
    content(item)
    .frame(
    minWidth: isZoomedOut ? nil : geometry.size.width,
    minHeight: isZoomedOut ? nil : geometry.size.height,
    alignment: alignment
    )
    .background(
    background(item)
    .frame(
    minWidth: isZoomedOut ? nil : geometry.size.width
    + geometry.safeAreaInsets.leading
    + geometry.safeAreaInsets.trailing,
    minHeight: isZoomedOut ? nil : geometry.size.height
    + geometry.safeAreaInsets.top
    + geometry.safeAreaInsets.bottom
    )
    .offset(
    x: isZoomedOut ? 0 : -geometry.safeAreaInsets.leading,
    y: isZoomedOut ? 0 : -geometry.safeAreaInsets.top
    ),
    alignment: .topLeading
    )
    .modifier(modifier(item))
    }
    }
    .scrollDisabled(true)
    .frame(
    minWidth: axes.contains(.vertical) ? geometry.size.width : nil,
    minHeight: axes.contains(.horizontal) ? geometry.size.height : nil,
    alignment: alignment
    )
    }
    }
    }