从 SwiftUI 列表访问底层 UITableView
Access underlying UITableView from SwiftUI List
使用 List
视图,有没有一种方法可以访问(并因此修改)底层 UITableView
对象而无需将整个 List
重新实现为 UIViewRepresentable
?
我已经尝试在我自己的 UIViewRepresentable
中初始化一个 List
,但我似乎无法让 SwiftUI 在我需要时初始化视图,我只是得到一个空的没有子视图的基本 UIView
。
这个问题是为了帮助找到 的答案。
或者,在 SwiftUI 中重新实现 UITableView
的库或其他项目也会回答这个问题。
答案是否定的。截至iOS13,SwiftUI 的 List 目前并未设计为取代 UITableView 的所有功能和可定制性。它旨在满足 UITableView 的最基本用途:一个标准外观、可滚动、可编辑的列表,您可以在其中的每个单元格中放置一个相对简单的视图。
换句话说,您放弃了可定制性,因为轻扫、导航、移动、删除等功能会自动为您实现。
我确信随着 SwiftUI 的发展,List(或等效视图)将变得更加可定制,我们将能够执行诸如从底部滚动、更改填充等操作。最好的方法确保发生这种情况是 file feedback suggestions with Apple。我确信 SwiftUI 工程师已经在努力设计将在 WWDC 2020 上出现的功能。他们为指导社区的需求提供的意见越多越好。
目前无法访问或修改底层 UITableView
我找到了一个名为 Rotoscope
on GitHub 的库(我不是它的作者)。
这个库被同一作者用来实现 RefreshUI
also on GitHub。
它的工作原理是 Rotoscope
有一个 tagging
方法,它在您的 List
之上覆盖一个 0 大小的 UIViewRepresentable
(因此它是不可见的)。该视图将挖掘视图链并最终找到托管 SwiftUI 视图的 UIHostingView
。然后,它会 return 托管视图的第一个子视图,其中应该包含 UITableView
的包装器,然后您可以通过获取包装器的子视图来访问 table 视图对象。
RefreshUI
库使用此库实现对 SwiftUI 的刷新控制 List
(您可以进入 GitHub link 并查看源代码以看看它是如何实现的)。
但是,我认为这更像是一种 hack,而不是实际的方法,因此由您决定是否要使用它。无法保证它会在主要更新之间继续工作,因为 Apple 可能会更改内部视图布局并且此库会中断。
答案是肯定的。有一个很棒的库可以让你检查底层的 UIKit 视图。这是一个link。
你可以的。但它需要一个黑客。
- 添加任何自定义 UIView
- 使用 UIResponder 回溯,直到找到 table 视图。
- 按照您喜欢的方式修改 UITableView。
添加拉取刷新的代码示例:
//1: create a custom view
final class UIKitView : UIViewRepresentable {
let callback: (UITableView) -> Void
init(leafViewCB: @escaping ((UITableView) -> Void)) {
callback = leafViewCB
}
func makeUIView(context: Context) -> UIView {
let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
y: CGFloat.leastNormalMagnitude,
width: CGFloat.leastNormalMagnitude,
height: CGFloat.leastNormalMagnitude))
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let superView = uiView.superview {
superView.backgroundColor = uiView.backgroundColor
}
if let tableView = uiView.next(UITableView.self) {
callback(tableView)
}
}
}
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
////Use:
struct Result: Identifiable {
var id = UUID()
var value: String
}
class RefreshableObject: ObservableObject {
let id = UUID()
@Published var items: [Result] = [Result(value: "Binding"),
Result(value: "ObservableObject"),
Result(value: "Published")]
let refreshControl: UIRefreshControl
init() {
refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action:
#selector(self.handleRefreshControl),
for: .valueChanged)
}
@objc func handleRefreshControl(sender: UIRefreshControl) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
sender.endRefreshing()
self?.items = [Result(value:"new"), Result(value:"data"), Result(value:"after"), Result(value:"refresh")]
}
}
}
struct ContentView: View {
@ObservedObject var refreshableObject = RefreshableObject()
var body: some View {
NavigationView {
Form {
Section(footer: UIKitView.init { (tableView) in
if tableView.refreshControl == nil {
tableView.refreshControl = self.refreshableObject.refreshControl
}
}){
ForEach(refreshableObject.items) { result in
Text(result.value)
}
}
}
.navigationBarTitle("Nav bar")
}
}
}
截图:
要从刷新操作更新,正在使用绑定 isUpdateOrdered。
此代码是我在网上找到的代码,找不到作者
import Foundation
import SwiftUI
class Model: ObservableObject{
@Published var isUpdateOrdered = false{
didSet{
if isUpdateOrdered{
update()
isUpdateOrdered = false
print("we got him!")
}
}
}
var random = 0
@Published var arr = [Int]()
func update(){
isUpdateOrdered = false
//your update code.... maybe some fetch request or POST?
}
}
struct ContentView: View {
@ObservedObject var model = Model()
var body: some View {
NavigationView {
LegacyScrollViewWithRefresh(isUpdateOrdered: $model.isUpdateOrdered) {
VStack{
if model.arr.isEmpty{
//this is important to fill the
//scrollView with invisible data,
//in other case scroll won't work
//because of the constraints.
//You may get rid of them if you like.
Text("refresh!")
ForEach(1..<100){ _ in
Text("")
}
}else{
ForEach(model.arr, id:\.self){ i in
NavigationLink(destination: Text(String(i)), label: { Text("Click me") })
}
}
}
}.environmentObject(model)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct LegacyScrollViewWithRefresh: UIViewRepresentable {
enum Action {
case idle
case offset(x: CGFloat, y: CGFloat, animated: Bool)
}
typealias Context = UIViewRepresentableContext<Self>
@Binding var action: Action
@Binding var isUpdateOrdered: Bool
private let uiScrollView: UIScrollView
private var uiRefreshControl = UIRefreshControl()
init<Content: View>(isUpdateOrdered: Binding<Bool>, content: Content) {
let hosting = UIHostingController(rootView: content)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
self._isUpdateOrdered = isUpdateOrdered
uiScrollView = UIScrollView()
uiScrollView.addSubview(hosting.view)
let constraints = [
hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
]
uiScrollView.addConstraints(constraints)
self._action = Binding.constant(Action.idle)
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, @ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, action: Binding<Action>, @ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
self._action = action
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
uiScrollView.addSubview(uiRefreshControl)
uiRefreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl(arguments:)), for: .valueChanged)
return uiScrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
switch self.action {
case .offset(let x, let y, let animated):
uiView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
DispatchQueue.main.async {
self.action = .idle
}
default:
break
}
}
class Coordinator: NSObject {
let legacyScrollView: LegacyScrollViewWithRefresh
init(_ legacyScrollView: LegacyScrollViewWithRefresh) {
self.legacyScrollView = legacyScrollView
}
@objc func handleRefreshControl(arguments: UIRefreshControl){
print("refreshing")
self.legacyScrollView.isUpdateOrdered = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
arguments.endRefreshing()
//refresh animation will
//always be shown for 2 seconds,
//you may connect this behaviour
//to your update completion
}
}
}
}
使用 List
视图,有没有一种方法可以访问(并因此修改)底层 UITableView
对象而无需将整个 List
重新实现为 UIViewRepresentable
?
我已经尝试在我自己的 UIViewRepresentable
中初始化一个 List
,但我似乎无法让 SwiftUI 在我需要时初始化视图,我只是得到一个空的没有子视图的基本 UIView
。
这个问题是为了帮助找到
或者,在 SwiftUI 中重新实现 UITableView
的库或其他项目也会回答这个问题。
答案是否定的。截至iOS13,SwiftUI 的 List 目前并未设计为取代 UITableView 的所有功能和可定制性。它旨在满足 UITableView 的最基本用途:一个标准外观、可滚动、可编辑的列表,您可以在其中的每个单元格中放置一个相对简单的视图。
换句话说,您放弃了可定制性,因为轻扫、导航、移动、删除等功能会自动为您实现。
我确信随着 SwiftUI 的发展,List(或等效视图)将变得更加可定制,我们将能够执行诸如从底部滚动、更改填充等操作。最好的方法确保发生这种情况是 file feedback suggestions with Apple。我确信 SwiftUI 工程师已经在努力设计将在 WWDC 2020 上出现的功能。他们为指导社区的需求提供的意见越多越好。
目前无法访问或修改底层 UITableView
我找到了一个名为 Rotoscope
on GitHub 的库(我不是它的作者)。
这个库被同一作者用来实现 RefreshUI
also on GitHub。
它的工作原理是 Rotoscope
有一个 tagging
方法,它在您的 List
之上覆盖一个 0 大小的 UIViewRepresentable
(因此它是不可见的)。该视图将挖掘视图链并最终找到托管 SwiftUI 视图的 UIHostingView
。然后,它会 return 托管视图的第一个子视图,其中应该包含 UITableView
的包装器,然后您可以通过获取包装器的子视图来访问 table 视图对象。
RefreshUI
库使用此库实现对 SwiftUI 的刷新控制 List
(您可以进入 GitHub link 并查看源代码以看看它是如何实现的)。
但是,我认为这更像是一种 hack,而不是实际的方法,因此由您决定是否要使用它。无法保证它会在主要更新之间继续工作,因为 Apple 可能会更改内部视图布局并且此库会中断。
答案是肯定的。有一个很棒的库可以让你检查底层的 UIKit 视图。这是一个link。
你可以的。但它需要一个黑客。
- 添加任何自定义 UIView
- 使用 UIResponder 回溯,直到找到 table 视图。
- 按照您喜欢的方式修改 UITableView。
添加拉取刷新的代码示例:
//1: create a custom view
final class UIKitView : UIViewRepresentable {
let callback: (UITableView) -> Void
init(leafViewCB: @escaping ((UITableView) -> Void)) {
callback = leafViewCB
}
func makeUIView(context: Context) -> UIView {
let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
y: CGFloat.leastNormalMagnitude,
width: CGFloat.leastNormalMagnitude,
height: CGFloat.leastNormalMagnitude))
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let superView = uiView.superview {
superView.backgroundColor = uiView.backgroundColor
}
if let tableView = uiView.next(UITableView.self) {
callback(tableView)
}
}
}
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
////Use:
struct Result: Identifiable {
var id = UUID()
var value: String
}
class RefreshableObject: ObservableObject {
let id = UUID()
@Published var items: [Result] = [Result(value: "Binding"),
Result(value: "ObservableObject"),
Result(value: "Published")]
let refreshControl: UIRefreshControl
init() {
refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action:
#selector(self.handleRefreshControl),
for: .valueChanged)
}
@objc func handleRefreshControl(sender: UIRefreshControl) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
sender.endRefreshing()
self?.items = [Result(value:"new"), Result(value:"data"), Result(value:"after"), Result(value:"refresh")]
}
}
}
struct ContentView: View {
@ObservedObject var refreshableObject = RefreshableObject()
var body: some View {
NavigationView {
Form {
Section(footer: UIKitView.init { (tableView) in
if tableView.refreshControl == nil {
tableView.refreshControl = self.refreshableObject.refreshControl
}
}){
ForEach(refreshableObject.items) { result in
Text(result.value)
}
}
}
.navigationBarTitle("Nav bar")
}
}
}
截图:
要从刷新操作更新,正在使用绑定 isUpdateOrdered。
此代码是我在网上找到的代码,找不到作者
import Foundation
import SwiftUI
class Model: ObservableObject{
@Published var isUpdateOrdered = false{
didSet{
if isUpdateOrdered{
update()
isUpdateOrdered = false
print("we got him!")
}
}
}
var random = 0
@Published var arr = [Int]()
func update(){
isUpdateOrdered = false
//your update code.... maybe some fetch request or POST?
}
}
struct ContentView: View {
@ObservedObject var model = Model()
var body: some View {
NavigationView {
LegacyScrollViewWithRefresh(isUpdateOrdered: $model.isUpdateOrdered) {
VStack{
if model.arr.isEmpty{
//this is important to fill the
//scrollView with invisible data,
//in other case scroll won't work
//because of the constraints.
//You may get rid of them if you like.
Text("refresh!")
ForEach(1..<100){ _ in
Text("")
}
}else{
ForEach(model.arr, id:\.self){ i in
NavigationLink(destination: Text(String(i)), label: { Text("Click me") })
}
}
}
}.environmentObject(model)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct LegacyScrollViewWithRefresh: UIViewRepresentable {
enum Action {
case idle
case offset(x: CGFloat, y: CGFloat, animated: Bool)
}
typealias Context = UIViewRepresentableContext<Self>
@Binding var action: Action
@Binding var isUpdateOrdered: Bool
private let uiScrollView: UIScrollView
private var uiRefreshControl = UIRefreshControl()
init<Content: View>(isUpdateOrdered: Binding<Bool>, content: Content) {
let hosting = UIHostingController(rootView: content)
hosting.view.translatesAutoresizingMaskIntoConstraints = false
self._isUpdateOrdered = isUpdateOrdered
uiScrollView = UIScrollView()
uiScrollView.addSubview(hosting.view)
let constraints = [
hosting.view.leadingAnchor.constraint(equalTo: uiScrollView.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: uiScrollView.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.topAnchor),
hosting.view.bottomAnchor.constraint(equalTo: uiScrollView.contentLayoutGuide.bottomAnchor),
hosting.view.widthAnchor.constraint(equalTo: uiScrollView.widthAnchor)
]
uiScrollView.addConstraints(constraints)
self._action = Binding.constant(Action.idle)
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, @ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
}
init<Content: View>(isUpdateOrdered: Binding<Bool>, action: Binding<Action>, @ViewBuilder content: () -> Content) {
self.init(isUpdateOrdered: isUpdateOrdered, content: content())
self._action = action
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UIScrollView {
uiScrollView.addSubview(uiRefreshControl)
uiRefreshControl.addTarget(context.coordinator, action: #selector(Coordinator.handleRefreshControl(arguments:)), for: .valueChanged)
return uiScrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
switch self.action {
case .offset(let x, let y, let animated):
uiView.setContentOffset(CGPoint(x: x, y: y), animated: animated)
DispatchQueue.main.async {
self.action = .idle
}
default:
break
}
}
class Coordinator: NSObject {
let legacyScrollView: LegacyScrollViewWithRefresh
init(_ legacyScrollView: LegacyScrollViewWithRefresh) {
self.legacyScrollView = legacyScrollView
}
@objc func handleRefreshControl(arguments: UIRefreshControl){
print("refreshing")
self.legacyScrollView.isUpdateOrdered = true
DispatchQueue.main.asyncAfter(deadline: .now() + 2){
arguments.endRefreshing()
//refresh animation will
//always be shown for 2 seconds,
//you may connect this behaviour
//to your update completion
}
}
}
}