Skip to content

Commit 4e0499d

Browse files
ivedeneevgetmaxx
andauthored
Fix formatting behavior (#7)
* fix readme; cleanup * improve formatting * minor formatting fixes. cleaup examples * bump version * fix center alignment for phone textfield * fixes text and placeholder positions * cleanup --------- Co-authored-by: Igor Vedeneev <[email protected]>
1 parent a465d14 commit 4e0499d

12 files changed

+306
-195
lines changed

AGInputControls.podspec

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
#
2-
# Be sure to run `pod lib lint IVCollectionKit.podspec' to ensure this is a
3-
# valid spec before submitting.
4-
#
5-
# Any lines starting with a # are optional, but their use is encouraged
6-
# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
7-
#
8-
91
Pod::Spec.new do |s|
102
s.name = 'AGInputControls'
11-
s.version = '1.1.2'
3+
s.version = '1.1.3'
124
s.summary = 'Commonly used text input controls'
135
s.description = <<-DESC
146
TODO: Add long description of the pod here.
157
DESC
168

179
s.homepage = 'https://github.com/ivedeneev/AGInputControls.git'
1810
s.license = { :type => 'MIT', :file => 'LICENSE' }
19-
s.author = { 'ivedeneev' => '[email protected]' }
11+
s.author = { 'ivedeneev' => '[email protected]' }
2012
s.source = { :git => 'https://github.com/ivedeneev/AGInputControls.git', :tag => s.version.to_s }
2113

2214
s.ios.deployment_target = '9.0'

AGInputControls/Source/FormattingTextField+Extension.swift

+3-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import UIKit
1010
extension FormattingTextField {
1111

1212
internal func setCursorPosition(offset: Int) {
13-
guard let newPosition = position(from: beginningOfDocument, in: .right, offset: offset) else {return }
13+
let fixedOffset = max(offset, prefix.count)
14+
guard let newPosition = position(from: beginningOfDocument, in: .right, offset: fixedOffset) else {return }
1415
selectedTextRange = textRange(from: newPosition, to: newPosition)
1516
}
1617

@@ -27,16 +28,11 @@ extension FormattingTextField {
2728
}
2829

2930
internal func sizeOfText(_ text: String) -> CGSize {
31+
guard let font else { return .zero }
3032
return (text as NSString).boundingRect(
3133
with: UIScreen.main.bounds.size,
3234
options: [.usesFontLeading, .usesLineFragmentOrigin],
3335
attributes: [.font : font],
3436
context: nil).size
3537
}
36-
37-
internal func assertForExampleMasksAndPrefix() {
38-
guard let mask = exampleMask, !mask.isEmpty, let formattingMask = formattingMask, formatter != nil else { return }
39-
assert(mask == formattedText(text: mask) && mask.count == formattingMask.count, "Formatting mask and example mask should be in same format. This is your responsibility as a developer\nExampleMask: \(mask)\nFormatting mask: \(formattingMask)")
40-
assert(prefix.first(where: { $0.isLetter || $0.isNumber }) == nil || hasConstantPrefix, "You cannot have 'semi constant' prefixes at this point ")
41-
}
4238
}

AGInputControls/Source/FormattingTextField.swift

+106-44
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ open class FormattingTextField: UITextField {
3030
}
3131
}
3232

33+
open override var text: String? {
34+
get {
35+
super.text
36+
}
37+
38+
set {
39+
let formatted = formattedText(text: newValue)
40+
let pos = currentPosition()
41+
super.text = formatted
42+
notifyDelegate(text: text)
43+
setCaretPositionAfterSettingText(
44+
currentPosition: pos,
45+
rawText: newValue,
46+
formattedText: formatted
47+
)
48+
}
49+
}
50+
3351
/// Formatter object in case you need your own formatting logic
3452
open var formatter: AGFormatter? { didSet { invalidateIntrinsicContentSize() } }
3553

