SwiftUI:如何让List自动适配内容高度且无内部滚动(嵌入ScrollView)
解决SwiftUI自动高度List嵌入ScrollView的问题
要实现一个自动适配内容高度、禁用内部滚动、保留原生样式的List,并嵌入到父ScrollView中,可通过以下方案实现:
核心思路
SwiftUI的List底层基于UITableView,直接用GeometryReader测量内容高度无法生效——因为List的内容容器是内部滚动视图。我们需要直接控制底层UITableView:禁用滚动功能,让它自动计算内容高度,再将高度同步给SwiftUI布局。
方案一:使用Introspect库(推荐)
Introspect可直接访问SwiftUI视图对应的UIKit底层控件,是最简洁的实现方式:
- 通过Swift Package Manager添加依赖:
https://github.com/siteline/SwiftUI-Introspect - 实现自动高度List组件:
import SwiftUI import Introspect // 专门处理KVO监听的对象 class ListHeightObserver: NSObject { var onHeightChange: ((CGFloat) -> Void)? override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == "contentSize", let tableView = object as? UITableView else { return } DispatchQueue.main.async { self.onHeightChange?(tableView.contentSize.height) } } } struct AutoSizingList<Content: View>: View { @ViewBuilder let content: () -> Content @State private var contentHeight: CGFloat = 0 @State private var heightObserver = ListHeightObserver() var body: some View { List { content() } .scrollDisabled(true) // SwiftUI层面禁用滚动 .frame(height: contentHeight) .introspect(.list, on: .iOS(.v16, .v17)) { tableView in tableView.isScrollEnabled = false // 彻底禁用UITableView滚动 tableView.layoutIfNeeded() // 避免重复添加观察者 tableView.removeObserver(heightObserver, forKeyPath: "contentSize") heightObserver.onHeightChange = { [weak self] height in self?.contentHeight = height } tableView.addObserver(heightObserver, forKeyPath: "contentSize", options: [.new], context: nil) } } }
方案二:纯SwiftUI替代方案(无第三方依赖)
如果不想依赖第三方库,可通过自定义UIViewRepresentable封装UITableView,模拟原生List样式:
import SwiftUI struct AutoSizingNativeList<Content: View>: UIViewRepresentable { @ViewBuilder let content: () -> Content func makeUIView(context: Context) -> UITableView { let tableView = UITableView() tableView.isScrollEnabled = false tableView.separatorStyle = .singleLine // 保留原生分隔线 tableView.register(HostingTableViewCell<Content>.self, forCellReuseIdentifier: "cell") tableView.dataSource = context.coordinator return tableView } func updateUIView(_ uiView: UITableView, context: Context) { context.coordinator.content = content() uiView.reloadData() } func makeCoordinator() -> Coordinator { Coordinator(content: content()) } class Coordinator: NSObject, UITableViewDataSource { var content: Content init(content: Content) { self.content = content } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! HostingTableViewCell<Content> cell.hostingController.rootView = content cell.setNeedsLayout() cell.layoutIfNeeded() // 自动计算行高 let height = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height tableView.rowHeight = height return cell } } } // 托管SwiftUI内容的UITableViewCell class HostingTableViewCell<Content: View>: UITableViewCell { let hostingController: UIHostingController<Content> init(content: Content) { self.hostingController = UIHostingController(rootView: content) super.init(style: .default, reuseIdentifier: nil) setupHostingView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupHostingView() { hostingController.view.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(hostingController.view) NSLayoutConstraint.activate([ hostingController.view.topAnchor.constraint(equalTo: contentView.topAnchor), hostingController.view.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), hostingController.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) } }
使用示例
ScrollView { VStack(spacing: 20) { Text("Above the list") .font(.title) AutoSizingList { Text("Row 1") Text("Row 2 — possibly multi-line text that grows. This is a very long text to test how the list adapts to multi-line content automatically without manual height calculation.") HStack { Text("Row 3") Spacer() Image(systemName: "chevron.right") } Button(action: {}) { Text("Row with Button") } } .listStyle(.insetGrouped) // 支持任意原生List样式 Text("Below the list") .font(.title) } .padding() }
关键注意点
- 禁用滚动需同时设置
scrollDisabled(true)和底层UITableView的isScrollEnabled = false,避免滚动冲突 - KVO监听需注意移除观察者,防止内存泄漏
- 纯SwiftUI方案需手动处理原生样式(如分隔线、行高),但完全无第三方依赖
内容的提问来源于stack exchange,提问作者János
相关产品推荐
相关产品推荐

