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

如何实现带行数限制的SwiftUI动态高度TextField?

解决SwiftUI中动态高度+最大行数+滚动的输入框问题

你的核心需求是实现一个初始单行、随输入自动扩展高度、达到最大行数后可滚动的输入框,原代码的问题在于TextField并非为多行动态高度场景设计,lineLimit仅限制显示行数但不会自动调整容器高度,也不支持滚动。下面提供两种可行的实现方案:

方案一:使用SwiftUI原生TextEditor(简洁版)

TextEditor是SwiftUI专为多行文本输入设计的组件,结合动态高度计算可以满足需求:

struct ContentView: View {
    @State private var text = ""
    // 定义最大行数和单行高度(可根据字体调整)
    private let maxLines = 5
    private let singleLineHeight: CGFloat = 20
    
    var body: some View {
        VStack {
            TextEditor(text: $text)
                .padding()
                .background(Color.gray.opacity(0.2))
                .cornerRadius(8)
                // 计算最小/最大高度:最小为单行高度+内边距,最大为最大行数*单行高度+内边距
                .frame(minHeight: singleLineHeight + 16, maxHeight: CGFloat(maxLines) * singleLineHeight + 16)
                .font(.system(size: 16))
                // 手动实现占位文本(TextEditor原生无占位功能)
                .overlay(
                    Group {
                        if text.isEmpty {
                            Text("Enter your text here")
                                .foregroundColor(.gray)
                                .padding(.horizontal, 8)
                                .padding(.vertical, 12)
                                .allowsHitTesting(false)
                        }
                    }
                )
        }
        .padding()
    }
}

关键说明:

  • 用TextEditor替代TextField,原生支持多行输入与滚动
  • 通过frame的minHeight和maxHeight控制高度范围:minHeight对应初始单行高度,maxHeight基于最大行数计算
  • 手动添加占位文本,模拟原生输入框的占位体验
  • 当文本内容超过最大高度时,TextEditor会自动启用滚动

方案二:封装UITextView(灵活定制版)

如果需要更精细的控制(比如精确的行高计算、自定义光标样式等),可以封装UITextView实现:

struct ResizableTextView: UIViewRepresentable {
    @Binding var text: String
    let placeholder: String
    let maxLines: Int
    let minHeight: CGFloat
    let font: UIFont
    
    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.font = font
        textView.backgroundColor = .clear
        textView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
        textView.delegate = context.coordinator
        // 初始关闭滚动,达到最大行数后开启
        textView.isScrollEnabled = false
        textView.text = placeholder
        textView.textColor = .lightGray
        return textView
    }
    
    func updateUIView(_ uiView: UITextView, context: Context) {
        // 处理占位文本逻辑
        if text.isEmpty {
            uiView.text = placeholder
            uiView.textColor = .lightGray
        } else {
            if uiView.text == placeholder {
                uiView.text = text
            } else {
                uiView.text = text
            }
            uiView.textColor = .label
        }
        // 动态调整高度
        context.coordinator.adjustTextViewHeight(uiView: uiView)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject, UITextViewDelegate {
        let parent: ResizableTextView
        
        init(parent: ResizableTextView) {
            self.parent = parent
        }
        
        func textViewDidChange(_ textView: UITextView) {
            // 更新绑定的文本
            if textView.text != parent.placeholder {
                parent.text = textView.text
            }
            // 调整高度
            adjustTextViewHeight(uiView: textView)
        }
        
        func textViewDidBeginEditing(_ textView: UITextView) {
            // 编辑时清空占位文本
            if textView.text == parent.placeholder {
                textView.text = ""
                textView.textColor = .label
            }
        }
        
        func textViewDidEndEditing(_ textView: UITextView) {
            // 结束编辑时如果为空,恢复占位文本
            if textView.text.isEmpty {
                textView.text = parent.placeholder
                textView.textColor = .lightGray
            }
        }
        
        func adjustTextViewHeight(uiView: UITextView) {
            // 计算文本所需高度
            let size = uiView.sizeThatFits(CGSize(width: uiView.frame.width, height: CGFloat.greatestFiniteMagnitude))
            let maxHeight = parent.font.lineHeight * CGFloat(parent.maxLines) + uiView.textContainerInset.top + uiView.textContainerInset.bottom
            
            // 控制滚动状态和高度
            if size.height > maxHeight {
                uiView.isScrollEnabled = true
                uiView.heightAnchor.constraint(equalToConstant: maxHeight).isActive = true
            } else {
                uiView.isScrollEnabled = false
                uiView.heightAnchor.constraint(greaterThanOrEqualToConstant: parent.minHeight).isActive = true
                uiView.heightAnchor.constraint(equalToConstant: size.height).isActive = true
            }
        }
    }
}

// 使用示例
struct ContentView: View {
    @State private var text = ""
    
    var body: some View {
        VStack {
            ResizableTextView(
                text: $text,
                placeholder: "Enter your text here",
                maxLines: 5,
                minHeight: 40,
                font: .systemFont(ofSize: 16)
            )
            .background(Color.gray.opacity(0.2))
            .cornerRadius(8)
        }
        .padding()
    }
}

关键说明:

  • 通过UITextViewDelegate监听文本变化,动态计算内容高度
  • 当内容高度超过最大行数对应的高度时,开启滚动并固定高度;否则关闭滚动,自动扩展高度
  • 内置占位文本逻辑,完全模拟原生输入框的占位体验
  • 可自定义字体、内边距、最小/最大高度等参数

内容的提问来源于stack exchange,提问作者Oladele

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 02:27:07