I have an UIPageViewController and a class which conforms to UIPageViewControllerDataSource and manages the UIViewControllers which UIPageViewController displays. It all runs well when turning the pages is initiated by the user. But now I want to do that programmatically.
I've found that there is a very similar question here on SO which answers the question for a static amount of ViewControllers: Is it possible to Turn page programmatically in UIPageViewController?
But I'm unable to adapt the answers to the use of UIPageViewControllerDataSource. I was thinking that there must be something like:
- (void)setCurrentViewController:(UIViewController *)viewController
I suppose I'm just overlooking something here.
The Method mentioned in the answer ( setViewControllers:direction:animated:completion: ) sets the current view controller and is exactly what you need.
With the first param you define the view controller(s), you want to display. Use one if you don't have a spine, if you book is left/right use two.
Old question but the following implementation calls delegate and dataSource methods for turning to previous or next page (But doesn't check if delegate or dataSource is set):
- (IBAction)navigateReverse:(id)sender
{
UIViewController *controller = [self.dataSource pageViewController:self viewControllerBeforeViewController:self.designViewControllers[self.currentIndex]];
if (!controller)
{
return;
}
NSArray *newViewControllers = #[controller];
NSArray *previousViewControllers = self.viewControllers;
__weak __typeof(self) weakSelf = self;
[self.delegate pageViewController:self willTransitionToViewControllers:newViewControllers];
[self setViewControllers:newViewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished) {
[weakSelf.delegate pageViewController:weakSelf didFinishAnimating:finished previousViewControllers:previousViewControllers transitionCompleted:finished];
}];
}
- (IBAction)navigateForward:(id)sender
{
UIViewController *controller = [self.dataSource pageViewController:self viewControllerAfterViewController:self.designViewControllers[self.currentIndex]];
if (!controller)
{
return;
}
NSArray *newViewControllers = #[controller];
NSArray *previousViewControllers = self.viewControllers;
__weak __typeof(self) weakSelf = self;
[self.delegate pageViewController:self willTransitionToViewControllers:newViewControllers];
[self setViewControllers:newViewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished) {
[weakSelf.delegate pageViewController:weakSelf didFinishAnimating:finished previousViewControllers:previousViewControllers transitionCompleted:finished];
}];
}
I've created a easy project with that functionality:
https://github.com/delarcomarta/AMPageViewController
Hope this can help you.
Related
This question already has answers here:
how to reload tableview of another uiviewcontroller in current viewcontroller
(4 answers)
Closed 7 years ago.
I have two view controllers. First one has a table view and using the second view controller I'm calling a method on first view controller. In that method I'm trying to adding an item to the objects array and refresh the table view.
[postsArr insertObject:post atIndex:postsArr.count];
dispatch_async(dispatch_get_main_queue(), ^{
[self.newsTableView reloadData];
});
I'm calling this method in 2nd view controller,
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController popViewControllerAnimated:YES];
[self.appContext.creator createWithText:userText completion:^(UserData *userData,NSError *error) {
if (error == nil){
if (userData != nil) {
[self.parent addNew:((UserData*)[userData.list objectAtIndex:0]) withImage:nil];
}
}else{
[self showAlertWith:#"Error" with:#"Error occurred!"];
}
}];
});
How may I refresh the table view from another view controller?
Add this on the top of the interface of your second viewcontroller
#protocol SecondViewControllerDelegate <NSObject>
- (void)addNewItem:(id)item;
#end
#interface SecondViewController : UIViewController
#property (nonatomic, weak) id <SecondViewControllerDelegate> delegate;
#end
The point in your firstViewController from where you are instantiating your secondViewController for navigation add secondviewController.delegate as self.
self.secondViewController.delegate = self;
From the point where you get response in your secondViewController and you want to addItem in your firstViewController call delegate method from here and pass that item to firstViewController in that delegate method.
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController popViewControllerAnimated:YES];
[self.appContext.creator createWithText:userText completion:^(UserData *userData,NSError *error) {
if (error == nil){
if (userData != nil) {
[self.delegate addNewItem:((UserData*)[userData.list objectAtIndex:0]) withImage:nil];
}
}else{
[self showAlertWith:#"Error" with:#"Error occurred!"];
}
}];
});
Add the implementation of addNewItem in your firstViewController
- (void)addNewItem:(id)item{
[postsArr insertObject:post atIndex:postsArr.count];
dispatch_async(dispatch_get_main_queue(), ^{
[self.newsTableView reloadData];
});
}
You can use NSNotificationCenter
Fire notification when you need to reload the table
Just follow the link to know how implement notifications
Send and receive messages through NSNotificationCenter in Objective-C?
There Are Many methods through you can achieve your Goal
NSNotification
Delegates
KeyValueObserver
But I found most reliable is
#property (copy) void (^valueTypeChangedBlock) (NSarray arrayTypeObject);
Add This Property In .h file
And In .m File Add This
self.valueTypeChangedBlock = ^(NSarray NewArr) {
postsArr = NewArr
[self.newsTableView reloadData];
};
Where Ever U want to change the array from which table view Reload
Add the new array here
self.valueTypeChangedBlock (self.NewArray);
I am a newbee in iOS development and recently run into this problem with customized transition in iOS 9.
I have an object conforms to UIViewControllerTransitioningDelegate protocol and implements animationControllerForDismissedController, something like:
#implementation MyCustomizedTransitioningDelegate
#pragma mark - UIViewControllerTransitioningDelegate
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
MyCustomizedTransitionAnimator *animator = [[MyCustomizedTransitionAnimator alloc] init];
animator.presenting = NO;
return animator;
}
#end
And the process that triggers the modal transition is something like:
#implementation MyViewController
#pragma mark - Initializers
+ (MyCustomizedTransitioningDelegate *)modalTransitioningDelegateSingletonInstance;
{
static MyCustomizedTransitioningDelegate *delegateInst = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
delegateInst = [[MyCustomizedTransitioningDelegate alloc] init];
});
return delegateInst;
}
#pragma mark - UIViewController
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion;
{
[self prepareForDismissViewControllerAnimated:animated completion:&completion];
[super dismissViewControllerAnimated:animated completion:completion];
}
- (void)prepareForDismissViewControllerAnimated:(BOOL)animated completion:(dispatch_block_t *)completion;
{
self.presentedViewController.modalPresentationStyle = UIModalPresentationCustom;
self.presentedViewController.transitioningDelegate = [[self class] modalTransitioningDelegateSingletonInstance];
}
#end
Since animationControllerForDismissedController method is not called, the MyCustomizedTransitionAnimator is not created, which leads to its animateTransition not called either, which causes unexpected problem in my app. (Sorry for my poor English...)
I am also attaching the screenshot of stack trace for both iOS8 & iOS9.
In iOS 8, animationControllerForDismissedController is called after the stack trace below.
But in iOS9, transitionDidFinish is called somehow in advance, which I guess probably prevent animationControllerForDismissedController being called?
I was wondering if this is an iOS 9 bug or not. Any explanation or work around solution will be greatly appreciated!
I faced the same issue.
I hope this will help someone.
What fixed it for me is to make the object which applies UIViewControllerTransitioningDelegate protocol as variable instance to keep strong relationship with it.
I think because it gets dismissed after the view is presented first time.
I had the same issue.
Turned out I needed to set the delegate on the navigationController of the UIViewController that contains the trigger button.
Having this old code that didn't work:
UIViewController *dvc = [self sourceViewController];
TransitionDelegate *transitionDelegate = [TransitionDelegate new];
dvc.modalPresentationStyle = UIModalPresentationCustom;
dvc.transitioningDelegate = transitionDelegate;
[dvc dismissViewControllerAnimated:YES completion:nil];
I changed the first line to:
UIViewController *dvc = [self sourceViewController].navigationController;
and it worked.
Hope this helps.
You need to say something like:
MyDestinationViewController *viewController = [[MyDestinationViewController alloc] init];
MyCustomizedTransitioningDelegate *transitioningDelegate = [[MyCustomizedTransitioningDelegate alloc]init];
viewController.transitioningDelegate = transitioningDelegate;
viewController.modalPresentationStyle = UIModalPresentationCustom;
[self presentViewController: viewController animated:YES completion:nil];
Or if you're using segues, in prepareForSegue say something like:
MyDestinationViewController *toVC = segue.destinationViewController;
MyCustomizedTransitioningDelegate *transitioningDelegate = [[MyCustomizedTransitioningDelegate alloc]init];
toVC.transitioningDelegate = transitioningDelegate;
When I give a good swipe to my tableView and press the "Back" button before the tableView ended it's scrolling, my app crashes. I've tried the following:
- (void) closeViewController
{
[self killScroll];
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)killScroll
{
CGPoint offset = sellersTableView.contentOffset;
[sellersTableView setContentOffset:offset animated:NO];
}
That didn't work, same crash. I don't see why, the error I'm getting is the following:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView dataSource must return a cell from tableView:cellForRowAtIndexPath:'
So that means that the tableView is still requesting a cell when everything is already being deallocated. Makes no sense.
Then I tried this:
- (void) closeViewController
{
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dealloc
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
sellersTableView = nil;
}
Gives me the same error. Any ideas?
Update:
My delegate methods
creation
if (textField == addSellerTextField) {
sellersTableView = [[UITableView alloc] initWithFrame:CGRectMake(addSellerTextField.frame.origin.x + addSellerTextField.frame.size.width + 10, addSellerTextField.frame.origin.y - [self heightForTableView] + 35, 200, [self heightForTableView])];
sellersTableView.delegate = self;
sellersTableView.dataSource = self;
sellersTableView.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.05];
sellersTableView.separatorColor = [[UIColor grayColor] colorWithAlphaComponent:0.15];
sellersTableView.rowHeight = 44;
sellersTableView.layer.opacity = 0;
[self.companyView addSubview:sellersTableView];
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 1;} completion:nil];
}
cellForRowAtIndexPath
if (tableView == sellersTableView) {
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.backgroundColor = [UIColor clearColor];
if ([sellersArray count] > 0) {
cell.textLabel.text = [sellersArray objectAtIndex:indexPath.row];
} else {
UILabel *noSellersYetLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, sellersTableView.frame.size.width, [self heightForTableView])];
noSellersYetLabel.text = #"no sellers yet";
noSellersYetLabel.textAlignment = NSTextAlignmentCenter;
noSellersYetLabel.textColor = [UIColor grayColor];
[cell addSubview:noSellersYetLabel];
sellersTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
}
removing
- (void) textFieldDidEndEditing:(UITextField *)textField
{
if (textField == addSellerTextField) {
[self updateSellers:textField];
}
}
- (void)updateSellers:(UITextField *)textField
{
[textField resignFirstResponder];
[self hideSellersTableView];
}
- (void)hideSellersTableView
{
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{sellersTableView.layer.opacity = 0;} completion:nil];
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[sellersTableView removeFromSuperview];
sellersTableView = nil;
}
Solution
So apparently putting the dataSource = nil and delegate = nil into textFieldDidEndEditing fixed the problem. Thanks everybody for the answers!
It's strange behaviour of UITableView. The easiest way to resolve this issue just set the dataSource and delegate property of UITAbleView to nil before you make a call of function popToRootViewControllerAnimated. Furthermore you can use more common solution and add the code that set the properties to nil into the -dealloc method. In addition you no need the -killScroll method.
After a short research I have realized what the problem is. This unusual behaviour appeared in iOS 7. The scroll view retained by its superview may send message to delegate after the delegate is released. It happens due to -removeFromSuperview implementation UIScrollView triggers -setContentOffset: and, eventually, send message to delegate.
Just add following lines at the beginning of dealloc method:
sellersTableView.delegate = nil;
sellersTableView.dataSource = nil;
No need to use hacks like your killScroll method.
Also, I can't see why you want to call both popToRootViewController and dismissViewController.
If you dismiss a view controller which is embedded in a navigation controller, navigation controller itself as well as all contained view controllers will be released.
In your case you'll have just weird animation.
setContentOffset method won't help you, try to set
sellersTableView.dataSource = nil;
somewhere in your viewWillDisappear method.
This is not a good practice of course.
Change you closeViewController like below and see if works
(void) closeViewController
{
sellersTableView.dataSource = nil;
sellersTableView.delegate = nil;
[self.navigationController popToRootViewControllerAnimated:YES];
[self dismissViewControllerAnimated:YES completion:nil];
}
I don't think that setting the tableView (or it's delegate) to nil is the issue. You should be able to perform both dismissViewControllerAnimated or popToRootViewController individually without having to modify the tableView in this way.
So the issue is most likely due to calling both of these methods at the same time (and with animated = YES), and in doing so asking your viewController setup to do something unnatural.
Looks like upon tapping a "close" button you are both popping to a rootViewController of a UINavigationController, as well as dismissing a modal viewController.
In doing so, you're dismissing a modal viewController which is likely presented by the topViewController of the navigationController (so top vc is holding a reference to modal vc). AND you're trying to kill the top vc via the popToRootViewController method call. And you're doing both of these things using animated = YES, which means they take some time to complete, and you can't be sure when each finishes (ie you can't be sure when dealloc will be called).
Depending on your needs you could do one of several things.
Consider adding a delegate property to your modal vc. Dismiss the modal vc, and in the completionBlock of the modal vc tell its delegate that it's finished dismissing. At that point call popToRootViewController (because at this point you can be sure that the modal is gone and scrolling wasn't interrupted).
If it's your navController that's been presented modally, then do this in the opposite order. Notifying the delegate that the pop operation has completed, and do the modal dismissal then.
I have two UIViewControllers, one is a UIPickerViewController, the Other a UITableViewController. Ideally the Picker should get a request from the user to add x amount of some item to the tableView. The Picker gets user inputs and assigns them to variables val1, val2, val3, where val1 is the number of items (number of rows) and val2 is the name or label for the item.
PickerViewController.m
- (IBAction)add:(id)sender
{
TableViewController *tvc = [[TableViewController alloc] init];
[tvc setValues:self.val1 :self.val2 :self.val3];
[self presentViewController:tvc animated:YES completion:nil];
}
TableViewController.m
-(void)setValues:(NSString *)newVal1 :(NSString *)newVal2 :(NSString *)newVal3
{
self.val1 = newVal1;
self.val2 = newVal2;
self.val3 = newVal3;
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"UITableViewCell"];
// This is just a header which holds my "Add" button
UIView *header = self.headerView;
[self.tableView setTableHeaderView:header];
[self addNew:self.val1 :self.val2 :self.val3];
}
- (void)addNew:(NSString *)newVal1 :(NSString *)newVal2 :(NSString *)newVal3
{
if(!self.numberOfRows){
NSLog(#"Initially no of rows = %d", self.numberOfRows);
self.numberOfRows = [self.val1 intValue];
NSLog(#"Then no of rows = %d", self.numberOfRows);
}
else
{
self.numberOfRows = self.numberOfRows + [newVal1 intValue];
NSLog(#"New no rows = %d", self.numberOfRows);
}
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.numberOfRows inSection:0];
// Only run when called again .. not initially
if(self.run != 0){
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath]withRowAnimation:UITableViewRowAnimationBottom];
self.run ++;
[self.tableView endUpdates];
}
}
// "ADD" button which should go back to the picker and get new items to add to the table
- (IBAction)testAdd:(id)sender
{
PickerViewController *pvc = [[PickerViewController alloc] init];
[self presentViewController:pvc animated:YES completion:nil];
}
Now, I realize every time I call the next view controller I am creating a new instance of it, but I don't know how else to do it, I figure this is the main problem. As of right now, I expect when I leave the tableview for the picker view and return the console should log "New no of rows = x" but that doesn't happen.
I know val3 isn't used and my addNew: may not be the best, but I just need it to handle the basic logging mentioned above and I should be able to take it from there.
Been stuck on this for days
Create a property for TableViewController, and only create it the first time you present it,
- (IBAction)add:(id)sender {
if (! self.tvc) {
self.tvc = [[TableViewController alloc] init];
}
[self.tvc setValues:self.val1 :self.val2 :self.val3];
[self presentViewController:self.tvc animated:YES completion:nil];
}
It's not entirely clear from you question, whether it's this presentation or the one you have in the table view class that you're talking about. It also looks like you're doing something wrong in terms of presentation -- you're presenting the picker view from the table view controller, and also presenting the table view controller from the picker. That's not correct, you should present which ever controller you want to appear second, and that controller should use dismissViewControllerAnimated to go back, not present another controller.
In testAdd you don't need to create a new instance and present it. If you want to go back to the presentingViewController, just use dismissViewControllerAnimated .
And you will go one controller up in the stack.
i've got a question regarding a UIScrollView with paging enabled that contains many UIView, each managed by an own UIViewController.
Right now there are about 20 to 30 UIViewControllers that COULD be contained in the UIScrollView. It's a catalog app on the iPad, and I started preloading all the views at the beginning, but with the amount of UIViewControllers getting bigger and bigger, that is not an option any more.
I'm looking for the perfect solution in terms of memory usage. It's no problem to reload the UIViewControllers when the ContentOffset of the ScrollView reaches a specific controller. And I think to nil the UIViewControllers when the ContentOffset tells me that the UIViewControllers is not needed any more isn't that hard as well.
What is the correct way to handle this? Is it enough to alloc the UIViewControllers when needed, putting them into a NSMutableDictionary or NSMutableArray and nil them when they are not needed any more? A little bit of help from someone already having done something similar would be great!
Thanks for your help!
I'm sure there are some good infinite scrolling classes out there, but if you were going to "roll your own", here is a minimalist bit of code that demonstrates the process of infinite scrolling, keeping the current, previous, and next pages in memory, but letting go of anything else. This assumes that:
you're doing horizontal scrolling and have turned on paging;
that you're using view controllers for the child views;
your child view controller class has a page property to keep track of what page it's for; and
you've made your view controller the delegate for your scroll view
Thus, it might look like:
- (void)viewDidLoad
{
[super viewDidLoad];
// my underlying model is just an array of strings, which I'll show on my child
// view; your model will be more elaborate, but I just want to illustrate the concept
self.objects = #[#"1", #"2", #"3", #"4", #"5", #"6", #"7", #"8", #"9"];
// set the `contentSize` for the scrollview
CGRect content = self.view.bounds;
content.size.width *= [self.objects count]; // make it wide enough to hold everything
self.scrollView.contentSize = content.size;
// set our current page and load the first pages (the first and the next pages)
self.currentPage = 0;
[self addChildPage:0 toScrollView:self.scrollView];
[self addChildPage:1 toScrollView:self.scrollView];
}
- (void)addChildPage:(NSInteger)page toScrollView:(UIScrollView *)scrollView
{
// create the child controller
ChildViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"child"];
// set whatever properties you need to in order for it to present its information correctly
controller.text = self.objects[page];
controller.page = page;
// now do the stuff to add it to the right place in the scrollview
CGRect frame = self.view.bounds;
frame.origin.x = frame.size.width * page;
controller.view.frame = frame;
[self addChildViewController:controller]; // containment call for adding child view controller
[scrollView addSubview:controller.view];
[controller didMoveToParentViewController:self]; // containment call when done adding child
}
- (ChildViewController *)childControllerForPage:(NSInteger)page
{
for (ChildViewController *controller in self.childViewControllers)
{
if (controller.page == page)
return controller;
}
return nil;
}
- (void)addChildIfNecessary:(NSInteger)page toScrollView:(UIScrollView *)scrollView
{
if (page < 0 || page >= [self.objects count])
return;
ChildViewController *controller = [self childControllerForPage:page];
if (controller == nil)
[self addChildPage:page toScrollView:scrollView];
}
- (void)removeChildController:(UIViewController *)controller
{
[controller willMoveToParentViewController:nil]; // containment call before removing child
[controller.view removeFromSuperview];
[controller removeFromParentViewController]; // containment call to remove child
}
- (void)updateChildrenViewsForPage:(NSInteger)page forScrollView:(UIScrollView *)scrollView
{
if (page == self.currentPage)
return;
// add child pages as necessary
[self addChildIfNecessary:page toScrollView:scrollView];
[self addChildIfNecessary:(page-1) toScrollView:scrollView];
[self addChildIfNecessary:(page+1) toScrollView:scrollView];
// find any pages that need removing
NSMutableArray *pagesToRemove = [NSMutableArray array];
for (ChildViewController *controller in self.childViewControllers)
{
if (controller.page < (page - 1) ||
controller.page > (page + 1))
{
[pagesToRemove addObject:controller];
}
}
// remove them if they need removing
for (UIViewController *controller in pagesToRemove)
{
[self removeChildController:controller];
}
// update our "current page" index
self.currentPage = page;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSInteger page = scrollView.contentOffset.x / scrollView.frame.size.width + 0.5;
[self updateChildrenViewsForPage:page forScrollView:scrollView];
}
This demonstrates the appropriate custom container calls and the handling of the scroll events. I hope this helps.