SwiftUI - 创建可滚动的水平日历式视图

问题描述 投票:0回答:1

我必须开发一个可滚动的水平日历式视图,就像这样。

最终目标是创建无限滚动视图。我面临着一些我不知道如何解决的挑战。我向您展示我现在拥有的代码,它与最终的样子相去甚远:

var body: some View {
    ScrollView(.horizontal) {
        LazyHStack {
            ForEach(datesToShow, id: \.self) { value in
                VStack {
                    Text("\(value.day)")
                    Text(value.toString(.custom("MMMM")).capitalized)
                        .padding(.top)
                }
                .foregroundStyle(.white)
                .frame(width: 100, height: 100)
                .background(.purple)
            }
        }
    }
    .frame(height: 200)
    .onAppear {
        
    }
}

datesToShow
是一个日期数组,包含今天 - 10 天到今天 + 10 天之间的日期。现在,艰难的时刻从这里开始:

  1. 当我到达数组/视图的两个边缘之一时,如何处理日期?今天是 10 月 19 日,如何加载 10 月 9 日之前或 10 月 29 日之后的日期并将它们添加到
    datesToShow
    数组中以便自动加载(当然性能最好)?
  2. 这是达到我需要的结果的方法吗?你有什么不同的建议吗?

谢谢!

arrays swift date swiftui calendar
1个回答
0
投票

按照我使用轮播的建议,这里有一个示例实现来展示它的工作原理。

日期在

ZStack
中显示为单独的视图,每个视图都有一个计算出的 x 偏移量。
ZStack
上的拖动手势可以更改当前日期。释放拖动后,它会捕捉到最近的日期。

在这个例子中,有 15 个底层视图,它们不断循环。我发现最好的办法是拥有比实际可见数量多得多的视图,这样当快速拖动时,您就不会看到更新过程中的日期。让数字取决于显示器的宽度,或者至少使每个视图的大小取决于宽度,这样它也可以在大显示器上工作,这可能是一个好主意。

希望有帮助!

struct DateView: View {
    let baseDate: Date
    let dayAdjustment: Int
    let dayOfMonthFormatter = DateFormatter()
    let monthNameFormatter = DateFormatter()

    init(baseDate: Date, dayAdjustment: Int) {
        self.baseDate = baseDate
        self.dayAdjustment = dayAdjustment
        dayOfMonthFormatter.dateFormat = "d"
        monthNameFormatter.dateFormat = "MMMM"
    }

    var body: some View {
        let dateToDisplay = Calendar.current.date(byAdding: .day, value: dayAdjustment, to: baseDate) ?? baseDate
        ZStack {
            Color.purple
                .cornerRadius(10)
            VStack {
                Text(dayOfMonthFormatter.string(from: dateToDisplay))
                Text(monthNameFormatter.string(from: dateToDisplay))
            }
            .foregroundStyle(.white)
        }
    }
}

struct ContentView: View {
    let nPanels = 15
    let dateItemSize: CGFloat = 100
    let gapSize: CGFloat = 10
    let baseDate = Date.now

    @State private var dayOffset = Double.zero
    @State private var dragBegin = 0

    private var positionWidth: CGFloat {
        CGFloat(dateItemSize + gapSize)
    }

    private func relativePositionForIndex(index: Int) -> Double {
        let midIndex = Double(nPanels / 2)
        var dIndex = (Double(index) - dayOffset - midIndex).truncatingRemainder(dividingBy: Double(nPanels))
        if dIndex < -midIndex {
            dIndex += Double(nPanels)
        } else if dIndex > midIndex {
            dIndex -= Double(nPanels)
        }
        return dIndex
    }

    private func dateView(index: Int, halfFullWidth: CGFloat) -> some View {
        let dIndex = relativePositionForIndex(index: index)
        let xOffset = dIndex * positionWidth
        let dayAdjustment = Int(dIndex.rounded()) + Int(dayOffset.rounded())
        return DateView(
            baseDate: baseDate,
            dayAdjustment: dayAdjustment
        )
        .frame(width: dateItemSize, height: dateItemSize)
        .offset(x: xOffset)

        // Setting opacity helps to avoid blinks when switching sides
        .opacity(xOffset + positionWidth < -halfFullWidth || xOffset - positionWidth > halfFullWidth ? 0 : 1)
    }

    private var dragged: some Gesture {
        DragGesture()
            .onChanged() { val in
                dayOffset = Double(dragBegin) - (val.translation.width / positionWidth)
            }
            .onEnded { val in
                dragBegin = Int(Double(dragBegin) - (val.translation.width / positionWidth).rounded())
                withAnimation(.easeInOut(duration: 0.15)) {
                    dayOffset = Double(dragBegin)
                }
            }
    }

    var body: some View {
        GeometryReader { proxy in
            let halfFullWidth = proxy.size.width / 2
            ZStack {
                ForEach(0..<nPanels, id: \.self) { index in
                    dateView(index: index, halfFullWidth: halfFullWidth)
                }
                RoundedRectangle(cornerRadius: 10)
                    .stroke(lineWidth: 3)
                    .opacity(0.7)
                    .frame(width: dateItemSize + gapSize, height: dateItemSize + gapSize)
            }
            .gesture(dragged)
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

Animation

© www.soinside.com 2019 - 2024. All rights reserved.