ios: fix height of compose view field when having a draft (#5655)

* ios: fix height of compose view field when having a draft

* changes

* simplified layout

* changes

* button size 29 -> 31

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Stanislav Dmitrenko
2025-02-22 12:15:33 +00:00
committed by GitHub
co-authored by Evgeny Poberezkin
parent bc9885675d
commit bf37c0762e
3 changed files with 100 additions and 102 deletions
@@ -392,7 +392,7 @@ struct ComposeView: View {
}
.disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
.frame(width: 25, height: 25)
.padding(.bottom, 12)
.padding(.bottom, 16)
.padding(.leading, 12)
.tint(theme.colors.primary)
if case let .group(g) = chat.chatInfo,
@@ -22,13 +22,7 @@ struct NativeTextEditor: UIViewRepresentable {
static let minHeight: CGFloat = 39
private let defaultHeight: CGFloat = {
let field = CustomUITextField(parent: nil, height: Binding.constant(0))
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
return min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, NativeTextEditor.minHeight), 360).rounded(.down)
}()
func makeUIView(context: Context) -> UITextView {
func makeUIView(context: Context) -> CustomUITextField {
let field = CustomUITextField(parent: self, height: _height)
field.backgroundColor = .clear
field.text = text
@@ -38,10 +32,9 @@ struct NativeTextEditor: UIViewRepresentable {
if !disableEditing {
text = newText
field.textAlignment = alignment(text)
updateFont(field)
field.updateFont()
// Speed up the process of updating layout, reduce jumping content on screen
updateHeight(field)
self.height = field.frame.size.height
field.updateHeight()
} else {
field.text = text
}
@@ -53,63 +46,38 @@ struct NativeTextEditor: UIViewRepresentable {
field.delegate = field
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
field.setPlaceholderView()
updateFont(field)
updateHeight(field)
field.updateFont()
field.updateHeight(updateBindingNow: false)
return field
}
func updateUIView(_ field: UITextView, context: Context) {
func updateUIView(_ field: CustomUITextField, context: Context) {
if field.markedTextRange == nil && field.text != text {
field.text = text
field.textAlignment = alignment(text)
updateFont(field)
updateHeight(field)
field.updateFont()
field.updateHeight(updateBindingNow: false)
}
let castedField = field as! CustomUITextField
if castedField.placeholder != placeholder {
castedField.placeholder = placeholder
if field.placeholder != placeholder {
field.placeholder = placeholder
}
if field.selectedRange != selectedRange {
field.selectedRange = selectedRange
}
}
private func updateHeight(_ field: UITextView) {
let maxHeight = min(360, field.font!.lineHeight * 12)
// When having emoji in text view and then removing it, sizeThatFits shows previous size (too big for empty text view), so using work around with default size
let newHeight = field.text == ""
? defaultHeight
: min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, NativeTextEditor.minHeight), maxHeight).rounded(.down)
if field.frame.size.height != newHeight {
field.frame.size = CGSizeMake(field.frame.size.width, newHeight)
(field as! CustomUITextField).invalidateIntrinsicContentHeight(newHeight)
}
}
private func updateFont(_ field: UITextView) {
let newFont = isShortEmoji(field.text)
? (field.text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont)
: UIFont.preferredFont(forTextStyle: .body)
if field.font != newFont {
field.font = newFont
}
}
}
private func alignment(_ text: String) -> NSTextAlignment {
isRightToLeft(text) ? .right : .left
}
private class CustomUITextField: UITextView, UITextViewDelegate {
class CustomUITextField: UITextView, UITextViewDelegate {
var parent: NativeTextEditor?
var height: Binding<CGFloat>
var newHeight: CGFloat = 0
var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in }
var onFocusChanged: (Bool) -> Void = { focused in }
private let placeholderLabel: UILabel = UILabel()
init(parent: NativeTextEditor?, height: Binding<CGFloat>) {
@@ -135,14 +103,44 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
if height.wrappedValue != newHeight ||
// when both heights equal to minHeight, we must update $height, even if it's the same, because only this way
// the swift ui wrapper will redisplay this view with updated height
newHeight == NativeTextEditor.minHeight {
DispatchQueue.main.async { self.height.wrappedValue = self.newHeight }
func updateHeight(updateBindingNow: Bool = true) {
let maxHeight = min(360, font!.lineHeight * 12)
let newHeight = min(max(sizeThatFits(CGSizeMake(frame.size.width, CGFloat.greatestFiniteMagnitude)).height, NativeTextEditor.minHeight), maxHeight).rounded(.down)
if self.newHeight != newHeight {
frame.size = CGSizeMake(frame.size.width, newHeight)
invalidateIntrinsicContentHeight(newHeight)
if updateBindingNow {
self.height.wrappedValue = newHeight
} else {
DispatchQueue.main.async {
self.height.wrappedValue = newHeight
}
}
}
return CGSizeMake(0, newHeight)
}
func updateFont() {
let newFont = isShortEmoji(text)
? (text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont)
: UIFont.preferredFont(forTextStyle: .body)
if font != newFont {
font = newFont
// force apply new font because it has problem with doing it when the field had two emojis
if text.count == 0 {
text = " "
text = ""
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
updateHeight()
}
override var intrinsicContentSize: CGSize {
CGSizeMake(0, newHeight)
}
func setOnTextChangedListener(onTextChanged: @escaping (String, [UploadContent]) -> Void) {
@@ -44,53 +44,53 @@ struct SendMessageView: View {
@UserDefault(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
var body: some View {
ZStack {
let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
HStack(alignment: .bottom) {
ZStack(alignment: .leading) {
if case .voicePreview = composeState.preview {
Text("Voice message…")
.font(teFont.italic())
.multilineTextAlignment(.leading)
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
} else {
NativeTextEditor(
text: $composeState.message,
disableEditing: $composeState.inProgress,
height: $teHeight,
focused: $keyboardVisible,
placeholder: Binding(get: { composeState.placeholder }, set: { _ in }),
selectedRange: $selectedRange,
onImagesAdded: onMediaAdded
)
.allowsTightening(false)
.fixedSize(horizontal: false, vertical: true)
}
}
if progressByTimeout {
ProgressView()
.scaleEffect(1.4)
.frame(width: 31, height: 31, alignment: .center)
.padding([.bottom, .trailing], 3)
} else {
VStack(alignment: .trailing) {
if teHeight > 100 && !composeState.inProgress {
deleteTextButton()
Spacer()
}
composeActionButtons()
}
.frame(height: teHeight, alignment: .bottom)
}
let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
ZStack(alignment: .leading) {
if case .voicePreview = composeState.preview {
Text("Voice message…")
.font(teFont.italic())
.multilineTextAlignment(.leading)
.foregroundColor(theme.colors.secondary)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.padding(.trailing, 32)
.frame(maxWidth: .infinity)
} else {
NativeTextEditor(
text: $composeState.message,
disableEditing: $composeState.inProgress,
height: $teHeight,
focused: $keyboardVisible,
placeholder: Binding(get: { composeState.placeholder }, set: { _ in }),
selectedRange: $selectedRange,
onImagesAdded: onMediaAdded
)
.padding(.trailing, 32)
.allowsTightening(false)
.fixedSize(horizontal: false, vertical: true)
}
.padding(.vertical, 1)
.background(theme.colors.background)
.clipShape(composeShape)
.overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7))
}
.overlay(alignment: .topTrailing, content: {
if !progressByTimeout && teHeight > 100 && !composeState.inProgress {
deleteTextButton()
}
})
.overlay(alignment: .bottomTrailing, content: {
if progressByTimeout {
ProgressView()
.scaleEffect(1.4)
.frame(width: 31, height: 31, alignment: .center)
.padding([.bottom, .trailing], 4)
} else {
composeActionButtons()
// required for intercepting clicks
.background(.white.opacity(0.000001))
}
})
.padding(.vertical, 1)
.background(theme.colors.background)
.clipShape(composeShape)
.overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7))
.onChange(of: composeState.message, perform: { text in updateFont(text) })
.onChange(of: composeState.inProgress) { inProgress in
if inProgress {
@@ -169,7 +169,7 @@ struct SendMessageView: View {
!composeState.sendEnabled ||
composeState.inProgress
)
.frame(width: 29, height: 29)
.frame(width: 31, height: 31)
.padding([.bottom, .trailing], 4)
}
@@ -192,7 +192,7 @@ struct SendMessageView: View {
composeState.endLiveDisabled ||
disableSendButton
)
.frame(width: 29, height: 29)
.frame(width: 31, height: 31)
.contextMenu{
sendButtonContextMenuItems()
}
@@ -269,7 +269,7 @@ struct SendMessageView: View {
.foregroundColor(theme.colors.primary)
}
.disabled(disabled)
.frame(width: 29, height: 29)
.frame(width: 31, height: 31)
.padding([.bottom, .trailing], 4)
._onButtonGesture { down in
if down {
@@ -325,7 +325,7 @@ struct SendMessageView: View {
.foregroundColor(theme.colors.secondary)
}
.disabled(composeState.inProgress)
.frame(width: 29, height: 29)
.frame(width: 31, height: 31)
.padding([.bottom, .trailing], 4)
}
@@ -410,7 +410,7 @@ struct SendMessageView: View {
.foregroundColor(theme.colors.primary)
}
.disabled(composeState.inProgress)
.frame(width: 29, height: 29)
.frame(width: 31, height: 31)
.padding([.bottom, .trailing], 4)
}