iOS: KVO Memory Leak? - ios

Note: I am posting this as a reference for other developers that might run into the same issue.
Why do I have a memory leak with this code:
#interface SPWKThing : NSObject
#property (strong, nonatomic) NSArray *things;
#end
#implementation SPWKThing {
BOOL _isKVORegistered;
}
- (id)init
{
self = [super init];
if (self) {
NSLog(#"initing SPWKThing");
[self registerKVO];
}
return self;
}
- (void)didChangeValueForKey:(NSString *)key {
if ([key isEqualToString:#"things"]) {
NSLog(#"didChangeValueForKey: things have changed!");
}
}
#pragma mark - KVO
- (void)registerKVO
{
if (!_isKVORegistered) {
NSLog(#"Registering KVO, and things is %#", _things);
[self addObserver:self forKeyPath:#"things" options:0 context:NULL];
_isKVORegistered = YES;
}
}
- (void)unregisterKVO
{
if (_isKVORegistered) {
NSLog(#"Unregistering KVO");
[self removeObserver:self forKeyPath:#"things"];
_isKVORegistered = NO;
}
}
- (void)dealloc
{
NSLog(#"SPWKThing dealloc");
[self unregisterKVO];
}
#end
#implementation SPWKViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self runDemo];
}
- (void)runDemo
{
SPWKThing *thing = [[SPWKThing alloc] init];
thing.things = #[#"one", #"two", #"three"];
thing = nil;
}
#end
My output is:
initing SPWKThing
Registering KVO, and things is (null)
didChangeValueForKey: things have changed!
dealloc is never called? Why? I am setting thing = nil in the last line of runDemo!
See a demo project here: https://github.com/jfahrenkrug/KVOMemoryLeak

The answer is:
Never override didChangeValueForKey: (at least not without calling super). The documentation does not warn you about this.
Use the correct method observeValueForKeyPath:ofObject:change:context: instead.
This project clearly demonstrates this: https://github.com/jfahrenkrug/KVOMemoryLeak

Related

iOS Objective-C block callback issue

I am trying to use pure code to create UI practice block pass value between viewController. But the callback block didn't work. The NSLog method didn't print anything on debug area. Here's the code. Give me some tips, thank you.
VC.h
#import <UIKit/UIKit.h>
#interface SecondViewController : UIViewController
#property (copy, nonatomic) void (^callBack)(NSString *text);
#end
VC.m
- (UITextField *)textField {
if (!_textField) {
_textField = [[UITextField alloc] init];
_textField.backgroundColor = [UIColor whiteColor];
}
return _textField;
}
- (UIButton *)button {
if (!_button) {
_button = [[UIButton alloc] init];
_button.backgroundColor = [UIColor blueColor];
[_button addTarget:self action:#selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
}
return _button;
}
- (void)setupUI {
[self.view addSubview:self.textField];
[self.view addSubview:self.button];
[self.textField mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(200);
make.height.mas_equalTo(50);
make.centerX.mas_equalTo(self.view.mas_centerX);
make.centerY.mas_equalTo(self.view);
}];
[self.button mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.mas_equalTo(200);
make.height.mas_equalTo(50);
make.centerX.mas_equalTo(self.view);
make.centerY.mas_equalTo(self.view).offset(100);
}];
}
- (void)buttonAction {
NSString *str = self.textField.text;
if (self.callBack != nil) {
self.callBack(str);
NSLog(#"This statement didnt print in log");
}
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor redColor];
}
update code
VC2.m
- (void)viewWillAppear:(BOOL)animated{
self.callBack = ^(NSString *text){
};
}
- (void)buttonAction {
if (self.callBack) {
NSLog(#"It worked on debug area %#", self.textField.text);
self.callBack(self.textField.text);
}
self.textField.text = #"";
}
VC1.m
- (void)viewDidLoad {
[super viewDidLoad];
_secondVc = [[SecondViewController alloc] init];
_secondVc.callBack = ^(NSString *str){
};
[self setupUI];
self.view.backgroundColor = [UIColor greenColor];
}
- (void)viewWillAppear:(BOOL)animated {
if (_secondVc.callBack != nil) {
NSLog(#"It wrked on debug screen");
_secondVc.callBack = ^(NSString *str){
NSLog(#"It didn't worked on debug screen");
//I want set my label.text = str;
};
};
}
The only way is that you property
#property (copy, nonatomic) void (^callBack)(NSString *text);
is empty. Try to put breakpoint in buttonAction method and look at the property.
As Sander and KrishnaCA mentioned your callBack is nil. I would suggest you create a definition of the block like this:
typedef void(^TextBlock)(NSString *text);
Then change your property to:
#property (copy, nonatomic) TextBlock callBack;
Create a copy of the block in your first view controller:
#interface FirstViewController()
#property (copy, nonatomic) TextBlock firstViewControllerCallBack;
#end
Initialize the callback copy (i.e. in viewDidLoad)
- (void)viewDidLoad {
[super viewDidLoad];
self.firstViewControllerCallBack = ^(NSString *text){
NSLog(#"Second view controller's button tapped!");
};
}
Assign the callback to the second view controller right before presenting/pushing it:
SecondViewController *secondVC = [[SecondViewController alloc] init];
secondVC.callBack = self.firstViewControllerCallBack; // Assign the callback
// ... Presenting the view controller
Clean up the completion block after you done with it (i.e. in viewWillDisappear):
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
self.firstViewControllerCallBack = nil;
}

Subclass WKWebview crash on dealloc in iOS

I'm writing an app using WKWebView framework. I declare a new class named CustomWebView inherited from WKWebView
#interface CustomWebView : WKWebView {
id customObject;
}
#end
#implement CustomWebView
- (id)init {
if (self = [super init]) {
customObject = [[NSObject alloc] init];
}
return self;
}
- (void)dealloc {
//Doing my dealloc stuff
[customObject release];
[super dealloc];
}
#end
I declare a new instance of CustomWebView on another UIView class
cusWebview = [[CustomWebView alloc] init];
cusWebview.backgroundColor = [UIColor clearColor];
cusWebview.layer.cornerRadius = 4;
cusWebview.UIDelegate = self;
cusWebview.navigationDelegate = self;
[self addSubView:cusWebview];
[cusWebview release];
But when CustomWebView instance dealloc, it crashes on its dealloc method on line
[super dealloc];
just break on this line with EXC_BAD_ACCESS
Can anyone know the reason
You don't need to be calling [super dealloc]
Also make sure you aren't adding observing any KVO's and not removing them in dealloc.
To be safe, I would also set your navigationDelegate and UIDelegate to nil on dealloc.

BOOL declared Globally but certain functions can't see it

Ok, I am going crazy here. I am using Xcode 6.4 and I also tried new 7 beta 3.
What happens is that anything (for example BOOL) that i declare as a global variable can't be seen by certain methods/functions.
-(void)loadView can see it no problem but
-(void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error and some others can't.
I know that globals are dangerous but please let me know what I am doing wrong. Thanks!
my h file:
#interface BannerViewController : UIViewController
{
BOOL isInternetActive;
}
m file:
-(void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error
{
isInternetActive = NO; //it doesn't compile because of this. Error is "Use of undeclared identifier 'isInternetActive'
}
EDITED to show entire h and m file:
h file:
#import <UIKit/UIKit.h>
#import <iAd/iAd.h>
//#import <GoogleMobileAds/GoogleMobileAds.h> //Google
#import GoogleMobileAds;
extern NSString * const BannerViewActionWillBegin;
extern NSString * const BannerViewActionDidFinish;
#interface TestBannerViewController : UIViewController
{
GADBannerView *admobBannerView;
}
#property (nonatomic) BOOL isInternetActive;
- (instancetype)initWithContentViewController:(UIViewController *)contentController;
#end
m file:
#import "TestBannerViewController.h"
//#import <GoogleMobileAds/GoogleMobileAds.h> //Google
#import GoogleMobileAds;
NSString * const BannerViewActionWillBegin = #"BannerViewActionWillBegin";
NSString * const BannerViewActionDidFinish = #"BannerViewActionDidFinish";
#interface TestBannerViewController ()
// This method is used by BannerViewSingletonController to inform instances of TestBannerViewController that the banner has loaded/unloaded.
- (void)updateLayout;
#end
#interface BannerViewManager : NSObject <ADBannerViewDelegate>
#property (nonatomic, readonly) ADBannerView *bannerView;
//#property (nonatomic, weak) GADBannerView *admobBannerView; //Google
+ (BannerViewManager *)sharedInstance;
- (void)addBannerViewController:(TestBannerViewController *)controller;
- (void)removeBannerViewController:(TestBannerViewController *)controller;
#end
#implementation TestBannerViewController {
UIViewController *_contentController;
}
#synthesize isInternetActive;
- (instancetype)initWithContentViewController:(UIViewController *)contentController
{
NSAssert(contentController != nil, #"Attempting to initialize a BannerViewController with a nil contentController.");
self = [super init];
if (self != nil) {
_contentController = contentController;
[[BannerViewManager sharedInstance] addBannerViewController:self];
}
return self;
}
- (void)dealloc
{
[[BannerViewManager sharedInstance] removeBannerViewController:self];
}
- (void)loadView
{
UIView *contentView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// Setup containment of the _contentController.
[self addChildViewController:_contentController];
[contentView addSubview:_contentController.view];
[_contentController didMoveToParentViewController:self];
NSLog(#"Google Mobile Ads SDK version: %#", [GADRequest sdkVersion]);
self.view = contentView;
}
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
return [_contentController shouldAutorotateToInterfaceOrientation:interfaceOrientation];
}
#endif
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
return [_contentController preferredInterfaceOrientationForPresentation];
}
- (NSUInteger)supportedInterfaceOrientations
{
return [_contentController supportedInterfaceOrientations];
}
- (void)viewDidLayoutSubviews
{
CGRect contentFrame = self.view.bounds, bannerFrame = CGRectZero;
ADBannerView *bannerView = [BannerViewManager sharedInstance].bannerView;
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0
NSString *contentSizeIdentifier;
if (contentFrame.size.width < contentFrame.size.height) {
contentSizeIdentifier = ADBannerContentSizeIdentifierPortrait;
} else {
contentSizeIdentifier = ADBannerContentSizeIdentifierLandscape;
}
bannerFrame.size = [ADBannerView sizeFromBannerContentSizeIdentifier:contentSizeIdentifier];
#else
bannerFrame.size = [bannerView sizeThatFits:contentFrame.size];
#endif
if (bannerView.bannerLoaded) {
contentFrame.size.height -= bannerFrame.size.height;
bannerFrame.origin.y = contentFrame.size.height;
} else {
//contentFrame.size.height -= bannerFrame.size.height;
bannerFrame.origin.y = contentFrame.size.height;
}
_contentController.view.frame = contentFrame;
if (self.isViewLoaded && (self.view.window != nil)) {
[self.view addSubview:bannerView];
bannerView.frame = bannerFrame;
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_6_0
bannerView.currentContentSizeIdentifier = contentSizeIdentifier;
#endif
}
}
- (void)updateLayout
{
[UIView animateWithDuration:0.25 animations:^{
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.view addSubview:[BannerViewManager sharedInstance].bannerView];
}
- (NSString *)title
{
return _contentController.title;
}
#end
#implementation BannerViewManager {
ADBannerView *_bannerView;
NSMutableSet *_bannerViewControllers;
}
+ (BannerViewManager *)sharedInstance
{
static BannerViewManager *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[BannerViewManager alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
self = [super init];
if (self != nil) {
if ([ADBannerView instancesRespondToSelector:#selector(initWithAdType:)]) {
_bannerView = [[ADBannerView alloc] initWithAdType:ADAdTypeBanner];
} else {
_bannerView = [[ADBannerView alloc] init];
}
_bannerView.delegate = self;
_bannerViewControllers = [[NSMutableSet alloc] init];
}
return self;
}
- (void)addBannerViewController:(TestBannerViewController *)controller
{
[_bannerViewControllers addObject:controller];
}
- (void)removeBannerViewController:(TestBannerViewController *)controller
{
[_bannerViewControllers removeObject:controller];
}
- (void)bannerViewDidLoadAd:(ADBannerView *)banner
{
for (TestBannerViewController *bvc in _bannerViewControllers) {
[bvc updateLayout];
}
}
-(void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error
{
for (TestBannerViewController *bvc in _bannerViewControllers) {
[bvc updateLayout];
}
isInternetActive = YES;
}
- (BOOL)bannerViewActionShouldBegin:(ADBannerView *)banner willLeaveApplication:(BOOL)willLeave
{
[[NSNotificationCenter defaultCenter] postNotificationName:BannerViewActionWillBegin object:self];
return YES;
}
- (void)bannerViewActionDidFinish:(ADBannerView *)banner
{
[[NSNotificationCenter defaultCenter] postNotificationName:BannerViewActionDidFinish object:self];
}
#end
You can create new header file in your project.
In that you can write like this :
static BOOL isInternetActive;
Import this header file where you want to access this bool value.
Hope this helps.
I think I got it. in the m file there are two #implementation files (two classes?) and that is why when declaring global in the h file the last class can't see it.

iOS: Is there currently a way to prevent two view controllers being pushed or popped at the same time?

The only solution I have seen was an answer to a stackoverflow question. I posted the link below. The answer I am referring is the 5th one. It seems that some users have some problems with the solution however. I don't know if there is another category to prevent two controllers from being pushed at the same time. Any tips or suggestions are appreciated.
#import "UINavigationController+Consistent.h"
#import <objc/runtime.h>
/// This char is used to add storage for the isPushingViewController property.
static char const * const ObjectTagKey = "ObjectTag";
#interface UINavigationController ()
#property (readwrite,getter = isViewTransitionInProgress) BOOL viewTransitionInProgress;
#end
#implementation UINavigationController (Consistent)
- (void)setViewTransitionInProgress:(BOOL)property {
NSNumber *number = [NSNumber numberWithBool:property];
objc_setAssociatedObject(self, ObjectTagKey, number , OBJC_ASSOCIATION_RETAIN);
}
- (BOOL)isViewTransitionInProgress {
NSNumber *number = objc_getAssociatedObject(self, ObjectTagKey);
return [number boolValue];
}
#pragma mark - Intercept Pop, Push, PopToRootVC
/// #name Intercept Pop, Push, PopToRootVC
- (NSArray *)safePopToRootViewControllerAnimated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
self.viewTransitionInProgress = YES;
}
//-- This is not a recursion, due to method swizzling the call below calls the original method.
return [self safePopToRootViewControllerAnimated:animated];
}
- (NSArray *)safePopToViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
self.viewTransitionInProgress = YES;
}
//-- This is not a recursion, due to method swizzling the call below calls the original method.
return [self safePopToViewController:viewController animated:animated];
}
- (UIViewController *)safePopViewControllerAnimated:(BOOL)animated {
if (self.viewTransitionInProgress) return nil;
if (animated) {
self.viewTransitionInProgress = YES;
}
//-- This is not a recursion, due to method swizzling the call below calls the original method.
return [self safePopViewControllerAnimated:animated];
}
- (void)safePushViewController:(UIViewController *)viewController animated:(BOOL)animated {
self.delegate = self;
//-- If we are already pushing a view controller, we dont push another one.
if (self.isViewTransitionInProgress == NO) {
//-- This is not a recursion, due to method swizzling the call below calls the original method.
[self safePushViewController:viewController animated:animated];
if (animated) {
self.viewTransitionInProgress = YES;
}
}
}
// This is confirmed to be App Store safe.
// If you feel uncomfortable to use Private API, you could also use the delegate method navigationController:didShowViewController:animated:.
- (void)safeDidShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
//-- This is not a recursion. Due to method swizzling this is calling the original method.
[self safeDidShowViewController:viewController animated:animated];
self.viewTransitionInProgress = NO;
}
// If the user doesnt complete the swipe-to-go-back gesture, we need to intercept it and set the flag to NO again.
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
id<UIViewControllerTransitionCoordinator> tc = navigationController.topViewController.transitionCoordinator;
[tc notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
self.viewTransitionInProgress = NO;
//--Reenable swipe back gesture.
self.interactivePopGestureRecognizer.delegate = (id<UIGestureRecognizerDelegate>)viewController;
[self.interactivePopGestureRecognizer setEnabled:YES];
}];
//-- Method swizzling wont work in the case of a delegate so:
//-- forward this method to the original delegate if there is one different than ourselves.
if (navigationController.delegate != self) {
[navigationController.delegate navigationController:navigationController
willShowViewController:viewController
animated:animated];
}
}
+ (void)load {
//-- Exchange the original implementation with our custom one.
method_exchangeImplementations(class_getInstanceMethod(self, #selector(pushViewController:animated:)), class_getInstanceMethod(self, #selector(safePushViewController:animated:)));
method_exchangeImplementations(class_getInstanceMethod(self, #selector(didShowViewController:animated:)), class_getInstanceMethod(self, #selector(safeDidShowViewController:animated:)));
method_exchangeImplementations(class_getInstanceMethod(self, #selector(popViewControllerAnimated:)), class_getInstanceMethod(self, #selector(safePopViewControllerAnimated:)));
method_exchangeImplementations(class_getInstanceMethod(self, #selector(popToRootViewControllerAnimated:)), class_getInstanceMethod(self, #selector(safePopToRootViewControllerAnimated:)));
method_exchangeImplementations(class_getInstanceMethod(self, #selector(popToViewController:animated:)), class_getInstanceMethod(self, #selector(safePopToViewController:animated:)));
}
#end
iOS app error - Can't add self as subview
Updated answer:
I prefer this solution by nonamelive on Github to what I originally posted: https://gist.github.com/nonamelive/9334458. By subclassing the UINavigationController and taking advantage of the UINavigationControllerDelegate, you can establish when a transition is happening, prevent other transitions from happening during that transition, and do so all within the same class. Here's an update of nonamelive's solution which excludes the private API:
#import "NavController.h"
#interface NavController ()
#property (nonatomic, assign) BOOL shouldIgnorePushingViewControllers;
#end
#implementation NavController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (!self.shouldIgnorePushingViewControllers)
{
[super pushViewController:viewController animated:animated];
}
self.shouldIgnorePushingViewControllers = YES;
}
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
self.shouldIgnorePushingViewControllers = NO;
}
#end
Previous answer:
Problem with this Previous Answer: isBeingPresented and isBeingDismissed only work in viewDidLoad: or viewDidApper:
Although I haven't tested this myself, here is a suggestion.
Since you're using a UINavigationController, you can access the contents of your navigation stack, like so:
NSArray *viewControllers = self.navigationController.viewControllers;
And through that array of view controllers, you can access some or all relevant indices if need be.
Luckily, two especially convenient methods were introduced in iOS 5: isBeingPresented and isBeingDismissed which return "YES" if the view controller is in the process of being presented or being dismissed, respectively; "NO" otherwise.
So, for example, here's one approach:
NSArray *viewControllers = self.navigationController.viewControllers;
for (UIViewController *viewController in viewControllers) {
if (viewController.isBeingPresented || viewController.isBeingDismissed) {
// In this case when a pop or push is already in progress, don't perform
// a pop or push on the current view controller. Perhaps return to this
// method after a delay to check this conditional again.
return;
}
}
// Else if you make it through the loop uninterrupted, perform push or pop
// of the current view controller.
In actuality, you probably won't have to loop through every view controller on the stack, but perhaps this suggestion will help set you off on the right foot.
Here is my approach, using a UINavigationController category and method swizzling.
The method -[UINavigationController didShowViewController:animated:] is private, so although it has been reported safe to use, use at you own risks.
Credits goes to this answer for the idea and NSHipster for the method swizzling code.
This answer also has an interesting approach.
//
// UINavigationController+Additions.h
//
#interface UINavigationController (Additions)
#property (nonatomic, getter = isViewTransitionInProgress) BOOL viewTransitionInProgress;
#end
//
// UINavigationController+Additions.m
//
#import "UINavigationController+Additions.h"
#import <objc/runtime.h>
static void *UINavigationControllerViewTransitionInProgressKey = &UINavigationControllerViewTransitionInProgressKey;
#interface UINavigationController ()
// Private method, use at your own risk.
- (void)didShowViewController:(UIViewController *)viewController animated:(BOOL)animated;
#end
#implementation UINavigationController (Additions)
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector1 = #selector(pushViewController:animated:);
SEL swizzledSelector1 = #selector(zwizzledForViewTransitionInProgress_pushViewController:animated:);
Method originalMethod1 = class_getInstanceMethod(class, originalSelector1);
Method swizzledMethod1 = class_getInstanceMethod(class, swizzledSelector1);
BOOL didAddMethod1 = class_addMethod(class, originalSelector1, method_getImplementation(swizzledMethod1), method_getTypeEncoding(swizzledMethod1));
if (didAddMethod1) {
class_replaceMethod(class, swizzledSelector1, method_getImplementation(originalMethod1), method_getTypeEncoding(originalMethod1));
} else {
method_exchangeImplementations(originalMethod1, swizzledMethod1);
}
SEL originalSelector2 = #selector(didShowViewController:animated:);
SEL swizzledSelector2 = #selector(zwizzledForViewTransitionInProgress_didShowViewController:animated:);
Method originalMethod2 = class_getInstanceMethod(class, originalSelector2);
Method swizzledMethod2 = class_getInstanceMethod(class, swizzledSelector2);
BOOL didAddMethod2 = class_addMethod(class, originalSelector2, method_getImplementation(swizzledMethod2), method_getTypeEncoding(swizzledMethod2));
if (didAddMethod2) {
class_replaceMethod(class, swizzledSelector2, method_getImplementation(originalMethod2), method_getTypeEncoding(originalMethod2));
} else {
method_exchangeImplementations(originalMethod2, swizzledMethod2);
}
});
}
- (void)zwizzledForViewTransitionInProgress_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if (self.viewTransitionInProgress) {
LogWarning(#"Pushing a view controller while an other view transition is in progress. Aborting.");
} else {
self.viewTransitionInProgress = YES;
[self zwizzledForViewTransitionInProgress_pushViewController:viewController animated:animated];
}
}
- (void)zwizzledForViewTransitionInProgress_didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[self zwizzledForViewTransitionInProgress_didShowViewController:viewController animated:YES];
self.viewTransitionInProgress = NO;
}
- (void)setViewTransitionInProgress:(BOOL)viewTransitionInProgress
{
NSNumber *boolValue = [NSNumber numberWithBool:viewTransitionInProgress];
objc_setAssociatedObject(self, UINavigationControllerViewTransitionInProgressKey, boolValue, OBJC_ASSOCIATION_RETAIN);
}
- (BOOL)isViewTransitionInProgress
{
NSNumber *viewTransitionInProgress = objc_getAssociatedObject(self, UINavigationControllerViewTransitionInProgressKey);
return [viewTransitionInProgress boolValue];
}
#end
Inspired by #Lindsey Scott answer I created UINavigationController subclass. The advantage of my solution is that it also handles popping, and you can actually execute all requests after each other without problems(this is controlled via acceptConflictingCommands flag).
MyNavigationController.h
#import <UIKit/UIKit.h>
#interface MyNavigationController : UINavigationController
#property(nonatomic, assign) BOOL acceptConflictingCommands;
#end
MyNavigationController.m
#import "MyNavigationController.h"
#interface MyNavigationController ()<UINavigationControllerDelegate>
#property(nonatomic, assign) BOOL shouldIgnoreStackRequests;
#property(nonatomic, strong) NSMutableArray* waitingCommands;
#end
#implementation MyNavigationController
-(instancetype)init
{
if( self = [super init] )
{
self.delegate = self;
_waitingCommands = [NSMutableArray new];
}
return self;
}
-(instancetype)initWithRootViewController:(UIViewController *)rootViewController
{
if( self = [super initWithRootViewController:rootViewController] )
{
self.delegate = self;
_waitingCommands = [NSMutableArray new];
_acceptConflictingCommands = YES;
}
return self;
}
-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
if( !_shouldIgnoreStackRequests )
{
[super pushViewController:viewController animated:animated];
_shouldIgnoreStackRequests = YES;
}
else if (_acceptConflictingCommands)
{
__weak typeof(self) weakSelf = self;
//store and push it after current transition ends
[_waitingCommands addObject:^{
id strongSelf = weakSelf;
[strongSelf pushViewController:viewController animated:animated];
}];
}
}
-(UIViewController *)popViewControllerAnimated:(BOOL)animated
{
__block UIViewController* popedController = nil;
if( 1 < self.viewControllers.count )
{
if( !_shouldIgnoreStackRequests )
{
popedController = [super popViewControllerAnimated:animated];
_shouldIgnoreStackRequests = YES;
}
else if( _acceptConflictingCommands )
{
__weak typeof(self) weakSelf = self;
[_waitingCommands addObject:^{
id strongSelf = weakSelf;
popedController = [strongSelf popViewControllerAnimated:animated];
}];
}
}
return popedController;
}
#pragma mark - uinavigationcontroller delegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
_shouldIgnoreStackRequests = NO;
if( 0 < _waitingCommands.count )
{
void(^waitingAction)() = _waitingCommands.lastObject;
[_waitingCommands removeLastObject];
waitingAction();
}
}
#end
Of course you can change default value of acceptConflictingCommands or control it externally.
If your code happens to use popToRootViewController, setViewControllers:animated: and/or popToViewController you have to override them in the same manner to make sure they won't brake navigation stack.

Custom alert view in iOS issue

In my app I've to create a custom alert view like the following:
So I followed this tutorial to create a custom alert view. I finished it but I'm getting issue in the following method:
- (void)addOrRemoveButtonWithTag:(int)tag andActionToPerform:(BOOL)shouldRemove {
NSMutableArray *items = [[NSMutableArray alloc]init];
[items addObject:self.buttonOk];
[items addObject:self.buttonClose];
int buttonIndex = (tag == 1);
if (shouldRemove) {
[items removeObjectAtIndex:buttonIndex];
} else {
if (tag == 1) {
[items insertObject:self.buttonOk atIndex:buttonIndex];
} else {
[items insertObject:self.buttonClose atIndex:buttonIndex];
}
}
}
I edited it than the tutorial because I don't need a UIToolBar for buttons. When I run the app it says me that I can't insert a nil object in an NSMutableArray, but I don't understand what's wrong, I hope you can help me to fix this issue.
UPDATE
Here's all the class code I developed:
#import "CustomAlertViewController.h"
#define ANIMATION_DURATION 0.25
#interface CustomAlertViewController ()
- (IBAction)buttonOk:(UIButton *)sender;
- (IBAction)buttonCancel:(UIButton *)sender;
#property (weak, nonatomic) IBOutlet UIButton *buttonClose;
#property (weak, nonatomic) IBOutlet UIButton *buttonOk;
#property (strong, nonatomic) IBOutlet UIView *viewAlert;
-(void)addOrRemoveButtonWithTag:(int)tag andActionToPerform:(BOOL)shouldRemove;
#end
#implementation CustomAlertViewController
- (id)init
{
self = [super init];
if (self) {
[self.viewAlert setFrame:CGRectMake(self.labelAlertView.frame.origin.x,
self.labelAlertView.frame.origin.y,
self.labelAlertView.frame.size.width,
self.viewAlert.frame.size.height)];
[self.buttonOk setTag:1];
[self.buttonClose setTag:0];
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)showCustomAlertInView:(UIView *)targetView withMessage:(NSString *)message {
CGFloat statusBarOffset;
if (![[UIApplication sharedApplication] isStatusBarHidden]) {
CGSize statusBarSize = [[UIApplication sharedApplication] statusBarFrame].size;
if (statusBarSize.width < statusBarSize.height) {
statusBarOffset = statusBarSize.width;
} else {
statusBarOffset = statusBarSize.height;
}
} else {
statusBarOffset = 0.0;
}
CGFloat width, height, offsetX, offsetY;
if ([[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationLandscapeLeft ||
[[UIApplication sharedApplication] statusBarOrientation] == UIInterfaceOrientationLandscapeRight) {
width = targetView.frame.size.width;
height = targetView.frame.size.height;
offsetX = 0.0;
offsetY = -statusBarOffset;
}
[self.view setFrame:CGRectMake(targetView.frame.origin.x, targetView.frame.origin.y, width, height)];
[self.view setFrame:CGRectOffset(self.view.frame, offsetX, offsetY)];
[targetView addSubview:self.view];
[self.viewAlert setFrame:CGRectMake(0.0, -self.viewAlert.frame.size.height, self.viewAlert.frame.size.width, self.viewAlert.frame.size.height)];
[UIView beginAnimations:#"" context:nil];
[UIView setAnimationDuration:ANIMATION_DURATION];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
[self.viewAlert setFrame:CGRectMake(0.0, 0.0, self.viewAlert.frame.size.width, self.viewAlert.frame.size.height)];
[UIView commitAnimations];
[self.labelAlertView setText:#"CIAO"];
}
- (void)removeCustomAlertFromView {
[UIView beginAnimations:#"" context:nil];
[UIView setAnimationDuration:ANIMATION_DURATION];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
[self.viewAlert setFrame:CGRectMake(0.0, -self.viewAlert.frame.size.height, self.viewAlert.frame.size.width, self.viewAlert.frame.size.height)];
[UIView commitAnimations];
[self.view performSelector:#selector(removeFromSuperview) withObject:nil afterDelay:ANIMATION_DURATION];
}
- (void)removeCustomAlertFromViewInstantly {
[self.view removeFromSuperview];
}
- (BOOL)isOkayButtonRemoved {
if (self.buttonOk == nil) {
return YES;
} else {
return NO;
}
}
- (BOOL)isCancelButtonRemoved {
if (self.buttonClose == nil) {
return YES;
} else {
return NO;
}
}
- (void)removeOkayButton:(BOOL)shouldRemove {
if ([self isOkayButtonRemoved] != shouldRemove) {
[self addOrRemoveButtonWithTag:1 andActionToPerform:shouldRemove];
}
}
- (void)removeCancelButton:(BOOL)shouldRemove {
if ([self isCancelButtonRemoved] != shouldRemove) {
[self addOrRemoveButtonWithTag:0 andActionToPerform:shouldRemove];
}
}
- (void)addOrRemoveButtonWithTag:(int)tag andActionToPerform:(BOOL)shouldRemove {
NSMutableArray *items = [[NSMutableArray alloc]init];
[items addObject:self.buttonOk];
[items addObject:self.buttonClose];
int buttonIndex = (tag == 1);
if (shouldRemove) {
[items removeObjectAtIndex:buttonIndex];
} else {
if (tag == 1) {
[items insertObject:self.buttonOk atIndex:buttonIndex];
} else {
[items insertObject:self.buttonClose atIndex:buttonIndex];
}
}
}
- (IBAction)buttonOk:(UIButton *)sender {
[self.delegate customAlertOk];
}
- (IBAction)buttonCancel:(UIButton *)sender {
[self.delegate customAlertCancel];
}
#end
UPDATE 2
Code in which I use the CustomAlertView:
#import "PromotionsViewController.h"
#import "CustomAlertViewController.h"
#interface PromotionsViewController () <CustomAlertViewControllerDelegate> {
BOOL isDeletingItem;
}
#property(nonatomic,strong) CustomAlertViewController *customAlert;
- (IBAction)buttonBack:(UIButton *)sender;
#property (weak, nonatomic) IBOutlet UIButton *buttonAlert;
- (IBAction)buttonAlert:(UIButton *)sender;
#end
#implementation PromotionsViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self.buttonAlert setTitle:self.promotionSelected forState:UIControlStateNormal];
[self.customAlert setDelegate:self];
isDeletingItem = NO;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)buttonBack:(UIButton *)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)buttonAlert:(UIButton *)sender {
self.customAlert = [[CustomAlertViewController alloc]init];
[self.customAlert removeOkayButton:NO];
[self.customAlert removeCancelButton:NO];
NSString *message = [NSString stringWithFormat:#"La tua offerta %# del 20%% è stata convertita in punti IoSi x10", self.promotionSelected];
[self.customAlert showCustomAlertInView:self.view withMessage:message];
isDeletingItem = YES;
}
- (void)customAlertOk {
if (isDeletingItem) {
[self.customAlert removeCustomAlertFromViewInstantly];
} else {
[self.customAlert removeCustomAlertFromView];
}
}
- (void)customAlertCancel {
[self.customAlert removeCustomAlertFromView];
if (isDeletingItem) {
isDeletingItem = NO;
}
}
#end
Maybe you're calling addOrRemoveButtonWithTag:andActionToPerform: at a time where your UI is not fully created, since UI elements are created asynchronously. So if you call this method, right after custom alert view instanciation, you'll get your crash because the buttons in the view are not created.
To solve this issue, you need to call addOrRemoveButtonWithTag:andActionToPerform: only once your custom alert has been added to the view hierarchy.
EDIT :
With the example code you gave in edit 2, you call these lines :
- (IBAction)buttonAlert:(UIButton *)sender {
self.customAlert = [[CustomAlertViewController alloc]init];
[self.customAlert removeOkayButton:NO];
[self.customAlert removeCancelButton:NO];
}
but when you have just instantiated CustomAlertViewController, its 2 buttons are not yet created, so I suggest you add 2 properties hasOkButton and hasCancelButton and a new constructor to your custom class like this one :
- (instancetype) initWithOk:(BOOL)OkButton AndCancel:(BOOL) CancelButton
{
if(self = [super init])
{
hasOkButton = OkButton;
hasCancelButton = CancelButton;
}
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// At this time, the custom UI buttons will be created in the UI view hierarchy
[self removeOkayButton: hasOkButton];
[self removeOkayButton: hasCancelButton];
}
And in the caller you can use the following to display a custom alert View:
- (IBAction)buttonAlert:(UIButton *)sender {
self.customAlert = [[CustomAlertViewController alloc] initWithOk:NO AndCancel:NO];
// ...
}
EDIT #2
I tried your solution in a real project, I made it work by using these lines int the caller :
- (IBAction)buttonAlert:(UIButton *)sender {
self.customAlert = [self.storyboard instantiateViewControllerWithIdentifier:#"customAlertView"];
self.customAlert.hasOK = NO;
self.customAlert.hasCancel = YES;
NSString *message = [NSString stringWithFormat:#"La tua offerta %# del 20%% è stata convertita in punti IoSi x10", self.promotionSelected];
[self.customAlert showCustomAlertInView:self.view withMessage:message];
isDeletingItem = YES;
}
In the CustomAlertViewController declare 2 visible properties hasOK and hasCancel in.h.
And modify your .m by adding method :
-(void)viewWillAppear:(BOOL)animated
{
[self removeOkayButton:self.hasOK];
[self removeCancelButton:self.hasCancel];
}
Be sure to modify your storyboard (if eligible) to have the "customAlertView" defined this way :
Don't forget also to bind your UIButton to the controller this can be a mistake too in your implementation :
Hope this will help you :)
I found on the web a tutorial to create custom alert view by using code, if you are interested you can go to this tutorial. I used it for my issue and it worked great! You have to fix a few things, because it uses deprecated command but it's easy to fix it.
If you are interested just take a look about this tutorial. I think you can integrate it in your app and after you can easily use for other stuff if it's necessary. I hope that my answer will help someone.

Resources