Adding view from another class as presentViewController causes UIEvents not working - ios

I am trying to build a simple library with working UIElements. What I am trying to do is, creating UIViewController objects from one class instances and push that new ViewController on the current VC Stack with the presentViewController method.
I can see that the UIElements has been successfully adding on the stack, but GestureRecognizer and UIButton's target does not work. When I am checking on ViewDebug, these settings are <NSNull null>.
This is my class method which I am creating the UI and putting on the current view stack.
-(void)displayAd{
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
//Background Thread
NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullpageCampaign.mainImage]];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
fullPageView = [[UIViewController alloc] init];
fullPageView.view.frame = CurrentVC.view.bounds;
fullPageView.view.backgroundColor = [UIColor redColor];
UIImageView *staticImageView = [[UIImageView alloc] init];
staticImageView.frame = CurrentVC.view.frame;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapDetected)];
singleTap.numberOfTapsRequired = 1;
[staticImageView addGestureRecognizer:singleTap];
staticImageView.userInteractionEnabled = YES;
[fullPageView.view addSubview:staticImageView];
staticImageView.image = [UIImage imageWithData:imageData];
[CurrentVC.view addSubview:fullPageView.view];
//[fullPageView didMoveToParentViewController:self];
[CurrentVC presentViewController:fullPageView animated:YES completion:^{
NSLog(#"Tagon Ads is about to showing.");
UIButton *closeButton = [self createButtonWithAssetName:#"tagonAssets.bundle/close_button" TargetMethod:#"closeModal" andView:staticImageView];
[staticImageView addSubview:closeButton];
[CurrentVC.view bringSubviewToFront:closeButton];
}];
});
});
}
CurrentVC is the current viewController that I am sending as a parameter through my library's method in order to add a new viewController stack on to it.

Where is closeModal action? Probably same class as your currentVC. If so, your closeButton referenced currentVC but you already gone to fullPageView from there. So, your button lost his reference.
Just create new controller, send imageData there, create custom initializer, create new UIImageView and UIButton in there. With this way, your button gonna be reference own root and your problem should be solved.

There are several problems with your code, but first of all, I would recommend a different approach to accomplish what you want. As you can see below, using an instance of UIViewController is not the recommended way. Instead, use a storyboard to set up your view controller and it's components. Your code will be much smaller and your design will be easy to understand and change.
You can read more about UIViewController here
You rarely create instances of the UIViewController class directly.
Instead, you create instances of UIViewController subclasses and use
those objects to provide the specific behaviors and visual appearances
that you need.
Here is another potential. Is fullpageCampaign.mainImage residing remotely or locally? If remotely located, then you should consider changing to NSURLSession instead.
Read more about NSData:dataWithContentsOfURL here
Do not use this synchronous method to request network-based URLs. For
network-based URLs, this method can block the current thread for tens
of seconds on a slow network, resulting in a poor user experience, and
in iOS, may cause your app to be terminated.
Another minor thing is that you add the button to the image view. While this is OK, and might work when you allow user interaction for the image view, a cleaner way to do it is to create a UIView container to hold the image view and the button. The container can then also be the view that you attach the tap gesture recognizer to. That way, the image view can stay as a pure image.
The storyboard approach
First, create a sub-class of UIIViewController. It should look something like this:
FullPageViewController.h
#import <UIKit/UIKit.h>
#interface FullPageViewController : UIViewController
- (void)setImage:(UIImage *)adImage;
#end
FullPageViewController.m
#import "FullPageViewController.h"
#interface FullPageViewController ()
#property (weak, nonatomic) IBOutlet UIImageView *adImageView;
#end
#implementation FullPageViewController
- (void)setImage:(UIImage *)adImage {
self.adImageView.image = adImage;
}
- (IBAction)tappedOnAd:(UIGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateEnded) {
// Do your ad thing here
}
}
- (IBAction)closeButtonPressed:(UIButton *)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
#end
Second, create a storyboard and add your ad view controller to it. Then add an image view, a button, and a tap gesture recognizer to your view controller. The tap gesture recognizer should be dropped on the image view to capture taps from there. You pull all of these objects from the Object Library down right. Also remember to enable user interaction for the image view. There is a property for that on the property page.
You should now have something that looks like this:
Notice the class name top right which should be the name of your new view controller class you just created. Also notice the storyboard ID adVC which you need when instantiating the view controller from code.
The next step is to connect the objects. Select the image view, then drag from the outlet (the ring) under Referencing Outlets to the view controller icon (the yellow icon with a square in) located on top of the view controller window, and select adImageView. The gesture recognizer should already be connected, if you dropped it on the image view when you placed it previously.
Next, connect the action for the close button. Drag from the Touch Up Inside outlet to the view controller icon (the yellow one), and select the closeButtonPressed: method.
Next, connect the tap gesture recognizer to your code. Select it from the list on the left, then drag from Sent Actions to the view controller icon and select tappedOnAd:.
Finally, your code to show the ad looks something like this. This method belongs in your parent view controller.
-(void)displayAd{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
//Background Thread
NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:fullpageCampaign.mainImage]];
dispatch_async(dispatch_get_main_queue(), ^(void){
//Run UI Updates
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"AdPage" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"adVC"];
[vc setImage:[UIImage imageWithData:imageData]];
[self presentViewController:vc animated:YES completion:^{}];
});
});
}

