SwiftUI 使用点作为指示器创建图像滑块 [英] SwiftUI create image slider with dots as indicators
问题描述
我想为图像创建滚动视图/滑块.查看我的示例代码:
ScrollView(.horizontal, showsIndicators: true) {堆栈{图像(shelter.background).resizable().frame(宽度:UIScreen.main.bounds.width,高度:300)图片(太平洋").resizable().frame(宽度:UIScreen.main.bounds.width,高度:300)}}
虽然这允许用户滑动,但我希望它有点不同(类似于 UIKit 中的 PageViewController).我希望它表现得像我们从许多以点为指示器的应用程序中了解到的典型图像滑块:
- 它应始终显示完整图像,没有中间 - 因此,如果用户拖动并停在中间,它应自动跳转到完整图像.
- 我想要点作为指标.
因为我看到很多应用程序使用这样的滑块,所以一定有已知的方法,对吧?
今年的 SwiftUI 中没有内置的方法.我相信将来会出现系统标准的实现.
在短期内,您有两种选择.正如 Asperi 所指出的,Apple 自己的教程中有一个部分是关于从 UIKit 包装 PageViewController 以在 SwiftUI 中使用(请参阅
两个注意事项:
- GIF 动画在展示动画的流畅程度方面做得非常糟糕,因为由于文件大小限制,我不得不降低帧率并进行大量压缩.在模拟器或真机上看起来很棒
- 模拟器中的拖动手势感觉很笨拙,但它在物理设备上非常有效.
I want to create a scroll view/slider for images. See my example code:
ScrollView(.horizontal, showsIndicators: true) {
HStack {
Image(shelter.background)
.resizable()
.frame(width: UIScreen.main.bounds.width, height: 300)
Image("pacific")
.resizable()
.frame(width: UIScreen.main.bounds.width, height: 300)
}
}
Though this enables the user to slide, I want it a little different (similar to a PageViewController in UIKit). I want it to behave like the typical image slider we know from a lot of apps with dots as indicators:
- It shall always show a full image, no in between - hence if the user drags and stops in the middle, it shall automatically jump to the full image.
- I want dots as indicators.
Since I've seen a lot of apps use such a slider, there must be known method, right?
There is no built-in method for this in SwiftUI this year. I'm sure a system-standard implementation will come along in the future.
In the short term, you have two options. As Asperi noted, Apple's own tutorials have a section on wrapping the PageViewController from UIKit for use in SwiftUI (see Interfacing with UIKit).
The second option is to roll your own. It's entirely possible to make something similar in SwiftUI. Here's a proof of concept, where the index can be changed by swipe or by binding:
struct PagingView<Content>: View where Content: View {
@Binding var index: Int
let maxIndex: Int
let content: () -> Content
@State private var offset = CGFloat.zero
@State private var dragging = false
init(index: Binding<Int>, maxIndex: Int, @ViewBuilder content: @escaping () -> Content) {
self._index = index
self.maxIndex = maxIndex
self.content = content
}
var body: some View {
ZStack(alignment: .bottomTrailing) {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 0) {
self.content()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
}
}
.content.offset(x: self.offset(in: geometry), y: 0)
.frame(width: geometry.size.width, alignment: .leading)
.gesture(
DragGesture().onChanged { value in
self.dragging = true
self.offset = -CGFloat(self.index) * geometry.size.width + value.translation.width
}
.onEnded { value in
let predictedEndOffset = -CGFloat(self.index) * geometry.size.width + value.predictedEndTranslation.width
let predictedIndex = Int(round(predictedEndOffset / -geometry.size.width))
self.index = self.clampedIndex(from: predictedIndex)
withAnimation(.easeOut) {
self.dragging = false
}
}
)
}
.clipped()
PageControl(index: $index, maxIndex: maxIndex)
}
}
func offset(in geometry: GeometryProxy) -> CGFloat {
if self.dragging {
return max(min(self.offset, 0), -CGFloat(self.maxIndex) * geometry.size.width)
} else {
return -CGFloat(self.index) * geometry.size.width
}
}
func clampedIndex(from predictedIndex: Int) -> Int {
let newIndex = min(max(predictedIndex, self.index - 1), self.index + 1)
guard newIndex >= 0 else { return 0 }
guard newIndex <= maxIndex else { return maxIndex }
return newIndex
}
}
struct PageControl: View {
@Binding var index: Int
let maxIndex: Int
var body: some View {
HStack(spacing: 8) {
ForEach(0...maxIndex, id: \.self) { index in
Circle()
.fill(index == self.index ? Color.white : Color.gray)
.frame(width: 8, height: 8)
}
}
.padding(15)
}
}
and a demo
struct ContentView: View {
@State var index = 0
var images = ["10-12", "10-13", "10-14", "10-15"]
var body: some View {
VStack(spacing: 20) {
PagingView(index: $index.animation(), maxIndex: images.count - 1) {
ForEach(self.images, id: \.self) { imageName in
Image(imageName)
.resizable()
.scaledToFill()
}
}
.aspectRatio(4/3, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 15))
PagingView(index: $index.animation(), maxIndex: images.count - 1) {
ForEach(self.images, id: \.self) { imageName in
Image(imageName)
.resizable()
.scaledToFill()
}
}
.aspectRatio(3/4, contentMode: .fit)
.clipShape(RoundedRectangle(cornerRadius: 15))
Stepper("Index: \(index)", value: $index.animation(.easeInOut), in: 0...images.count-1)
.font(Font.body.monospacedDigit())
}
.padding()
}
}
Two notes:
- The GIF animation does a really poor job of showing how smooth the animation is, as I had to drop the framerate and compress heavily due to file size limits. It looks great on simulator or a real device
- The drag gesture in the simulator feels clunky, but it works really well on a physical device.
这篇关于SwiftUI 使用点作为指示器创建图像滑块的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!