Here is my code:
import SwiftUI
struct TestTextView: View {
@State var title: String = ""
var body: some View {
GeometryReader { geo in
ScrollView {
VStack(spacing: 16) {
TextField(text: $title) {
Text("Title")
}
RichTestTextView(attributedString: NSAttributedString(string: "Here is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is \n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new lineinstead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screen ------end"), fixedWidth: geo.size.width-32)
.frame(width: geo.size.width-32)
.frame(minHeight: 100)
Color.pink
.frame(height: 100)
.cornerRadius(12)
}
.padding(.vertical, 24)
.padding(.horizontal, 16)
}
}
}
}
struct RichTestTextView: UIViewRepresentable {
var attributedString: NSAttributedString
var fixedWidth: CGFloat
let textView = UITextView(frame: .zero)
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: RichTestTextView
init(_ parent: RichTestTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
textView.scrollToBottom()
}
}
func makeUIView(context: Context) -> UITextView {
textView.isEditable = true
textView.isScrollEnabled = false
textView.backgroundColor = .cyan // just to make it easier to see
textView.delegate = context.coordinator
textView.sizeToFit()
textView.attributedText = self.attributedString
textView.translatesAutoresizingMaskIntoConstraints = false
textView.widthAnchor.constraint(equalToConstant: fixedWidth).isActive = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
extension UITextView {
func scrollToBottom() {
if !self.text.isEmpty {
let range = NSMakeRange(self.text.count - 1, 1)
self.scrollRangeToVisible(range)
}
}
}
#Preview {
TestTextView()
}
My Goal: Some views, including UITextView, and others as SwiftUI Views, should scroll and allow typing seamlessly within a SwiftUI ScrollView.
The key point is that I prefer not to use UIKit code to achieve my goal, as it is neither elegant nor time-efficient, even though I already have existing UIKit code that accomplishes the desired effect.
I am currently developing a diary app, and this presents a challenge for me when it comes to editing diary content.
I want to type and scroll normally in UITextView and ScrollView
Question update: 2023-11-03
I sovled this problem!!! The key method is updateContentSize(textView) in textViewDidChange(_ textView: UITextView)
struct RichTextView: UIViewRepresentable {
var text: NSAttributedString
var isEditable: Bool = true
var width: CGFloat
var onTextChanged: ((_ textView: UITextView) -> Void)?
private let textView = UITextView(frame: .zero)
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: RichTextView
init(_ parent: RichTextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
DispatchQueue.main.async {
self.parent.onTextChanged?(textView)
}
updateContentSize(textView)
}
func updateContentSize(_ textView: UITextView) {
let size = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: .infinity))
if textView.frame.size != size {
textView.frame.size = size
}
}
}
func makeUIView(context: Context) -> UITextView {
textView.isEditable = isEditable
textView.isScrollEnabled = false
textView.backgroundColor = .clear
textView.dataDetectorTypes = .all
textView.delegate = context.coordinator
textView.sizeToFit()
textView.attributedText = self.text
textView.translatesAutoresizingMaskIntoConstraints = false
textView.widthAnchor.constraint(equalToConstant: width).isActive = true
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
}
}
struct TestTextView: View {
@State var title: String = ""
var body: some View {
GeometryReader { geo in
ScrollView {
VStack(spacing: 16) {
TextField(text: $title) {
Text("Title")
}
RichTextView(text: NSAttributedString(string: "Here is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is \n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new line\n1new lineinstead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screenHere is a long string as you can see it is not wrapping but instead scrolling off the edge of the screen ------end"), width: geo.size.width-32)
.frame(width: geo.size.width-32)
.frame(minHeight: 100)
Color.pink
.frame(height: 100)
.cornerRadius(12)
}
.padding(.vertical, 24)
.padding(.horizontal, 16)
}
}
}
}
#Preview {
TestTextView()
}
KeyboardAdaptive.swift to solve The cusor did not change followling my typing line if TestTextView is in a NavigationStack or NavigationLink
import SwiftUI
import NotificationCenter
import Combine
extension Notification {
var keyboardHeight: CGFloat {
return (userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
struct KeyboardAdaptive: ViewModifier {
@State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
content
.padding(.bottom, keyboardHeight)
.onReceive(Publishers.keyboardHeight) { self.keyboardHeight = $0 }
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}