My target at the moment is:
ScrollView
must to have Height of TextField
's content up to 5 lines of text. If TextField
's content size is equal 5 lines or more than 5 lines - ScrollView must to have height 5 lines an user must to have ability to scroll text up and downSo I'm trying to do something like the following:
When no text in text field OR there is 1 line of text:
When >= 5 lines of text in text field:
But at the moment it's have a static height
SwiftUI ContentView:
import Combine
import SwiftUI
@available(macOS 12.0, *)
struct ContentView: View {
@State var text: String = textSample
var body: some View {
ZStack {
VStack{
Spacer()
Text("Hello")
Spacer()
}
VStack {
Spacer()
DescriptionTextField(text: $text)
.padding(EdgeInsets(top: 3, leading: 3, bottom: 6, trailing: 3) )
.background(Color.green)
}
}
.frame(minWidth: 450, minHeight: 300)
}
}
let textSample =
"""
hello 1
hello 2
hello 3
hello 4
hello 5
hello 6
hello 7
hello 8
"""
import Foundation
import SwiftUI
import AppKit
struct DescriptionTextField: NSViewRepresentable {
@Binding var text: String
var isEditable: Bool = true
var font: NSFont? = .systemFont(ofSize: 17, weight: .regular)
var onEditingChanged: () -> Void = { }
var onCommit : () -> Void = { }
var onTextChange : (String) -> Void = { _ in }
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeNSView(context: Context) -> CustomTextView {
let textView = CustomTextView(text: text, isEditable: isEditable, font: font)
textView.delegate = context.coordinator
return textView
}
func updateNSView(_ view: CustomTextView, context: Context) {
view.text = text
view.selectedRanges = context.coordinator.selectedRanges
}
}
extension DescriptionTextField {
class Coordinator: NSObject, NSTextViewDelegate {
var parent: DescriptionTextField
var selectedRanges: [NSValue] = []
init(_ parent: DescriptionTextField) {
self.parent = parent
}
func textDidBeginEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.text = textView.string
self.parent.onEditingChanged()
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.text = textView.string
self.selectedRanges = textView.selectedRanges
if let txtView = textView.superview?.superview?.superview as? CustomTextView {
txtView.refreshScrollViewConstrains()
}
}
func textDidEndEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
self.parent.text = textView.string
self.parent.onCommit()
}
}
}
// MARK: - CustomTextView
final class CustomTextView: NSView {
private var isEditable: Bool
private var font: NSFont?
weak var delegate: NSTextViewDelegate?
var text: String { didSet { textView.string = text } }
var selectedRanges: [NSValue] = [] {
didSet {
guard selectedRanges.count > 0 else { return }
textView.selectedRanges = selectedRanges
}
}
private lazy var scrollView: NSScrollView = {
let scrollView = NSScrollView()
scrollView.drawsBackground = false
scrollView.borderType = .noBorder
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalRuler = false
scrollView.autoresizingMask = [.width, .height]
// scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
private lazy var textView: NSTextView = {
let contentSize = scrollView.contentSize
let textStorage = NSTextStorage()
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer()
textContainer.widthTracksTextView = true
textContainer.containerSize = NSSize( width: contentSize.width, height: CGFloat.greatestFiniteMagnitude )
layoutManager.addTextContainer(textContainer)
let textView = NSTextView(frame: .zero, textContainer: textContainer)
textView.autoresizingMask = [.width, .height]
textView.backgroundColor = NSColor.clear
textView.delegate = self.delegate
textView.drawsBackground = true
textView.font = self.font
textView.isHorizontallyResizable = false
textView.isVerticallyResizable = true
textView.minSize = NSSize( width: 150, height: min(contentSize.height, 13) )
textView.maxSize = NSSize( width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.textColor = NSColor.labelColor
textView.allowsUndo = true
textView.isRichText = true
return textView
} ()
// MARK: - Init
init(text: String, isEditable: Bool, font: NSFont?) {
self.font = font
self.isEditable = isEditable
self.text = text
super.init(frame: .zero)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
// MARK: - Life cycle
override func viewWillDraw() {
super.viewWillDraw()
setupScrollViewConstraints()
scrollView.documentView = textView
}
private func setupScrollViewConstraints() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
addSubview(scrollView)
refreshScrollViewConstrains()
}
func refreshScrollViewConstrains() {
print("Constrains updated!")
let finalHeight = min(textView.contentSize.height, font!.pointSize * 6)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(lessThanOrEqualTo: topAnchor),
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
scrollView.heightAnchor.constraint(lessThanOrEqualToConstant: finalHeight)
])
scrollView.needsUpdateConstraints = true
}
}
extension NSTextView {
var contentSize: CGSize {
get {
guard let layoutManager = layoutManager, let textContainer = textContainer else {
print("textView no layoutManager or textContainer")
return .zero
}
layoutManager.ensureLayout(for: textContainer)
return layoutManager.usedRect(for: textContainer).size
}
}
}
So reason of my issues was:
No call of refreshScrollViewConstrains()
in textDidChange()
(thanks to
@VonC )
I didn't removed an old constraints before assign/activate a new one set of constraints :)
Code of the solution is the following:
import Foundation
import SwiftUI
import AppKit
struct DescriptionTextField: NSViewRepresentable {
@Binding var text: String
var isEditable: Bool = true
var font: NSFont? = .systemFont(ofSize: 17, weight: .regular)
var onEditingChanged: () -> Void = { }
var onCommit : () -> Void = { }
var onTextChange : (String) -> Void = { _ in }
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeNSView(context: Context) -> CustomTextView {
let textView = CustomTextView(text: text, isEditable: isEditable, font: font)
textView.delegate = context.coordinator
return textView
}
func updateNSView(_ view: CustomTextView, context: Context) {
view.text = text
view.selectedRanges = context.coordinator.selectedRanges
}
}
extension DescriptionTextField {
class Coordinator: NSObject, NSTextViewDelegate {
var parent: DescriptionTextField
var selectedRanges: [NSValue] = []
init(_ parent: DescriptionTextField) {
self.parent = parent
}
func textDidBeginEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.text = textView.string
self.parent.onEditingChanged()
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else {
return
}
self.parent.text = textView.string
self.selectedRanges = textView.selectedRanges
if let txtView = textView.superview?.superview?.superview as? CustomTextView {
txtView.refreshScrollViewConstrains()
}
}
func textDidEndEditing(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
self.parent.text = textView.string
self.parent.onCommit()
}
}
}
// MARK: - CustomTextView
final class CustomTextView: NSView {
private var isEditable: Bool
private var font: NSFont?
weak var delegate: NSTextViewDelegate?
var text: String { didSet { textView.string = text } }
var selectedRanges: [NSValue] = [] {
didSet {
guard selectedRanges.count > 0 else { return }
textView.selectedRanges = selectedRanges
}
}
private lazy var scrollView: NSScrollView = {
let scrollView = NSScrollView()
scrollView.drawsBackground = false
scrollView.borderType = .noBorder
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalRuler = false
scrollView.autoresizingMask = [.width, .height]
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
private lazy var textView: NSTextView = {
let contentSize = scrollView.contentSize
let textStorage = NSTextStorage()
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(containerSize: scrollView.frame.size)
textContainer.widthTracksTextView = true
textContainer.containerSize = NSSize(
width: contentSize.width,
height: CGFloat.greatestFiniteMagnitude
)
layoutManager.addTextContainer(textContainer)
let textView = NSTextView(frame: .zero, textContainer: textContainer)
textView.autoresizingMask = .width
textView.backgroundColor = NSColor.clear
textView.delegate = self.delegate
textView.drawsBackground = true
textView.font = self.font
textView.isEditable = self.isEditable
textView.isHorizontallyResizable = false
textView.isVerticallyResizable = true
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.minSize = NSSize(width: 0, height: contentSize.height)
textView.textColor = NSColor.labelColor
textView.allowsUndo = true
textView.isRichText = true
return textView
} ()
// MARK: - Init
init(text: String, isEditable: Bool, font: NSFont?) {
self.font = font
self.isEditable = isEditable
self.text = text
super.init(frame: .zero)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
// MARK: - Life cycle
override func viewWillDraw() {
super.viewWillDraw()
setupScrollViewConstraints()
scrollView.documentView = textView
}
private func setupScrollViewConstraints() {
scrollView.translatesAutoresizingMaskIntoConstraints = false
addSubview(scrollView)
refreshScrollViewConstrains()
}
func refreshScrollViewConstrains() {
print("Constrains updated!")
let finalHeight = min(textView.contentSize.height, font!.pointSize * 6)
scrollView.removeConstraints(scrollView.constraints)
NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(lessThanOrEqualTo: topAnchor),
scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),
scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
scrollView.heightAnchor.constraint(equalToConstant: finalHeight)
])
scrollView.needsUpdateConstraints = true
}
}
extension NSTextView {
var contentSize: CGSize {
get {
guard let layoutManager = layoutManager, let textContainer = textContainer else {
print("textView no layoutManager or textContainer")
return .zero
}
layoutManager.ensureLayout(for: textContainer)
return layoutManager.usedRect(for: textContainer).size
}
}
}