Skip to content

Commit cdda738

Browse files
carlosypuntokzaher
authored andcommitted
Add rx.attributedText to UITextView and UITextField
1 parent f46a54d commit cdda738

File tree

4 files changed

+100
-16
lines changed

4 files changed

+100
-16
lines changed

RxCocoa/iOS/UITextField+Rx.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ extension Reactive where Base: UITextField {
3636
)
3737
}
3838

39+
/// Bindable sink for `attributedText` property.
40+
public var attributedText: UIBindingObserver<Base, NSAttributedString?> {
41+
return UIBindingObserver(UIElement: self.base) { textField, attributedText in
42+
textField.attributedText = attributedText
43+
}
44+
}
45+
3946
}
4047

4148
#endif

RxCocoa/iOS/UITextView+Rx.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,45 @@ extension Reactive where Base: UITextView {
5353

5454
return ControlProperty(values: source, valueSink: bindingObserver)
5555
}
56+
57+
58+
/// Reactive wrapper for `attributedText` property.
59+
public var attributedText: ControlProperty<NSAttributedString?> {
60+
let source: Observable<NSAttributedString?> = Observable.deferred { [weak textView = self.base] in
61+
let attributedText = textView?.attributedText
62+
63+
let textChanged: Observable<NSAttributedString?> = textView?.textStorage
64+
// This project uses text storage notifications because
65+
// that's the only way to catch autocorrect changes
66+
// in all cases. Other suggestions are welcome.
67+
.rx.didProcessEditingRangeChangeInLength
68+
// This observe on is here because attributedText storage
69+
// will emit event while process is not completely done,
70+
// so rebinding a value will cause an exception to be thrown.
71+
.observeOn(MainScheduler.asyncInstance)
72+
.map { _ in
73+
guard let textStorage = textView?.textStorage else { return nil }
74+
return textStorage.attributedSubstring(from: NSRange(location: 0, length: textStorage.string.utf8.count))
75+
}
76+
?? Observable.empty()
77+
78+
return textChanged
79+
.startWith(attributedText)
80+
}
81+
82+
let bindingObserver = UIBindingObserver(UIElement: self.base) { (textView, attributedText: NSAttributedString?) in
83+
// This check is important because setting text value always clears control state
84+
// including marked text selection which is imporant for proper input
85+
// when IME input method is used.
86+
if textView.attributedText != attributedText {
87+
print(textView.attributedText)
88+
print(attributedText)
89+
textView.attributedText = attributedText
90+
}
91+
}
92+
93+
return ControlProperty(values: source, valueSink: bindingObserver)
94+
}
5695

5796
/// Reactive wrapper for `delegate` message.
5897
public var didBeginEditing: ControlEvent<()> {

Tests/RxCocoaTests/UITextField+RxTests.swift

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,46 @@ import XCTest
1313

1414
// UITextField
1515
final class UITextFieldTests : RxTest {
16+
1617
func test_TextCompletesOnDealloc() {
1718
ensurePropertyDeallocated({ UITextField() }, "a", comparer: { $0 == $1 }) { (view: UITextField) in view.rx.text }
1819
}
1920
func test_ValueCompletesOnDealloc() {
2021
ensurePropertyDeallocated({ UITextField() }, "a", comparer: { $0 == $1 }) { (view: UITextField) in view.rx.value }
2122
}
22-
23+
2324
func testSettingTextDoesntClearMarkedText() {
2425
let textField = UITextFieldSubclass(frame: CGRect.zero)
25-
26+
2627
textField.text = "Text1"
27-
textField.set = false
28+
textField.settedText = false
2829
textField.rx.text.on(.next("Text1"))
29-
XCTAssertTrue(!textField.set)
30+
XCTAssertTrue(!textField.settedText)
3031
textField.rx.text.on(.next("Text2"))
31-
XCTAssertTrue(textField.set)
32+
XCTAssertTrue(textField.settedText)
33+
}
34+
35+
func testLabel_attributedTextObserver() {
36+
let label = UILabel()
37+
XCTAssertEqual(label.attributedText, nil)
38+
let text = NSAttributedString(string: "Hello!")
39+
_ = Observable.just(text).bind(to: label.rx.attributedText)
40+
41+
XCTAssertEqual(label.attributedText, text)
3242
}
3343
}
3444

3545
final class UITextFieldSubclass : UITextField {
36-
var set: Bool = false
37-
46+
var settedText = false
47+
3848
override var text: String? {
3949
get {
4050
return super.text
4151
}
4252
set {
43-
set = true
53+
settedText = true
4454
super.text = newValue
4555
}
4656
}
57+
4758
}

Tests/RxCocoaTests/UITextView+RxTests.swift

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,32 @@ final class UITextViewTests : RxTest {
2222
let createView: () -> UITextView = { UITextView(frame: CGRect(x: 0, y: 0, width: 1, height: 1)) }
2323
ensurePropertyDeallocated(createView, "text", comparer: { $0 == $1 }) { (view: UITextView) in view.rx.value }
2424
}
25-
25+
2626
func testSettingTextDoesntClearMarkedText() {
2727
let textView = UITextViewSubclass2(frame: CGRect.zero)
28-
28+
2929
textView.text = "Text1"
30-
textView.set = false
30+
textView.settedText = false
3131
textView.rx.text.on(.next("Text1"))
32-
XCTAssertTrue(!textView.set)
32+
XCTAssertTrue(!textView.settedText)
3333
textView.rx.text.on(.next("Text2"))
34-
XCTAssertTrue(textView.set)
34+
XCTAssertTrue(textView.settedText)
35+
}
36+
37+
func testSettingTextDoesntClearMarkedAttributtedText() {
38+
let textView = UITextViewSubclass2(frame: CGRect.zero)
39+
40+
let initialAttributedString = NSAttributedString(string: "Test1")
41+
let nextAttributedString = NSAttributedString(string: "Test1")
42+
43+
textView.attributedText = initialAttributedString
44+
let textViewSettedAttributedText = textView.attributedText
45+
textView.settedAttributedText = false
46+
47+
textView.rx.attributedText.on(.next(textViewSettedAttributedText))
48+
XCTAssertTrue(!textView.settedAttributedText)
49+
textView.rx.attributedText.on(.next(nextAttributedString))
50+
XCTAssertTrue(textView.settedAttributedText)
3551
}
3652

3753
func testDidBeginEditing() {
@@ -116,15 +132,26 @@ final class UITextViewTests : RxTest {
116132
}
117133

118134
final class UITextViewSubclass2 : UITextView {
119-
var set: Bool = false
120-
135+
var settedText = false
136+
var settedAttributedText = false
137+
121138
override var text: String? {
122139
get {
123140
return super.text
124141
}
125142
set {
126-
set = true
143+
settedText = true
127144
super.text = newValue
128145
}
129146
}
147+
148+
override var attributedText: NSAttributedString? {
149+
get {
150+
return super.attributedText
151+
}
152+
set {
153+
settedAttributedText = true
154+
super.attributedText = newValue
155+
}
156+
}
130157
}

0 commit comments

Comments
 (0)