ios: send from software keyboard

This commit is contained in:
2026-05-02 16:39:39 -07:00
parent 05989e9fae
commit d03b4c4dd7

View File

@@ -1,5 +1,6 @@
import Observation
import SwiftUI
import UIKit
struct SybilWorkspaceView: View {
@Bindable var viewModel: SybilViewModel
@@ -216,19 +217,23 @@ struct SybilWorkspaceView: View {
private var composerBar: some View {
HStack(alignment: .bottom, spacing: 10) {
TextField(
viewModel.isSearchMode ? "Search the web" : "Message Sybil",
text: $viewModel.composer,
axis: .vertical
)
.focused($composerFocused)
.textInputAutocapitalization(.sentences)
.autocorrectionDisabled(false)
.lineLimit(1 ... 6)
.submitLabel(.send)
.onSubmit {
Task {
await viewModel.sendComposer()
ZStack(alignment: .topLeading) {
ComposerTextView(
text: $viewModel.composer,
isFocused: $composerFocused,
isDisabled: viewModel.isSending
) {
Task {
await viewModel.sendComposer()
}
}
.frame(minHeight: 24, maxHeight: 132)
if viewModel.composer.isEmpty {
Text(viewModel.isSearchMode ? "Search the web" : "Message Sybil")
.font(.body)
.foregroundStyle(SybilTheme.textMuted.opacity(0.72))
.allowsHitTesting(false)
}
}
.padding(.horizontal, 12)
@@ -278,3 +283,91 @@ struct SybilWorkspaceView: View {
)
}
}
private struct ComposerTextView: UIViewRepresentable {
@Binding var text: String
var isFocused: FocusState<Bool>.Binding
var isDisabled: Bool
var onSubmit: () -> Void
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.backgroundColor = .clear
textView.font = .preferredFont(forTextStyle: .body)
textView.textColor = UIColor(SybilTheme.text)
textView.tintColor = UIColor(SybilTheme.primary)
textView.textContainerInset = .zero
textView.textContainer.lineFragmentPadding = 0
textView.isScrollEnabled = true
textView.alwaysBounceVertical = false
textView.keyboardDismissMode = .interactive
textView.returnKeyType = .send
textView.enablesReturnKeyAutomatically = true
textView.autocapitalizationType = .sentences
textView.autocorrectionType = .default
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
context.coordinator.parent = self
if textView.text != text {
textView.text = text
}
textView.isEditable = !isDisabled
textView.alpha = isDisabled ? 0.74 : 1
if isFocused.wrappedValue, !textView.isFirstResponder {
textView.becomeFirstResponder()
} else if !isFocused.wrappedValue, textView.isFirstResponder {
textView.resignFirstResponder()
}
}
func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? {
let width = proposal.width ?? uiView.bounds.width
let fittingSize = uiView.sizeThatFits(
CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
)
return CGSize(width: width, height: min(max(fittingSize.height, 24), 132))
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
final class Coordinator: NSObject, UITextViewDelegate {
var parent: ComposerTextView
init(parent: ComposerTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
parent.isFocused.wrappedValue = true
}
func textViewDidEndEditing(_ textView: UITextView) {
parent.isFocused.wrappedValue = false
}
func textView(
_ textView: UITextView,
shouldChangeTextIn range: NSRange,
replacementText replacement: String
) -> Bool {
if replacement == "\n" {
parent.onSubmit()
return false
}
return true
}
}
}