mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-05 03:25:10 +08:00
feat: input box ui
This commit is contained in:
@@ -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")
|
||||
]),
|
||||
]
|
||||
)
|
||||
|
||||
+39
@@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+239
@@ -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)
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "inputbox.add.attachment.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "inputbox.deep.thinking.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "inputbox.network.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "inputbox.send.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "inputbox.tool.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
+20
-21
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user