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

Swift自定义文本输入视图实现求助:仿消息应用交互

嘿,这个需求可是消息类APP的标准配置啦,我帮你拆解成几个关键环节,一步步实现就没问题:

一、先搭好自定义输入视图的基础结构

首先创建一个CustomInputView的自定义UIView子类,里面要包含三个核心元素:

  • UITextView:用它代替UITextField,因为要支持多行文本和高度自适应
  • 上传按钮(UIButton):用来触发文本上传操作
  • 基础布局约束:确保元素排列合理,初始状态贴紧屏幕底部

布局的时候用Auto Layout,这里给你个简化版的代码示例:

class CustomInputView: UIView, UITextViewDelegate {
    private let textView = UITextView()
    private let uploadButton = UIButton(type: .system)
    // 用来限制TextView最大高度的约束
    private var textViewHeightConstraint: NSLayoutConstraint?

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
        setupConstraints()
        setupNotifications()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupViews()
        setupConstraints()
        setupNotifications()
    }

    private func setupViews() {
        backgroundColor = .systemBackground
        // 配置TextView
        textView.font = .systemFont(ofSize: 16)
        textView.isScrollEnabled = false // 禁用滚动,让它自动撑高
        textView.delegate = self
        textView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) // 增加内边距,避免文字贴边
        addSubview(textView)

        // 配置上传按钮
        uploadButton.setTitle("上传", for: .normal)
        uploadButton.addTarget(self, action: #selector(uploadTapped), for: .touchUpInside)
        addSubview(uploadButton)

        // 给TextView加占位符(UITextView默认没有这个功能)
        let placeholderLabel = UILabel()
        placeholderLabel.text = "输入消息..."
        placeholderLabel.textColor = .lightGray
        placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
        textView.addSubview(placeholderLabel)
        NSLayoutConstraint.activate([
            placeholderLabel.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 8),
            placeholderLabel.topAnchor.constraint(equalTo: textView.topAnchor, constant: 8)
        ])
        // 监听文字变化,控制占位符显示/隐藏
        textView.addObserver(self, forKeyPath: "text", options: .new, context: nil)
    }

    private func setupConstraints() {
        textView.translatesAutoresizingMaskIntoConstraints = false
        uploadButton.translatesAutoresizingMaskIntoConstraints = false

        let constraints = [
            // 上传按钮约束:右对齐、垂直居中、固定宽高
            uploadButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
            uploadButton.centerYAnchor.constraint(equalTo: centerYAnchor),
            uploadButton.widthAnchor.constraint(equalToConstant: 44),
            uploadButton.heightAnchor.constraint(equalToConstant: 32),

            // TextView约束:左对齐、和按钮间距、上下贴紧输入视图
            textView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
            textView.trailingAnchor.constraint(equalTo: uploadButton.leadingAnchor, constant: -8),
            textView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
            textView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),

            // 输入视图最小高度:确保初始状态有足够空间
            heightAnchor.constraint(greaterThanOrEqualToConstant: 44)
        ]
        NSLayoutConstraint.activate(constraints)

        // 限制TextView最大高度(比如最多显示3行)
        textViewHeightConstraint = textView.heightAnchor.constraint(lessThanOrEqualToConstant: 132)
        textViewHeightConstraint?.isActive = true
    }

    @objc private func uploadTapped() {
        // 这里写上传逻辑:获取文本、发送到服务器等
        guard let text = textView.text, !text.isEmpty else { return }
        print("上传文本:\(text)")
        textView.text = "" // 上传后清空文本
    }

    // MARK: - KVO处理占位符
    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "text" {
            let placeholderLabel = textView.subviews.first(where: { $0 is UILabel }) as? UILabel
            placeholderLabel?.isHidden = !textView.text.isEmpty
        }
    }
}
二、监听键盘,让视图跟着上下移动

这部分的核心是监听键盘的弹出/收起通知,然后动态调整输入视图的底部约束。上面的setupNotifications方法可以这么写:

private func setupNotifications() {
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(keyboardWillShow),
        name: UIResponder.keyboardWillShowNotification,
        object: nil
    )
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(keyboardWillHide),
        name: UIResponder.keyboardWillHideNotification,
        object: nil
    )
}

@objc private func keyboardWillShow(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
          let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
          let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
          let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { return }

    // 计算键盘高度,要减去安全区域底部内边距(适配iPhone底部的Home Indicator)
    let keyboardHeight = keyboardFrame.height - superview?.safeAreaInsets.bottom ?? 0

    // 获取并更新输入视图的底部约束(建议在ViewController里直接持有这个约束,更方便)
    if let parentVC = superview?.next as? UIViewController,
       let bottomConstraint = parentVC.view.constraints.first(where: {
           $0.firstItem as? UIView == self && $0.secondAnchor == parentVC.view.safeAreaLayoutGuide.bottomAnchor
       }) {
        bottomConstraint.constant = -keyboardHeight
        // 用和键盘一致的动画参数更新布局,过渡更自然
        UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: curve)) {
            parentVC.view.layoutIfNeeded()
        }
    }
}

@objc private func keyboardWillHide(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
          let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval,
          let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt else { return }

    if let parentVC = superview?.next as? UIViewController,
       let bottomConstraint = parentVC.view.constraints.first(where: {
           $0.firstItem as? UIView == self && $0.secondAnchor == parentVC.view.safeAreaLayoutGuide.bottomAnchor
       }) {
        bottomConstraint.constant = 0
        UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions(rawValue: curve)) {
            parentVC.view.layoutIfNeeded()
        }
    }
}

deinit {
    NotificationCenter.default.removeObserver(self)
    textView.removeObserver(self, forKeyPath: "text")
}
三、实现TextView的高度自适应

这部分靠TextView的代理方法textViewDidChange来实现,当文字变化时,自动计算需要的高度:

// MARK: - UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
    // 计算文本内容需要的高度
    let maxSize = CGSize(width: textView.frame.width, height: .infinity)
    let estimatedSize = textView.sizeThatFits(maxSize)

    // 因为我们设置了isScrollEnabled=false,TextView会自动根据内容撑高
    // 触发布局更新,让输入视图跟着调整高度
    layoutIfNeeded()
}
四、ViewController里的整合

在ViewController中,把自定义输入视图添加到页面上,并设置初始约束:

class ChatViewController: UIViewController {
    private let inputView = CustomInputView()
    private var inputViewBottomConstraint: NSLayoutConstraint?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupInputView()
    }

    private func setupInputView() {
        inputView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(inputView)

        inputViewBottomConstraint = inputView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
        NSLayoutConstraint.activate([
            inputView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            inputView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            inputViewBottomConstraint!
        ])

        // 点击空白区域收起键盘
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard))
        view.addGestureRecognizer(tapGesture)
    }

    @objc private func hideKeyboard() {
        view.endEditing(true)
    }
}

这样一来,你的自定义输入视图就完全符合需求了:初始在屏幕底部,点击TextView后键盘弹出,视图自动移到键盘上方;输入文字时TextView自动调整高度(最多3行),超过后会滚动;点击上传按钮可以处理文本上传。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 05:20:38