how can I let UIPageViewController know when my async download is complete? - ios

I currently have a UIPageViewController set up in my project almost exactly like the default page-based application template.
However, in the init method for my ModelController I am using NSURLConnection to async download data into an array (of images) that is supposed to be displayed on the PageViewController.
That means when my root view controller goes and inits a starting view controller the resources might not be downloaded yet and then the model controller is fetching things from an empty array which crashes the app.
How can I implement a safe way to show the images in a PageView? I was thinking of using an empty view controller with an activity indicator as the starting view controller but I don't know how I'd then let the model controller know when the download is completed so I can then update the views with the images.
my root view controller (this is the uipageviewcontroller delegate)
#interface CSAPromoViewController ()
#property (readonly, strong, nonatomic) CSAPromoModelController *modelController;
#end
#implementation CSAPromoViewController
#synthesize modelController = _modelController;
- (void)viewDidLoad
{
[super viewDidLoad];
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
self.pageViewController.delegate = self;
CSAPageDataViewController *startingViewController = [self.modelController viewControllerAtIndex:0 storyboard:self.storyboard];
NSArray *viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
self.pageViewController.dataSource = self.modelController;
[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
//set page view controller's bounds
CGRect pageViewRect = self.view.bounds;
self.pageViewController.view.frame = pageViewRect;
[self.pageViewController didMoveToParentViewController:self];
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;
}
my model controller (this is the data source)
#interface CSAPromoModelController()
#property (readonly, strong, nonatomic) NSArray *promosArray;
#end
#implementation CSAPromoModelController
-(id)init
{
self = [super init];
if (self) {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://blah.com"]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
_promosArray = [self parseJSON:data];
}];
}
return self;
}
- (CSAPageDataViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard
{
// Return the data view controller for the given index.
if (([self.promosArray count] == 0) || (index >= [self.promosArray count] / 2)) {
return nil;
}
// Create a new view controller and pass suitable data.
CSAPageDataViewController *dataViewController = [storyboard instantiateViewControllerWithIdentifier:#"CSAPageDataViewController"];
dataViewController.promoOne = [self.promosArray objectAtIndex:index * 2];
dataViewController.promoTwo = [self.promosArray objectAtIndex:(index * 2) + 1];
return dataViewController;
}
the data view controller
#implementation CSAPageDataViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.promoLabelTop.text = [self.promoOne name];
self.promoImageTop.image = [self.promoOne image];
self.promoLabelBottom.text = [self.promoTwo name];
self.promoImageBottom.image = [self.promoTwo image];
}

Your problem you're trying to solve is an asynchronous one. Your approach however is for solving a synchronous problem.
For example, your class CSAPromoModelController is inherently asynchronous. This is because it's init method invokes an asynchronous method, and thus your class gets "infected" by asynchronism.
You might consider a re-design, where class CSAPromoModelController becomes a subclass of NSOperation with a complete handler, e.g. CSAPromoModelOperation. It's eventual result is the array of images. The imageArray becomes an ivar of your CSAPromoViewController. The CSAPromoViewController will have a method for creating a CSAPromoModelController object which will be initialized with an image. The completion handler of the operation passes the array of images. Within the completion handler you basically execute the same statements as in your original viewDidLoad method in order to setup the controllers.
You would use the operation as follows:
- (void)viewDidLoad
{
[super viewDidLoad];
self.pageViewController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];
self.pageViewController.delegate = self;
NSURLRequest *request = ...
CSAPromoModelOperation* op =
[CSAPromoModelOperation alloc] initWithRequest:request
completion:^(NSArray* result, NSError*error)
{
// assuming we are executing on the main thread!
if (error == nil) {
self.imageArray = result;
CSAPageDataViewController* startingViewController =
[self viewControllerWithImage:self.imageArray[0]
storyboard:self.storyboard];
NSArray* viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:nil];
...
}
else {
// handle error
}
}];
[op start];
}

Related

Make pageViewController After NSURLSession is complete

