ios SwiftUI中的双向无限PageView

093gszye  于 2023-05-19  发布在  iOS
关注(0)|答案(2)|浏览(209)

我正在尝试创建一个双向的TabView(使用.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))),它的数据源会随着时间的推移而改变。
下面是描述结果中预期内容的代码:

class TabsViewModel: ObservableObject {
    @Published var items = [-2, -1, 0, 1, 2]
    @Published var selectedItem = 2 {
        didSet {
            if let index = items.firstIndex(of: selectedItem), index >= items.count - 2 {
                items = items + [items.last! + 1]
                items.removeFirst()
            }
            
            if let index = items.firstIndex(of: selectedItem), index <= 1 {
                items = [items.first! - 1] + items
                items.removeLast()
            }
        }
    }
}

struct TabsView: View {
    @StateObject var vm = TabsViewModel()
    
    var body: some View {
        TabView(selection: $vm.selectedItem) {
            ForEach(vm.items, id: \.self) { item in
                Text(item.description)
                    .frame(minWidth: 0,
                           maxWidth: .infinity,
                           minHeight: 0,
                           maxHeight: .infinity,
                           alignment: .topLeading
                    )
                    .background(Color(hue: .random(in: 0...0.99), saturation: .random(in: 0...0.99), brightness: 0.5))
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    }
}

在这种情况下,当您尝试转到下一页或上一页时,动画中断,原始页面立即返回到其位置。
有没有可能在没有UIKit和第三方框架的帮助下,用TabView实现这一点?
我猜测可能的解决方案之一是在过渡动画完成时更改数据源,但我无法在SwiftUI中找到这样做的方法。
我猜的对吗?也许还有其他更优雅的方法来实现此功能,我的观点是错误的。

x8diyxa7

x8diyxa71#

任务本身可以使用SwiftUIPager框架来解决。

import SwiftUI
import SwiftUIPager

class PagerConteinerViewModel: ObservableObject {
    @Published var items = [-3, -2, -1, 0, 1, 2, 3]
    @Published var selectedItemIndex = 3
    
    func updateItems (_ direction: Double) {
        withAnimation {
            if direction > 0 && selectedItemIndex >= items.count - 3 {
                items = items + [items.last! + 1]
                items.removeFirst()
            }
            
            if direction < 0 && selectedItemIndex <= 2 {
                items = [items.first! - 1] + items
                items.removeLast()
            }
        }
    }
}

struct PagerContainerView: View {
    @StateObject var vm = PagerConteinerViewModel()
    
    var body: some View {
        Pager(page: $vm.selectedItemIndex, data: vm.items,  id: \.self) { item in
            Text("\(item)")
                .frame(minWidth: 0,
                       maxWidth: .infinity,
                       minHeight: 0,
                       maxHeight: .infinity,
                       alignment: .topLeading
                )
                .background(Color(hue: .random(in: 0...0.99), saturation: .random(in: 0...0.99), brightness: 0.5))
        }
        .onDraggingEnded(vm.updateItems)
    }
}

文档使用onPageChanged来更新数据源,但是在数组的中间或开头插入一个值会使应用程序崩溃。

polkgigr

polkgigr2#

对于那些正在寻找没有外部依赖的简单解决方案的人:我刚刚实现了自己的变体,基于TabView.page选项卡视图样式。它基于这样的想法:你有两个函数before()after(),它们返回index(!)的当前所选页面(存储在selection中)之前或之后的页面。有一个view方法,它返回给定页面索引的视图。
所以基本上总是有三个页面:* 前一页 (标记为-1)、 当前页 *(标记为0)和 * 下一页 *(标记为1)。当用户滑动页面(= TabView内容)时,currentTab将更新为TabView(selection:)。然后,当 * 当前 * 页面不在屏幕上时(=调用onDisappear),此currentTab将传播到selection变量,再次将currentTab更新为0。由于新设置的selection,重新生成 previouscurrentnext 页。
解决方案是使用Swift泛型。我一直在使用IntDate索引来呈现页面。

struct InfinitePageView<C, T>: View where C: View, T: Hashable {
    @Binding var selection: T

    let before: (T) -> T
    let after: (T) -> T

    @ViewBuilder let view: (T) -> C

    @State private var currentTab: Int = 0

    var body: some View {
        let previousIndex = before(selection)
        let nextIndex = after(selection)
        TabView(selection: $currentTab) {
            view(previousIndex)
                .tag(-1)

            view(selection)
                .onDisappear() {
                    if currentTab != 0 {
                        selection = currentTab < 0 ? previousIndex : nextIndex
                        currentTab = 0
                    }
                }
                .tag(0)

            view(nextIndex)
                .tag(1)
        }
        .tabViewStyle(.page(indexDisplayMode: .never))
        .disabled(currentTab != 0) // FIXME: workaround to avoid glitch when swiping twice very quickly
    }
}

下面是一个如何使用它的例子:这是一个颜色数组,当你越过边界(=数组的开始或结束)时,它会“循环”/“滚动”,所以你可以在任何方向上永远滑动颜色。

struct Content: View {
    private let systemColors: [Color] = [
        .red, .orange, .yellow, .green,
        .mint, .teal, .cyan, .blue,
        .indigo, .purple, .pink, .brown
    ]

    @State private var colorIndex = 0

    var body: some View {
        InfinitePageView(
            selection: $colorIndex,
            before: { correctedIndex(for: $0 - 1) },
            after: { correctedIndex(for: $0 + 1) },
            view: { index in
                systemColors[index]
                    .overlay(
                        Text("\(index)     \(index)")
                            .colorInvert()
                    )
                    .font(.system(size: 100, weight: .heavy))
            }
        )
    }

    private func correctedIndex(for index: Int) -> Int {
        let count = systemColors.count
        return (count + index) % count
    }
}

类似的代码可以用于在日历或无尽的约会列表中滑动。

相关问题