@@ -57,25 +75,27 @@ open class FormattingTextField: UITextField {
5775

5876
//MARK: Overriden properties
5977
open override var intrinsicContentSize: CGSize {
60-
guard let exampleMask = formattingMask ??
61-
formattingMask?.replacingOccurrences(of: "#", with: "0"),
62-
!exampleMask.isEmpty // in case of monospaced digit fonts calculatiing width againts only digit text produces more accurate results
78+
guard let exampleMask = formattingMask?
79+
.replacingOccurrences(of: "#", with: "0")
80+
.replacingUnderscoresWithZeros(),
81+
!exampleMask.isEmpty // in case of monospaced digit fonts calculatiing width againts only digit text produces more accurate results
6382
else {
6483
return super.intrinsicContentSize
6584
}
6685

67-
let font_ = font ?? UIFont.systemFont(ofSize: 17)
86+
let font_ = font ?? UIFont.preferredFont(forTextStyle: .body)
6887
let height = font_.lineHeight
6988
let width = sizeOfText(exampleMask).width
7089

7190
let caretWidth: CGFloat = caretRect(for: endOfDocument).width
91+
let padding: CGFloat = 0
7292

73-
return CGSize(width: width + caretWidth, height: height)
93+
return CGSize(width: width + caretWidth + padding * 2, height: height)
7494
}
7595

7696
open override var font: UIFont? {
7797
didSet {
78-
minimumFontSize = font?.pointSize ?? 17
98+
minimumFontSize = font?.pointSize ?? UIFont.systemFontSize
7999
invalidateIntrinsicContentSize()
80100
}
81101
}
@@ -97,20 +117,7 @@ open class FormattingTextField: UITextField {
97117
}
98118

99119
@objc internal func didChangeEditing() {
100-
var pos = currentPosition()
101-
let textCount = text?.count ?? 0
102-
103-
let formatted = formattedText(text: text)
104-
self.text = formatted
105-
notifyDelegate(text: self.text)
106-
guard let last = text?.prefix(pos).last else { return }
107-
108-
if !last.isNumber {
109-
pos = pos + 1 // не 1, а количество элементов до первой цифры с конца
110-
}
111-
if pos < textCount {
112-
setCursorPosition(offset: pos)
113-
}
120+
self.text = text
114121
}
115122

116123
//MARK: UITextField methods overrides
@@ -160,7 +167,7 @@ open class FormattingTextField: UITextField {
160167
if !range.isEmpty {
161168
if mask.contains("*") || mask.contains("?") {
162169
let text = String(txt.prefix(currentPosition(forStartOfRange: true)))
163-
setFormattedText(text)
170+
self.text = text
164171
return
165172
}
166173
} else if hasConstantPrefix && String(txt.prefix(cursorPosition - 1)) == prefix {
@@ -170,7 +177,7 @@ open class FormattingTextField: UITextField {
170177
if hasConstantPrefix && range.end == endOfDocument {
171178
let stringByRemovingPrefix = String(txt.prefix(cursorPosition).dropFirst(prefix.count))
172179
if stringByRemovingPrefix.filter({ $0.isLetter || $0.isNumber }).isEmpty {
173-
setFormattedText(stringByRemovingPrefix)
180+
self.text = stringByRemovingPrefix
174181
return
175182
}
176183
}
@@ -184,16 +191,15 @@ open class FormattingTextField: UITextField {
184191

185192
charsToRemove += 1
186193
txt.remove(at: .init(utf16Offset: cursorPosition - charsToRemove, in: txt))
187-
188-
setFormattedText(txt)
194+
text = txt
189195
setCursorPosition(offset: cursorPosition - charsToRemove)
190196
return
191197
}
192198

193199
if !isNumberOrLetter(txt.dropLast().last) && range.end == endOfDocument {
194200
let numberToDrop = min(txt.count, 2) // what if last 2-3 symbols are invalid? is it possible?
195201
txt.removeLast(numberToDrop)
196-
setFormattedText(txt)
202+
text = txt
197203
setCursorPosition(offset: cursorPosition - numberToDrop)
198204
return
199205
}
@@ -206,7 +212,45 @@ open class FormattingTextField: UITextField {
206212
}
207213
}
208214

215+
open override func becomeFirstResponder() -> Bool {
216+
if text.isEmptyOrTrue && !showsMaskIfEmpty && !_showsMask && formatter != nil {
217+
_showsMask = true
218+
setNeedsDisplay()
219+
}
220+
return super.becomeFirstResponder()
221+
}
222+
223+
open override func resignFirstResponder() -> Bool {
224+
if text.isEmptyOrTrue && !showsMaskIfEmpty && _showsMask && formatter != nil {
225+
_showsMask = false
226+
setNeedsDisplay()
227+
}
228+
return super.resignFirstResponder()
229+
}
230+
231+
open override func textRect(forBounds bounds: CGRect) -> CGRect {
232+
guard let exampleMask = exampleMask?.replacingUnderscoresWithZeros() else {
233+
return super.editingRect(forBounds: bounds)
234+
}
235+
236+
let w = sizeOfText(exampleMask).width
237+
let originX = (bounds.width - w) / 2
238+
return CGRect(x: originX, y: 0, width: w, height: bounds.height)
239+
}
240+
241+
open override func editingRect(forBounds bounds: CGRect) -> CGRect {
242+
guard let exampleMask = exampleMask?.replacingUnderscoresWithZeros() else {
243+
return super.editingRect(forBounds: bounds)
244+
}
245+
246+
let caretWidth: CGFloat = caretRect(for: endOfDocument).width
247+
let w = sizeOfText(exampleMask).width
248+
let originX = (bounds.width - w) / 2
249+
return CGRect(x: originX, y: 0, width: w + caretWidth, height: bounds.height)
250+
}
251+
209252
//MARK: Public methods
253+
@available(*, deprecated, renamed: "text", message: "Use regular text setter to set formatted text programmatically ")
210254
open func setFormattedText(_ text: String?) {
211255
self.text = formattedText(text: text)
212256
notifyDelegate(text: self.text)
@@ -218,12 +262,12 @@ open class FormattingTextField: UITextField {
218262

219263
open func drawExampleMask(rect: CGRect) {
220264
assertForExampleMasksAndPrefix()
221-
let text = self.text ?? ""
265+
let text = text ?? ""
222266

223267
guard let mask = exampleMask,
224268
!mask.isEmpty,
225-
let font = self.font,
226-
let textColor = self.textColor,
269+
let font,
270+
let textColor,
227271
!text.isEmpty || _showsMask
228272
else { return }
229273

@@ -233,13 +277,22 @@ open class FormattingTextField: UITextField {
233277
.foregroundColor : placeholderColor
234278
])
235279

236-
if hasConstantPrefix {
280+
if !text.isEmpty {
281+
textToDraw.addAttributes(
282+
[.foregroundColor : UIColor.clear],
283+
range: .init(location: 0, length: text.count)
284+
)
285+
} else if hasConstantPrefix {
237286
textToDraw.addAttributes(
238287
[.foregroundColor : textColor],
239288
range: .init(location: 0, length: prefix.count)
240289
)
241290
}
242-
textToDraw.draw(at: CGPoint(x: 0, y: ((bounds.height - font.lineHeight) / 2)))
291+
292+
let w = sizeOfText(mask.replacingUnderscoresWithZeros()).width
293+
let originX = (bounds.width - w) / 2
294+
295+
textToDraw.draw(at: CGPoint(x: originX, y: ((bounds.height - font.lineHeight) / 2)))
243296
}
244297

245298
open func formattedText(text: String?) -> String? {
@@ -248,9 +301,7 @@ open class FormattingTextField: UITextField {
248301
setNeedsDisplay()
249302
}
250303
}
251-
guard let formatter = formatter else {
252-
return text
253-
}
304+
guard let formatter else { return text }
254305

255306
let result = formatter.formattedText(text: text)
256307
return result
@@ -271,19 +322,30 @@ open class FormattingTextField: UITextField {
271322
}
272323
}
273324

274-
open override func becomeFirstResponder() -> Bool {
275-
if text.isEmptyOrTrue && !showsMaskIfEmpty && !_showsMask && formatter != nil {
276-
_showsMask = true
277-
setNeedsDisplay()
278-
}
279-
return super.becomeFirstResponder()
325+
//MARK: Private & internal
326+
internal func assertForExampleMasksAndPrefix() {
327+
guard let mask = exampleMask, !mask.isEmpty, let formattingMask = formattingMask, formatter != nil else { return }
328+
assert(mask == formattedText(text: mask) && mask.count == formattingMask.count, "Formatting mask and example mask should be in same format. This is your responsibility as a developer\nExampleMask: \(mask)\nFormatting mask: \(formattingMask)")
329+
assert(prefix.first(where: { $0.isLetter || $0.isNumber }) == nil || hasConstantPrefix, "You cannot have 'semi constant' prefixes at this point ")
280330
}
281331

282-
open override func resignFirstResponder() -> Bool {
283-
if text.isEmptyOrTrue && !showsMaskIfEmpty && _showsMask && formatter != nil {
284-
_showsMask = false
285-
setNeedsDisplay()
332+
func setCaretPositionAfterSettingText(currentPosition: Int, rawText:String?, formattedText: String?) {
333+
var pos = currentPosition
334+
let textCount = rawText?.count ?? 0
335+
guard let last = formattedText?.prefix(pos).last else { return }
336+
337+
if !last.isNumber {
338+
pos = pos + 1 // не 1, а количество элементов до первой цифры с конца
339+
}
340+
if pos < textCount {
341+
setCursorPosition(offset: pos)
342+
} else if let count = formattedText?.count {
343+
let delta = count - textCount
344+
if abs(delta) > 2 {
345+
DispatchQueue.main.async { // async because it may interfere with setting cursor position initiated by system
346+
self.setCursorPosition(offset: pos + delta)
347+
}
348+
}
286349
}
287-
return super.resignFirstResponder()
288350
}
289351
}

AGInputControls/Source/PhoneTextField.swift

+26
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ open class PhoneTextField: FormattingTextField {
1919
formatter = PhoneNumberFormatter(mask: formattingMask)
2020
}
2121
}
22+
23+
open override var textAlignment: NSTextAlignment {
24+
get {
25+
super.textAlignment
26+
}
27+
28+
set {
29+
/// `.center` just doesnt make sense from UX perspective if mask is enabled. We would expect to gradually replace placeholder with text which doesnt work with center alignment
30+
guard formattingMask != nil && newValue == .center else {
31+
super.textAlignment = newValue
32+
return
33+
}
34+
35+
super.textAlignment = .left //TODO: check RTL languages
36+
}
37+
}
2238

2339
override public init(frame: CGRect) {
2440
super.init(frame: frame)
@@ -36,4 +52,14 @@ open class PhoneTextField: FormattingTextField {
3652
}
3753
keyboardType = .phonePad
3854
}
55+
56+
override func assertForExampleMasksAndPrefix() {
57+
guard let mask = exampleMask, !mask.isEmpty, let formattingMask = formattingMask, formatter != nil else { return }
58+
59+
// It is common for phone fields to have _
60+
let fixedMask = mask.replacingOccurrences(of: "_", with: "0")
61+
62+
assert(fixedMask == formattedText(text: fixedMask) && fixedMask.count == formattingMask.count, "Formatting mask and example mask should be in same format. This is your responsibility as a developer\nExampleMask: \(mask)\nFormatting mask: \(formattingMask)")
63+
assert(prefix.first(where: { $0.isLetter || $0.isNumber }) == nil || hasConstantPrefix, "You cannot have 'semi constant' prefixes at this point ")
64+
}
3965
}

AGInputControls/Source/String+Extension.swift

+4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ extension String {
3535
func alphanumeric() -> String {
3636
components(separatedBy: CharacterSet.letters.inverted).joined()
3737
}
38+
39+
func replacingUnderscoresWithZeros() -> String {
40+
replacingOccurrences(of: "_", with: "0")
41+
}
3842
}
3943

4044
internal extension Optional where Wrapped == String {

AGInputControlsTests/AcceptedLettersTests.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,21 @@ class AcceptedLettersTests: XCTestCase {
2727
}
2828

2929
func testUnacceptedLettersDelegateMethodCalled() {
30-
textField.setFormattedText("")
30+
textField.text = ""
3131
XCTAssertEqual(delegate.unacceptedCharCalled, 1)
3232
}
3333

3434

3535
func testNotifyingDelegateWithEmptyMaskAndNonEmptyString() {
3636
textField.formatter = EmptyMaskFormatter()
37-
textField.setFormattedText("gfhdskjfhdskjfhds")
37+
textField.text = "gfhdskjfhdskjfhds"
3838
XCTAssertEqual(delegate.isValidCalled, 1)
3939
XCTAssertEqual(delegate.isValid, true)
4040
}
4141

4242
func testNotifyingDelegateWithEmptyMaskAndEmptyString() {
4343
textField.formatter = EmptyMaskFormatter()
44-
textField.setFormattedText("")
44+
textField.text = ""
4545
XCTAssertEqual(delegate.isValidCalled, 1)
4646
XCTAssertEqual(delegate.isValid, false)
4747
}

0 commit comments

Comments
 (0)