SwiftUI 使用点作为指示器创建图像滑块 [英] SwiftUI create image slider with dots as indicators

查看:25
本文介绍了SwiftUI 使用点作为指示器创建图像滑块的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想为图像创建滚动视图/滑块.查看我的示例代码:

ScrollView(.horizo​​ntal, showsIndicators: true) {堆栈{图像(shelter.background).resizable().frame(宽度:UIScreen.main.bounds.width,高度:300)图片(太平洋").resizable().frame(宽度:UIScreen.main.bounds.width,高度:300)}}

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

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

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

解决方案

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

在短期内,您有两种选择.正如 Asperi 所指出的,Apple 自己的教程中有一个部分是关于从 UIKit 包装 PageViewController 以在 SwiftUI 中使用(请参阅

两个注意事项:

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

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:

  1. 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.
  2. 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:

  1. 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
  2. The drag gesture in the simulator feels clunky, but it works really well on a physical device.

这篇关于SwiftUI 使用点作为指示器创建图像滑块的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