I have to do some operation whenever UICollectionView has been loaded completely, i.e. at that time all the UICollectionView's datasource / layout methods should be called. How do I know that?? Is there any delegate method to know UICollectionView loaded status?
This worked for me:
[self.collectionView reloadData];
[self.collectionView performBatchUpdates:^{}
completion:^(BOOL finished) {
/// collection-view finished reload
}];
Swift 4 syntax:
collectionView.reloadData()
collectionView.performBatchUpdates(nil, completion: {
(result) in
// ready
})
// In viewDidLoad
[self.collectionView addObserver:self forKeyPath:#"contentSize" options:NSKeyValueObservingOptionOld context:NULL];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// You will get here when the reloadData finished
}
- (void)dealloc
{
[self.collectionView removeObserver:self forKeyPath:#"contentSize" context:NULL];
}
It's actually rather very simple.
When you for example call the UICollectionView's reloadData method or it's layout's invalidateLayout method, you do the following:
dispatch_async(dispatch_get_main_queue(), ^{
[self.collectionView reloadData];
});
dispatch_async(dispatch_get_main_queue(), ^{
//your stuff happens here
//after the reloadData/invalidateLayout finishes executing
});
Why this works:
The main thread (which is where we should do all UI updates) houses the main queue, which is serial in nature, i.e. it works in the FIFO fashion. So in the above example, the first block gets called, which has our reloadData method being invoked, followed by anything else in the second block.
Now the main thread is blocking as well. So if you're reloadData takes 3s to execute, the processing of the second block will be deferred by those 3s.
Just to add to a great #dezinezync answer:
Swift 3+
collectionView.collectionViewLayout.invalidateLayout() // or reloadData()
DispatchQueue.main.async {
// your stuff here executing after collectionView has been layouted
}
A different approaching using RxSwift/RxCocoa:
collectionView.rx.observe(CGSize.self, "contentSize")
.subscribe(onNext: { size in
print(size as Any)
})
.disposed(by: disposeBag)
Do it like this:
UIView.animateWithDuration(0.0, animations: { [weak self] in
guard let strongSelf = self else { return }
strongSelf.collectionView.reloadData()
}, completion: { [weak self] (finished) in
guard let strongSelf = self else { return }
// Do whatever is needed, reload is finished here
// e.g. scrollToItemAtIndexPath
let newIndexPath = NSIndexPath(forItem: 1, inSection: 0)
strongSelf.collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
})
Try forcing a synchronous layout pass via layoutIfNeeded() right after the reloadData() call. Seems to work for both UICollectionView and UITableView on iOS 12.
collectionView.reloadData()
collectionView.layoutIfNeeded()
// cellForItem/sizeForItem calls should be complete
completion?()
As dezinezync answered, what you need is to dispatch to the main queue a block of code after reloadData from a UITableView or UICollectionView, and then this block will be executed after cells dequeuing
In order to make this more straight when using, I would use an extension like this:
extension UICollectionView {
func reloadData(_ completion: #escaping () -> Void) {
reloadData()
DispatchQueue.main.async { completion() }
}
}
It can be also implemented to a UITableView as well
SWIFT 5
override func viewDidLoad() {
super.viewDidLoad()
// "collectionViewDidLoad" for transitioning from product's cartView to it's cell in that view
self.collectionView?.addObserver(self, forKeyPath: "contentSize", options: NSKeyValueObservingOptions.new, context: nil)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let observedObject = object as? UICollectionView, observedObject == self.collectionView {
print("collectionViewDidLoad")
self.collectionView?.removeObserver(self, forKeyPath: "contentSize")
}
}
The best solution I have found so far is to use CATransaction in order to handle completion.
Swift 5:
CATransaction.begin()
CATransaction.setCompletionBlock {
// UICollectionView is ready
}
collectionView.reloadData()
CATransaction.commit()
Updated:
The above solution seems to work in some cases and in some cases it doesn't. I ended up using the accepted answer and it's definitely the most stable and proved way. Here is Swift 5 version:
private var contentSizeObservation: NSKeyValueObservation?
contentSizeObservation = collectionView.observe(\.contentSize) { [weak self] _, _ in
self?.contentSizeObservation = nil
completion()
}
collectionView.reloadData()
I just did the following to perform anything after collection view is reloaded. You can use this code even in API response.
self.collectionView.reloadData()
DispatchQueue.main.async {
// Do Task after collection view is reloaded
}
Simply reload collectionView inside batch updates and then check in the completion block whether it is finished or not with the help of boolean "finish".
self.collectionView.performBatchUpdates({
self.collectionView.reloadData()
}) { (finish) in
if finish{
// Do your stuff here!
}
}
This works for me:
__weak typeof(self) wself= self;
[self.contentCollectionView performBatchUpdates:^{
[wself.contentCollectionView reloadData];
} completion:^(BOOL finished) {
[wself pageViewCurrentIndexDidChanged:self.contentCollectionView];
}];
I needed some action to be done on all of the visible cells when the collection view get loaded before it is visible to the user, I used:
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if shouldPerformBatch {
self.collectionView.performBatchUpdates(nil) { completed in
self.modifyVisibleCells()
}
}
}
Pay attention that this will be called when scrolling through the collection view, so to prevent this overhead, I added:
private var souldPerformAction: Bool = true
and in the action itself:
private func modifyVisibleCells() {
if self.shouldPerformAction {
// perform action
...
...
}
self.shouldPerformAction = false
}
The action will still be performed multiple times, as the number of visible cells at the initial state. but on all of those calls, you will have the same number of visible cells (all of them). And the boolean flag will prevent it from running again after the user started interacting with the collection view.
Most of the solutions here are not reliable or have non-deterministic behavior (which may cause random bugs), because of the confusing asynchronous nature of UICollectionView.
A reliable solution is to subclass UICollectionView to run a completion block at the end of layoutSubviews().
Code in Objectice-C:
https://stackoverflow.com/a/39648633
Code in Swift:
https://stackoverflow.com/a/39798079
Def do this:
//Subclass UICollectionView
class MyCollectionView: UICollectionView {
//Store a completion block as a property
var completion: (() -> Void)?
//Make a custom funciton to reload data with a completion handle
func reloadData(completion: #escaping() -> Void) {
//Set the completion handle to the stored property
self.completion = completion
//Call super
super.reloadData()
}
//Override layoutSubviews
override func layoutSubviews() {
//Call super
super.layoutSubviews()
//Call the completion
self.completion?()
//Set the completion to nil so it is reset and doesn't keep gettign called
self.completion = nil
}
}
Then call like this inside your VC
let collection = MyCollectionView()
self.collection.reloadData(completion: {
})
Make sure you are using the subclass!!
This work for me:
- (void)viewDidLoad {
[super viewDidLoad];
int ScrollToIndex = 4;
[self.UICollectionView performBatchUpdates:^{}
completion:^(BOOL finished) {
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:ScrollToIndex inSection:0];
[self.UICollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
}];
}
Below is the only approach that worked for me.
extension UICollectionView {
func reloadData(_ completion: (() -> Void)? = nil) {
reloadData()
guard let completion = completion else { return }
layoutIfNeeded()
completion()
}
}
This is how I solved problem with Swift 3.0:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if !self.collectionView.visibleCells.isEmpty {
// stuff
}
}
Try this:
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return _Items.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell;
//Some cell stuff here...
if(indexPath.row == _Items.count-1){
//THIS IS THE LAST CELL, SO TABLE IS LOADED! DO STUFF!
}
return cell;
}
You can do like this...
- (void)reloadMyCollectionView{
[myCollectionView reload];
[self performSelector:#selector(myStuff) withObject:nil afterDelay:0.0];
}
- (void)myStuff{
// Do your stuff here. This will method will get called once your collection view get loaded.
}
Related
I was looking for a way to dismiss all the modally presented viewControllers in a UINavigationController hierarchically without knowing the name of them. so I ended up to the while loop as follow:
Swift
while(navigationController.topViewController != navigationController.presentedViewController) {
navigationController.presentedViewController?.dismiss(animated: true, completion: nil)
}
Objective-c
while(![self.navigationController.topViewController isEqual:self.navigationController.presentedViewController]) {
[self.navigationController.presentedViewController dismissViewControllerAnimated:YES completion:nil];
}
I want to dismiss all the presentedControllers one by one till the presentedViewController and topViewcontroller become equal.
the problem is that the navVC.presentedViewController doesn't changed even after dismissing.
It remains still the same even after dismissing and I end up to an infiniteLoop.
Does anyone knows where is the problem?
In my case nothing works but:
func dismissToSelf(completion: (() -> Void)?) {
// Collecting presented
var presentedVCs: [UIViewController] = []
var vc: UIViewController? = presentedViewController
while vc != nil {
presentedVCs.append(vc!)
vc = vc?.presentedViewController
}
// Dismissing all but first
while presentedVCs.count > 1 {
presentedVCs.last?.dismiss(animated: false, completion: nil)
presentedVCs.removeLast()
}
// Dismissing first with animation and completion
presentedVCs.first?.dismiss(animated: true, completion: completion)
}
I've found the answer. I can dismiss all presentedViewControllers on a navigationController by:
navigationController.dismiss(animated: true, completion: nil)
It keeps the topViewController and dismiss all other modals.
Form your question I understood that you want to dismiss all view controllers above the root view controller. For that you can do it like this:
self.view.window!.rootViewController?.dismiss(animated: false, completion: nil)
Not need to used self.navigationController.presentedViewController.
Might be help! my code is as follows:
Objective-c
[self dismissViewControllerAnimated:YES completion:^{
}];
// Or using this
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:nil];
});
Please check this code
-(void)dismissModalStack {
UIViewController *vc = self.window.rootViewController;
while (vc.presentedViewController) {
vc = vc.presentedViewController;
[vc dismissViewControllerAnimated:false completion:nil];
}
}
Glad to see you have found the answer, and I've done this by another way.
You can create a BaseViewController(actually lots of app do that), and defined a property like 'presentingController' in appdelegate that indicate the presenting ViewController, then in the viewWillAppear method, set the property so that it always indicate the top view controller.
-(void)viewWillAppear:(BOOL)animated{
AppDelegate *delegate=(AppDelegate *)[[UIApplication sharedApplication]delegate];
delegate.presentingController = self;
}
All the class inherited from BaseViewController will call it. When you want to dismiss all the controller, just loop as follow:
- (void)clickButton:(id)sender {
AppDelegate *delegate=(AppDelegate *)[[UIApplicationsharedApplication]delegate];
if (delegate.presentingController)
{
UIViewController *vc =self.presentingViewController;
if ( !vc.presentingViewController ) return;
while (vc.presentingViewController)
{
vc = vc.presentingViewController;
}
[vc dismissViewControllerAnimated:YEScompletion:^{
}];
}
}
Hope this will help you :)
I had a similar issue of deleting/dismissing existing/previous push notification when a new push notification arrives where different pictures are sent as a push notification.
In my situation, using Swift 5, I wanted to delete/dismiss previous push notification and display a new push notification all by itself regardless whether the user acknowledged the previous notification or not (i.e. without user's acknowledgement).
I tried Kadian's recommendation with a minor change and it worked flawlessly.
Here is my NotificationDelegate.swift
import UIKit
import UserNotifications
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.alert, .sound, .badge])
}
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
defer { completionHandler() }
guard response.actionIdentifier == UNNotificationDefaultActionIdentifier else {return}
let payload = response.notification.request.content
let pn = payload.body
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: pn)
//***Below cmd will erase previous push alert***
self.window!.rootViewController?.dismiss(animated: false, completion: nil)
//Below cmd will display a newly received push notification
self.window!.rootViewController!.present(vc, animated: false)
}
}
This seems to be one of the most frequently discussed topics here but I couldn't find a solution which actually works. I'm posting this question to share a solution which I found as well as hoping to find a better/cleaner solution
Description of situation:
There is a UIWebview in my application
There is text input/area in the webview
Long pressing on the text area/input brings up a context menu with 'cut', 'copy', 'define' etc.
We need to disable this menu without disabling user input.
What I've tried so far
(Stuff that doesn't work) :
Override canPerformAction
This solution tells us to add canPerformAction:withSender: to either subclass of UIWebview or in a delegate of UIWebview.
- (BOOL) canPerformAction:(SEL)action withSender:(id)sender
{
if (action == #selector(defineSelection:))
{
return NO;
}
else if (action == #selector(translateSelection:))
{
return NO;
}
else if (action == #selector(copy:))
{
return NO;
}
return [super canPerformAction:action withSender:sender];
}
Does not work because the canPerformAction: in this class is does not get called for menu items displayed.
Since the sharedMenuController interacts with the first responder in the Responder chain, implementing canPerformAction in the container skipped select and selectAll because they had already been handled by a child menu.
Manipulating CSS
Add the following to CSS:
html {
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color:rgba(0,0,0,0);
}
This does work on images and hyperlinks but not on inputs.
:(
The root cause of the first solution not working is the subview called UIWebBrowserView. This seems to be the view whose canPerformAction returns true for any action displayed in the context menu.
Since this UIWebBrowserView is a private class we shouldn't try to subclass it (because it will get your app rejected).
So what we do instead is we make another method called mightPerformAction:withSender:, like so-
- (BOOL)mightPerformAction:(SEL)action withSender:(id)sender {
NSLog(#"******Action!! %#******",NSStringFromSelector(action));
if (action == #selector(copy:))
{
NSLog(#"Copy Selector");
return NO;
}
else if (action == #selector(cut:))
{
NSLog(#"cut Selector");
return NO;
}
else if (action == NSSelectorFromString(#"_define:"))
{
NSLog(#"define Selector");
return NO;
}
else if (action == #selector(paste:))
{
NSLog(#"paste Selector");
return NO;
}
else
{
return [super canPerformAction:action withSender:sender];
}
}
and add another method to replace canPerformAction:withSender: with mightPerformAction:withSender:
- (void) replaceUIWebBrowserView: (UIView *)view
{
//Iterate through subviews recursively looking for UIWebBrowserView
for (UIView *sub in view.subviews) {
[self replaceUIWebBrowserView:sub];
if ([NSStringFromClass([sub class]) isEqualToString:#"UIWebBrowserView"]) {
Class class = sub.class;
SEL originalSelector = #selector(canPerformAction:withSender:);
SEL swizzledSelector = #selector(mightPerformAction:withSender:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self.class, swizzledSelector);
//add the method mightPerformAction:withSender: to UIWebBrowserView
BOOL didAddMethod =
class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
//replace canPerformAction:withSender: with mightPerformAction:withSender:
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
}
}
And finally call it in the viewDidLoad of the ViewController:
[self replaceUIWebBrowserView:self.webView];
Note: Add #import <objc/runtime.h> to your viewController then error(Method) will not shown.
Note: I am using NSSelectorFromString method to avoid detection of private API selectors during the review process.
Also you can hide menu:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(menuWillBeShown:) name:UIMenuControllerWillShowMenuNotification object:nil];
...
- (void)menuWillBeShown:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(),^{
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:NO];
});
}
The essential trick here is dispatch_async.
I have added custom menu controller when long press on UICollectionViewCell
[self becomeFirstResponder];
UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:#"Custom Action"
action:#selector(customAction:)];
[[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];
[[UIMenuController sharedMenuController] setTargetRect: self.frame inView:self.superview];
[[UIMenuController sharedMenuController] setMenuVisible:YES animated: YES];
canBecomeFirstResponder Is also being called
- (BOOL)canBecomeFirstResponder {
// NOTE: This menu item will not show if this is not YES!
return YES;
}
//This method is not being called
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
NSLog(#"canPerformAction");
// The selector(s) should match your UIMenuItem selector
if (action == #selector(customAction:)) {
return YES;
}
return NO;
}
I have Also Implemented these methods
- (BOOL)collectionView:(UICollectionView *)collectionView
canPerformAction:(SEL)action
forItemAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender {
if([NSStringFromSelector(action) isEqualToString:#"customAction:"]){
NSLog(#"indexpath : %#",indexPath);
UIAlertView *alertview = [[UIAlertView alloc] initWithTitle:#"warning.." message:#"Do you really want to delete this photo?" delegate:nil cancelButtonTitle:#"Ok" otherButtonTitles:nil, nil];
[alertview show];
return YES;
}
return YES;
}
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)collectionView:(UICollectionView *)collectionView
performAction:(SEL)action
forItemAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender {
NSLog(#"performAction");
}
Though it is showing only "cut, copy, and paste" menus
Maybe a bit late but i maybe found a better solution for those who are still search for this:
In viewDidLoad of your UICollectionViewController add your item:
UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:#"Title" action:#selector(action:)];
[[UIMenuController sharedMenuController] setMenuItems:[NSArray arrayWithObject:menuItem]];
Add the following delegate methods:
//This method is called instead of canPerformAction for each action (copy, cut and paste too)
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == #selector(action:)) {
return YES;
}
return NO;
}
//Yes for showing menu in general
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
Subclass UICollectionViewCell if you didn't already. Add the method you specified for your item:
- (void)action:(UIMenuController*)menuController {
}
This way you don't need any becomeFirstResponder or other methods. You have all actions in one place and you can easily handle different cells if you call a general method with the cell itself as a parameter.
Edit: Somehow the uicollectionview needs the existence of this method (this method isn't called for your custom action, i think the uicollectionview just checks for existance)
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
}
You need to trigger delegate functions from custom UICollectionViewCell
Here is my working sample code for Swift3
CollectionViewController
override func viewDidLoad() {
super.viewDidLoad()
let editMenuItem = UIMenuItem(title: "Edit", action: NSSelectorFromString("editCollection"))
let deleteMenuItem = UIMenuItem(title: "Delete", action: NSSelectorFromString("deleteCollection"))
UIMenuController.shared.menuItems = [editMenuItem, deleteMenuItem]
}
override func collectionView(_ collectionView: UICollectionView, shouldShowMenuForItemAt indexPath: IndexPath) -> Bool {
return true
}
override func collectionView(_ collectionView: UICollectionView, canPerformAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) -> Bool {
return action == NSSelectorFromString("editCollection") || action == NSSelectorFromString("deleteCollection")
}
override func collectionView(_ collectionView: UICollectionView, performAction action: Selector, forItemAt indexPath: IndexPath, withSender sender: Any?) {
print("action:\(action.description)")
//Custom actions here..
}
Add following functions to your custom UICollectionViewCell
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
return action == NSSelectorFromString("editCollection") || action == NSSelectorFromString("deleteCollection")
}
To call delegate function from cell (needs to be in your custom UICollectionViewCell)
func editCollection()
{
let collectionView = self.superview as! UICollectionView
let d:UICollectionViewDelegate = collectionView.delegate!
d.collectionView!(collectionView, performAction: NSSelectorFromString("editCollection"), forItemAt: collectionView.indexPath(for: self)!, withSender: self)
}
func deleteCollection()
{
let collectionView = self.superview as! UICollectionView
let d:UICollectionViewDelegate = collectionView.delegate!
d.collectionView!(collectionView, performAction: NSSelectorFromString("deleteCollection"), forItemAt: collectionView.indexPath(for: self)!, withSender: self)
}
I've just spent two days trying to figure out the "correct" way of doing this, and barking up the wrong tree with some of the suggestions that are around.
This article shows the correct way of doing this. I hope that by posting it here someone will be saved a few hours.
http://dev.glide.me/2013/05/custom-item-in-uimenucontroller-of.html
Swift 3 Solution:
Simply do all stuff inside UICollectionView class and assign this class to UICollectionView object.
import UIKit
class MyAppCollectionView: UICollectionView {
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addLongPressGesture()
}
func addLongPressGesture() {
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(MyAppCollectionView.longPressed(_:)))
longPressGesture.minimumPressDuration = 0.5
self.addGestureRecognizer(longPressGesture)
}
func longPressed(_ gesture: UILongPressGestureRecognizer) {
let point = gesture.location(in: self)
let indexPath = self.indexPathForItem(at: point)
if indexPath != nil {
MyAppViewController.cellIndex = indexPath!.row
let editMenu = UIMenuController.shared
becomeFirstResponder()
let custom1Item = UIMenuItem(title: "Custom1", action: #selector(MyAppViewController.custome1Method))
let custom2Item = UIMenuItem(title: "Custom2", action: #selector(MyAppViewController.custome2Method))
editMenu.menuItems = [custom1Item, custom2Item]
editMenu.setTargetRect(CGRect(x: point.x, y: point.y, width: 20, height: 20), in: self)
editMenu.setMenuVisible(true, animated: true)
}
}
override var canBecomeFirstResponder: Bool {
return true
}
}
class MyAppViewController: UIViewController {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
// You need to only return true for the actions you want, otherwise you get the whole range of
// iOS actions. You can see this by just removing the if statement here.
//For folder edit
if action == #selector(MyAppViewController.custome1Method) {
return true
}
if action == #selector(MyAppViewController.custome2Method) {
return true
}
return false
}
}
When people have trouble getting menus to work on long press in a collection view (or table view, for that matter), it is always for one of two reasons:
You're using the long press gesture recognizer for something. You cannot, for example, have both dragging and menus in the same collection view.
You've forgotten to implement the selector in the cell.
For example, the OP's code says:
UIMenuItem *menuItem = [[UIMenuItem alloc] initWithTitle:#"Custom Action"
action:#selector(customAction:)];
The implication is that customAction is a method this class. This is wrong. customAction: must be a method of the cell class. The reason is that the runtime will look at the cell class and will not show the menu item unless the cell implements the menu item's action method.
For a complete minimal working example (in Swift), see my answer here: https://stackoverflow.com/a/51898182/341994
On iOS 9 with Swift to SHOW ONLY CUSTOM ITEMS (without the default cut, paste and so on), I only managed to make work with the following code.
On method viewDidLoad:
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(contextMenuHandler))
longPressRecognizer.minimumPressDuration = 0.3
longPressRecognizer.delaysTouchesBegan = true
self.collectionView?.addGestureRecognizer(longPressRecognizer)
Override method canBecomeFirstResponder:
override func canBecomeFirstResponder() -> Bool {
return true
}
Override these two collection related methods:
override func collectionView(collectionView: UICollectionView, shouldShowMenuForItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
override func collectionView(collectionView: UICollectionView, canPerformAction action: Selector,
forItemAtIndexPath indexPath: NSIndexPath, withSender sender: AnyObject?) -> Bool {
return (action == #selector(send) || action == #selector(delete))
}
Create the gesture handler method:
func contextMenuHandler(gesture: UILongPressGestureRecognizer) {
if gesture.state == UIGestureRecognizerState.Began {
let indexPath = self.collectionView?.indexPathForItemAtPoint(gesture.locationInView(self.collectionView))
if indexPath != nil {
self.selectedIndexPath = indexPath!
let cell = self.collectionView?.cellForItemAtIndexPath(self.selectedIndexPath)
let menu = UIMenuController.sharedMenuController()
let sendMenuItem = UIMenuItem(title: "Send", action: #selector(send))
let deleteMenuItem = UIMenuItem(title: "Delete", action: #selector(delete))
menu.setTargetRect(CGRectMake(0, 5, 60, 80), inView: (cell?.contentView)!)
menu.menuItems = [sendMenuItem, deleteMenuItem]
menu.setMenuVisible(true, animated: true)
}
}
}
And, finally, create the selector's methods:
func send() {
print("Send performed!")
}
func delete() {
print("Delete performed!")
}
Hope that helps. :)
Cheers.
I have a UIViewController that manages a UISearchBar and UITableView. I've read that Apple discourage having multiple UIViewControllers manage part of your application, so I did not used UITableViewController to manage the UITableView. Instead, I implemented the UITableViewDelegate and UITableViewDataSource protocol in my own UIViewController.
My question is, since I am no longer using UITableViewController, how do I actually change the clearsSelectionOnViewWillAppear behavior? This property is part of UITableViewController.
Simply by calling
[myTableView deselectRowAtIndexPath:[myTableView indexPathForSelectedRow] animated:YES];
in your viewWillAppear: method.
Here the Swift code:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if let indexPath = tableView.indexPathForSelectedRow() {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
}
You are most likely overriding the viewWillAppear:animated method and missing the [super viewWillAppear:animated] call.
If you want an interactive animation:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSIndexPath *selectedIndexPath = [self.tableView indexPathForSelectedRow];
if(selectedIndexPath) {
if(self.transitionCoordinator != nil) {
[self.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
[self.tableView deselectRowAtIndexPath:selectedIndexPath animated:YES];
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
}];
[self.transitionCoordinator notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
if(context.cancelled) {
[self.tableView selectRowAtIndexPath:selectedIndexPath
animated:YES
scrollPosition:UITableViewScrollPositionNone];
}
}];
} else {
[self.tableView deselectRowAtIndexPath:selectedIndexPath animated:animated];
}
}
}
Swift Version:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let selectedIndexPath = self.tableView.indexPathForSelectedRow {
if let transitionCoordinator = self.transitionCoordinator {
transitionCoordinator.animate(alongsideTransition: { (context) in
self.tableView.deselectRow(at: selectedIndexPath, animated: true)
}, completion: nil)
transitionCoordinator.notifyWhenInteractionChanges { (context) in
if context.isCancelled {
self.tableView.selectRow(at: selectedIndexPath, animated: true, scrollPosition: .none)
}
}
} else {
self.tableView.deselectRow(at: selectedIndexPath, animated: animated)
}
}
}
Now you can scrub the animation, i.e. when interactively popping from a navigation controller. It also reselects the row if the interaction was cancelled. This is closer to what is happening inside UITableViewController.
Updated for Swift 5:
override func viewWillAppear(_ animated: Bool) {
if let indexPath = tableView.indexPathForSelectedRow {
tableView.deselectRow(at: indexPath, animated: true)
}
self.tableView.reloadData()
}
I submitted my app a little over a week ago and got the dreaded rejection email today. It tells me that my app cannot be accepted because I'm using a non-public API; specifically, it says,
The non-public API that is included in your application is firstResponder.
Now, the offending API call is actually a solution I found here on SO:
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
UIView *firstResponder = [keyWindow performSelector:#selector(firstResponder)];
How do I get the current first responder on the screen? I'm looking for a way that won't get my app rejected.
If your ultimate aim is just to resign the first responder, this should work: [self.view endEditing:YES]
In one of my applications I often want the first responder to resign if the user taps on the background. For this purpose I wrote a category on UIView, which I call on the UIWindow.
The following is based on that and should return the first responder.
#implementation UIView (FindFirstResponder)
- (id)findFirstResponder
{
if (self.isFirstResponder) {
return self;
}
for (UIView *subView in self.subviews) {
id responder = [subView findFirstResponder];
if (responder) return responder;
}
return nil;
}
#end
iOS 7+
- (id)findFirstResponder
{
if (self.isFirstResponder) {
return self;
}
for (UIView *subView in self.view.subviews) {
if ([subView isFirstResponder]) {
return subView;
}
}
return nil;
}
Swift:
extension UIView {
var firstResponder: UIView? {
guard !isFirstResponder else { return self }
for subview in subviews {
if let firstResponder = subview.firstResponder {
return firstResponder
}
}
return nil
}
}
Usage example in Swift:
if let firstResponder = view.window?.firstResponder {
// do something with `firstResponder`
}
A common way of manipulating the first responder is to use nil targeted actions. This is a way of sending an arbitrary message to the responder chain (starting with the first responder), and continuing down the chain until someone responds to the message (has implemented a method matching the selector).
For the case of dismissing the keyboard, this is the most effective way that will work no matter which window or view is first responder:
[[UIApplication sharedApplication] sendAction:#selector(resignFirstResponder) to:nil from:nil forEvent:nil];
This should be more effective than even [self.view.window endEditing:YES].
(Thanks to BigZaphod for reminding me of the concept)
Here's a category that allows you to quickly find the first responder by calling [UIResponder currentFirstResponder]. Just add the following two files to your project:
UIResponder+FirstResponder.h:
#import <Cocoa/Cocoa.h>
#interface UIResponder (FirstResponder)
+(id)currentFirstResponder;
#end
UIResponder+FirstResponder.m:
#import "UIResponder+FirstResponder.h"
static __weak id currentFirstResponder;
#implementation UIResponder (FirstResponder)
+(id)currentFirstResponder {
currentFirstResponder = nil;
[[UIApplication sharedApplication] sendAction:#selector(findFirstResponder:) to:nil from:nil forEvent:nil];
return currentFirstResponder;
}
-(void)findFirstResponder:(id)sender {
currentFirstResponder = self;
}
#end
The trick here is that sending an action to nil sends it to the first responder.
(I originally published this answer here: https://stackoverflow.com/a/14135456/322427)
Here is a Extension implemented in Swift based on Jakob Egger's most excellent answer:
import UIKit
extension UIResponder {
// Swift 1.2 finally supports static vars!. If you use 1.1 see:
// http://stackoverflow.com/a/24924535/385979
private weak static var _currentFirstResponder: UIResponder? = nil
public class func currentFirstResponder() -> UIResponder? {
UIResponder._currentFirstResponder = nil
UIApplication.sharedApplication().sendAction("findFirstResponder:", to: nil, from: nil, forEvent: nil)
return UIResponder._currentFirstResponder
}
internal func findFirstResponder(sender: AnyObject) {
UIResponder._currentFirstResponder = self
}
}
Swift 4
import UIKit
extension UIResponder {
private weak static var _currentFirstResponder: UIResponder? = nil
public static var current: UIResponder? {
UIResponder._currentFirstResponder = nil
UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
return UIResponder._currentFirstResponder
}
#objc internal func findFirstResponder(sender: AnyObject) {
UIResponder._currentFirstResponder = self
}
}
It's not pretty, but the way I resign the firstResponder when I don't know what that the responder is:
Create an UITextField, either in IB or programmatically. Make it Hidden. Link it up to your code if you made it in IB.
Then, when you want to dismiss the keyboard, you switch the responder to the invisible text field, and immediately resign it:
[self.invisibleField becomeFirstResponder];
[self.invisibleField resignFirstResponder];
For a Swift 3 & 4 version of nevyn's answer:
UIApplication.shared.sendAction(#selector(UIView.resignFirstResponder), to: nil, from: nil, for: nil)
Here's a solution which reports the correct first responder (many other solutions won't report a UIViewController as the first responder, for example), doesn't require looping over the view hierarchy, and doesn't use private APIs.
It leverages Apple's method sendAction:to:from:forEvent:, which already knows how to access the first responder.
We just need to tweak it in 2 ways:
Extend UIResponder so it can execute our own code on the first responder.
Subclass UIEvent in order to return the first responder.
Here is the code:
#interface ABCFirstResponderEvent : UIEvent
#property (nonatomic, strong) UIResponder *firstResponder;
#end
#implementation ABCFirstResponderEvent
#end
#implementation UIResponder (ABCFirstResponder)
- (void)abc_findFirstResponder:(id)sender event:(ABCFirstResponderEvent *)event {
event.firstResponder = self;
}
#end
#implementation ViewController
+ (UIResponder *)firstResponder {
ABCFirstResponderEvent *event = [ABCFirstResponderEvent new];
[[UIApplication sharedApplication] sendAction:#selector(abc_findFirstResponder:event:) to:nil from:nil forEvent:event];
return event.firstResponder;
}
#end
Using Swift and with a specific UIView object this might help:
func findFirstResponder(inView view: UIView) -> UIView? {
for subView in view.subviews as! [UIView] {
if subView.isFirstResponder() {
return subView
}
if let recursiveSubView = self.findFirstResponder(inView: subView) {
return recursiveSubView
}
}
return nil
}
Just place it in your UIViewController and use it like this:
let firstResponder = self.findFirstResponder(inView: self.view)
Take note that the result is an Optional value so it will be nil in case no firstResponder was found in the given views subview hierarchy.
The first responder can be any instance of the class UIResponder, so there are other classes that might be the first responder despite the UIViews. For example UIViewController might also be the first responder.
In this gist you will find a recursive way to get the first responder by looping through the hierarchy of controllers starting from the rootViewController of the application's windows.
You can retrieve then the first responder by doing
- (void)foo
{
// Get the first responder
id firstResponder = [UIResponder firstResponder];
// Do whatever you want
[firstResponder resignFirstResponder];
}
However, if the first responder is not a subclass of UIView or UIViewController, this approach will fail.
To fix this problem we can do a different approach by creating a category on UIResponder and perform some magic swizzeling to be able to build an array of all living instances of this class. Then, to get the first responder we can simple iterate and ask each object if -isFirstResponder.
This approach can be found implemented in this other gist.
Hope it helps.
Iterate over the views that could be the first responder and use - (BOOL)isFirstResponder to determine if they currently are.
Rather than iterate through the collection of views looking for the one that has isFirstResponder set, I too send a message to nil, but I store the receiver of the message so I can return it and do whatever I wish with it.
Additionally, I zero out the optional that holds the found responder in a defer statement from within the call itself. This ensures no references remain--even weak ones--at the end of the call.
import UIKit
private var _foundFirstResponder: UIResponder? = nil
extension UIResponder {
static var first:UIResponder? {
// Sending an action to 'nil' implicitly sends it to the first responder
// where we simply capture it and place it in the _foundFirstResponder variable.
// As such, the variable will contain the current first responder (if any) immediately after this line executes
UIApplication.shared.sendAction(#selector(UIResponder.storeFirstResponder(_:)), to: nil, from: nil, for: nil)
// The following 'defer' statement runs *after* this getter returns,
// thus releasing any strong reference held by the variable immediately thereafter
defer {
_foundFirstResponder = nil
}
// Return the found first-responder (if any) back to the caller
return _foundFirstResponder
}
// Make sure to mark this with '#objc' since it has to be reachable as a selector for `sendAction`
#objc func storeFirstResponder(_ sender: AnyObject) {
// Capture the recipient of this message (self), which is the first responder
_foundFirstResponder = self
}
}
With the above, I can resign the first responder by simply doing this...
UIResponder.first?.resignFirstResponder()
But since my API actually hands back whatever the first responder is, I can do whatever I want with it.
Here's an example that checks if the current first responder is a UITextField with a helpMessage property set, and if so, shows it in a help bubble right next to the control. We call this from a 'Quick Help' button on our screen.
func showQuickHelp(){
if let textField = UIResponder?.first as? UITextField,
let helpMessage = textField.helpMessage {
textField.showHelpBubble(with:helpMessage)
}
}
The support for the above is defined in an extension on UITextField like so...
extension UITextField {
var helpMessage:String? { ... }
func showHelpBubble(with message:String) { ... }
}
Now to support this feature, all we have to do is decide which text fields have help messages and the UI takes care of the rest for us.
Peter Steinberger just tweeted about the private notification UIWindowFirstResponderDidChangeNotification, which you can observe if you want to watch the firstResponder change.
If you just need to kill the keyboard when the user taps on a background area why not add a gesture recognizer and use it to send the [[self view] endEditing:YES] message?
you can add the Tap gesture recogniser in the xib or storyboard file and connect it to an action,
looks something like this then finished
- (IBAction)displayGestureForTapRecognizer:(UITapGestureRecognizer *)recognizer{
[[self view] endEditing:YES];
}
Just it case here is Swift version of awesome Jakob Egger's approach:
import UIKit
private weak var currentFirstResponder: UIResponder?
extension UIResponder {
static func firstResponder() -> UIResponder? {
currentFirstResponder = nil
UIApplication.sharedApplication().sendAction(#selector(self.findFirstResponder(_:)), to: nil, from: nil, forEvent: nil)
return currentFirstResponder
}
func findFirstResponder(sender: AnyObject) {
currentFirstResponder = self
}
}
This is what I did to find what UITextField is the firstResponder when the user clicks Save/Cancel in a ModalViewController:
NSArray *subviews = [self.tableView subviews];
for (id cell in subviews )
{
if ([cell isKindOfClass:[UITableViewCell class]])
{
UITableViewCell *aCell = cell;
NSArray *cellContentViews = [[aCell contentView] subviews];
for (id textField in cellContentViews)
{
if ([textField isKindOfClass:[UITextField class]])
{
UITextField *theTextField = textField;
if ([theTextField isFirstResponder]) {
[theTextField resignFirstResponder];
}
}
}
}
}
This is what I have in my UIViewController Category. Useful for many things, including getting first responder. Blocks are great!
- (UIView*) enumerateAllSubviewsOf: (UIView*) aView UsingBlock: (BOOL (^)( UIView* aView )) aBlock {
for ( UIView* aSubView in aView.subviews ) {
if( aBlock( aSubView )) {
return aSubView;
} else if( ! [ aSubView isKindOfClass: [ UIControl class ]] ){
UIView* result = [ self enumerateAllSubviewsOf: aSubView UsingBlock: aBlock ];
if( result != nil ) {
return result;
}
}
}
return nil;
}
- (UIView*) enumerateAllSubviewsUsingBlock: (BOOL (^)( UIView* aView )) aBlock {
return [ self enumerateAllSubviewsOf: self.view UsingBlock: aBlock ];
}
- (UIView*) findFirstResponder {
return [ self enumerateAllSubviewsUsingBlock:^BOOL(UIView *aView) {
if( [ aView isFirstResponder ] ) {
return YES;
}
return NO;
}];
}
With a category on UIResponder, it is possible to legally ask the UIApplication object to tell you who the first responder is.
See this:
Is there any way of asking an iOS view which of its children has first responder status?
You can choose the following UIView extension to get it (credit by Daniel):
extension UIView {
var firstResponder: UIView? {
guard !isFirstResponder else { return self }
return subviews.first(where: {$0.firstResponder != nil })
}
}
You can try also like this:
- (void) touchesBegan: (NSSet *) touches withEvent: (UIEvent *) event {
for (id textField in self.view.subviews) {
if ([textField isKindOfClass:[UITextField class]] && [textField isFirstResponder]) {
[textField resignFirstResponder];
}
}
}
I didn't try it but it seems a good solution
This is good candidate for recursion! No need to add a category to UIView.
Usage (from your view controller):
UIView *firstResponder = [self findFirstResponder:[self view]];
Code:
// This is a recursive function
- (UIView *)findFirstResponder:(UIView *)view {
if ([view isFirstResponder]) return view; // Base case
for (UIView *subView in [view subviews]) {
if ([self findFirstResponder:subView]) return subView; // Recursion
}
return nil;
}
you can call privite api like this ,apple ignore:
UIWindow *keyWindow = [[UIApplication sharedApplication] keyWindow];
SEL sel = NSSelectorFromString(#"firstResponder");
UIView *firstResponder = [keyWindow performSelector:sel];
Swift version of #thomas-müller's response
extension UIView {
func firstResponder() -> UIView? {
if self.isFirstResponder() {
return self
}
for subview in self.subviews {
if let firstResponder = subview.firstResponder() {
return firstResponder
}
}
return nil
}
}
I would like to shared with you my implementation for find first responder in anywhere of UIView. I hope it helps and sorry for my english. Thanks
+ (UIView *) findFirstResponder:(UIView *) _view {
UIView *retorno;
for (id subView in _view.subviews) {
if ([subView isFirstResponder])
return subView;
if ([subView isKindOfClass:[UIView class]]) {
UIView *v = subView;
if ([v.subviews count] > 0) {
retorno = [self findFirstResponder:v];
if ([retorno isFirstResponder]) {
return retorno;
}
}
}
}
return retorno;
}
The solution from romeo https://stackoverflow.com/a/2799675/661022 is cool, but I noticed that the code needs one more loop. I was working with tableViewController.
I edited the script and then I checked. Everything worked perfect.
I recommed to try this:
- (void)findFirstResponder
{
NSArray *subviews = [self.tableView subviews];
for (id subv in subviews )
{
for (id cell in [subv subviews] ) {
if ([cell isKindOfClass:[UITableViewCell class]])
{
UITableViewCell *aCell = cell;
NSArray *cellContentViews = [[aCell contentView] subviews];
for (id textField in cellContentViews)
{
if ([textField isKindOfClass:[UITextField class]])
{
UITextField *theTextField = textField;
if ([theTextField isFirstResponder]) {
NSLog(#"current textField: %#", theTextField);
NSLog(#"current textFields's superview: %#", [theTextField superview]);
}
}
}
}
}
}
}
Update: I was wrong. You can indeed use UIApplication.shared.sendAction(_:to:from:for:) to call the first responder demonstrated in this link: http://stackoverflow.com/a/14135456/746890.
Most of the answers here can't really find the current first responder if it is not in the view hierarchy. For example, AppDelegate or UIViewController subclasses.
There is a way to guarantee you to find it even if the first responder object is not a UIView.
First lets implement a reversed version of it, using the next property of UIResponder:
extension UIResponder {
var nextFirstResponder: UIResponder? {
return isFirstResponder ? self : next?.nextFirstResponder
}
}
With this computed property, we can find the current first responder from bottom to top even if it's not UIView. For example, from a view to the UIViewController who's managing it, if the view controller is the first responder.
However, we still need a top-down resolution, a single var to get the current first responder.
First with the view hierarchy:
extension UIView {
var previousFirstResponder: UIResponder? {
return nextFirstResponder ?? subviews.compactMap { $0.previousFirstResponder }.first
}
}
This will search for the first responder backwards, and if it couldn't find it, it would tell its subviews to do the same thing (because its subview's next is not necessarily itself). With this we can find it from any view, including UIWindow.
And finally, we can build this:
extension UIResponder {
static var first: UIResponder? {
return UIApplication.shared.windows.compactMap({ $0.previousFirstResponder }).first
}
}
So when you want to retrieve the first responder, you can call:
let firstResponder = UIResponder.first
Code below work.
- (id)ht_findFirstResponder
{
//ignore hit test fail view
if (self.userInteractionEnabled == NO || self.alpha <= 0.01 || self.hidden == YES) {
return nil;
}
if ([self isKindOfClass:[UIControl class]] && [(UIControl *)self isEnabled] == NO) {
return nil;
}
//ignore bound out screen
if (CGRectIntersectsRect(self.frame, [UIApplication sharedApplication].keyWindow.bounds) == NO) {
return nil;
}
if ([self isFirstResponder]) {
return self;
}
for (UIView *subView in self.subviews) {
id result = [subView ht_findFirstResponder];
if (result) {
return result;
}
}
return nil;
}
Simplest way to find first responder:
func sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool
The default implementation dispatches the action method to the given
target object or, if no target is specified, to the first responder.
Next step:
extension UIResponder
{
private weak static var first: UIResponder? = nil
#objc
private func firstResponderWhereYouAre(sender: AnyObject)
{
UIResponder.first = self
}
static var actualFirst: UIResponder?
{
UIApplication.shared.sendAction(#selector(findFirstResponder(sender:)), to: nil, from: nil, for: nil)
return UIResponder.first
}
}
Usage:
Just get UIResponder.actualFirst for your own purposes.