滚动 SwiftUI 列表以显示新选择

21

如果您有一个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($0))
            }
            VStack {
                Button(action: { self.changeSelection(-1) }) {
                    Text("Move Up")
                }
                Button(action: { self.changeSelection(1) }) {
                    Text("Move Down")
                }
            }
        }
    }
}

什么是sea?是否缺少一些代码? - matt
抱歉,修正了打字错误。 - Brandon Horst
3个回答

5

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

  1. 我没有找到在SwiftUI中滚动列表的方法,在文档中也没有提及;
  2. 您可以尝试ScrollView(我的变体如下,这里是其他解决方案),但这些东西可能看起来很丑陋;
  3. 也许最好的方法是使用UITableView:Apple的教程并尝试scrollToRowAtIndexPath方法(就像在这个答案中)。)

正如我所写的,这是我的示例,当然需要完善。首先,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")
                }
            }
        }
    }
}

当然,您需要创建自己的"列表行":
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
        }

    }

}

希望这对你有所帮助。


我自己也采用了类似的解决方案,但如果有完全基于SwiftUI的解决方案,我会保持开放。 - Brandon Horst
我有一些相对简单易用的黑科技,你可以快速尝试。 - Michał Ziobro
请纠正我,如果我错了,但是这个解决方案遭受了所有其他我见过的解决方案所遇到的同样的问题。您假设列表中每个视图的宽度都是屏幕宽度,因此用户一次只能在屏幕上看到一个选择。我正在尝试找出如何让屏幕上显示多个项目,以便用户有一个视觉提示,知道他/她必须滚动。 - Moebius

4
在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)

Credit: www.hackingwithswift.com


我该如何告诉ScrollView从ScrollView外部滚动到特定的锚点? - ediheld
1
我认为这是不可能的,因为value只能在ScrollViewReader中使用,所以.scrollTo()方法只能在ScrollViewReader内部调用。 - Luca
你可以将Reader移动到ScrollView之外(并将ScrollView和其他视图包装在其中)。顺便说一句,这与文档中的示例相匹配:https://developer.apple.com/documentation/swiftui/scrollviewreader - calimarkus

1
我会这样做:
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以查找层次结构中的顶级视图控制器

extension 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) 自定义部分 - 在这里,您可以实现UITableView在List后面的滚动解决方案:

将其用作List中任何视图的修饰符

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

1
问题涉及 macOS。 - Brandon Horst
感谢您的解决方案,非常有帮助~ - Young
4
我喜欢苹果简化事物的做法,比如创建一个基本上无法使用的列表,因为它缺乏许多功能。 - Duck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接