forked from bevy/photo-editor
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGrowingTextView.swift
190 lines (162 loc) · 6.83 KB
/
GrowingTextView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//
// GrowingTextView.swift
// iOSPhotoEditor
//
// Created by Kamal Kumar on 12/05/24.
//
import Foundation
import UIKit
@objc public protocol GrowingTextViewDelegate: UITextViewDelegate {
@objc optional func textViewDidChangeHeight(_ textView: GrowingTextView, height: CGFloat)
}
@IBDesignable @objc
open class GrowingTextView: UITextView {
override open var text: String! {
didSet { setNeedsDisplay() }
}
private var heightConstraint: NSLayoutConstraint?
// Maximum length of text. 0 means no limit.
@IBInspectable open var maxLength: Int = 0
// Trim white space and newline characters when end editing. Default is true
@IBInspectable open var trimWhiteSpaceWhenEndEditing: Bool = true
// Customization
@IBInspectable open var minHeight: CGFloat = 0 {
didSet { forceLayoutSubviews() }
}
@IBInspectable open var maxHeight: CGFloat = 0 {
didSet { forceLayoutSubviews() }
}
@IBInspectable open var placeholder: String? {
didSet { setNeedsDisplay() }
}
@IBInspectable open var placeholderColor: UIColor = UIColor(white: 0.8, alpha: 1.0) {
didSet { setNeedsDisplay() }
}
@IBInspectable open var attributedPlaceholder: NSAttributedString? {
didSet { setNeedsDisplay() }
}
// Initialize
override public init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
commonInit()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
contentMode = .redraw
associateConstraints()
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), name: UITextView.textDidChangeNotification, object: self)
NotificationCenter.default.addObserver(self, selector: #selector(textDidEndEditing), name: UITextView.textDidEndEditingNotification, object: self)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
open override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: 30)
}
private func associateConstraints() {
// iterate through all text view's constraints and identify
// height,from: https://github.com/legranddamien/MBAutoGrowingTextView
for constraint in constraints {
if (constraint.firstAttribute == .height) {
if (constraint.relation == .equal) {
heightConstraint = constraint;
}
}
}
}
// Calculate and adjust textview's height
private var oldText: String = ""
private var oldSize: CGSize = .zero
private func forceLayoutSubviews() {
oldSize = .zero
setNeedsLayout()
layoutIfNeeded()
}
private var shouldScrollAfterHeightChanged = false
override open func layoutSubviews() {
super.layoutSubviews()
if text == oldText && bounds.size == oldSize { return }
oldText = text
oldSize = bounds.size
let size = sizeThatFits(CGSize(width: bounds.size.width, height: CGFloat.greatestFiniteMagnitude))
var height = size.height
// Constrain minimum height
height = minHeight > 0 ? max(height, minHeight) : height
// Constrain maximum height
height = maxHeight > 0 ? min(height, maxHeight) : height
// Add height constraint if it is not found
if (heightConstraint == nil) {
heightConstraint = NSLayoutConstraint(item: self, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1.0, constant: height)
addConstraint(heightConstraint!)
}
// Update height constraint if needed
if height != heightConstraint!.constant {
shouldScrollAfterHeightChanged = true
heightConstraint!.constant = height
if let delegate = delegate as? GrowingTextViewDelegate {
delegate.textViewDidChangeHeight?(self, height: height)
}
} else if shouldScrollAfterHeightChanged {
shouldScrollAfterHeightChanged = false
scrollToCorrectPosition()
}
}
private func scrollToCorrectPosition() {
if self.isFirstResponder {
self.scrollRangeToVisible(NSMakeRange(-1, 0)) // Scroll to bottom
} else {
self.scrollRangeToVisible(NSMakeRange(0, 0)) // Scroll to top
}
}
// Show placeholder if needed
override open func draw(_ rect: CGRect) {
super.draw(rect)
if text.isEmpty {
let xValue = textContainerInset.left + textContainer.lineFragmentPadding
let yValue = textContainerInset.top
let width = rect.size.width - xValue - textContainerInset.right
let height = rect.size.height - yValue - textContainerInset.bottom
let placeholderRect = CGRect(x: xValue, y: yValue, width: width, height: height)
if let attributedPlaceholder = attributedPlaceholder {
// Prefer to use attributedPlaceholder
attributedPlaceholder.draw(in: placeholderRect)
} else if let placeholder = placeholder {
// Otherwise user placeholder and inherit `text` attributes
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = textAlignment
var attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: placeholderColor,
.paragraphStyle: paragraphStyle
]
if let font = font {
attributes[.font] = font
}
placeholder.draw(in: placeholderRect, withAttributes: attributes)
}
}
}
// Trim white space and new line characters when end editing.
@objc func textDidEndEditing(notification: Notification) {
if let sender = notification.object as? GrowingTextView, sender == self {
if trimWhiteSpaceWhenEndEditing {
text = text?.trimmingCharacters(in: .whitespacesAndNewlines)
setNeedsDisplay()
}
scrollToCorrectPosition()
}
}
// Limit the length of text
@objc func textDidChange(notification: Notification) {
if let sender = notification.object as? GrowingTextView, sender == self {
if maxLength > 0 && text.count > maxLength {
let endIndex = text.index(text.startIndex, offsetBy: maxLength)
text = String(text[..<endIndex])
undoManager?.removeAllActions()
}
setNeedsDisplay()
}
}
}