I have a carousel made with ScrollView and I am trying to control/init the .scrollPosition(id:) from a parent view.
Here is the Custom Slider:
struct Item: Identifiable, Hashable {
private(set) var id: UUID = .init()
var color: Color
var title: String
var subTitle: String
static var previews: [Item] = [
.init(color: .red, title: "World Clock", subTitle: "View the time in multiple cities around the world."),
.init(color: .blue, title: "City Digital", subTitle: "Add a clock for a city to check the time at that location."),
.init(color: .green, title: "City Analouge", subTitle: "Add a clock for a city to check the time at that location."),
.init(color: .yellow, title: "Next Alarm", subTitle: "Display upcomiong alarm.")
]
}
struct CustomPagingSlider<Content: View, TitleContent: View, Item: RandomAccessCollection>: View where Item: MutableCollection, Item.Element: Identifiable {
/// View Properties
@Binding var activeID: UUID?
@State var data: Item
/// Customization Properties
var showsIndicator: ScrollIndicatorVisibility = .hidden
var showPagingControl: Bool = true
var disablePagingInteraction: Bool = false
var titleScrollSpeed: CGFloat = 0.6
var pagingControlSpacing: CGFloat = 20
var spacing: CGFloat = 10
@ViewBuilder var content: (Binding<Item.Element>) -> Content
@ViewBuilder var titleContent: (Binding<Item.Element>) -> TitleContent
var body: some View {
VStack(spacing: pagingControlSpacing) {
ScrollView(.horizontal) {
HStack(spacing: spacing) { // not working when using HStack
ForEach($data) { item in
VStack(spacing: 0) {
titleContent(item)
.frame(maxWidth: .infinity)
.visualEffect { content, geometryProxy in
content
.offset(x: scrollOffset(geometryProxy))
}
content(item)
}
.containerRelativeFrame(.horizontal)
}
}
.scrollTargetLayout()
}
.scrollIndicators(showsIndicator)
.scrollTargetBehavior(.viewAligned)
.scrollPosition(id: $activeID)
}
}
func scrollOffset(_ proxy: GeometryProxy) -> CGFloat {
let minX = proxy.bounds(of: .scrollView)?.minX ?? 0
return -minX * min(titleScrollSpeed, 1.0)
}
}
In this DetailedView I will use onChange(of: ) to do something when the activeID changes from the CustomPagingSlider.
struct DetailedView: View {
/// View Properties
@State var items: [Item]
@State var activeID: UUID?
/// Customization Properties
@State private var showPagingControl: Bool = false
@State private var disablePagingInteraction: Bool = false
@State private var pagingSpacing: CGFloat = 20
@State private var titleScrollSpeed: CGFloat = 0.75
@State private var stretchContent: Bool = true
init(items: [Item], activeID: UUID?) {
self._items = State(wrappedValue: items)
self._activeID = State(wrappedValue: activeID)
if let item = self.items.first(where: { $0.id == activeID }) {
print("DetailedView init: \(item.title)")
}
}
var body: some View {
VStack {
CustomPagingSlider(activeID: $activeID,
data: items,
showPagingControl: showPagingControl,
disablePagingInteraction: disablePagingInteraction,
titleScrollSpeed: titleScrollSpeed,
pagingControlSpacing: pagingSpacing
) { $item in
RoundedRectangle(cornerRadius: 15)
.fill(item.color.gradient)
.frame(width: stretchContent ? nil : 150, height: stretchContent ? 220 : 120)
} titleContent: { $item in
VStack(spacing: 5) {
Text(item.title)
.font(.largeTitle.bold())
.frame(height: 45)
.background(.cyan)
Text(item.subTitle)
.foregroundStyle(.gray)
.multilineTextAlignment(.center)
.frame(height: 45)
}
}
.safeAreaPadding([.horizontal], 35)
List {
Toggle("Show Paging Control", isOn: $showPagingControl)
Toggle("Disable Page Interaction", isOn: $disablePagingInteraction)
Toggle("Stretch Content", isOn: $stretchContent)
Section("Title Scroll Speed") {
Slider(value: $titleScrollSpeed)
}
Section("Paging Spacing") {
Slider(value: $pagingSpacing, in: 20...40)
}
}
.clipShape(.rect(cornerRadius: 15))
.padding(15)
}
.navigationTitle("Detailed View")
.navigationBarTitleDisplayMode(.inline)
}
}
But I want to make the CustomPagingSlider to position on a specific item (selected from the ContentView when displaying the DetailedView, not always on the 1st item.
struct ContentView: View {
@State private var items: [Item] = Item.previews
@State private var showDetailedView: Bool = false
var body: some View {
NavigationStack {
List {
ForEach(items) { item in
NavigationLink(value: item) {
VStack(alignment: .leading, spacing: 5) {
Text(item.title)
.font(.largeTitle.bold())
Text(item.subTitle)
.foregroundStyle(.gray)
.multilineTextAlignment(.leading)
.frame(height: 45)
}
}
}
}
.navigationTitle("Items")
.navigationDestination(for: Item.self) { item in
DetailedView(items: items, activeID: item.id)
}
}
}
}
It does not work at all when using a HStack within the CustomPagingSlider (see comment). It works when using LazyHStack, but the positioning is not correctly aligned (check video). When selecting the 3rd item, the positioning is not aligned.
enter link description here




