UITraitCollection - swift to objective-C - ios

I'm trying to convert swift to objective-C. Here is my code below:
Swift
import UIKit
protocol TraitCollectionOverridable {
func preferredTraitCollection() -> UITraitCollection?
}
class CustomNavigationController: UINavigationController {
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
guard let conformingViewController = childViewController as? TraitCollectionOverridable else {
return super.overrideTraitCollectionForChildViewController(childViewController)
}
return conformingViewController.preferredTraitCollection()
}
}
Objective-C
header file
#protocol TraitCollectionOverridable <NSObject>
- (UITraitCollection *) preferredTraitCollection;
#end
#interface CustomNavigationController : UINavigationController
#property (nonatomic, weak) id <TraitCollectionOverridable> traitsDelegate;
#end
.m file
#implementation CustomNavigationController
- (void)viewDidLoad {
[super viewDidLoad];
[self.traitsDelegate preferredTraitCollection];
// Do any additional setup after loading the view.
}
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if (CGRectGetWidth(self.view.bounds) < CGRectGetHeight(self.view.bounds)) {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassCompact];
} else {
return [UITraitCollection traitCollectionWithHorizontalSizeClass:UIUserInterfaceSizeClassRegular];
}
}
#end
My guess is overrideTraitCollectionForChildViewController is not converted properly. Any help on that would be great.

