How to check if a view controller can perform a segue - objective-c

This might be a very simple question but didn't yield any results when searching for it so here it is...
I am trying to work out a way to check if a certain view controller can perform a segue with identifier XYZ before calling the performSegueWithIdentifier: method.
Something along the lines of:
if ([self canPerformSegueWithIdentifier:#"SegueID"])
[self performSegueWithIdentifier:#"SegueID"];
Possible?

To check whether the segue existed or not, I simply surrounded the call with a try-and-catch block. Please see the code example below:
#try {
[self performSegueWithIdentifier:[dictionary valueForKey:#"segue"] sender:self];
}
#catch (NSException *exception) {
NSLog(#"Segue not found: %#", exception);
}
Hope this helps.

- (BOOL)canPerformSegueWithIdentifier:(NSString *)identifier
{
NSArray *segueTemplates = [self valueForKey:#"storyboardSegueTemplates"];
NSArray *filteredArray = [segueTemplates filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:#"identifier = %#", identifier]];
return filteredArray.count>0;
}

This post has been updated for Swift 4.
Here is a more correct Swift way to check if a segue exists:
extension UIViewController {
func canPerformSegue(withIdentifier id: String) -> Bool {
guard let segues = self.value(forKey: "storyboardSegueTemplates") as? [NSObject] else { return false }
return segues.first { $0.value(forKey: "identifier") as? String == id } != nil
}
/// Performs segue with passed identifier, if self can perform it.
func performSegueIfPossible(id: String?, sender: AnyObject? = nil) {
guard let id = id, canPerformSegue(withIdentifier: id) else { return }
self.performSegue(withIdentifier: id, sender: sender)
}
}
// 1
if canPerformSegue("test") {
performSegueIfPossible(id: "test") // or with sender: , sender: ...)
}
// 2
performSegueIfPossible(id: "test") // or with sender: , sender: ...)

As stated in the documentation:
Apps normally do not need to trigger segues directly.
Instead, you configure an object in Interface Builder associated with
the view controller, such as a control embedded in its view hierarchy,
to trigger the segue. However, you can call this method to trigger a
segue programmatically, perhaps in response to some action that cannot
be specified in the storyboard resource file. For example, you might
call it from a custom action handler used to process shake or
accelerometer events.
The view controller that receives this message must have been loaded
from a storyboard. If the view controller does not have an associated
storyboard, perhaps because you allocated and initialized it yourself,
this method throws an exception.
That being said, when you trigger the segue, normally it's because it's assumed that the UIViewController will be able to respond to it with a specific segue's identifier. I also agree with Dan F, you should try to avoid situations where an exception could be thrown. As the reason for you not to be able to do something like this:
if ([self canPerformSegueWithIdentifier:#"SegueID"])
[self performSegueWithIdentifier:#"SegueID"];
I am guessing that:
respondsToSelector: only checks if you are able to handle that message in runtime. In this case you can, because the class UIViewController is able to respond to performSegueWithIdentifier:sender:. To actually check if a method is able to handle a message with certain parameters, I guess it would be impossible, because in order to determine if it's possible it has to actually run it and when doing that the NSInvalidArgumentException will rise.
To actually create what you suggested, it would be helpful to receive a list of segue's id that the UIViewController is associated with. From the UIViewController documentation, I wasn't able to find anything that looks like that
As for now, I am guessing your best bet it's to keep going with the #try #catch #finally.

You can override the -(BOOL)shouldPerformSegueWithIdentifier:sender: method and do your logic there.
- (BOOL) shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"someSegue"]) {
if (!canIPerformSegue) {
return NO;
}
}
return YES;
}
Hope this helps.

Reference CanPerformSegue.swift
import UIKit
extension UIViewController{
func canPerformSegue(identifier: String) -> Bool {
guard let identifiers = value(forKey: "storyboardSegueTemplates") as? [NSObject] else {
return false
}
let canPerform = identifiers.contains { (object) -> Bool in
if let id = object.value(forKey: "_identifier") as? String {
return id == identifier
}else{
return false
}
}
return canPerform
}
}

Swift version of Evgeny Mikhaylov's answer, which worked for me:
I reuse a controller for two views. This helps me reuse code.
if(canPerformSegueWithIdentifier("segueFoo")) {
self.performSegueWithIdentifier("segueFoo", sender: nil)
}
else {
self.performSegueWithIdentifier("segueBar", sender: nil)
}
func canPerformSegueWithIdentifier(identifier: NSString) -> Bool {
let templates:NSArray = self.valueForKey("storyboardSegueTemplates") as! NSArray
let predicate:NSPredicate = NSPredicate(format: "identifier=%#", identifier)
let filteredtemplates = templates.filteredArrayUsingPredicate(predicate)
return (filteredtemplates.count>0)
}

It will be useful, before call performSegue, check native storyboard property on base UIViewController (for example screen was from StoryBoard or Manual Instance)
extension UIViewController {
func performSegueWithValidate(withIdentifier identifier: String, sender: Any?) {
if storyboard != nil {
performSegue(withIdentifier: identifier, sender: sender)
}
}
}
enter image description here

There is no way to check that using the standard functions, what you can do is subclass UIStoryboardSegue and store the information in the source view controller (which is passed to the constructor). In interface builder select "Custom" as the segue type as type the class name of your segue, then your constructor will be called for every segue instantiated and you can query the stored data if it exists.
You must also override the perform method to call [source presentModalViewController:destination animated:YES] or [source pushViewController:destination animated:YES] depending on what transition type you want.

Related

What is the swift equivalent to setting properties on `id`?

I wonder what's the Swift equivalent in calling a method on id in which the availability of the method is determined at runtime. Specifically I'm looking to do this pattern in Swift:
-(IBAction) handleEvent:(id) sender {
BOOL didDisable = NO;
if([sender respondsToSelector:#selector(setEnabled:)]) {
[sender setEnabled:NO];
didDisable = YES;
}
[self doSomethingAsyncWithCompletionHandler:^{
if(didDisable) {
[sender setEnabled:YES];
}
}];
}
The biggest problem is that setEnabled: is imported in Swift as a property (e.g. UIBarItem) and none of the following constructs compile
func handleEvent(sender: AnyObject) {
// Error: AnyObject does not have a member named "enabled"
sender.enabled? = false
// Error: (BooleanLiteralCompatible) -> _ is not identical to Bool
sender.setEnabled?(false)
}
You can in fact do it exactly the same way you were doing it before: by calling respondsToSelector:. Indeed, that is exactly what your proposed expression does:
sender.setEnabled?(false)
That expression is actually a shorthand - it calls respondsToSelector: first, and then calls setEnabled: only if the respondsToSelector: test passes. Unfortunately, as you say, you can't get that code to compile. That, however, is merely a quirk of Swift's known repertory of available methods. The fact is that, although it is a little tricky to get it to compile, it can be done - and once you get it to compile, it behaves just as you would expect.
However, I'm not going to explain how to make it compile, because I don't want to encourage this kind of trickery. This sort of dynamic messaging is discouraged in Swift. In general, dynamic messaging tricks such as key-value coding, introspection, and so forth are not needed in Swift and are not consonant with Swift's strong typing approach. It would be better to do things the Swift way, by casting optionally to something that you have reason to believe this thing might be and that has an enabled property. For example:
#IBAction func doButton(sender: AnyObject) {
switch sender {
case let c as UIControl: c.enabled = false
case let b as UIBarItem: b.enabled = false
default:break
}
}
Or:
#IBAction func doButton(sender: AnyObject) {
(sender as? UIControl)?.enabled = false
(sender as? UIBarItem)?.enabled = false
}
In Swift 2.0 beta 4, your prayers are answered; this code becomes legal:
#IBAction
func handleEvent(sender: AnyObject) {
if sender.respondsToSelector("setHidden:") {
sender.performSelector("setHidden:", withObject: true)
}
}
If you want to avoid using the respondsToSelector: method you could define a protocol instead. Then extend the classes you want to use that is already in conformance with this protocol's definition (enabled) and define the function with a generic variable conforming to your protocol.
protocol Enablable{
var enabled:Bool { get set }
}
extension UIButton : Enablable {}
extension UIBarButtonItem : Enablable {}
//....
func handleEvent<T:Enablable>(var sender: T) {
sender.enabled = false
}
If you need to use it with an IBAction method a little bit of a work around is required since you cannot use generics directly on them.
#IBAction func handleEventPressed(sender:AnyObject){
handleEvent(sender);
}
We also need a matching generic function without Enablable conformance so that we can call handleEvent without knowing wether or not sender is Enablable. Luckily the compiler is smart enough to figure out which of the two generic functions to use.
func handleEvent<T>(var sender: T) {
//Do Nothing case if T does not conform to Enablable
}
As a workaround/alternative, you can use Key-Value Coding:
#IBAction func handler(sender: AnyObject) {
if sender.respondsToSelector("setEnabled:") {
sender.setValue(false, forKey:"enabled")
}
}
This works with both Swift 1.2 (Xcode 6.4) and Swift 2.0 (Xcode 7 beta).

How to refresh UICollectionViewCell in iOS 7?

I am trying to develop my app in Xcode 5 and debug it under iOS 7 environment.
I have a customized UICollectionViewLayoutAttributes.
I plan to do something after long pressing on UICollectionViewCell, so I override the method in UICollectionViewCell.m
- (void)applyLayoutAttributes:(MyUICollectionViewLayoutAttributes *)layoutAttributes
{
[super applyLayoutAttributes:layoutAttributes];
if ([(MyUICollectionViewLayoutAttributes *)layoutAttributes isActived])
{
[self startShaking];
}
else
{
[self stopShaking];
}
}
In iOS 6 or below, - applyLayoutAttributes: is called after I call the statements below.
UICollectionViewLayout *layout = (UICollectionViewLayout *)self.collectionView.collectionViewLayout;
[layout invalidateLayout];
However, in iOS 7, - applyLayoutAttributes: is NOT being called even if I reload the CollectionView.
Is that a bug which is gonna be fixed by Apple later on, or I have to do something?
In iOS 7, you must override isEqual: in your UICollectionViewLayoutAttributes subclass to compare any custom properties that you have.
The default implementation of isEqual: does not compare your custom properties and thus always returns YES, which means that -applyLayoutAttributes: is never called.
Try this:
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if (!other || ![[other class] isEqual:[self class]]) {
return NO;
}
if ([((MyUICollectionViewLayoutAttributes *) other) isActived] != [self isActived]) {
return NO;
}
return YES;
}
Yes. As Calman said you must override isEqual: method to compare custom properties that you have. See the apple documentation here
If you subclass and implement any custom layout attributes, you must also override the inherited isEqual: method to compare the values of your properties. In iOS 7 and later, the collection view does not apply layout attributes if those attributes have not changed. It determines whether the attributes have changed by comparing the old and new attribute objects using the isEqual: method. Because the default implementation of this method checks only the existing properties of this class, you must implement your own version of the method to compare any additional properties. If your custom properties are all equal, call super and return the resulting value at the end of your implementation.
In this case, the most efficient method would be
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if(![super isEqual:other]) {
return NO;
}
return ([((MyUICollectionViewLayoutAttributes *) other) isActived] == [self isActived]);
}

Is it possible to check whether an identifier exists in a storyboard before instantiating the object?

In my code I have this line, but I was wondering if there is way to check whether #"SomeController" exists before I use it with the "instantiateViewControllerWithIdentifier" method. If the identifier doesn't exist then the app crashes.
It's not a huge problem if there isn't a good way to do it, I can just be a bit more careful not to fat finger the identifier names, but I was hoping I could handle it more gracefully.
UIViewController *newTopViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"SomeController"];
As Tom said, the best solution to this problem is the try-catch block:
#try {
UIViewController *newViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"identifier"];
}
#catch (NSException *exception) {
UIAlertView *catchView;
catchView = [[UIAlertView alloc]
initWithTitle: NSLocalizedString(#"Error", #"Error")
message: NSLocalizedString(#"Identifier not found on SB".", #"Error")
delegate: self
cancelButtonTitle: NSLocalizedString(#"OK", #"Error") otherButtonTitles: nil];
[catchView show];
}
I hope it helps! even though the answer is really late.
#Kevin's solution works. Here is a pretty the same piece of code for Swift 3 as function, that I am using in my code:
func instantiateViewController(fromStoryboardName storyboardName: String, withIdentifier identifier: String) -> UIViewController? {
let mainStoryboard = UIStoryboard(name: storyboardName, bundle: nil)
if let availableIdentifiers = mainStoryboard.value(forKey: "identifierToNibNameMap") as? [String: Any] {
if availableIdentifiers[identifier] != nil {
if let poiInformationViewController = mainStoryboard.instantiateViewController(withIdentifier: identifier) as? UIViewController {
return viewController
}
}
}
return nil
}
Use this function as follows:
if let viewController = self.instantiateViewController(fromStoryboardName: "YourStoryboardName", withIdentifier: "YourViewControllerStoryboardID") {
// Here you are sure your viewController is available in the Storyboard
} else {
print("Error: The Storyboard with the name YourStoryboardName or the Storyboard identifier YourViewControllerStoryboardID is not available")
}
You can use valueForKey: on UIStoryboards. UIStoryboards have a key called "identifierToNibNameMap", its value is an NSDictionary with the UIViewControllers in that storyboard. This inner NSDictionary uses the viewcontroller's names as keys so you can actually check if a viewcontroller exists in a storyboard with the following code:
if ([[storyboard valueForKey:#"identifierToNibNameMap"] objectForKey:myViewControllerName]) {
// the view controller exists, instantiate it here
UIViewController* myViewController = [storyboard instantiateViewControllerWithIdentifier:myViewControllerName];
} else {
//the view controller doesn't exist, do fallback here
}
Note: Apple has been known to reject apps that query the underlying properties of cocoa classes using valueForKey:. These underlying properties could change at any time in the future, breaking app functionality without warning. There is no deprecation process for these things.
No, there is no check for this. However, you don't need to. This method will return nil if the identifier doesn't exist, so just check for that with an NSAssert.
EDIT Actually this is wrong!! That's weird...the return value section of the documentation contradicts another portion...but still the answer is ultimately no (there is no method to check for the existence of an identifier)
You can wrap the code with try-catch exception handling and decide how to react if such an exception occurs.
I use this method to dynamically instantiate view controllers without having to know if they are represented in the Storyboard or a nib file.
Swift 4.2.
Declare an extension below.
extension UIStoryboard {
func instantiateVC(withIdentifier identifier: String) -> UIViewController? {
// "identifierToNibNameMap" – dont change it. It is a key for searching IDs
if let identifiersList = self.value(forKey: "identifierToNibNameMap") as? [String: Any] {
if identifiersList[identifier] != nil {
return self.instantiateViewController(withIdentifier: identifier)
}
}
return nil
}
}
Use this methods like this anywhere:
if let viewController = self.storyboard?.instantiateVC(withIdentifier: "yourControllerID") {
// Use viewController here
viewController.view.tag = 0; // for example
}
or
if let viewController = UIStoryboard(name: "yourStoryboardID", bundle: nil).instantiateVC(withIdentifier: "yourControllerID") {
// Use viewController here
viewController.view.tag = 0; // for example
}
Replace "yourControllerID" with your controller's ID.

Iterate over all subviews of a specific type

Iterating over all UIViews in the subviews of a class and then checking the type in the loop using isKindOfClass made my code look redundant. So I wrote the following method which executes a block for each subview.
#implementation Util
+ (void)iterateOverSubviewsOfType:(Class)viewType
view:(UIView*)view
blockToExecute:(void (^)(id subview))block
{
for (UIView* subview in view.subviews) {
if ([subview isKindOfClass:viewType]) {
block(subview);
}
}
}
#end
The block passed to this method takes an argument of type id. The type used here should be of course the same as passed as the first argument. However so far I haven't figured out a way to make this more type safe.
Try it like this, should be safe enough.
for (id subview in view.subviews) {
if ([subview isMemberOfClass:viewType]) {
block(subview);
}
}

How to check if a UIViewController is of a particular sub-class in objective c?

I want to be able to check the type of a UIViewController to see if it is of a certain type like this
c code
if (typeof(instance1) == customUIViewController)
{
customUIViewController test = (customViewController)instance1;
// do more stuff
}
The isKindOfClass: method indicates whether an object is an instance of given class or an instance of a subclass of that class.
if ([instance1 isKindOfClass:[CustomUIViewController class]]) {
// code
}
If you want to check whether an object is an instance of a given class (but not an instance of a subclass of that class), use isMemberOfClass: instead.
var someVC: UIViewController
if someVC is MyCustomVC {
//code
}
Swift version:
var someVC: UIViewController
if someVC.isKindOfClass(MyCustomVC) {
//code
}
Try:
[vc isKindOfClass:[CustomViewController class]];
I just wanted to add in addition to this answer that if you're wanting to see if a view controller is of a certain type in a switch statement (in Swift) you can do it like this:
var someVC: UIViewController?
switch someVC {
case is ViewController01: break
case is ViewController02: break
case is ViewController03: break
default: break
}
Swift 3.0
in latest, we have to add a self along with the class name
or it will throw an error "Expected member name or constructor call after type name"
the below code u can use for Swift 3 and above
for viewController in viewControllers {
if viewController.isKind(of: OurViewController.self){
print("yes it is OurViewController")
self.navigationController?.popToViewController(viewController, animated: true)
}
}