Skip to content

Instantly share code, notes, and snippets.

@ms-tii
Last active February 7, 2025 08:35
Show Gist options
  • Save ms-tii/14ee64fa65f418516cd0c5170a5dd042 to your computer and use it in GitHub Desktop.
Save ms-tii/14ee64fa65f418516cd0c5170a5dd042 to your computer and use it in GitHub Desktop.
Chat feature with messages that take up multiple lines text box like UITextView
struct ChatView: View {
@State var text: String = ""
@State var messages: [Message] = []
var body: some View {
ZStack {
Color.background
.edgesIgnoringSafeArea(.all)
VStack(spacing: 0) {
Spacer()
Text("Multiline Text View with chat view")
.font(.system(size: 34, weight: .bold, design: .default))
.multilineTextAlignment(.center)
.foregroundColor(.red)
Spacer()
ScrollViewReader { scrollView in
ScrollView(showsIndicators: false) {
VStack(spacing: 16) {
Spacer()
ForEach(messages, id: \.id) { message in
messageView(text: message.text)
.id(message.id) // Assign ID for scrolling
}
}
.padding(.vertical, 16)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.onChange(of: messages.count) { _ in
if let lastMessage = messages.last {
withAnimation {
scrollView.scrollTo(lastMessage.id, anchor: .bottom)
}
}
}
}
.background(Color.inputBorder)
Spacer().background(Color.inputBorder)
Divider()
HStack(alignment: .center) {
MultilineTextInputView(text: Binding<String?>(
get: { text },
set: { newValue in text = newValue ?? "" }
), placeholder: "Message")
.cornerRadius(10)
Button(action: sendAction) {
Text("Send")
}
}.padding()
}
}
}
func sendAction() {
guard !text.isEmpty else { return }
let newMessage = Message(text: text)
messages.append(newMessage) // Append message to bottom
text = ""
UIApplication.shared.windows.forEach { $0.endEditing(true) }
}
func messageView(text: String) -> some View {
return Text(text)
.foregroundColor(.white)
.padding(.all, 12)
.background(Color.messageBackground)
.cornerRadius(14)
.padding(.trailing, 12)
.padding(.leading, 32)
}
struct Message {
let id = UUID()
let text: String
}
}
struct MultilineTextInputView: View {
init(text: Binding<String?>, placeholder: String?) {
self._text = text
self.placeholder = placeholder
}
@Binding var text: String?
@State var focused: Bool = false
@State var contentHeight: CGFloat = 0
let placeholder: String?
let minHeight: CGFloat = 39
let maxHeight: CGFloat = 150
var countedHeight: CGFloat {
min(max(minHeight, contentHeight), maxHeight)
}
var body: some View {
ZStack(alignment: .topLeading) {
Color.white
ZStack(alignment: .topLeading) {
placeholderView
TextViewWrapper(text: $text, focused: $focused, contentHeight: $contentHeight)
}.padding(.horizontal, 4)
}.frame(height: countedHeight)
}
var placeholderView: some View {
ViewBuilder.buildIf(
showPlaceholder ?
placeholder.map {
Text($0)
.foregroundColor(.gray)
.font(.system(size: 16))
.padding(.vertical, 8)
.padding(.horizontal, 4)
} : nil
)
}
var showPlaceholder: Bool {
!focused && text?.isEmpty == true
}
}

Multiline TextView with ChatView

If you want to create a chat feature with messages that take up multiple lines, this project might be useful for you. SwiftUI doesn’t have a built-in text box like UITextView, so you need to create a custom one. Also, UITextView doesn’t support placeholders. This component solves both problems by allowing placeholders and automatically expanding up to a set height.

Screenshots

AppScreenshot

Table of Contents

Features

  • Custom TextViewWrapper to integrate UITextView into SwiftUI.
  • MultilineTextInputView that supports placeholders and dynamic height adjustment.
  • ChatView to display messages in a chat-like interface.

Project Structure

  • TextViewWrapper: A UIViewRepresentable that wraps UITextView for use in SwiftUI.
  • MultilineTextInputView: A SwiftUI view that uses TextViewWrapper to provide a text input field with placeholder support and dynamic height adjustment.
  • ChatView: A SwiftUI view that displays a list of messages and includes the MultilineTextInputView for message input.

Ensure that you have the necessary components (TextViewWrapper, MultilineTextInputView, and ChatView) included in your project.

Acknowledgments

This project is inspired by the need to implement multiline text input in SwiftUI and aims to provide a reusable solution for the community.

import UIKit
struct TextViewWrapper: UIViewRepresentable {
init(text: Binding<String?>, focused: Binding<Bool>, contentHeight: Binding<CGFloat>) {
self._text = text
self._focused = focused
self._contentHeight = contentHeight
}
@Binding var text: String?
@Binding var focused: Bool
@Binding var contentHeight: CGFloat
// MARK: - UIViewRepresentable
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = .systemFont(ofSize: 16)
textView.backgroundColor = .clear
textView.autocorrectionType = .no
return textView
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, focused: $focused, contentHeight: $contentHeight)
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
class Coordinator: NSObject, UITextViewDelegate {
init(text: Binding<String?>, focused: Binding<Bool>, contentHeight: Binding<CGFloat>) {
self._text = text
self._focused = focused
self._contentHeight = contentHeight
}
@Binding private var text: String?
@Binding private var focused: Bool
@Binding private var contentHeight: CGFloat
// MARK: - UITextViewDelegate
func textViewDidChange(_ textView: UITextView) {
text = textView.text
contentHeight = textView.contentSize.height
}
func textViewDidBeginEditing(_ textView: UITextView) {
focused = true
}
func textViewDidEndEditing(_ textView: UITextView) {
focused = false
contentHeight = text == nil ? 0 : textView.contentSize.height
}
}
}

Comments are disabled for this gist.