My guess is overrideTraitCollectionForChildViewController is not converted properly.
The Obj-C & Swift versions of overrideTraitCollectionForChildViewController aren't equivalent. Here is the corrected code:
Obj-C
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController
{
if( [childViewController conformsToProtocol:#protocol(TraitCollectionOverridable)] ) {
return [(NSObject<TraitCollectionOverridable>*)childViewController preferredTraitCollection];
} else {
return [super overrideTraitCollectionForChildViewController:childViewController];
}
}
Swift
override func overrideTraitCollectionForChildViewController(childViewController: UIViewController) -> UITraitCollection? {
guard let conformingViewController = childViewController as? TraitCollectionOverridable else {
return super.overrideTraitCollectionForChildViewController(childViewController)
}
return conformingViewController.preferredTraitCollection()
}

Related

Is it okay to have a lot of segues between view controllers?

So I'm quite new to iOS developing and have decided to venture down the swift path. I'm now working with multiple view controllers and passing data between, and what I'm wondering is if it is bad practice to have multiple (like 9-15) segues between 2 view controllers?
Basically what I have is a normal VC with 9 different buttons. Now, what I want is for each button to send different data to the subsequent tabbed VC instead of having a different VC for each button/catagory.
it would look something like this :
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let tabBarController = segue.destinationViewController as! UITabBarController
let svc = tabBarController.viewControllers![0] as! InfoViewController
let svc2 = tabBarController.viewControllers![1] as! DIYViewController
if segue.identifier == "wind" {
svc.titleString = "Wind Power... what a powerful thing"
} else if segue.identifier == "geothermal" {
//send info about geothermal
} else if segue.identifier == "hydroelectric" {
//send infor about hydroelectricity
} else if segue.identifier == "" {
//code
} else if segue.identifier == "" {
//code
} else if segue.identifier == "" {
//code
} else if segue.identifier == "" {
//code
}
}
is there a more conventional way to accomplish this?
You should have only one segue, and pass the data in prepareForSegue(_:sender:). E.g. In your first view controller, you pass the variable:
class ViewController: UIViewController {
var myVariable: String!
#IBAction func button1Action(sender: AnyObject) {
myVariable = "Hello"
}
#IBAction func button2Action(sender: AnyObject) {
myVariable = "Hola"
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue == "MySegue" {
let secondViewController = segue.destinationViewController as! ViewController
secondViewController.myVariable = myVariable
}
}
}
And in your second view controller, you can do whatever you want with your variable:
class SecondViewController: UIViewController {
var myVariable: String!
override func viewDidLoad() {
title = myVariable
}
}
You can create a NSObject subclass (e.g. MyAtmospherologyObject) and put all the needed properties within to store all the info for all 9 categories you might needed.
Then give it a type property and use enum for the only segue to determine what it was getting and how it should handle the rest of the data.
After this, you only need to send one object, which is the MyAtmospherologyObject. This could be useful as you can pass around the object anywhere as a whole once you initialized it, and reuse it from there.
e.g. Objective-C (I know it's not Swift, but you get the idea.)
MyAtmospherologyObject.h
//
// MyAtmospherologyObject.h
//
#import Foundation;
typedef NS_ENUM(NSUInteger, MyAtmospherologyType) {
MyAtmospherologyGeothermalType = 0,
MyAtmospherologyHydroelectricType
};
#interface MyAtmospherologyObject : NSObject
#property (nonatomic) MyAtmospherologyType type;
#property (nonatomic, strong) NSString *mySting;
- (MyAtmospherologyType)type;
#end
MyAtmospherologyObject.m
//
// MyAtmospherologyObject.m
//
#import "MyAtmospherologyObject.h"
#implementation MyAtmospherologyObject
- (instancetype)initWithType:(MyAtmospherologyType)aType
{
self = [super init];
if (self) {
_type = aType;
}
return self;
}
#pragma mark - NSKeyedArchiver
- (id)initWithCoder:(NSCoder *)decoder
{
if (self = [super init]) {
_type = [decoder decodeIntegerForKey:#"type"];
_mySting = [decoder decodeObjectForKey:#"mySting"];
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeInteger:_type forKey:#"type"];
[encoder encodeObject:_mySting forKey:#"mySting"];
}
#pragma mark - Methods
- (MyAtmospherologyObject)type
{
return _type;
}
#end
MyAtmospherologyViewController.m
// ...
#import "MyAtmospherologyObject.h"
#implementation MyAtmospherologyViewController
// ...
- (IBAction)buttonAction:(id)sender
{
MTKGSJournalQuestionnaireObject *object = [[MTKGSJournalQuestionnaireObject alloc] initWithType:MyAtmospherologyGeothermalType];
object.myString = #"aString";
[self performSegueWithIdentifier:#"MySegueIdentifier"
sender:object];
}
#pragma mark - UIStoryboardSegue
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
MyAtmospherologyObject *object = (MyAtmospherologyObject *)sender;
MyAtmospherologySegueViewController *vc = (MyAtmospherologySegueViewController *)segue.destinationViewController;
switch (object.type) {
case MyAtmospherologyGeothermalType:
vc.navigationItem.title = object.myString;
break;
case MyAtmospherologyHydroelectricType:
vc.navigationItem.title = object.myString;
break;
default:
break;
}
}
// ...
#end

Extending a delegate from a base class

I have an objc base class:
#protocol BaseClassDelegate;
#interface BaseClass : NSObject
#property (nonatomic, weak) id <BaseClassDelegate> delegate;
#end
#protocol BaseClassDelegate <NSObject>
-(void)baseDelegateMethod;
#end
I am creating a swift sub-class in which I want to extend my delegate...
protocol SubClassDelegate : BaseClassDelegate {
func additionalSubClassDelegateMethod();
}
class SubClass: BaseClass {
#IBAction func onDoSomething(sender: AnyObject) {
delegate?.additionalSubClassDelegateMethod(); <--- No such method in 'delegate'
}
}
Now, when I create my sub-class, I can say it conforms to the SubClassDelegate and set the delegate. The problem is (of course), is that 'delegate' doesn't exist in this sub-class. Is there a way to tell the compiler to 'extend' my delegate into my sub-class? (or am I being insane here and missed something obvious)
I'd either create a wrapper delegate to make it the correct type in SubClass.
class SubClass: BaseClass {
var myDelegate: SubClassDelegate? {
get { return delegate as? SubClassDelegate }
set { delegate = newValue }
}
#IBAction func onDoSomething(sender: AnyObject) {
myDelegate?.additionalSubClassDelegateMethod();
}
}
Or simply cast the delegate to the expected type:
(delegate as? SubClassDelegate)?.additionalSubClassDelegateMethod();
Here's a more comprehensive example of how to do this. Thanks to redent84 for pointing me in the right direction.
protocol SubclassDelegate: ClassDelegate {
func subclassDelegateMethod()
}
class Subclass: Class {
// here we assume that super.delegate property exists
#IBAction func buttonPressedOrSomeOtherTrigger() {
if let delegate: SubclassDelegate = self.delegate as? SubclassDelegate {
delegate.subclassDelegateMethod()
}
}
}
And then in your implementation:
extension SomeOtherClass: SubclassDelegate {
let someObject = Subclass()
someObject.delegate = self
func subclassDelegateMethod() {
// yay!
}
}

UIViewController! Does not conform to protocol "LogicValue"

I am trying to convert the following objective-c code to swift:
- (UIViewController *)currentViewController
{
UIViewController *viewController = self.rootViewController;
while (viewController.presentedViewController) {
viewController = viewController.presentedViewController;
}
return viewController;
}
#ifdef __IPHONE_7_0
- (UIViewController *)viewControllerForStatusBarStyle
{
UIViewController *currentViewController = [self currentViewController];
while ([currentViewController childViewControllerForStatusBarStyle]) {
currentViewController = [currentViewController childViewControllerForStatusBarStyle];
}
return currentViewController;
}
- (UIViewController *)viewControllerForStatusBarHidden
{
UIViewController *currentViewController = [self currentViewController];
while ([currentViewController childViewControllerForStatusBarHidden]) {
currentViewController = [currentViewController childViewControllerForStatusBarHidden];
}
return currentViewController;
}
#endif
//SWIFT
func currentViewController() -> UIViewController {
var viewController = self.rootViewController
while (viewController.presentedViewController) {
viewController = viewController.presentedViewController
}
return viewController
}
func viewControllerForStatusBarStyle() -> UIViewController {
var cViewController = currentViewController()
while (cViewController.childViewControllerForStatusBarStyle) {
cViewController = cViewController.childViewControllerForStatusBarStyle
}
return cViewController
}
func viewControllerForStatusBarHidden() -> UIViewController {
var cViewController = currentViewController()
while (cViewController.childViewControllerForStatusBarHidden) {
cViewController = cVC.childViewControllerForStatusBarHidden
}
return cViewController
}
But I get some errors saying that UIViewController! Does not conform to protocol "LogicValue" at the following line " while (cViewController.childViewControllerForStatusBarStyle)"
How can I fix this issue?
childViewControllerForStatusBarStyle() is a method not a property
while (cViewController.childViewControllerForStatusBarStyle()) {
cViewController = cViewController.childViewControllerForStatusBarStyle()
}

How can I create a custom UIActivity in iOS?

How can I create a custom UIActivity in iOS?
The reason I want this is to add a Review App button in one of my apps that takes the user to the review section in the App Store. How can I create such a custom UIActivity?
First, create the files. I chose to name mine ActivityViewCustomActivity
Make ActivityViewCustomActivity.h look like this:
#import <UIKit/UIKit.h>
#interface ActivityViewCustomActivity : UIActivity
#end
Make ActivityViewCustomActivity.m look like this:
#import "ActivityViewCustomActivity.h"
#implementation ActivityViewCustomActivity
- (NSString *)activityType
{
return #"yourappname.Review.App";
}
- (NSString *)activityTitle
{
return #"Review App";
}
- (UIImage *)activityImage
{
// Note: These images need to have a transparent background and I recommend these sizes:
// iPadShare#2x should be 126 px, iPadShare should be 53 px, iPhoneShare#2x should be 100
// px, and iPhoneShare should be 50 px. I found these sizes to work for what I was making.
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
return [UIImage imageNamed:#"iPadShare.png"];
}
else
{
return [UIImage imageNamed:#"iPhoneShare.png"];
}
}
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems
{
NSLog(#"%s", __FUNCTION__);
return YES;
}
- (void)prepareWithActivityItems:(NSArray *)activityItems
{
NSLog(#"%s",__FUNCTION__);
}
- (UIViewController *)activityViewController
{
NSLog(#"%s",__FUNCTION__);
return nil;
}
- (void)performActivity
{
// This is where you can do anything you want, and is the whole reason for creating a custom
// UIActivity
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:#"itms-apps://ax.itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=yourappid"]];
[self activityDidFinish:YES];
}
#end
This is what my image looked like:
Here is the .PSD I made: -- malicious link removed --
And here is the original 250 px .png http://i.imgur.com/pGWVj.png
Now in your view controller do this:
#import "ActivityViewCustomActivity.h"
And now wherever you want to display your UIActivityViewController:
NSString *textItem = #"Check out the yourAppNameHere app: itunes http link to your app here";
UIImage *imageToShare = [UIImage imageNamed:#"anyImage.png"];
NSArray *items = [NSArray arrayWithObjects:textItem,imageToShare,nil];
ActivityViewCustomActivity *aVCA = [[ActivityViewCustomActivity alloc]init];
UIActivityViewController *activityVC =
[[UIActivityViewController alloc] initWithActivityItems:items
applicationActivities:[NSArray arrayWithObject:aVCA]];
activityVC.excludedActivityTypes = #[UIActivityTypePostToWeibo, UIActivityTypeAssignToContact, UIActivityTypePrint, UIActivityTypeCopyToPasteboard, UIActivityTypeSaveToCameraRoll];
activityVC.completionHandler = ^(NSString *activityType, BOOL completed)
{
NSLog(#"ActivityType: %#", activityType);
NSLog(#"Completed: %i", completed);
};
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
self.popoverController = [[UIPopoverController alloc] initWithContentViewController:activityVC];
CGRect rect = [[UIScreen mainScreen] bounds];
[self.popoverController
presentPopoverFromRect:rect inView:self.view
permittedArrowDirections:0
animated:YES];
}
else
{
[self presentViewController:activityVC animated:YES completion:nil];
}
Here is my Swift Version - I needed multiple custom actions so I created this class. No need for delegates or protocols.
1. Add the custom Class
class ActivityViewCustomActivity: UIActivity {
var customActivityType = ""
var activityName = ""
var activityImageName = ""
var customActionWhenTapped:( (Void)-> Void)!
init(title: String, imageName:String, performAction: (() -> ()) ) {
self.activityName = title
self.activityImageName = imageName
self.customActivityType = "Action \(title)"
self.customActionWhenTapped = performAction
super.init()
}
override func activityType() -> String? {
return customActivityType
}
override func activityTitle() -> String? {
return activityName
}
override func activityImage() -> UIImage? {
return UIImage(named: activityImageName)
}
override func canPerformWithActivityItems(activityItems: [AnyObject]) -> Bool {
return true
}
override func prepareWithActivityItems(activityItems: [AnyObject]) {
// nothing to prepare
}
override func activityViewController() -> UIViewController? {
return nil
}
override func performActivity() {
customActionWhenTapped()
}
}
2 Use in your View Controller
I've attached it to a UIBarButtonItem, which calls the the following
#IBAction func actionButtonPressed(sender: UIBarButtonItem) {
var sharingItems = [AnyObject]() // nothing to share...
let myCustomActivity = ActivityViewCustomActivity(title: "Mark Selected", imageName: "removePin") {
println("Do something")
}
let anotherCustomActivity = ActivityViewCustomActivity(title: "Reset All", imageName: "reload") {
println("Do something else")
}
let activityViewController = UIActivityViewController(activityItems:sharingItems, applicationActivities:[myCustomActivity, anotherCustomActivity])
activityViewController.excludedActivityTypes = [UIActivityTypeMail, UIActivityTypeAirDrop, UIActivityTypeMessage, UIActivityTypeAssignToContact, UIActivityTypePostToFacebook, UIActivityTypePrint, UIActivityTypeCopyToPasteboard, UIActivityTypeSaveToCameraRoll]
activityViewController.popoverPresentationController?.barButtonItem = sender
self.presentViewController(activityViewController, animated: true, completion: nil)
}
My Swift 3 Implementation based off of DogCoffee's:
class ActivityViewCustomActivity: UIActivity {
// MARK: Properties
var customActivityType: UIActivityType
var activityName: String
var activityImageName: String
var customActionWhenTapped: () -> Void
// MARK: Initializer
init(title: String, imageName: String, performAction: #escaping () -> Void) {
self.activityName = title
self.activityImageName = imageName
self.customActivityType = UIActivityType(rawValue: "Action \(title)")
self.customActionWhenTapped = performAction
super.init()
}
// MARK: Overrides
override var activityType: UIActivityType? {
return customActivityType
}
override var activityTitle: String? {
return activityName
}
override class var activityCategory: UIActivityCategory {
return .share
}
override var activityImage: UIImage? {
return UIImage(named: activityImageName)
}
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
return true
}
override func prepare(withActivityItems activityItems: [Any]) {
// Nothing to prepare
}
override func perform() {
customActionWhenTapped()
}
}
Here's an example of putting up an email composer interface using the -activityViewController method of UIActivity. This shows how to put up a UIKit viewController or your own custom viewController for whatever purpose you choose. It supplants the -performActivity method.
#import <MessageUI/MessageUI.h>
#import <UIKit/UIKit.h>
#interface EPSuggestionsActivity : UIActivity <MFMailComposeViewControllerDelegate>
#end
#implementation EPSuggestionsActivity
....
- (UIViewController *)activityViewController{
MFMailComposeViewController *picker = [[MFMailComposeViewController alloc] init];
picker.mailComposeDelegate = self;
NSString *emailAddress = #"developer#apple.com";
NSArray *toRecipients = #[emailAddress];
[picker setToRecipients:toRecipients];
[picker setSubject:#"Suggestions"];
return picker;
}
#pragma mark - MFMailComposeViewControllerDelegate Method
- (void)mailComposeController:(MFMailComposeViewController*)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError*)error {
[self activityDidFinish:YES]; // Forces the activityViewController to be dismissed
}
#end
Notice that -activityDidFinish is called from the mail composer delegate after the user has dismissed the email interface. This is required to get the UIActivityViewController interface to disappear. If you write your own viewController it will need a delegate method that it calls when finished and you will have to make your subclass of UIActivity the delegate.

Completion handler for UINavigationController "pushViewController:animated"?

I'm about creating an app using a UINavigationController to present the next view controllers.
With iOS5 there´s a new method to presenting UIViewControllers:
presentViewController:animated:completion:
Now I ask me why isn´t there a completion handler for UINavigationController?
There are just
pushViewController:animated:
Is it possible to create my own completion handler like the new presentViewController:animated:completion: ?
See par's answer for another and more up to date solution
UINavigationController animations are run with CoreAnimation, so it would make sense to encapsulate the code within CATransaction and thus set a completion block.
Swift:
For swift I suggest creating an extension as such
extension UINavigationController {
public func pushViewController(viewController: UIViewController,
animated: Bool,
completion: #escaping (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
pushViewController(viewController, animated: animated)
CATransaction.commit()
}
}
Usage:
navigationController?.pushViewController(vc, animated: true) {
// Animation done
}
Objective-C
Header:
#import <UIKit/UIKit.h>
#interface UINavigationController (CompletionHandler)
- (void)completionhandler_pushViewController:(UIViewController *)viewController
animated:(BOOL)animated
completion:(void (^)(void))completion;
#end
Implementation:
#import "UINavigationController+CompletionHandler.h"
#import <QuartzCore/QuartzCore.h>
#implementation UINavigationController (CompletionHandler)
- (void)completionhandler_pushViewController:(UIViewController *)viewController
animated:(BOOL)animated
completion:(void (^)(void))completion
{
[CATransaction begin];
[CATransaction setCompletionBlock:completion];
[self pushViewController:viewController animated:animated];
[CATransaction commit];
}
#end
iOS 7+ Swift
Swift 4:
// 2018.10.30 par:
// I've updated this answer with an asynchronous dispatch to the main queue
// when we're called without animation. This really should have been in the
// previous solutions I gave but I forgot to add it.
extension UINavigationController {
public func pushViewController(
_ viewController: UIViewController,
animated: Bool,
completion: #escaping () -> Void)
{
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator else {
DispatchQueue.main.async { completion() }
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
func popViewController(
animated: Bool,
completion: #escaping () -> Void)
{
popViewController(animated: animated)
guard animated, let coordinator = transitionCoordinator else {
DispatchQueue.main.async { completion() }
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
}
EDIT: I've added a Swift 3 version of my original answer. In this version I've removed the example co-animation shown in the Swift 2 version as it seems to have confused a lot of people.
Swift 3:
import UIKit
// Swift 3 version, no co-animation (alongsideTransition parameter is nil)
extension UINavigationController {
public func pushViewController(
_ viewController: UIViewController,
animated: Bool,
completion: #escaping (Void) -> Void)
{
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator else {
completion()
return
}
coordinator.animate(alongsideTransition: nil) { _ in completion() }
}
}
Swift 2:
import UIKit
// Swift 2 Version, shows example co-animation (status bar update)
extension UINavigationController {
public func pushViewController(
viewController: UIViewController,
animated: Bool,
completion: Void -> Void)
{
pushViewController(viewController, animated: animated)
guard animated, let coordinator = transitionCoordinator() else {
completion()
return
}
coordinator.animateAlongsideTransition(
// pass nil here or do something animated if you'd like, e.g.:
{ context in
viewController.setNeedsStatusBarAppearanceUpdate()
},
completion: { context in
completion()
}
)
}
}
Based on par's answer (which was the only one that worked with iOS9), but simpler and with a missing else (which could have led to the completion never being called):
extension UINavigationController {
func pushViewController(_ viewController: UIViewController, animated: Bool, completion: #escaping () -> Void) {
pushViewController(viewController, animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
func popViewController(animated: Bool, completion: #escaping () -> Void) {
popViewController(animated: animated)
if animated, let coordinator = transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion()
}
} else {
completion()
}
}
}
Currently the UINavigationController does not support this. But there's the UINavigationControllerDelegate that you can use.
An easy way to accomplish this is by subclassing UINavigationController and adding a completion block property:
#interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>
#property (nonatomic,copy) dispatch_block_t completionBlock;
#end
#implementation PbNavigationController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.delegate = self;
}
return self;
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
NSLog(#"didShowViewController:%#", viewController);
if (self.completionBlock) {
self.completionBlock();
self.completionBlock = nil;
}
}
#end
Before pushing the new view controller you would have to set the completion block:
UIViewController *vc = ...;
((PbNavigationController *)self.navigationController).completionBlock = ^ {
NSLog(#"COMPLETED");
};
[self.navigationController pushViewController:vc animated:YES];
This new subclass can either be assigned in Interface Builder or be used programmatically like this:
PbNavigationController *nc = [[PbNavigationController alloc]initWithRootViewController:yourRootViewController];
Here is the Swift 4 version with the Pop.
extension UINavigationController {
public func pushViewController(viewController: UIViewController,
animated: Bool,
completion: (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
pushViewController(viewController, animated: animated)
CATransaction.commit()
}
public func popViewController(animated: Bool,
completion: (() -> Void)?) {
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
popViewController(animated: animated)
CATransaction.commit()
}
}
Just in case someone else needs this.
To expand on #Klaas' answer (and as a result of this question) I've added completion blocks directly to the push method:
#interface PbNavigationController : UINavigationController <UINavigationControllerDelegate>
#property (nonatomic,copy) dispatch_block_t completionBlock;
#property (nonatomic,strong) UIViewController * pushedVC;
#end
#implementation PbNavigationController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.delegate = self;
}
return self;
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
NSLog(#"didShowViewController:%#", viewController);
if (self.completionBlock && self.pushedVC == viewController) {
self.completionBlock();
}
self.completionBlock = nil;
self.pushedVC = nil;
}
-(void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.pushedVC != viewController) {
self.pushedVC = nil;
self.completionBlock = nil;
}
}
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(dispatch_block_t)completion {
self.pushedVC = viewController;
self.completionBlock = completion;
[self pushViewController:viewController animated:animated];
}
#end
To be used as follows:
UIViewController *vc = ...;
[(PbNavigationController *)self.navigationController pushViewController:vc animated:YES completion:^ {
NSLog(#"COMPLETED");
}];
Since iOS 7.0,you can use UIViewControllerTransitionCoordinator to add a push completion block:
UINavigationController *nav = self.navigationController;
[nav pushViewController:vc animated:YES];
id<UIViewControllerTransitionCoordinator> coordinator = vc.transitionCoordinator;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
} completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
NSLog(#"push completed");
}];
Swift 2.0
extension UINavigationController : UINavigationControllerDelegate {
private struct AssociatedKeys {
static var currentCompletioObjectHandle = "currentCompletioObjectHandle"
}
typealias Completion = #convention(block) (UIViewController)->()
var completionBlock:Completion?{
get{
let chBlock = unsafeBitCast(objc_getAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle), Completion.self)
return chBlock as Completion
}set{
if let newValue = newValue {
let newValueObj : AnyObject = unsafeBitCast(newValue, AnyObject.self)
objc_setAssociatedObject(self, &AssociatedKeys.currentCompletioObjectHandle, newValueObj, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
func popToViewController(animated: Bool,comp:Completion){
if (self.delegate == nil){
self.delegate = self
}
completionBlock = comp
self.popViewControllerAnimated(true)
}
func pushViewController(viewController: UIViewController, comp:Completion) {
if (self.delegate == nil){
self.delegate = self
}
completionBlock = comp
self.pushViewController(viewController, animated: true)
}
public func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool){
if let comp = completionBlock{
comp(viewController)
completionBlock = nil
self.delegate = nil
}
}
}
It takes a little more pipework to add this behavior and retain the ability to set an external delegate.
Here's a documented implementation that maintains delegate functionality:
LBXCompletingNavigationController

Resources