Skip to content

Commit 4e4adf4

Browse files
committed
Add support displaying placeholder while editing
1 parent 3a3a6fd commit 4e4adf4

File tree

2 files changed

+120
-21
lines changed

2 files changed

+120
-21
lines changed

FormattedTextField.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@
392392
DYLIB_INSTALL_NAME_BASE = "@rpath";
393393
INFOPLIST_FILE = FormattedTextField/Info.plist;
394394
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
395-
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
395+
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
396396
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
397397
PRODUCT_BUNDLE_IDENTIFIER = com.ey.FormattedTextField;
398398
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -413,7 +413,7 @@
413413
DYLIB_INSTALL_NAME_BASE = "@rpath";
414414
INFOPLIST_FILE = FormattedTextField/Info.plist;
415415
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
416-
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
416+
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
417417
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
418418
PRODUCT_BUNDLE_IDENTIFIER = com.ey.FormattedTextField;
419419
PRODUCT_NAME = "$(TARGET_NAME)";

FormattedTextField/FormattedTextField.swift

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ import UIKit
1313
}
1414

1515
open class FormattedTextField: UITextField {
16+
public typealias Delegate = FormattedTextFieldDelegate
17+
18+
public enum PlaceholderMode {
19+
case whileEmpty
20+
case always
21+
}
22+
23+
deinit {
24+
removeTarget(self, action: #selector(self.textViewEditingChanged(_:)), for: .editingChanged)
25+
}
1626

1727
public override init(frame: CGRect) {
1828
placeholderLabel = UILabel()
@@ -47,16 +57,15 @@ open class FormattedTextField: UITextField {
4757
} else if let placeholder = super.placeholder {
4858
placeholderLabel.text = placeholder
4959
}
50-
// iOS 11: placeholderRect(forBounds:) returns the empty rect when the placeholder is empty
51-
super.placeholder = " "
52-
if #available(iOS 11, *) {
60+
addSubview(placeholderLabel)
61+
62+
addTarget(self, action: #selector(self.textViewEditingChanged(_:)), for: .editingChanged)
5363

64+
if #available(iOS 11, *) {
5465
if smartInsertDeleteType != .no {
5566
print("[FormattedTextField] warning: smartInsertDeleteType is unsupported");
5667
}
5768
}
58-
59-
addSubview(placeholderLabel)
6069
}
6170

6271
open var textFormatter: TextFromatter? {
@@ -106,7 +115,6 @@ open class FormattedTextField: UITextField {
106115
} else {
107116
text = nil
108117
}
109-
placeholderLabel.isHidden = !formattedText.isEmpty
110118
}
111119
}
112120

@@ -119,12 +127,19 @@ open class FormattedTextField: UITextField {
119127
}
120128
}
121129

130+
open var placeholderMode: PlaceholderMode = .whileEmpty {
131+
didSet {
132+
setNeedsLayout()
133+
}
134+
}
135+
122136
open override var placeholder: String? {
123137
get {
124138
return placeholderLabel.text
125139
}
126140
set(value) {
127141
placeholderLabel.text = value
142+
setNeedsLayout()
128143
}
129144
}
130145

@@ -147,6 +162,16 @@ open class FormattedTextField: UITextField {
147162
}
148163
}
149164

