feat: input box ui

This commit is contained in:
Lakr
2025-06-18 01:50:47 +08:00
parent 1aca314299
commit 8065fa4bf4
15 changed files with 381 additions and 21 deletions
@@ -26,6 +26,8 @@ let package = Package(
"Then",
.product(name: "Apollo", package: "apollo-ios"),
.product(name: "OrderedCollections", package: "swift-collections"),
], resources: [
.process("Interface/View/InputBox/InputBox.xcassets")
]),
]
)
@@ -9,6 +9,10 @@ class MainViewController: UIViewController {
$0.delegate = self
}
private lazy var inputBox = InputBox().then {
$0.delegate = self
}
// MARK: - Lifecycle
override func viewDidLoad() {
@@ -32,11 +36,17 @@ class MainViewController: UIViewController {
view.backgroundColor = .systemBackground
view.addSubview(headerView)
view.addSubview(inputBox)
headerView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.leading.trailing.equalToSuperview()
}
inputBox.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.bottom.equalTo(view.keyboardLayoutGuide.snp.top)
}
}
}
@@ -57,3 +67,32 @@ extension MainViewController: MainHeaderViewDelegate {
print("Menu tapped")
}
}
// MARK: - InputBoxDelegate
extension MainViewController: InputBoxDelegate {
func inputBoxDidTapAddAttachment() {
}
func inputBoxDidTapTool() {
}
func inputBoxDidTapNetwork() {
}
func inputBoxDidTapDeepThinking() {
}
func inputBoxDidTapSend() {
}
func inputBoxTextDidChange(_ text: String) {
}
}
@@ -0,0 +1,239 @@
import SnapKit
import Then
import UIKit
protocol InputBoxDelegate: AnyObject {
func inputBoxDidTapAddAttachment()
func inputBoxDidTapTool()
func inputBoxDidTapNetwork()
func inputBoxDidTapDeepThinking()
func inputBoxDidTapSend()
func inputBoxTextDidChange(_ text: String)
}
class InputBox: UIView {
weak var delegate: InputBoxDelegate?
private lazy var containerView = UIView().then {
$0.backgroundColor = .systemBackground
$0.layer.cornerRadius = 12
$0.layer.borderWidth = 0.5
$0.layer.borderColor = UIColor.systemGray4.cgColor
$0.layer.shadowColor = UIColor.black.cgColor
$0.layer.shadowOffset = CGSize(width: 0, height: 2)
$0.layer.shadowRadius = 6
$0.layer.shadowOpacity = 0.04
$0.clipsToBounds = false
}
private lazy var textView = UITextView().then {
$0.backgroundColor = .clear
$0.font = .systemFont(ofSize: 16)
$0.textColor = .label
$0.isScrollEnabled = false
$0.textContainer.lineFragmentPadding = 0
$0.textContainerInset = .zero
$0.delegate = self
$0.text = "This is AFFiNE AI"
}
private lazy var placeholderLabel = UILabel().then {
$0.text = "Write your message..."
$0.font = .systemFont(ofSize: 16)
$0.textColor = .systemGray3
$0.isHidden = true
}
private lazy var addButton = UIButton(type: .system).then {
$0.backgroundColor = .systemBackground
$0.layer.cornerRadius = 6
$0.layer.borderWidth = 0.5
$0.layer.borderColor = UIColor.systemGray4.cgColor
$0.setImage(UIImage(named: "inputbox.add.attachment", in: .module, with: nil), for: .normal)
$0.tintColor = .secondaryLabel
$0.imageView?.contentMode = .scaleAspectFit
$0.addTarget(self, action: #selector(addButtonTapped), for: .touchUpInside)
}
private lazy var toolButton = UIButton(type: .system).then {
$0.setImage(UIImage(named: "inputbox.tool", in: .module, with: nil), for: .normal)
$0.tintColor = .secondaryLabel
$0.imageView?.contentMode = .scaleAspectFit
$0.addTarget(self, action: #selector(toolButtonTapped), for: .touchUpInside)
}
private lazy var webButton = UIButton(type: .system).then {
$0.setImage(UIImage(named: "inputbox.network", in: .module, with: nil), for: .normal)
$0.tintColor = .secondaryLabel
$0.imageView?.contentMode = .scaleAspectFit
$0.addTarget(self, action: #selector(webButtonTapped), for: .touchUpInside)
}
private lazy var reactButton = UIButton(type: .system).then {
$0.setImage(UIImage(named: "inputbox.deep.thinking", in: .module, with: nil), for: .normal)
$0.tintColor = .secondaryLabel
$0.imageView?.contentMode = .scaleAspectFit
$0.addTarget(self, action: #selector(reactButtonTapped), for: .touchUpInside)
}
private lazy var sendButton = UIButton(type: .system).then {
$0.backgroundColor = UIColor.systemBlue
$0.layer.cornerRadius = 19
$0.setImage(UIImage(named: "inputbox.send", in: .module, with: nil), for: .normal)
$0.tintColor = .white
$0.imageView?.contentMode = .scaleAspectFit
$0.addTarget(self, action: #selector(sendButtonTapped), for: .touchUpInside)
}
private lazy var leftButtonsStackView = UIStackView().then {
$0.axis = .horizontal
$0.spacing = 16
$0.alignment = .center
$0.addArrangedSubview(addButton)
}
private lazy var rightButtonsStackView = UIStackView().then {
$0.axis = .horizontal
$0.spacing = 16
$0.alignment = .center
$0.addArrangedSubview(toolButton)
$0.addArrangedSubview(webButton)
$0.addArrangedSubview(reactButton)
$0.addArrangedSubview(sendButton)
}
private lazy var functionsStackView = UIStackView().then {
$0.axis = .horizontal
$0.spacing = 12
$0.alignment = .center
$0.addArrangedSubview(leftButtonsStackView)
$0.addArrangedSubview(UIView()) // spacer
$0.addArrangedSubview(rightButtonsStackView)
}
private lazy var mainStackView = UIStackView().then {
$0.axis = .vertical
$0.spacing = 16
$0.alignment = .fill
$0.addArrangedSubview(textView)
$0.addArrangedSubview(functionsStackView)
}
private var textViewHeightConstraint: Constraint?
private let minTextViewHeight: CGFloat = 22
private let maxTextViewHeight: CGFloat = 100
var text: String {
get { textView.text ?? "" }
set {
textView.text = newValue
updatePlaceholderVisibility()
updateTextViewHeight()
}
}
init() {
super.init(frame: .zero)
setupViews()
setupConstraints()
updatePlaceholderVisibility()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
backgroundColor = .clear
addSubview(containerView)
containerView.addSubview(mainStackView)
containerView.addSubview(placeholderLabel)
}
private func setupConstraints() {
containerView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
mainStackView.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(16)
}
addButton.snp.makeConstraints { make in
make.size.equalTo(38)
}
for button in [toolButton, webButton, reactButton] {
button.snp.makeConstraints { make in
make.size.equalTo(24)
}
}
sendButton.snp.makeConstraints { make in
make.size.equalTo(38)
}
textView.snp.makeConstraints { make in
textViewHeightConstraint = make.height.equalTo(minTextViewHeight).constraint
}
placeholderLabel.snp.makeConstraints { make in
make.left.right.equalTo(textView)
make.top.equalTo(textView)
}
}
private func updateTextViewHeight() {
let size = textView.sizeThatFits(CGSize(width: textView.frame.width, height: CGFloat.greatestFiniteMagnitude))
let newHeight = max(minTextViewHeight, min(maxTextViewHeight, size.height))
textViewHeightConstraint?.update(offset: newHeight)
textView.isScrollEnabled = size.height > maxTextViewHeight
UIView.animate(
withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.8,
initialSpringVelocity: 1.0,
options: [.curveEaseInOut]
) {
self.layoutIfNeeded()
self.superview?.layoutIfNeeded()
}
}
private func updatePlaceholderVisibility() {
placeholderLabel.isHidden = !textView.text.isEmpty
}
@objc private func addButtonTapped() {
delegate?.inputBoxDidTapAddAttachment()
}
@objc private func toolButtonTapped() {
delegate?.inputBoxDidTapTool()
}
@objc private func webButtonTapped() {
delegate?.inputBoxDidTapNetwork()
}
@objc private func reactButtonTapped() {
delegate?.inputBoxDidTapDeepThinking()
}
@objc private func sendButtonTapped() {
delegate?.inputBoxDidTapSend()
}
}
// MARK: - UITextViewDelegate
extension InputBox: UITextViewDelegate {
func textViewDidChange(_ textView: UITextView) {
updatePlaceholderVisibility()
updateTextViewHeight()
delegate?.inputBoxTextDidChange(textView.text)
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "inputbox.add.attachment.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "inputbox.deep.thinking.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "inputbox.network.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "inputbox.send.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "original"
}
}
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "inputbox.tool.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}
@@ -9,9 +9,8 @@ protocol MainHeaderViewDelegate: AnyObject {
}
class MainHeaderView: UIView {
weak var delegate: MainHeaderViewDelegate?
private lazy var closeButton = UIButton(type: .system).then {
$0.imageView?.contentMode = .scaleAspectFit
$0.setImage(UIImage(systemName: "xmark"), for: .normal)
@@ -19,21 +18,21 @@ class MainHeaderView: UIView {
$0.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
$0.setContentHuggingPriority(.required, for: .horizontal)
}
private lazy var titleLabel = UILabel().then {
$0.text = "AFFiNE"
$0.font = .systemFont(ofSize: 16, weight: .medium)
$0.textColor = .black
$0.textAlignment = .center
}
private lazy var dropdownButton = UIButton(type: .system).then {
$0.imageView?.contentMode = .scaleAspectFit
$0.setImage(UIImage(systemName: "chevron.down"), for: .normal)
$0.tintColor = .systemGray
$0.addTarget(self, action: #selector(dropdownButtonTapped), for: .touchUpInside)
}
private lazy var centerStackView = UIStackView().then {
$0.axis = .horizontal
$0.spacing = 8
@@ -41,7 +40,7 @@ class MainHeaderView: UIView {
$0.addArrangedSubview(titleLabel)
$0.addArrangedSubview(dropdownButton)
}
private lazy var menuButton = UIButton(type: .system).then {
$0.imageView?.contentMode = .scaleAspectFit
$0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
@@ -49,11 +48,11 @@ class MainHeaderView: UIView {
$0.addTarget(self, action: #selector(menuButtonTapped), for: .touchUpInside)
$0.setContentHuggingPriority(.required, for: .horizontal)
}
private lazy var leftSpacerView = UIView()
private lazy var rightSpacerView = UIView()
private lazy var mainStackView = UIStackView().then {
$0.axis = .horizontal
$0.alignment = .center
@@ -65,53 +64,53 @@ class MainHeaderView: UIView {
$0.addArrangedSubview(rightSpacerView)
$0.addArrangedSubview(menuButton)
}
init() {
super.init(frame: .zero)
backgroundColor = .white
addSubview(mainStackView)
mainStackView.snp.makeConstraints { make in
make.left.right.equalToSuperview().inset(16)
make.centerY.equalToSuperview()
}
closeButton.snp.makeConstraints { make in
make.size.equalTo(titleLabel.font.pointSize)
}
menuButton.snp.makeConstraints { make in
make.size.equalTo(titleLabel.font.pointSize)
}
dropdownButton.snp.makeConstraints { make in
make.size.equalTo(titleLabel.font.pointSize)
}
// ensure center stack to be center
leftSpacerView.snp.makeConstraints { make in
make.width.equalTo(rightSpacerView)
}
snp.makeConstraints { make in
make.height.equalTo(52)
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
fatalError()
}
@objc private func closeButtonTapped() {
delegate?.mainHeaderViewDidTapClose()
}
@objc private func dropdownButtonTapped() {
delegate?.mainHeaderViewDidTapDropdown()
}
@objc private func menuButtonTapped() {
delegate?.mainHeaderViewDidTapMenu()
}