SwiftUI 创建以点为指标的图像 slider

标签 swift swiftui

我想为图像创建一个 ScrollView / slider 。请参阅我的示例代码:

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)。我希望它的行为类似于我们从许多以点为指示符的应用程序中知道的典型图像 slider :
  • 它应始终显示完整图像,中间没有 - 因此如果用户在中间拖动并停止,它将自动跳转到完整图像。
  • 我想要点作为指标。

  • 既然我看到很多应用程序都使用这样的 slider ,那么肯定有已知的方法,对吧?

    最佳答案

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

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

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

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

    PagingView demo

    两个注意事项:
  • GIF 动画在显示动画的流畅度方面做得非常糟糕,因为由于文件大小的限制,我不得不降低帧率并进行大量压缩。在模拟器或真实设备上看起来很棒
  • 模拟器中的拖动手势感觉很笨拙,但在物理设备上效果很好。
  • 关于SwiftUI 创建以点为指标的图像 slider ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58896661/

    相关文章:

    swift - 重叠凹面 SKShapeNode

    swiftui - iOS 14 中等 WidgetKit 大小背景图像拒绝填充

    SwiftUI:在执行下面的代码之前等待 Alamofire 请求完成

    ios - 删除列表内披露组中的前导空格 (SwiftUI)

    ios - 使用 Swift 在 Firebase 数据库中将一个 child 添加到一个 child

    ios - 滑动手势给出 NSException

    SwiftUI 从配置文件选项卡栏项目进行导航的最佳方式

    swift - ForEach 循环遍历不同的变量类型 swift

    json - 使用 Swift 访问 JSON 中的嵌套字典

    Swift:将 AnyObject 转换为 Float 失败