You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI:如何让List自动适配内容高度且无内部滚动(嵌入ScrollView)

解决SwiftUI自动高度List嵌入ScrollView的问题

要实现一个自动适配内容高度、禁用内部滚动、保留原生样式的List,并嵌入到父ScrollView中,可通过以下方案实现:

核心思路

SwiftUI的List底层基于UITableView,直接用GeometryReader测量内容高度无法生效——因为List的内容容器是内部滚动视图。我们需要直接控制底层UITableView:禁用滚动功能,让它自动计算内容高度,再将高度同步给SwiftUI布局。

方案一:使用Introspect库(推荐)

Introspect可直接访问SwiftUI视图对应的UIKit底层控件,是最简洁的实现方式:

  1. 通过Swift Package Manager添加依赖:https://github.com/siteline/SwiftUI-Introspect
  2. 实现自动高度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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 08:41:00