I'm a newbie on coding in objective-c.
I'm currently making an exercise application where I need to get some json-data from an API with a NSURLSession and send the date to a PageViewController.
So currently I'm getting my json-data in the method 'getWeer',which I call before making the pageViewController (in viewDidLoad). But because the NSURLSession runs async I think and isn't complete, the json-data always is null when I try to access it in my pageViewController.
How can I make the pageViewController after the NSURLSession is complete?
#interface SecondViewController ()<CLLocationManagerDelegate>
#property (nonatomic, strong) CLLocationManager *locationManager;
#property (nonatomic, strong) NSString *AppId;
#property (nonatomic, weak) NSDictionary *json;
#end
#implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.AppId = #"feda1f13263bb730deeb89fb3936a76e";
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[[self locationManager] requestWhenInUseAuthorization];
[[self locationManager] startUpdatingLocation];
[self getWeer];
// Create page view controller
self.pageViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"PageViewController"];
self.pageViewController.dataSource = self;
PageContentViewController *startingViewController = [self viewControllerAtIndex:0];
NSArray *viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
// Change the size of page view controller
self.pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 30);
[self addChildViewController:_pageViewController];
[self.view addSubview:_pageViewController.view];
[self.pageViewController didMoveToParentViewController:self];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (PageContentViewController *)viewControllerAtIndex:(NSUInteger)index{
// Create a new view controller and pass suitable data.
PageContentViewController *pageContentViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"PageContentViewController"];
pageContentViewController.pageIndex = index;
NSLog(#"%#",self.json);
pageContentViewController.json = self.json;
return pageContentViewController;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController{
NSUInteger index = ((PageContentViewController*) viewController).pageIndex;
if ((index == 0) || (index == NSNotFound)) {
return nil;
}
index--;
return [self viewControllerAtIndex:index];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController{
NSUInteger index = ((PageContentViewController*) viewController).pageIndex;
if (index == NSNotFound) {
return nil;
}
index++;
if (index == 3) {
return nil;
}
return [self viewControllerAtIndex:index];
}
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController{
return 3;
}
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController{
return 0;
}
-(void)getWeer{
NSString *dataUrl = [NSString stringWithFormat:#"http://api.openweathermap.org/data/2.5/forecast/daily?lat=%f&lon=%f&cnt=4&&APPID=%#&units=metric&lang=nl", self.locationManager.location.coordinate.latitude, self.locationManager.location.coordinate.longitude, self.AppId];
NSLog(#"%f",self.locationManager.location.coordinate.latitude);
NSLog(#"%f",self.locationManager.location.coordinate.longitude);
NSURL *url = [NSURL URLWithString:dataUrl];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
self.json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
});
}];
[dataTask resume];
}
#end
Call this method after async finish.
// Create page view controller
-(void)setupPageViewController {
self.pageViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"PageViewController"];
self.pageViewController.dataSource = self;
PageContentViewController *startingViewController = [self viewControllerAtIndex:0];
NSArray *viewControllers = #[startingViewController];
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
// Change the size of page view controller
self.pageViewController.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height - 30);
[self addChildViewController:_pageViewController];
[self.view addSubview:_pageViewController.view];
[self.pageViewController didMoveToParentViewController:self];
}

Bool value not being sent to destination ViewController

