Я создал горизонтальную карусель, используя ScrollView и HStack.
Это мой код:
struct CarouselView<Content: View>: View {
let content: Content
private let spacing: CGFloat
private let shouldSnap: Bool
init(spacing: CGFloat = .zero,
shouldSnap: Bool = false,
@ViewBuilder content: @escaping () -> Content) {
self.content = content()
self.spacing = spacing
self.shouldSnap = shouldSnap
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: spacing) {
content
}.apply {
if #available(iOS 17.0, *), shouldSnap {
$0.scrollTargetLayout()
} else {
$0
}
}
}
.clipped()
.apply {
if #available(iOS 17.0, *), shouldSnap {
$0.scrollTargetBehavior(.viewAligned)
} else {
$0
}
}
}
}
Затем я могу использовать его следующим образом:
CarouselView(spacing: 10) {
ForEach(0 ..< imagesNames.count, id: \.self) { index in
DemoView()
}
}
Как я могу определить, когда пользователь прокрутил до конца просмотра прокрутки.
Я попробовал некоторые ответы здесь - основной из них - добавление представления в конец hstack и выполнение работы над onAppear этого представления не работает, поскольку оно запускается сразу же при создании HStack.
Это могло бы сработать, если бы я использовал LazyHStack, однако мне приходится использовать HStack из-за некоторых ограничений.
Есть ли другие способы добиться этого?





Вы можете использовать этот DetectOnScreen модификатор представления, который я написал в этом ответе, чтобы определить, находится ли на экране невидимое представление.
struct IsOnScreenKey: PreferenceKey {
static let defaultValue: Bool? = nil
static func reduce(value: inout Value, nextValue: () -> Value) {
if let next = nextValue() {
value = next
}
}
}
struct DetectIsOnScreen: ViewModifier {
func body(content: Content) -> some View {
GeometryReader { reader in
content
.preference(
key: IsOnScreenKey.self,
// bounds(of: .scrollView) gives us the scroll view's frame
// frame(in: .local) gives us the frame of the invisible view
// both are in the local coordinate space
value: reader.bounds(of: .scrollView)?.intersects(reader.frame(in: .local)) ?? false
)
}
}
}
Использование:
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: spacing) {
content
Color.clear
.frame(width: 0, height: 0)
.modifier(DetectIsOnScreen())
.onPreferenceChange(IsOnScreenKey.self) { value in
if value == true {
print("Has reached end!")
}
}
}
}
Это означает, что HStack будет иметь дополнительный вид, поэтому между последним представлением и невидимым представлением будет некоторое дополнительное пространство (размером spacing). Если это нежелательно, вы можете обернуть последнее представление в собственное HStack и поместить невидимое представление во внутреннее HStack. Но это нужно делать на месте использования.
ForEach(0..<imagesNames.count, id: \.self) { index in
if index == imageNames.count - 1 {
HStack(spacing: 0) {
DemoView()
Color.clear
.frame(width: 0, height: 0)
...
}
} else { DemoView() }
}
Если вы не против использовать View Extractor, вы можете сделать это в CarouselView
HStack(spacing: spacing) {
ExtractMulti(content) { views in
ForEach(views) { view in
if view.id == views.last?.id {
HStack(spacing: 0) {
view
Color.clear
.frame(width: 0, height: 0)
...
}
} else {
view
}
}
}
}
Спасибо, ваше первое решение действительно работает хорошо. Я попытался использовать его для вертикальной прокрутки и сделал дополнительный интервал, который показался неожиданным. Отдельный вопрос здесь: stackoverflow.com/questions/78488242/…
Вы можете попробовать этот подход, используя простой .scrollPosition(id: $scrolledID), чтобы определить, когда пользователь прокрутил страницу до конца.
Пример кода:
struct ContentView: View {
@State private var scrolledID: Int? = 0 // <-- here
var body: some View {
Text("scroll position: \(scrolledID ?? 0)") // <-- for testing
Text("is at end: \(scrolledID == 4 ? "YES" : "NO") ") // <-- for testing
CarouselView(spacing: 10, scrolledID: $scrolledID) { // <-- here
ForEach(0 ..< 5, id: \.self) { index in
DemoView()
}
}
}
}
struct CarouselView<Content: View>: View {
let content: Content
private let spacing: CGFloat
private let shouldSnap: Bool
@Binding var scrolledID: Int? // <-- here
init(spacing: CGFloat = .zero, shouldSnap: Bool = false, scrolledID: Binding<Int?>, @ViewBuilder content: @escaping () -> Content) {
self.content = content()
self.spacing = spacing
self.shouldSnap = shouldSnap
self._scrolledID = scrolledID // <-- here
}
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: spacing) {
content
}
.scrollTargetLayout() // <-- here
}
.scrollPosition(id: $scrolledID) // <-- here
.scrollTargetBehavior(.paging)
.clipped()
}
}
// -- for testing
struct DemoView: View {
var body: some View {
ZStack {
Text("DemoView")
Rectangle().stroke(Color.red, lineWidth: 6)
}.frame(width: 345, height: 345)
}
}
Вы можете попробовать что-то простое, например, использовать
.scrollPosition(id: $scrollID), см. ScrollPosition, чтобы определить, когда пользователь прокрутил до конца представления прокрутки.