将 SwiftUI 列表滚动到新选择

Scroll SwiftUI List to new selection

如果您有一个允许单选的 SwiftUI 列表,您可以通过单击列表(大概这使其成为关键响应者)然后使用箭头键来更改选择。如果该选择到达可见区域的末尾,它将滚动整个列表以保持选择可见。

但是,如果选择对象以其他方式更新(例如使用按钮),列表将不会滚动。

有什么方法可以在以编程方式设置时强制列表滚动到新选择吗?

示例应用:

import SwiftUI

struct ContentView: View {
    @State var selection: Int? = 0

    func changeSelection(_ by: Int) {
        switch self.selection {
        case .none:
            self.selection = 0
        case .some(let sel):
            self.selection = max(min(sel + by, 20), 0)
        }
    }

    var body: some View {
        HStack {
            List((0...20), selection: $selection) {
                Text(String([=10=]))
            }
            VStack {
                Button(action: { self.changeSelection(-1) }) {
                    Text("Move Up")
                }
                Button(action: { self.changeSelection(1) }) {
                    Text("Move Down")
                }
            }
        }
    }
}

我尝试了几种解决方案,其中一种是我在我的项目中使用的(我需要为 3 个列表水平分页)。以下是我的观察:

  1. 我没有找到任何在 SwiftUI 中滚动列表的方法,文档中也没有提到它;
  2. 您可以尝试使用 ScrollView(我在下面的变体, 是其他解决方案),但这些东西可能看起来很怪异;
  3. 也许最好的方法是使用 UITableView:tutorial from Apple and try scrollToRowAtIndexPath method (like in this 回答)。

正如我所写,这是我的例子,当然,它需要改进。首先 ScrollView 需要在 GeometryReader 内部,你可以了解内容的真实大小。第二件事是你需要控制你的手势,这可能很难。最后一个:你需要计算 ScrollViews 内容的当前偏移量,它可能与我的代码不同(记住,我试着给你举个例子):

struct ScrollListView: View {

    @State var selection: Int?
    @State private var offset: CGFloat = 0
    @State private var isGestureActive: Bool = false

    func changeSelection(_ by: Int) {
        switch self.selection {
        case .none:
            self.selection = 0
        case .some(let sel):
            self.selection = max(min(sel + by, 30), 0)
        }
    }

    var body: some View {

        HStack {

            GeometryReader { geometry in

                VStack {
                    ScrollView(.vertical, showsIndicators: false) {

                        ForEach(0...29, id: \.self) { line in
                            ListRow(line: line, selection: self.$selection)
                                .frame(height: 20)
                        }

                        }
                    .content.offset(y: self.isGestureActive ? self.offset : geometry.size.height / 4 - CGFloat((self.selection ?? 0) * 20))
                    .gesture(DragGesture()
                        .onChanged({ value in
                            self.isGestureActive = true
                            self.offset = value.translation.width + -geometry.size.width * CGFloat(self.selection ?? 1)

                        })
                    .onEnded({ value in
                        DispatchQueue.main.async { self.isGestureActive = false }
                    }))

                }

            }


            VStack {
                Button(action: { self.changeSelection(-1) }) {
                    Text("Move Up")
                }
                Spacer()
                Button(action: { self.changeSelection(1) }) {
                    Text("Move Down")
                }
            }
        }
    }
}

当然你需要创建自己的 "list row":

struct ListRow: View {

    @State var line: Int
    @Binding var selection: Int?

    var body: some View {

        HStack(alignment: .center, spacing: 2){

            Image(systemName: line == self.selection ? "checkmark.square" : "square")
                .padding(.horizontal, 3)
            Text(String(line))
            Spacer()

        }
        .onTapGesture {
            self.selection = self.selection == self.line ? nil : self.line
        }

    }

}

希望对您有所帮助。

我是这样做的:

1) 可重复使用的复制粘贴组件:

import SwiftUI

struct TableViewConfigurator: UIViewControllerRepresentable {

    var configure: (UITableView) -> Void = { _ in }

    func makeUIViewController(context: UIViewControllerRepresentableContext<TableViewConfigurator>) -> UIViewController {

        UIViewController()
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<TableViewConfigurator>) {

        //let tableViews = uiViewController.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()

        let tableViews = UIApplication.nonModalTopViewController()?.navigationController?.topViewController?.view.subviews(ofType: UITableView.self) ?? [UITableView]()

        for tableView in tableViews {
            self.configure(tableView)
        }
    }
}

2) UIApplication 的扩展以在层次结构中查找顶视图控制器

扩展 UIApplication {

class var activeSceneRootViewController: UIViewController? {

    if #available(iOS 13.0, *) {
        for scene in UIApplication.shared.connectedScenes {
            if scene.activationState == .foregroundActive {
                return ((scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate)?.window??.rootViewController
            }
        }

    } else {
        // Fallback on earlier versions
        return UIApplication.shared.keyWindow?.rootViewController
    }

    return nil
}

class func nonModalTopViewController(controller: UIViewController? = UIApplication.activeSceneRootViewController) -> UIViewController? {

        print(controller ?? "nil")

        if let navigationController = controller as? UINavigationController {
            return nonModalTopViewController(controller: navigationController.topViewController ?? navigationController.visibleViewController)
        }
        if let tabController = controller as? UITabBarController {
            if let selected = tabController.selectedViewController {
                return nonModalTopViewController(controller: selected)
            }
        }
        if let presented = controller?.presentedViewController {
            let top = nonModalTopViewController(controller: presented)
            if top == presented { // just modal
                return controller
            } else {
                print("Top:", top ?? "nil")
                return top
            }
        }

        if let navigationController = controller?.children.first as? UINavigationController {

            return nonModalTopViewController(controller: navigationController.topViewController ?? navigationController.visibleViewController)
        }

        return controller
        }
}

3) 自定义部分 - 在这里,您可以在 List 后面实现 UITableView 的解决方案,如滚动:

在视图中的列表中的任何视图上像修饰符一样使用它

.background(TableViewConfigurator(configure: { tableView in
                if self.viewModel.statusChangeMessage != nil {
                    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
                        let lastIndexPath = IndexPath(row: tableView.numberOfRows(inSection: 0) - 1, section: 0)
                        tableView.scrollToRow(at: lastIndexPath, at: .bottom, animated: true)
                    }
                }
            }))

在针对 iOs 14 和 MacOs Big Sur 的新版 SwiftUI 中,他们添加了使用新的 ScrollViewReader 以编程方式滚动到特定单元格的功能:

struct ContentView: View {
    let colors: [Color] = [.red, .green, .blue]

    var body: some View {
        ScrollView {
            ScrollViewReader { value in
                Button("Jump to #8") {
                    value.scrollTo(8)
                }

                ForEach(0..<10) { i in
                    Text("Example \(i)")
                        .frame(width: 300, height: 300)
                        .background(colors[i % colors.count])
                        .id(i)
                }
            }
        }
    }
}

然后就可以这样使用方法.scrollTo()

value.scrollTo(8, anchor: .top)

来源:www.hackingwithswift.com