I want to send a bool value, didAddNewItem, from my SearchViewController to MatchCenterViewController, and then run a function depending on the state of the bool value. I attempt to send a didAddNewItem value of YES to my destination, MatchCenterViewController, but it doesn't seem to send correctly, as the function below never runs.
Here's how I'm sending it from SearchViewController (edited to reflect Rob's answer):
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"ShowMatchCenterSegue"]) {
_didAddNewItem = YES;
MatchCenterViewController *controller = (MatchCenterViewController *) segue.destinationViewController;
NSLog(#"we're about to set controller values before segueing to MC");
// Send over the matching item criteria
controller.itemSearch = self.itemSearch.text;
controller.matchingCategoryId = self.matchingCategoryId1;
controller.matchingCategoryMinPrice = self.matchingCategoryMinPrice1;
controller.matchingCategoryMaxPrice = self.matchingCategoryMaxPrice1;
controller.matchingCategoryCondition = self.matchingCategoryCondition1;
controller.matchingCategoryLocation = self.matchingCategoryLocation1;
controller.itemPriority = self.itemPriority;
[self.tabBarController setSelectedIndex:1];
}
}
And here's where I try to make use of it in the destination, MatchViewController:
- (void)viewDidAppear:(BOOL)animated
{
if (_didAddNewItem == YES) {
NSLog(#"well then lets refresh the MC");
// Start loading indicator
UIActivityIndicatorView *activityIndicator = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
activityIndicator.center = CGPointMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0);
[self.view addSubview: activityIndicator];
[activityIndicator startAnimating];
// Disable ability to scroll until table is MatchCenter table is done loading
self.matchCenter.scrollEnabled = NO;
_matchCenterDone = NO;
// Add new item to MatchCenter Array with the criteria from the matching userCategory instance, plus the search term
[PFCloud callFunctionInBackground:#"addToMatchCenter"
withParameters:#{
#"searchTerm": self.itemSearch,
#"categoryId": self.matchingCategoryId,
#"minPrice": self.matchingCategoryMinPrice,
#"maxPrice": self.matchingCategoryMaxPrice,
#"itemCondition": self.matchingCategoryCondition,
#"itemLocation": self.matchingCategoryLocation,
#"itemPriority": self.itemPriority,
}
block:^(NSString *result, NSError *error) {
if (!error) {
NSLog(#"'%#'", result);
self.matchCenterArray = [[NSArray alloc] init];
[PFCloud callFunctionInBackground:#"MatchCenter3"
withParameters:#{}
block:^(NSArray *result, NSError *error) {
if (!error) {
_matchCenterArray = result;
[_matchCenter reloadData];
[activityIndicator stopAnimating];
// Reenable scrolling/reset didAddNewItem bool
_matchCenterDone = YES;
self.matchCenter.scrollEnabled = YES;
//_didAddNewItem = NO;
NSLog(#"Result: '%#'", result);
}
}];
}
}];
}
}
I made sure it was properly setup as a property in the headers of both ViewControllers, so I'm not sure why it's not setting the value in the destination VC correctly. I know for a fact that addToMatchCenter function is running correctly without error, so it should be working.
#property (assign) BOOL didAddNewItem;
In your prepareForSegue, you are calling callFunctionInBackground asynchronously, meaning that it is quite likely that the segue will finish and the new view controller will be presented well before you set didAddNewItem in the block of callFunctionInBackground.
I'd be inclined to change that destination controller to initiate this asynchronous request itself, but have it show a UIActivityIndicatorView (or something) to suggest that the dependent request has not yet been finished, and then in the block you can remove the activity indicator view and update the UI accordingly.

SplitViewController reference logic

I read allot about the SplitViewControllers but i am walking in circles because i dont understand something.
You have a masterviewcontroller and a popoverview as a bar button item (filter)
lets say masterviewcontroller is a tableview and in the popoverview is a uiview controller
On the iphone i always alloced the masterviewcontroller and update the reference after some modifications, when you hit the button "search", it pushed a new controller with new data (come to think of it,maybe this wasnt the best idea) now that logic doesnt work anymore.
I have read you have to reference the controllers to each other, so i did it like this.
in the filtercontroller (this is the popoverview)
.h
#property (strong, nonatomic) MasterViewController *masterviewController;
#property (weak, nonatomic) IBOutlet UISlider *filterPrice;
- (IBAction)filterSearch:(id)sender;
.m
- (IBAction)filterSearch:(id)sender {
self.masterviewController.filterSearchPrice = [NSNumber numberWithInt:self.filterPrice.value];
[self.masterviewController performFilterSearch];
}
the performFilterSearch checks the fields, makes a call to an url with the filternames and json objects come back,parse and reload data happens..
Now i expect the masterviewcontroller to show new data but that doesnt happen, in fact nothing happens...
Update this is FilterSearch:
-(void)performFilterSearch
{
[queue cancelAllOperations];
[[AFImageCache sharedImageCache] removeAllObjects];
[[NSURLCache sharedURLCache] removeAllCachedResponses];
isLoading =YES;
[self.tableView reloadData];
searchResults = [NSMutableArray arrayWithCapacity:10];
NSURL *url = [self urlFilterWithSearchPrice:filterSearchPrice];
NSLog(#"%#",url);
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
[self parseDictionary:JSON];
isLoading = NO;
[self.tableView reloadData];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
[self showNetworkError];
isLoading = NO;
[self.tableView reloadData];
}];
operation.acceptableContentTypes = [NSSet setWithObjects:#"application/json", #"text/json", #"text/javascript",#"text/html", nil];
[queue addOperation:operation];
}
btw when i Nslog in filterSearch to check if its updated:
NSLog(#"%d",self.masterviewController.filterSearchPrice);
NSLog(#"%d",[self.filterTypeSegmentedControl selectedSegmentIndex]);
the first one never gets updated the second one gets updated off course
Update 2: (how do i launch the popview):
I added a bar button item on the masterviewcontrollers navigation that has an action.
I added a popover segue from the masterviewcontroller -> filtercontroller
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
filterPopOver = [(UIStoryboardPopoverSegue *)segue popoverController];
}
- (IBAction)filterPopButton:(id)sender {
if (filterPopOver){
[filterPopOver dismissPopoverAnimated:YES];
}
else{
[self performSegueWithIdentifier:#"showFilterPopover" sender:sender];
}
}
When you launch your filterController, you need to pass in a reference to the MasterViewController. You have a property for it in the filter controller, but you never assign a value to that property.
After Edit:
Your prepareForSegue method should look like this:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
FilterController *fc = (FilterController *)segue.destinationViewController;
fc.masterViewController = self;
}
Make sure that you've imported MasterViewController.h into you FilterController.m

iOS: How to pass data to applicationDidFinishLaunching:?

I have the following Code in my XMLAppDelegate.m file:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
[self.window makeKeyAndVisible];
self.products = [NSMutableArray array];
XMLViewController *viewController = [[XMLViewController alloc] init];
viewController.entries = self.products; // 2. Here my Array is EMPTY. Why?
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:productData]];
self.XMLConnection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
NSAssert(self.XMLConnection != nil, #"Failure to create URL connection.");
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}
- (void)handleLoadedXML:(NSArray *)loadedData {
[self.products addObjectsFromArray:loadedData]; // 1. here I get my Data (works fine)
XMLViewController *viewController = [[XMLViewController alloc] init];
[viewController.tableView reloadData];
}
I marked the problem. Is there any possibility to pass the loaded data (loadedData) to applicationDidFinishLaunching:?
Thanks in advance..
Where is your handleLoadedXML get called? If you want to pass it to applicationDidFinishLaunching can you just have your handleLoadedXML return that array and you can then call that method in applicationDidFinishLaunching.
Edit:
Think of it this way:
You first have this:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
[self.window makeKeyAndVisible];
self.products = [NSMutableArray array];
XMLViewController *viewController = [[XMLViewController alloc] init];
viewController.entries = self.products; // 2. Here my Array is EMPTY. Why?
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:productData]];
self.XMLConnection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
NSAssert(self.XMLConnection != nil, #"Failure to create URL connection.");
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}
Note that here you don't have self.products set up yet. It's only allocated.
After your application finished launching, then you have:
// say you have something like this
- (NSArray *)didFinishParsing {
return someArray;
}
This method gets called somewhere, and then it calls the method below to set your self.products. Not until now is your self.products populated.
- (void)handleLoadedXML:(NSArray *)loadedData {
[self.products addObjectsFromArray:loadedData]; // 1. here I get my Data (works fine)
XMLViewController *viewController = [[XMLViewController alloc] init];
[viewController.tableView reloadData];
}
So if you want self.products to be populated in applicationDidFinishLaunching, you need to call the method that generates the array in applicationDidFinishLaunching, say didFinishParsing, and you can do self.products = [self didFinishParsing];, and then it will be set.

