SwiftUI 创建以点为指标的图像滑块

Posted

技术标签:

【中文标题】SwiftUI 创建以点为指标的图像滑块【英文标题】:SwiftUI create image slider with dots as indicators 【发布时间】:2019-11-17 01:13:52 【问题描述】:

我想为图像创建一个滚动视图/滑块。请参阅我的示例代码:

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)
      

虽然这可以让用户滑动,但我希望它有点不同(类似于 UIKit 中的 PageViewController)。我希望它的行为类似于我们从许多应用程序中知道的以点为指标的典型图像滑块:

    它应始终显示完整图像,中间没有 - 因此如果用户在中间拖动并停止,它将自动跳转到完整图像。 我想要点作为指标。

因为我看到很多应用程序都使用这样的滑块,所以一定有已知的方法,对吧?

【问题讨论】:

在Interfacing with UIKit Apple 教程中,准确地演示了如何按照您的要求进行操作。 【参考方案1】:

今年的 SwiftUI 中没有内置的方法。我相信将来会出现系统标准的实现。

在短期内,您有两种选择。正如 Asperi 所指出的,Apple 自己的教程中有一节介绍了从 UIKit 包装 PageViewController 以在 SwiftUI 中使用(请参阅Interfacing with UIKit)。

第二种选择是自己动手。在 SwiftUI 中制作类似的东西是完全可能的。这是一个概念证明,可以通过滑动或绑定来更改索引:

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)
    

还有一个演示

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()
    

两个音符:

    GIF 动画在显示动画的流畅度方面做得很差,因为由于文件大小限制,我不得不降低帧速率并进行大量压缩。在模拟器或真实设备上看起来很棒 模拟器中的拖动手势感觉很笨拙,但在物理设备上效果非常好。

【讨论】:

Apple 的PageViewController 示例提出的问题(对我而言)比它回答的要多。也许在这个例子之后(我见过的最有启发性的 SwiftUI 例子)它会更清楚。 这太棒了。如何实现自动播放系统? 我发现了一个问题,如果我在滚动视图中有这个问题,如果我的手指在 PagingView 上方,我将无法向上或向下滚动 @FilipeSá 在let content: () -&gt; Content 下添加private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect(),然后在PageControl(index: $index, maxIndex: maxIndex) 下添加.onReceive(self.timer) _ in self.index = (self.index + 1) % 3 @FilipeSá 更改为 DragGesture(minimumDistance: 30, coordinateSpace: .local) 以启用上下滚动【参考方案2】:

您可以通过以下代码轻松实现此目的

struct ContentView: View 
    public let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
    @State private var selection = 0
    
    ///  images with these names are placed  in my assets
    let images = ["1","2","3","4","5"]
    
    var body: some View 
        
        ZStack
            
            Color.black
            
            TabView(selection : $selection)
                
                ForEach(0..<5) i in
                    Image("\(images[i])")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                

                
            .tabViewStyle(PageTabViewStyle())
            .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
            .onReceive(timer, perform:  _ in
                    
                withAnimation
                    print("selection is",selection)
                    selection = selection < 5 ? selection + 1 : 0
                                
            )  
        
    

【讨论】:

但你不能把它放在一个导航视图中 当 TabView 放置在 ScrollView 中时,UI 会滞后。我很想听听一些关于我们如何改进它的 cmets?

以上是关于SwiftUI 创建以点为指标的图像滑块的主要内容,如果未能解决你的问题,请参考以下文章

以点为分隔符拆分字符串

13:SwiftUI-slider滑块

SwiftUI HStack 滑块不出现

openscales 怎样把点标出来的同时以点为中心画一个圆或者以这个点为顶点画一个扇形,方向可以调

带有 Swift 的步进滑块 [重复]

在 SwiftUI 中使用滑块创建音频播放器单元格