SwiftUI 创建带有点作为指示器的图像滑块

Tim*_*Tim 17 swift swiftui

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

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)
      }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

Joh*_* M. 46

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

短期内,您有两种选择。正如 Asperi 所指出的,Apple 自己的教程中有一个部分是关于从 UIKit 包装 PageViewController 以便在 SwiftUI 中使用(请参阅与 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)
    }
}
Run Code Online (Sandbox Code Playgroud)

和一个演示

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()
    }
}
Run Code Online (Sandbox Code Playgroud)

分页查看演示

两个注意事项:

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

  • 我发现一个问题,如果我在滚动视图中出现此问题,如果我的手指位于 PagingView 上,我将无法向上或向下滚动 (2认同)
  • @FilipeSá 在 `let content: () -&gt; Content` 下添加 `private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()`,然后添加 `.onReceive(self) .timer) { _ in self.index = (self.index + 1) % 3 }` 在 `PageControl(index: $index, maxIndex: maxIndex)` 下面。 (2认同)

小智 7

您可以通过下面的代码轻松实现这一点

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
                }                
            })  
        }
    }
}
Run Code Online (Sandbox Code Playgroud)