如果您有一个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")
}
}
}
}
}
答案 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)
答案 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个列表进行水平分页)。这是我的观察结果:
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
}
}
}
希望会有所帮助。