RxSwift wrapper for textView:shouldInteractWithURL:inRange:interaction: - ios

I want to have a method to intercept links tap in a UITextView through RxSwift something similar to:
textView.rx.didTapLink
.subscribe(onNext: { link, characterRange, interaction in
// handle link tap
})
I saw there is no implementation for delegate forwarding for the textView:shouldInteractWithURL:inRange:interaction: method so I presume I must add an extension for the RxTextViewDelegateProxy to implement the missing delegate method but don't know how to continue from there or if what I want is event possible without forking RxSwift but it should be possible I presume. I really appreciate any help.

Because the method in question returns a value (the OS pulls data from your app using this method,) it doesn't fit well with the Rx "push data" ecosystem. The appropriate way to implement this is as follows:
Given:
class MyTextViewDelegate: NSObject, UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith url: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
return true // do what you think best here.
}
}
You can connect it to the text view like this:
textView.rx.delegate.setForwardToDelegate(MyTextViewDelegate(), retainDelegate: true)
Using the forwardToDelegate allows you to continue to use the push type delegate methods using the Rx system.

Related

Dismiss keyboard in an iOS Notification Content Extension

I'm presenting custom action buttons in my iOS 11 notification window via a Notification Content Extension target. One of them is a 'comment' button. If I press it the keyboard shows up properly, but I am not able to figure out how to have the keyboard go away and get back to the other buttons on the notification. There's not really anything I can see to call resignFirstResponder on. Am I just missing something really obvious?
There is more than one way to do this.
Without A Content Extension
The first does not even require a notification content extension! The UNTextInputNotificationAction does all of the work for you. When initializing the action you specify parameters for the text field that will be presented when the action is triggered. That action is attached to your notification category during registration (i.e. inside willFinishLaunchingWithOptions):
userNotificationCenter.getNotificationCategories { (categories) in
var categories: Set<UNNotificationCategory> = categories
let inputAction: UNTextInputNotificationAction = UNTextInputNotificationAction(identifier: "org.quellish.textInput", title: "Comment", options: [], textInputButtonTitle: "Done", textInputPlaceholder: "This is awesome!")
let category: UNNotificationCategory = UNNotificationCategory(identifier: notificationCategory, actions: [inputAction], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: "Placeholder", options: [])
categories.insert(category)
userNotificationCenter.setNotificationCategories(categories)
}
This will produce an experience like this:
Note that by default, the "Done" button dismisses the keyboard and notification.
With more than one action you get this:
There is no going back to the action buttons that were presented with the notification - notifications can't do that. To see those actions choices again would require showing another notification.
With a Content Extension
First, the above section works with a content extension as well. When the user finishes entering text and hits the "textInputButton" the didReceive(_:completionHandler:) method of the content extension is called. This is an opportunity to use the input or dismiss the extension. The WWDC 2016 session Advanced Notifications describes this same use case and details ways it can be customized further.
This may not meet your needs. You may want to have a customized text entry user interface, etc. In that case it is up to your extension to handle showing and hiding the keyboard. The responder that handles text input - a UITextField, for example - should become first responder when the notification is received. Doing so will show the keyboard. Resigning first responder will hide it. This can be done inside a UITextField delegate method.
For example, this:
override var canBecomeFirstResponder: Bool {
get {
return true
}
}
func didReceive(_ notification: UNNotification) {
self.label?.text = notification.request.content.body
self.textField?.delegate = self
self.becomeFirstResponder()
self.textField?.becomeFirstResponder()
return
}
// UITextFieldDelegate
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.textField?.resignFirstResponder()
self.resignFirstResponder()
return true
}
Produces a result like this:
Keep in mind that on iOS 10 and 11 any taps on the notification itself - like on your text field - may result in it being dismissed! For this and many other reasons going this route is probably not desirable.

Delegates VS IBAction for handling events iOS

I am new to swift and new to iOS development. I am currently struggling to understand why delegates are used to handle events that happen in the UI.
So far, in the tutorial I am working through, I have only ever done things like the following code segment to handle UI events:
#IBAction func fahrenheitFieldEditingChanged(_ textField: UITextField) {
if let text = textField.text, let value = Double(text) {
fahrenheitValue = Measurement(value: value, unit: .fahrenheit)
} else {
fahrenheitValue = nil
}
}
But now the tutorial I am working through is having me use a delegate to to handle another UI event from the same text field. Why is it done this way? What is the point of using delegates rather than just write actions to handle the events?
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
let replacementTextHasDecimalSeparator = string.range(of: ".")
if existingTextHasDecimalSeparator != nil, replacementTextHasDecimalSeparator != nil {
return false
} else {
return true
}
}
Take a look at UITextViewDelegate. It'll become easier to understand.
Responding to Editing Notifications
func textViewShouldBeginEditing(UITextView)
Asks the delegate if editing should begin in the specified text view.
func textViewDidBeginEditing(UITextView)
Tells the delegate that editing of the specified text view has begun.
func textViewShouldEndEditing(UITextView)
Asks the delegate if editing should stop in the specified text view.
func textViewDidEndEditing(UITextView)
Tells the delegate that editing of the specified text view has ended.
Responding to Text Changes
func textView(UITextView, shouldChangeTextIn: NSRange, replacementText: String)
Asks the delegate whether the specified text should be replaced in the text view.
func textViewDidChange(UITextView)
Tells the delegate that the text or attributes in the specified text view were changed by the user.
There is more, but I won't copy/paste anymore you can see yourself. What you see here are things that you can tune in if you want.
If you want you can tune in and get the callback of when the user starts typing. Or you can tune in and get the callback of when the user ended his editing.
How can you find out when the user stoped or started using IBAction?!
Once* you set yourself as a delegate (textViewInstance.delegate = self, you can then choose to get any of these callbacks.
*To be 100% accurate, you need to do that, but also adopt the UITextViewDelegate protocol and then conform to it using the mentioned delegate callbacks
Delegate is a design pattern that allows you to easily customize the behavior of several objects in one central object.
Actions are merely user interactions.
It's not possible to replace delegate callbacks with actions. For instance, if you have a WebView which fails to load, then how does it inform your object using an action?
In your example, an action cannot return properties like shouldChangeCharactersIn and replacementString. You need a delegate for that.
Read more...
Delegate pattern (Apple docs)

Creating a tappable text section inside a UITextView

I have the following problem, I have a textview that says something like this "By tapping the "I agree" button, I certify that I have read and agree to the Online Terms and Conditions"
what I want to do is to add a tap gesture recognizer to the part that says "Online Terms and Conditions" and call a method when the user taps this text section, if the user clicks in any other section of the text nothing should happen.
How can I do this? (I can't post any code due to the NDA I signed) :( I hope I can still be helped.
Thanks in advance.
You can use attributed string with the tag NSLinkAttributeName set for the range of tappable text. Then in the textView delegate implement
optional func textView(_ textView: UITextView, shouldInteractWith URL:URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
Note that your text field must be selectable and non-editable for this to work.
Reference: https://developer.apple.com/reference/uikit/uitextviewdelegate/1649337-textview
Prior to iOS 10 you can use:
optional func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool`
Reference:
https://developer.apple.com/reference/uikit/uitextviewdelegate/1618606-textview
If you need to support both use the #available api - put #available(iOS, deprecated: 10.0) before pre-iOS 10 version and #available(iOS 10.0, *) before the one for iOS 10. That way you will avoid compile-time errors.

How to subscribe to delegate events globally?

I have a custom delegate that triggers certain events. For context, it's a bluetooth device that fires events arbitrarily. I'd like my view controllers to optionally subscribe to these events that get triggered by the device delegate.
It doesn't make sense that each view controller conforms to the custom delegate because that means the device variable would be local and would only fire in that view controller. Other view controllers wouldn't be aware of the change. Another concrete example would be CLLocationManagerDelegate - for example what if I wanted all view controllers to listen to the GPS coordinate changes?
Instead, I was thinking more of a global delegate that all view controllers can subscribe to. So if one view controller triggers a request, the device would call the delegate function for all subscribed view controllers that are listening.
How can I achieve this architectural design? Are delegates not the right approach? I thought maybe NotificationCenter can help here, but seems too loosely typed, perhaps throwing protocols would help makes things more manageable/elegant? Any help would be greatly appreciated!
You could have an array of subscribers that would get notified.
class CustomNotifier {
private var targets : [AnyObject] = [AnyObject]()
private var actions : [Selector] = [Selector]()
func addGlobalEventTarget(target: AnyObject, action: Selector) {
targets.append(target)
actions.append(action)
}
private func notifyEveryone () {
for index in 0 ..< targets.count {
if targets[index].respondsToSelector(actions[index]) {
targets[index].performSelector(actions[index])
}
}
}
}
Naturally, you'd have to plan further to maintain the lifecycle of targets and actions, and provide a way to unsubscribe etc.
Note: Also ideal would be for the array of targets and actions to be an of weak objects. This SO question, for instance, deals with the subject.
• NotificationCenter is first solution that comes in mind. Yes, it is loosely typed. But you can improve it. For example like this:
extension NSNotificationCenter {
private let myCustomNotification = "MyCustomNotification"
func postMyCustomNotification() {
postNotification(myCustomNotification)
}
func addMyCustomNotificationObserverUsingBlock(block: () -> ()) -> NSObjectProtocol {
return addObserverForName(myCustomNotification, object: nil, queue: nil) { _ in
block()
}
}
}
• Second solution would be to create some shared object, which will store all delegates or blocks/closures and will trigger them when needed. Such object basically will be the same as using NotificationCenter, but gives you more control.

Requesting caretRectForPosition: while the NSTextStorage has outstanding changes

I've been recently getting the error:
requesting caretRectForPosition: while the NSTextStorage has oustanding changes {x, x}
* "Oustanding" is literally what it says, and is not my typo.
This is being called when I am iterating through the NSTextStorage of a subclass of NSTextView with the enumerateAttribute() method and manipulating the NSTextAttachments in the text view after every change in the text view.
func manipulateText() {
let text = customTextView.textStorage
text.enumerateAttribute(NSAttachmentAttributeName, inRange: NSMakeRange(0, text.length), options: NSAttributedStringEnumerationOptions(rawValue: 0)) {
//
}
}
extension ThisViewController: UITextViewDelegate {
func textViewDidChange(textView: UITextView) {
manipulateText()
}
}
Questions such as this seem to be online, but I have yet to find any occurrences of this and seems to be relevant to iOS 9 only.
This only happens when using a physical keyboard on iPad.
This happens if you call caretRectForPosition (or any method that calls that like firstRectForRange) while the text storage has edits.
I was able to prevent these logs by deferring some stuff until after endEditing is called in the NSTextStorage and dispatch_async to the main queue to do my work. There aren't any visible UI flashes or anything as a result of the async.
There has to be a better way to solve this, but this is all I could figure out.

Resources