将SwiftUI列表滚动到新选择

时间:2019-07-20 04:20:44

标签: macos swiftui

如果您有一个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")
                }
            }
        }
    }
}

3 个答案:

答案 0 :(得分:4)

在针对iO 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

答案 1 :(得分:2)

我正在这样做:

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

答案 2 :(得分:1)

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

  1. 在SwiftUI中我没有找到任何滚动列表的方法,文档中还没有提及它;
  2. 您可以尝试ScrollView(下面是我的变体,here是另一种解决方案),但是这些东西看起来像是类似动物;
  3. 也许最好的方法是使用UITableView:tutorial from Apple并尝试scrollToRowAtIndexPath方法(例如在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")
                }
            }
        }
    }
}

当然您需要创建自己的“列表行”:

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
        }

    }

}

希望会有所帮助。