Related

Triggering a Method from another ViewController using a button in a UIView

The issue I am having is that I want to call a method in a navigationController by clicking a button inside a UIView (footer). When I press the button, it should call the method I'm trying to access to open the Video recorder in the code below.
I was told I could implement a delegate method or use a NSNotification. Below is what I have:
My footer (ESPhotoDetailsFooterView.m) has my button that I created. My footer only contains a UIView.
The method I'm trying to access in my footer resides in (ESTabBarController.m)
This is what I am trying to trigger when pressing my button:
RecorderViewController *viewController = [[RecorderViewController alloc] init];
[viewController setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
[self.navController setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
[self.navController pushViewController:viewController animated:NO];
dispatch_async(dispatch_get_main_queue(), ^{
[self presentViewController:self.navController animated:YES completion:nil];
});
I am new to Objective C and understand the basics. I cannot figure out what I need to do to accomplish this. Any help would be much appreciated.
The code for the button is as follows:
// Create a standard UIButton programmatically using convenience method
UIButton *camButton = [UIButton buttonWithType:UIButtonTypeCustom];
// Set the location (x,y) and size (width,height) of the button
camButton.frame = CGRectMake(9.0f, 8.0f, 35.0f, 35.0f);
// Create UIImages from image resources in your application bundle
// using convenience methods (no need to release)
UIImage *normal = [UIImage imageNamed:#"BingComm"];
UIImage *highlighted = [UIImage imageNamed:#"BingCommClick"];
// Set the button's background to an image
[camButton setBackgroundImage:normal forState:UIControlStateNormal];
[camButton setBackgroundImage:highlighted forState:UIControlStateHighlighted];
// Add the target-action for the touch event
#pragma GCC diagnostic ignored "-Wundeclared-selector"
[camButton addTarget:self action:#selector(btnClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.mainView addSubview:camButton];
Yes, in this case delegation is a good choice, basically you will need a delegate object in your footer view. Then you set the delegate to TabBarController at runtime.
When user clicks the button, call you delegate with your method [delegate method]
This is a very important design pattern objective-c, it would be very helpful if you can follow this tutorial to fully understand delegation.
http://www.alexefish.com/post/522641eb31fa2a0015000002
I agree that delegation is important and very much worth learning, but another way to solve the same problem would be to the following:
In your method definition for btnClicked, call the following code
if([self tabBarController]) {
[[self tabBarController] methodToCall];
}
Since your view controller should be embedded within a tab bar controller, it will have this property set. You can then call any public methods contained within the tabBarController class.

Toggle edit mode in another ViewController

I have a ViewController (with a container view) embedded in a Navigation controller. The container contains a pageViewController with one of the 'pages' being a TableViewController (with UITableView outlet: 'aTableView'). I want to trigger the edit mode in the tableViewController when tapping a custom editButton in the navigation bar. When I create a custom editutton in the tableViewController the edit mode works as expected, but when I use the custom editButton in the navigation bar the setEditing bool value remains zero even when I setEditing to YES in the editButton selector. Here's the code:
ViewController.m
-(void)viewDidLoad {
self.editBtn = [UIButton buttonWithType:UIButtonTypeCustom];
[self.editBtn setFrame:CGRectMake(0, 0, 40, 22)];
[self.editBtn addTarget:self action:#selector(goToToggleEdit:) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *editButton=[[UIBarButtonItem alloc]initWithCustomView:self.editBtn];
self.navigationItem.rightBarButtonItem = editButton;
}
-(void)goToToggleEdit:(id)sender
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
TableViewController *tvc = [storyboard instantiateViewControllerWithIdentifier:#"aTableViewController"];
if(something==foo){
[tvc toggleEdit];
}
}
aTableViewController.h
#interface aTableViewController : UITableViewController <UITextFieldDelegate> {
IBOutlet UITableView *aTableView;
}
-(void) toggleEdit ;
#end
aTableViewController.m
-(void)toggleEdit {
[aTableView setEditing:YES animated:YES];
NSLog(aTableView.editing ? #"Yes" : #"No"); // --> logss 'No'.
if (aTableView.editing){
//do something
}
else {
//do something else
}
}
How can I efficiently trigger the edit mode in the tableViewController this way?
Edit
#Bagrat Kirakosian pointed out to me that my view hierarchy (Navigation Controller > View Controller (with containter) > Page View Controller (in container) > Table View Controller) might be the problem. I just want to create a Navigation Bar (with an edit button) that is fixed, therefore I can't embed the Table View Controller directly in a Navigation Controller.
Thanks.
UPDATE: Solution
I have accepted #sebastien's solution although both #sebastien's and #Bagrat's solution work great. #Bagrat's answer includes direct access to the Table View Controllers, while #sebastien's solution calls edit mode in the pageViewController. I think, considering the tricky hierarchy, the latter is a bit more secure.
Here is the code for my View Controller that totally work fine. be sure you configure your bar button in the right method of View Controller lifecycle. Also be sure that your #selector is properly implemented in your code.
In the same View Controller put these two blocks of code
-(void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
UIBarButtonItem *rightBarButton = [[UIBarButtonItem alloc] initWithImage:[[UIImage imageNamed:#"edit_icon.png" ] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] style:UIBarButtonItemStylePlain target:self action:#selector(edit:)];
[rightBarButton setTintColor:[UIColor whiteColor]];
self.navigationItem.rightBarButtonItem = rightBarButton;
}
Afterwards you need also to put your edit selector
-(void)edit:(UIButton *)sender {
// Toggle edit by inverting current edit state
// Also in this block change your right bar button text or image
[self.tableView setEditing:!self.tableView.editing animated:YES];
}
UPDATE 1
After your comment we got to whole another question. You problem is not in the part where you try to call toggle edit. Your problem is the wrong hierarchy of controllers (Navigation Controller > View Controller > Page View Controller > Table View Controller). This might cause a problem. Try to change your controllers like this;
UINavigationController > UIPageViewController > UIViewController(s)
Also it's a good practice to have a UITableView in UIViewController rather than using really dead UITableViewController. Don't forget to connect your tableView IBOutlet (by the way you didn't need it in UITableViewController), also connect datasource and delegate to Files owner. In your MyTableViewVC.h file add this line
#interface MyTableViewVC : UIViewController <UITableViewDataSource, UITableViewDelegate>
After that all your calls will work fine.
UPDATE 2
After analyzing your entire structure I found a mistake that you do every time on the button click.
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
tvc = [[TodolistTableViewController alloc] init];
When you call storyboard every time it's ok but when you do [[TodolistTableViewController alloc] init] you are RE-MAKING the same table view controller every time but not even adding it to your main view. As I told you, your tricky hierarchy might cause difficulties but it has a solution.
In your PageViewController.m make tv1 and tv2 properties in .h file, like so.
#property (strong, nonatomic) UITableViewController *tv1;
#property (strong, nonatomic) UITableViewController *tv2;
Then in the view controller file do this
-(void)toggleEdit:(id)sender
{
PageViewController *current = (PageViewController *)[[self childViewControllers] firstObject];
if ([current isKindOfClass:[PageViewController class]])
{
[((TodolistTableViewController *)[current tv1]) toggleEdit];
}
}
Answer includes all security checks and direct access to your table view controllers, because you might need to change other properties/call functions later.
Now in -(void)toggleEdit:(id)sender you don't re-create your views every time but you catch the ones you already have in your current View Controller.
Good Luck!
Ok, your issue here is that you are trying to access an embedded controller in a wrong way.
You are actually managing 2 differents PageViewController:
The one you generated through your storyboard
The other one you are initiating in your code further
That's why you can't reach the expected result.
First of all, add a new method to your PageViewController:
PageViewController.h:
- (void)editTableAtIndex:(int)index;
PageViewController.m:
- (void)editTableAtIndex:(int)index {
[[self viewControllerAtIndex:index] setEditing:YES];
}
Now, in your main ViewController, access the PageViewController by using childViewControllers:
-(void)toggleEdit:(id)sender
{
PageViewController *pvc = self.childViewControllers[0];
[pvc editTableAtIndex:0];
}
It should be editing your TodoListTableView:
(Please notice that I used [pvc editTableAtIndex:0];, instead you should be calling something like [pvc editTableAtIndex:_PageViewController_current_index_];)

Switch between ViewControllers using a button?

I am very new to programming, especially iOS, so any answer should be given as if I am a baby with ADHD.
This is what I have in my buttonclick:
ItemCreateViewController *itemCreateViewConrtoller = [[ItemCreateViewController alloc] init];
[[self navigationController] pushViewController:itemCreateViewConrtoller animated:YES];
What am I missing?
There are a lot of reasons why this could be happening among which is your button is not triggering the intended action. Among the reason why a button could not trigger is your XIB button is not connected to the button object defined in your header and method file.
If you look at the .h file look at all your IBOutlet items there should be a circle on the left most column if it is darkened out it is connected other wise it is not, and you need to connect this in your XIB file.
If this is not the problem please display the code where you specify the selector it should be something like this:
[cancel_button addTarget:self action:#selector(Cancel:) forControlEvents:UIControlEventTouchUpInside];
Make sure that the selector is an IBAction function -
-(IBAction)Cancel:(id)sender {
[self dismissViewControllerAnimated:YES completion:NULL];
return;
}
One other thing, any statement after pushViewController will get executed prior to any actual change in the view. The actual View change only happens once control is returned to IOS. This is significant if your push view controller command is within a long conditional process and you fail to code the return / exit properly.
Since you are using a XIB file for your second view, you need to create it as follows -
ItemCreateViewController *itemCreateViewConrtoller = [[ItemCreateViewController alloc] initWithNibName:#"ItemCreateViewController" bundle:nil]; //Ensure you use correct nib file name
Then you can push it
[[self navigationController] pushViewController:itemCreateViewConrtoller animated:YES];
But since you are using a Storyboard you could have just added the new view to your storyboard, linked the button to the new view with a "push" segue and everything would have been done for you.

Show UIActivityIndicatorView when switching view controllers

I have a view controller that segues to a second view controller which loads several images but it hangs for a second or two before seguing from the first VC to the second. I am trying to add a UIActivityIndicatorView so that the user doesn't think the app is frozen (which is currently what it feels like). However I can't seem to get it to work properly and all of the examples I've seen are using a web view or are accessing some kind of data from a server whereas I'm loading images that are stored in the app.
I have some code below to show what I have attempted.
.h file
#interface SecondViewController: UIViewController
#property (strong, nonatomic) UIActivityIndicatorView *indicator;
.m file
-(void)viewWillAppear:(BOOL)animated
{
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.indicator.center = CGPointMake(160, 240);
[self.view addSubview:self.indicator];
//Loading a lot of images in a for loop.
//The images are attached to buttons which the user can press to bring up
//an exploded view in a different controller with additional information
[self.indicator startAnimating];
for{....}
[self.indicator stopAnimating];
}
I have tried also using dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) immediately after the call to [self.indicator startAnimating] but all that happened was that the view controller loaded instantly and the images/buttons never loaded at all.
How can I get rid of the delay when the user clicks the "next" button on the first view controller? The app hangs on the first VC for about a second or two then finally loads the second view controller with all the images/buttons. Do I need to add the UIActivityIndicatorView to the first view controller instead or am I going about this completely the wrong way? I'm open to any and all methods to get this done, thanks in advance.
You need to call the initialization code and stopAnimating in the next run loop. One easy thing you can do is the following:
-(void)viewWillAppear:(BOOL)animated
{
self.indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
self.indicator.center = CGPointMake(160, 240);
[self.view addSubview:self.indicator];
//Loading a lot of images in a for loop.
//The images are attached to buttons which the user can press to bring up
//an exploded view in a different controller with additional information
[self.indicator startAnimating];
[self performSelector:#selector(loadUI) withObject:nil afterDelay:0.01];
}
-(void) loadUI {
for{....}
[self.indicator stopAnimating];
}
Of course there are other ways to run loadUI in the next run loop (such as using a timer).

Adding another view controller's view as subview

I am trying to get a popup effect and want to design the popup view in another view controller so i can use the xib to do it.
When i used the presentViewController or pushViewController and set the background to transparent, i end up seeing the Window's background color.
I tried this code to add subview to the navigation controller's view so that i can have the Info view cover the entire screen with a transparent background. I also have tab bar to cover up as well.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
My problem is inside my InfoVC when i try to dismiss it, the app will crash with some EXC_BAD_ACCESS message:
[self.view removeFromSuperview];
EDIT:
I found a way to stop it crashing but setting the InfoVC as a property in the MainVC. I think the reason for crash is when i call "self.view" in the action inside the InfoVC, it doesn't know that self is the InfoVC inside MainVC.
InfoVC *vc = [[InfoVC alloc] initWithNibName:#"InfoVC" bundle:nil];
[self.navigationController.view addSubview:vc.view];
No no no no. Never never do that.
There is an elaborate dance that you must traverse in order to put a view controller's view inside another view controller's view (or remove it afterwards) if it doesn't come with built-in facilities for doing this (the way a UISplitViewController does, or the way a navigation controller manages the views of the view controllers that are pushed and popped within it).
Read up on customer container controllers. One of the examples from my book is here:
https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/ch19p556containerController/p476containerController/ViewController.m
Shouldn't you be using the following to remove the view from its superview?
[vc.view removeFromSuperview];
You can never have a UIView remove it's subviews, the subviews themselves must remove themselves from it's superview. You can easily loop through subviews and have them removed like so
for (UIView *view in vc.view.subviews) {
[view removeFromSuperview];
}
Docs for reference:
http://developer.apple.com/library/ios/#documentation/uikit/reference/uiview_class/uiview/uiview.html
After a "modally" presented view controller has appeared the views under the now presented view controller will be removed; this saves memory, and eases rendering. In your case, though, you also end up seeing the window behind the "modally" presented view.
The natural, and seemingly logical, next step is to simply take one view controller's view and cram it into another. However, as you have discovered, this is problematic. With the newly inserted view safely retained by the view hierarchy it is safe, but the new view controller is not so lucky, it is quickly deallocated. So when this new view tries to contact its controller you will get an EXC_BAD_ACCESS and crash. One workaround, again as you have found, is to simply have the original view controller keep a strong reference to the new view controller. And this can work... badly. There's still a good chance you will get an UIViewControllerHierarchyInconsistencyException.
Of course if you simply want to add a small view you create in IB you don't need to use a view controller as the "File's Owner" and there are many examples of creating an instance of a view from a xib file.
The more interesting question here is, "How would/does apple do it?" Apple consistently says that a view controller is the correct controller for an encapsulated unit of work. For example, their TWTweetComposeViewController, you present it, and it seems to float. How?
The first way of accomplishing this that comes to my mind is to have a clear background that isn't clear. That is, create an image of the screen before the presented view controller appears and set that as the background before the presenting view is removed. So for example(Explanation to follow):
QuickSheetViewController.xib
QuickSheetViewController.h
#import <UIKit/UIKit.h>
#interface QuickSheetViewController : UIViewController
- (IBAction)dismissButtonPressed:(id)sender;
#end
QuickSheetViewController.m
#import "QuickSheetViewController.h"
#import <QuartzCore/QuartzCore.h>
#implementation QuickSheetViewController {
UIImage *_backgroundImage;
}
-(void)renderAndSaveBackgroundImageFromVC:(UIViewController *)vc{
UIGraphicsBeginImageContext(vc.view.bounds.size);
[vc.view.layer renderInContext:UIGraphicsGetCurrentContext()];
_backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// save an image of the current view, and set our background to clear so we can see the slide-in.
[self renderAndSaveBackgroundImageFromVC:self.presentingViewController];
self.view.backgroundColor = [UIColor clearColor];
}
-(void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
// Time to use our saved background image.
self.view.backgroundColor = [UIColor colorWithPatternImage:_backgroundImage];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
// Set our background to clear so we can see the slide-out.
self.view.backgroundColor = [UIColor clearColor];
}
- (IBAction)dismissButtonPressed:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation{
return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
#end
The majority of this example hinges upon the renderAndSaveBackgroundImageFromVC: method. In which, we create a graphics context render the view we are about to cover into it, and then create a UIImage to later (in viewDidAppear) use as a background.
Now simply use it like:
QuickSheetViewController *newVC = [[QuickSheetViewController alloc] initWithNibName:nil bundle:nil];
[self presentViewController:newVC animated:YES completion:nil];
You will see through the background just long enough for the animation to happen, then we use our saved image to hide the removal of the presenting view.

Resources