Last active
September 29, 2025 14:48
-
-
Save muukii/a1036048aa7611035b7563ce95c2457f to your computer and use it in GitHub Desktop.
Carousel
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| /** | |
| Successor of TabView | |
| */ | |
| @available(iOS 17, *) | |
| public struct Carousel<Selection: Hashable, Content: View>: View { | |
| private let content: Content | |
| private let scrollPosition: Binding<Selection?>? | |
| /// Creates a carousel whose currently aligned item is bound to a selection. | |
| /// | |
| /// Use this initializer when you want to programmatically control which item is | |
| /// scrolled into view and/or observe which item the user has scrolled to. The | |
| /// selection binding is kept in sync with the currently view-aligned child view, | |
| /// identified by the value you provide via `.id(_:)` on each child. | |
| /// | |
| /// - Important: Each child view in `content` must have a stable identity set with | |
| /// `.id(_:)`, and the `id`'s type must match `Selection`. Without IDs, the | |
| /// carousel cannot determine which item to align to or update the binding with. | |
| /// | |
| /// - Parameters: | |
| /// - selection: A binding to the optional identifier of the currently aligned | |
| /// item. Setting this to a value will smoothly scroll the carousel to the | |
| /// corresponding child view. Setting it to `nil` clears the programmatic | |
| /// target; after user interaction, the binding will update to the nearest | |
| /// aligned item’s ID. | |
| /// - content: A view builder that produces the carousel’s items. Each item | |
| /// should be tagged with `.id(_:)` using a `Selection` value. | |
| /// | |
| /// - Discussion: | |
| /// - When `selection` changes, the carousel animates to the matching item using | |
| /// a smooth animation. | |
| /// - As the user scrolls and alignment settles, the binding updates to reflect | |
| /// the currently aligned item’s ID. | |
| /// - If `selection` does not match any child’s ID, no scrolling occurs. | |
| /// | |
| /// - Availability: iOS 17 and later. | |
| /// | |
| /// - Example: | |
| /// ```swift | |
| /// struct Example: View { | |
| /// @State private var selection: String? = "Two" | |
| /// | |
| /// var body: some View { | |
| /// Carousel(selection: $selection) { | |
| /// Text("One").id("One") | |
| /// Text("Two").id("Two") | |
| /// Text("Three").id("Three") | |
| /// } | |
| /// } | |
| /// } | |
| /// ``` | |
| public init( | |
| selection: Binding<Selection?>, | |
| @ViewBuilder content: () -> Content | |
| ) { | |
| self.scrollPosition = selection | |
| self.content = content() | |
| } | |
| public init( | |
| @ViewBuilder content: () -> Content | |
| ) where Selection == Never { | |
| self.scrollPosition = nil | |
| self.content = content() | |
| } | |
| public var body: some View { | |
| let view = ScrollView(.horizontal, showsIndicators: false) { | |
| // In iOS17, scrollTargetLayout only works if its content is LazyHStack. 🤦 | |
| UnaryViewReader(readingContent: content) { children in | |
| LazyHStack(spacing: 16) { | |
| ForEach(children, id: \.id) { child in | |
| child | |
| .containerRelativeFrame(.horizontal) | |
| } | |
| } | |
| .scrollTargetLayout() | |
| } | |
| } | |
| .scrollTargetBehavior(.viewAligned) | |
| .safeAreaPadding(.horizontal, 32) | |
| if let scrollPosition { | |
| view | |
| .animation( | |
| .smooth, | |
| body: { | |
| $0.scrollPosition(id: scrollPosition) | |
| } | |
| ) | |
| } else { | |
| view | |
| } | |
| } | |
| } | |
| @available(iOS 17, *) | |
| #Preview("Carousel") { | |
| @Previewable @State var selection: String? | |
| VStack { | |
| HStack { | |
| Button("One") { | |
| selection = "One" | |
| } | |
| Button("Two") { | |
| selection = "Two" | |
| } | |
| Button("Three") { | |
| selection = "Three" | |
| } | |
| Button("Clear") { | |
| selection = nil | |
| } | |
| } | |
| Carousel(selection: $selection) { | |
| Text("One") | |
| .background(.red.opacity(0.3)) | |
| .id("One") | |
| Text("Two") | |
| .id("Two") | |
| Text("Three") | |
| .id("Three") | |
| } | |
| } | |
| } | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment