when keyboard show, i hope cell was scroll up, so i would like to observer if some textview was become first responder
so:
cell.textView.rac_signalForSelector("becomeFirstResponder")
.takeUntil(cell.rac_prepareForReuseSignal)
.flattenMap { (x) -> RACStream! in
return NSNotificationCenter.defaultCenter().rac_addObserverForName(UIKeyboardDidShowNotification, object: nil)
}
.subscribeNext { [weak self](notify) -> Void in
__logln("be4")
}
however when i click at cell.textView, the "be4" was output 4 times
then when i leave the pop the viewcontroller and push back,
and click at a cell.textView again, the output was 8 times
it seem that, the singal was not clear when cell was reuse
I get the answer, it was because the SoGou input method, it send three time UIKeyboardDidShowNotification notify
....
Related
I've looked at different sources to find an answer to this and possibly tried several ways to reloadData() but no luck so far.
Here is my case:
I have a tabBar where I present the content in a tableView. When you go into that tab, I present a modal view if you are not purchased the content yet.
Let's say you have purchased the content. The modal view dismisses itself but tableview doesn't show anything unless you kill the app and re-run again. When you this, tableView reloads itself with the content. No problem there. But this is definitely problematic and I do lose users just because of this.
I do keep track of the purchase with a key like this:
private var hiddenStatus: Bool = UserDefaults.standard.bool(forKey:"purchaseStatus")
I set it to true on a successful purchase as shown below:
self.setNonConsumablePurchase(true)
And finally the code where I check if the key is set to "true" on purchase. I call this on the viewDidLoad but it doesn't make sense because it's been already called when user touched on this tabBar item.
if hiddenStatus == true {
DispatchQueue.main.async(execute: {
self.segmentioView.valueDidChange = { [weak self] _, segmentIndex in
switch self?.segmentioView.selectedSegmentioIndex {
case 0:
self?.observeAugustProducts()
self?.tableView.reloadData()
case 1:
self?.observeSeptemberProducts()
self?.tableView.reloadData()
default:
self?.tableView.reloadData()
}
}
})
}
So I'm looking for two things:
1- To keep everything as it is and tweak the code to make this work.
2- Alternative ideas to give users a better in-app purchase experience (I once used UIAlertViewController for in-app purchase but decided not to go with it)
I recommend using this extension:
extension NotificationCenter {
func setUniqueObserver(_ observer: AnyObject, selector: Selector, name: NSNotification.Name, object: AnyObject?) {
NotificationCenter.default.removeObserver(observer, name: name, object: object)
NotificationCenter.default.addObserver(observer, selector: selector, name: name, object: object)
}
}
Before dismissing your Modal View, have your Modal View's ViewController post a Notification.
Something like:
NotificationCenter.default.post(name: "Purchased", object: nil)
Then have your TableView's ViewController register to listen for that Notification (in viewWillAppear) - and handle it.
Something like:
NotificationCenter.default.setUniqueObserver(self, selector: #selector(handlePurchased), name: "Purchased", object: nil)
and
#objc func handlePurchased() {
// Reload your TableView, etc.
}
Hope this helps someone!
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.
In order to display a text field right above the user's keyboard, I overrode inputAccessoryView in my custom view controller.
I also made sure that the view controller may become the first responder by overriding canBecomeFirstResponder (and returning true) and by calling self.becomeFirstResponder() in viewWillAppear().
Now, as I am displaying some messages as UICollectionViewCells in my view controller, I want to scroll down whenever the keyboard shows up. So I added a notification in viewDidLoad():
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: Notification.Name.UIKeyboardDidShow, object: nil)
keyboardDidShow() then calls the scrolling function:
#objc private final func scrollToLastMessage() {
// ('messages' holds all messages, one cell represents a message.)
guard messages.count > 0 else { return }
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
}
Indeed, by setting breakpoints in Xcode, I found out that the function gets triggered after the keyboard has appeared. But additionally, it also triggers after I resigned the first responder (f.ex. by hitting the return key [I resign the first responder and return true in textFieldShouldReturn ]) and the keyboard has disappeared. Although I think that it shouldn't: as the Apple docs say:
Posted immediately after the display of the keyboard.
The notification also triggers when accessing the view controller, so after the main view has appeared and when clicking on a (customized) UICollectionViewCell (the cell does not have any editable content, only static labels or image views, so the keyboard shouldn't even appear).
To give some more information: I pretty much followed this tutorial on Youtube: https://www.youtube.com/watch?v=ky7YRh01by8
The UIKeyboardDidShow notification may be posted more often than you might expect, not just when it initially appears. For example, when the frame changes after it was already visible, UIKeyboardDidShow is posted.
However you can know if the keyboard is truly visible by inspecting the keyboard's end frame from within the userInfo dictionary. This will tell you its size and position on screen, which you can then use to determine how best to react in your user interface.
I'm using SWRevealController to display 3 (left, center, right) panels. Basically, the right panel is a tableview of numbers and the middle panel shows possible even divisions when a user clicks on a number. I connected a segue to the IBAction of a reusable tablecell in the right view controller which loads the MainViewController. This all works fine. The problem is that if the number can't be divided evenly it triggers a notification, which the main view controller observes on ViewDidLoad. This notification sets the alpha of a pseudo-"alert" (UIView at the bottom of screen) to 1.0 for 4 seconds, at which point it returns to 0. Unfortunately this is where the problem starts: the notification box appears for a brief second while the animation runs but when the main viewcontroller finishes animating, the alert box disappears. I have a hunch it's because the ViewDidLoad fires at this point and resets the NSNotificationOberser – if I remove the segue on Touch Up Inside and manually switch view controllers the alert-box remains present.
Can you help me think of what I'm getting wrong? Like I said, I think it's because the observer is initialized in the ViewDidLoad. Assuming this is the case, where should I initialize the observer so that this doesn't happen anymore?
Basically the main VC displays a calculator, the code for which runs in a Calculator.swift file. If, when the number is passed through Calculate(), there is an error, it triggers an "alert" which the main VC picks up on, then reveals the box so that the user knows. Each time Calculate() is called, it logs the user's calculation to a tableview in the RVC – idea being they can reload previous calculations. Is this an improper usage of Notification Center?
The way I want the timeline of events to be:
User clicks on cell in the right panel
Main view controller (the calculator) is pushed via segue
Calculate() is called on the selected number, if there is a remainder a notification is triggered
The main view displays the results from Calculate(), if an alert fired then it would unhide a popup box on main view.
What is currently happening:
User clicks on the cell in right panel
Calculate() is called on the number, if there is a remainder the notification fires
Before the main view is pushed via segue, I can see briefly in the animation the result of the trigger firing and the calculation
As soon as the segue animation completes the view hides
My main VC Code (PopUpView is the alert box)
class CalculatorVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "triggerAlert:", name: "alert", object: nil)
self.PopUpView.alpha = 0.0
}
the triggerAlert function:
func triggerAlert(notification: NSNotification) {
PopUpView.alpha = 1.0
let returnedRemainder = (notification.userInfo)
let sample: Double = (returnedRemainder!["userTotal"]as! Double)
self.label.text = "Warning! Remainder: \(Double(round(100 * sample)/100))"
}
the tableView didSelect of the right VC:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
newUser.calculate()
//code here assigns the value in the selected table cell to newUser
}
Then in the Calculate() function:
dispatch_async(dispatch_get_main_queue(), {
NSNotificationCenter.defaultCenter().postNotificationName("alert", object: nil, userInfo: userTotalDictionary)
}
Notification observation/de-observation should generally be balanced between viewDidAppear/viewWillDisappear or viewWillAppear/viewDidDisappear. Either pair is usually fine, but it's wise to keep things that happen "when just offscreen" separate from things that happen "when just onscreen".
viewDidLoad is a poor place to set up observation, because you don't have a good place to balance removing the observation (viewDidUnload no longer exists). You should only remove observations in deinit that you set up in init, and view controllers really should never be observing things when they are not on screen.
I have a UITableView with cells that have swappable UIContextualActions to delete or rename (edit) individual cell's TextFields.
Since I made the switch to Swift 5 / iOS 13, triggering the rename UIContextualAction on these cells causes the keyboard to launch and instantly dismiss before the user has a chance to type. Not only does the keyboard go away, the particular cell I'm trying to edit becomes completely empty, and the following warning gets generated:
[Snapshotting] Snapshotting a view (0x10c90a470, _UIReplicantView) that has not been rendered at least once requires afterScreenUpdates:YES.
Below is the code for the rename UIContextualAction:
let actionRename = UIContextualAction(style: .normal, title: "") { (action, view, completionHandler) in
let cell = self.tableLayers.cellForRow(at: indexPath) as! LayerUITableViewCell
cell.layerTitle.isEnabled = true // enable UITextField editing
cell.layerTitle.becomeFirstResponder() // launch keyboard
cell.layerTitle.selectedTextRange = cell.layerTitle.textRange(from: (cell.layerTitle.beginningOfDocument), to: (cell.layerTitle.endOfDocument)) // select all text
completionHandler(true)
} // end of let actionRename
I'm guessing the animation of the UIContextual action is somehow triggering the keyboard's resignFirstResponder.
To summarize, prior to swift 5/iOS 13, the order of events went something like this:
user swipes cell left/right
user hits UIContextual button
cell returns to center
text gets selected
keyboard launches
user types, hits return
keyboard resignFirstResponder
Whereas the behavior I'm seeing after the migration looks like this:
user swipes cell left/right
user hits UIContextual button
text gets selected
keyboard launches
cell returns to center (which somehow triggers resignFirstResponder)
keyboard resignFirstResponder
Update 2019/10/02
I have confirmed that it's the cell animation that is causing the premature keyboard dismissal. If I introduce a delay after the completionHandler as follows:
let actionRename = UIContextualAction(style: .normal, title: "") { (action, view, completionHandler) in
completionHandler(true)
self.perform(#selector(self.layerRenameDos), with: nil, afterDelay: 1.0)
// layerRenameDos has the editing/firstResponder code from above
} // end of let actionRename
With this change, the cell animates back to center, keyboard launches, and I'm able to type away. This, however, is obviously a hacky work-around. Any suggestions would be appreciated
I think this is a better solution. Just be sure to execute your code on the next runloop.
CATransaction.begin()
CATransaction.setCompletionBlock {
DispatchQueue.main.async {
//your code on the next runloop after the animation has finished
}
}
complitionHandler(true)
// or tableView.setEditing(false, animated: true)
CATransaction.commit()