165+
open override var textAlignment: NSTextAlignment {
166+
get {
167+
return super.textAlignment
168+
}
169+
set {
170+
super.textAlignment = newValue
171+
setNeedsLayout()
172+
}
173+
}
174+
150175
open override var delegate: UITextFieldDelegate? {
151176
get {
152177
return delegateProxy
@@ -159,20 +184,96 @@ open class FormattedTextField: UITextField {
159184
open override func layoutSubviews() {
160185
super.layoutSubviews()
161186

162-
var placeholderFrame = self.placeholderRect(forBounds:bounds)
163-
if let text = self.text {
164-
var attributes: [NSAttributedString.Key: Any]? = nil
165-
if let placeholderFont = font {
166-
attributes = [ .font: placeholderFont]
187+
layoutPlaceholder()
188+
}
189+
190+
open override func editingRect(forBounds bounds: CGRect) -> CGRect {
191+
return super.editingRect(forBounds: bounds).inset(by: textRectInset)
192+
}
193+
194+
open override func textRect(forBounds bounds: CGRect) -> CGRect {
195+
return super.textRect(forBounds: bounds).inset(by: textRectInset)
196+
}
197+
198+
// MARK: - Private
199+
200+
private var textRectInset: UIEdgeInsets {
201+
return isPlaceholderVisible ? UIEdgeInsets(top: 0, left: 0, bottom: 0, right: placeholderLabelWidth) : .zero
202+
}
203+
204+
@objc private func textViewEditingChanged(_ sender: AnyObject?) {
205+
layoutPlaceholder()
206+
}
207+
208+
private func layoutPlaceholder() {
209+
placeholderLabel.frame = placeholderFrame
210+
}
211+
212+
private var placeholderFrame: CGRect {
213+
if !isPlaceholderVisible {
214+
return .zero
215+
}
216+
217+
let textRect = isEditing ? editingRect(forBounds: bounds) : self.textRect(forBounds: bounds)
218+
219+
var placeholderLabelFrame = textRect
220+
placeholderLabelFrame.size.width = placeholderLabelWidth
221+
222+
switch textAlignment {
223+
case .center:
224+
placeholderLabelFrame.origin.x = textRect.midX + enteredTextWidth * 0.5
225+
case .left, .justified:
226+
fallthrough
227+
case .natural where UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .leftToRight:
228+
placeholderLabelFrame.origin.x += enteredTextWidth
229+
case .right:
230+
placeholderLabelFrame.origin.x = textRect.maxX
231+
default:
232+
// TODO: Add support for right-to-left direction
233+
placeholderLabelFrame = .zero
234+
}
235+
return placeholderLabelFrame
236+
}
237+
238+
private var isPlaceholderVisible: Bool {
239+
if placeholder?.isEmpty ?? true {
240+
return false
241+
}
242+
243+
// Hides placeholder before text field adds scrolling text
244+
var isVisible = (placeholderAndTextRect.width - enteredTextWidth - placeholderHiddingGap >= placeholderLabelWidth)
245+
if isVisible {
246+
switch placeholderMode {
247+
case .always:
248+
isVisible = true
249+
case .whileEmpty:
250+
isVisible = unformattedText?.isEmpty ?? true
167251
}
168-
let prefixWidth = (text as NSString).size(withAttributes: attributes).width
169-
placeholderFrame.origin.x += prefixWidth
170-
placeholderFrame.size.width -= prefixWidth
171252
}
172-
self.placeholderLabel.frame = placeholderFrame;
253+
return isVisible
173254
}
174255

175-
// MARK: - Private
256+
private var placeholderAndTextRect: CGRect {
257+
return isEditing ? super.editingRect(forBounds: bounds) : super.textRect(forBounds: bounds)
258+
}
259+
260+
// UITextFields adds scrolling before entered text fills all available width
261+
private var placeholderHiddingGap: CGFloat = 10
262+
263+
private var enteredTextWidth: CGFloat {
264+
guard let text = self.text else {
265+
return 0
266+
}
267+
var attributes: [NSAttributedString.Key: Any]? = nil
268+
if let placeholderFont = font {
269+
attributes = [ .font: placeholderFont]
270+
}
271+
return (text as NSString).size(withAttributes: attributes).width
272+
}
273+
274+
private var placeholderLabelWidth: CGFloat {
275+
return placeholderLabel.sizeThatFits(CGSize(width: CGFloat.infinity, height: CGFloat.infinity)).width
276+
}
176277

177278
private let placeholderLabel: UILabel
178279

@@ -208,7 +309,7 @@ open class FormattedTextField: UITextField {
208309
unformattedRange = unformattedText.index(before: unformattedRange.lowerBound)..<unformattedRange.upperBound
209310
}
210311

211-
if let originDelegate = (delegateProxy.delegate as? FormattedTextFieldDelegate),
312+
if let originDelegate = (delegateProxy.delegate as? Delegate),
212313
originDelegate.responds(to: #selector(FormattedTextFieldDelegate.textField(_:shouldChangeUnformattedText:in:replacementString:))) {
213314
guard let utf16UnformattedRange = unformattedText.utf16Nsrange(fromRange: unformattedRange) else {
214315
return false
@@ -233,8 +334,6 @@ open class FormattedTextField: UITextField {
233334
self.text = formattedText
234335
selectedCharactersRange = formattedRange.upperBound..<formattedRange.upperBound
235336

236-
placeholderLabel.isHidden = !newUnformattedText.isEmpty
237-
238337
sendActions(for: .editingChanged)
239338

240339
return false

0 commit comments

Comments
 (0)