DismissModalView Not Working

I've been pulling my hair out a bit over this. I'm creating a very simple app, it simply downloads an rss feed and displays it in a UITableview, which is inside a UINavigationController. Whilst it's downloading the feed I'm presenting a Modal View.
In my modal view I'm displaying a UIImageView and a UIActivityIndicatorView that is set to spin. I'm using ASIHTTRequest to asynchronously grab the feed and then using the either the completion block to get the response string and stop the spinner or the failure block to get the NSError and display a alert View. This all works perfectly.
I've then created a protocol to dismiss the modal view from the tableview which is called inside the completion block. But the modal view is never dismissed! I've tried pushing it into the navigation controller but exactly the same problem occurs. I even have tried setting the modal view delegate to nil but still no luck.
I've checked it without blocks using the ASIHTTPRequest delegate methods and it's the same, and if I don't present the modal view the table view is displayed normally.
Any Ideas? I've skipped out all the tableview delegate and datasource methods as well as the dealloc and any unused functions.
#interface MainTableViewController ()
-(void)loadModalView;
#end
#implementation MainTableViewController
#synthesize tableView;
#synthesize modalView;
// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView
{
[super loadView];
tableView = [[UITableView alloc]initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) style:UITableViewStylePlain];
tableView.delegate = self;
tableView.dataSource = self;
[self.view addSubview:tableView];
[self loadModalView];
}
-(void)loadModalView
{
modalView = [[ModalViewController alloc]init];
modalView.delegate = self;
[self presentModalViewController:modalView animated:NO];
}
//Modal View Delegate
-(void)downloadComplete
{
modalView.delegate = nil;
[self dismissModalViewControllerAnimated:NO];
}
#end
#interface ModalViewController ()
- (void)loadView
{
[super loadView];
backgroundImage = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 320, 460)];
[self.view addSubview:backgroundImage];
spinner = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
spinner.frame = CGRectMake(160, 240, spinner.bounds.size.width, spinner.bounds.size.height);
spinner.hidesWhenStopped = YES;
[self.view addSubview:spinner];
[spinner startAnimating];
NSString* urlString = FEED_URL;
NSURL* url = [NSURL URLWithString:urlString];
ASIHTTPRequest* request = [ASIHTTPRequest requestWithURL:url];
[request setCompletionBlock:^{
// Use when fetching text data
NSString *responseString = [request responseString];
[spinner stopAnimating];
[delegate downloadComplete];
// Use when fetching binary data
}];
[request setFailedBlock:^{
NSError *error = [request error];
UIAlertView* alert = [[UIAlertView alloc]initWithTitle:#"Error" message:error.description delegate:self cancelButtonTitle:#"Continute" otherButtonTitles: nil];
[alert show];
[alert release];
}];
[request startAsynchronous];
}
Matt
In my understanding.. you solution is quite complicated..
wouldn't it be better if the class MainTableViewController is the
one who downloads the Feeds.. for the ModalView it will just act as an ActivityIndicator and dismiss after downloading..
so inside your MainTableViewController loadview:
- (void)loadView
{
NSString* urlString = FEED_URL;
NSURL* url = [NSURL URLWithString:urlString];
ASIHTTPRequest* request = [ASIHTTPRequest requestWithURL:url];
[request startAsynchronous];
//after starting the request show immediately the modalview
modalView = [[ModalViewController alloc]init];
[self presentModalViewController:modalView animated:NO];
[request setCompletionBlock:^{
// Use when fetching text data
NSString *responseString = [request responseString];
//then when it is complete dissmiss the modal
[modalView dismissModalViewControllerAnimated:NO];
// Use when fetching binary data
}];
[request setFailedBlock:^{
NSError *error = [request error];
UIAlertView* alert = [[UIAlertView alloc]initWithTitle:#"Error" message:error.description delegate:self cancelButtonTitle:#"Continute" otherButtonTitles: nil];
[alert show];
[alert release];
}];
}
i didnt use blocks in my projects, but i think it will work the same..
also I use a plain UIActivityIndicatorView (large) as subviews not modalViews.. sadly i cant test the code here now.. but i can check it later though
The only way I solved this error was to synchronously download the data and push and pop the download view onto the navigation stack. Not ideal but it works.

Resources