Access already created view controllers in a UIPageViewController - ios

I'm using a UIPageViewController for displaying images, and I download the images asynchronously and throw them into the gallery when they're ready (but you're still able to go to the page in the gallery where the image will be).
My issue is that, when the image does load, I need to supply it for the view controller it corresponds to. Right now, I add it to an NSCache instance, and when UIPageViewController's data source method viewControllerAfterViewController: is called, I check if the image for the view controller being requested has already been downloaded (is in the cache) and if it is, I call initWithImage: on the specific view controller, passing the downloaded image.
My issue is with when viewControllerAfterViewController: is called and the image hasn't been downloaded yet. Right now I just load the view controller without the image, and when the imageDidFinishDownloading: delegate method is called, I try to supply the view controller with the image.
However, it seems that even though UIPageViewController asks for the next view controller the previous gets displayed (i.e.: when I get to the 3rd page, it requests the 4th page's view controller) I'm not able to access this requested view controller that I hand off.
If I access pageViewController.viewControllers, the count of the NSArray is never more than 1. So even though it seems like it should have 2 (the currently showing view controller and the next one that it requested), it only ever has one, the currently visible view controller.
The problem is that I need the other one. The image finishes downloading, and I really only have two options.
Give it to my NSCache, so when the init method is called for the view controller, it is handed off.
In the event that it has already been inited, hand it off to the view controller that's already been inited.
But 1 doesn't always work, as sometimes the init method is called when there's no image yet (it hasn't finished downloading), so we'd sometimes depend on 2, but pageViewController.viewControllers only holds the current visible view controller.
So there's basically this third situation where the view controller has already been inited without an image, and the image finishes downloading and I want to assign it to that view controller, but I'm currently on the one before it, and my only reference to the view controllers in the UIPageViewController is on the current visible one.
How do I handle 3? I need to assign something to a view controller but I can't find any way to access that view controller.

I'd use something like a future from PMConcurrency to do this. A future is just an object that promises to return your image as soon as it's available. So by giving the future to your view controller instead of the image, you don't need to worry about when the image arrives.
Your view controller's initializer would look something like this:
- initWithImageFuture:(PMFuture *)future
{
if (self = [super init]) {
[[future onMainThread] onComplete:^(id result, NSError *error) {
if (!error) {
_image = result;
}
}];
}
return self;
}
By adding an onComplete block to the future, the view controller will receive the image as soon as it's available (immediately if the image is already downloaded). Futures run in the background by default, so you'd use onMainThread to have the result returned on the main thread.
So instead of populating your cache with images, you'd populate it with image futures. There are several ways to create futures, but the block-based one is pretty simple:
PMFuture *imageFuture = [PMFuture futureWithBlock:^id{
UIImage *image = ...; // Your image fetching code here
return image;
}];
[myCache setObject:imageFuture forKey:myImageKey];

I have a similar sort of function but I load the image in the view controller that is displaying the image (the VC handed to the UIPageViewController). In your context I guess I'd had the image ID info and the NSCache object to the VC. That isn't what you want to do of course, but just something to think about.
Originally I tried to handle image loading in the parent like you are. As I recall, I kept a weak pointer to the target view controller. When the image arrived if the VC was still alive I could set the UIImageView property.

Related

WKInterface button doesn't change title

I'm trying to change the title of a button after I call back from a notification but it doesn't respond at all. I checked it's not nil and checked the text Im' assigning and all is good. I made the property type strong instead of weak but no success.
- (void) setButtonTitleFromSelectedSearchResult:(NSNotification *)notif
{
[self popController];
self.sourceMapItem = [[notif userInfo] valueForKey:#"SelectedResult"];
NSLog(#"The Selected Result is: %#", self.sourceMapItem.name);
//Testing
NSLog(#"%#", self.fromButton); // check it's not nil
[self.fromButton setTitle:self.sourceMapItem.name];
}
With WatchKit, if a user interface element isn't currently visible, it cannot be updated. So, if you've presented another interface controller "on top", you can't update any of the presenting controller's interface elements until you've dismissed the presented controller. At that point, you can safely update the presenting controller in its willActivate method.
SushiGrass' method of passing blocks is certainly one valid approach. In my testing, however, I ended up having to manage multiple blocks, and many of the subsequent blocks reversed what earlier queued blocks had accomplished (for example, first changing a label's text to "foo", then "bar", then "foo" again. While this can work, it isn't optimal.
I'd suggest that anyone who is working on a WatchKit app takes a moment to consider how they want to account for off-screen (i.e. not-currently-visible) interface elements. willActivate is your friend, and coming up with a way to manage updates in that method is worthwhile if you're moving from controller to controller.
For what it's worth, I've encapsulated a lot of this logic in a JBInterfaceController subclass that handles a lot of this for you. By using this as a base class for your own interface controller, you can simply update your elements in the added didUpdateInterface method. Unfortunately, I haven't yet had the time to write proper documentation, but the header files and sample project should get you going: https://github.com/mikeswanson/JBInterfaceController
I'm using latest XCode 6.3 and below code working with me.
self.testBtn is bind with Storyboard and its WKInterfaceButton
I also have attached screenshot with affected result.
I'm setting initial text in - (void)willActivate
- (void)willActivate {
[super willActivate];
[self.testBtn setTitle:#"Test"];
[self performSelector:#selector(justDelayed) withObject:nil afterDelay:5.0]
}
-(void)justDelayed
{
[self.testBtn setTitle:#"Testing completed...!!"];
}
If you're using an IBOutlet for the property fromButton be sure that is connected to WKInteface on the storyboard, like below:
I solved this kind of issue by creating a model object that has a property that is a block of type () -> (Void) (in swift). I create the model object, set the action in the block that I'd like the pushing WKInterfaceController to do on completion, and finally pass that model object in the context to the pushed WKInterfaceController. The pushed WKInterfaceController holds a reference to the model object as a property and calls it's completion block when it's done with whatever it needs to do and after func popController().
This worked for me for patterns like what you are describing along with removing rows on detail controller deletion, network calls, location fetches and other tasks.
You can see what I'm talking about here: https://gist.github.com/jacobvanorder/9bf5ada8a7ce93317170

Detecting if view still exists or active in completionHandler block

In my app I'm polling a web service for status updates, using a completionHandler block and making changes to the current view based on returned results when the callback executes.
- (void) tickTimer
{
[MyWebService myWebMethod:param1 completionHandler:^(NSString *result) {
// does view still exist?
[self myUpdateMethod];
// does property still exist?
self.theResult = result;
// does child view still exist?
_txtUpdate.text = result;
}];
}
But in the interim, it's possible that the view may have been unloaded as the user navigates elsewhere.
So a couple of questions:
What happens to a view when a new one is loaded and it gets pushed to the background? I imagine it gets garbage collected at some point, but how do I tell if it's still safe to access by any of the references above, and what would happen if it's not?
If the view does still exist, how do I tell if it is also still the foreground view?
So, blocks create strong references to all objects pointers that are referred to in their closure. Due to this, your block is going to force [self] to stay in memory until the block is destroyed. If this isn't the behavior you want you should create a weak pointer to self and refer to it inside of the block:
__weak typeof(self) weakSelf = self;
So a couple of questions:
What happens to a view when a new one is loaded and it gets pushed to
the background? I imagine it gets garbage collected at some point, but
how do I tell if it's still safe to access by any of the references
above, and what would happen if it's not?
If your view stays in the view hierarchy, it will stay in memory. Once there are no more references to the view it will be dealloced.
If you use a weak pointer like outlined above, then [weakSelf] will be nil if the view has been dealloced
If the view does still exist, how do I tell if it is also still the
foreground view?
I'm not sure what you mean by foreground view, but if you want to see if it's still in the view hierarchy then you can check the property -(UIView *)superview. If superview is nil, then it's not on the screen
If you use ARC right, it will not let you use deallocated viewcontroller.
You can use viewDidAppear and viewDidDisappear methods to know visible yours viewcontroller or not.

How can I add new pages to a UIPageViewController after the user has reached the last page?

I have a UIPageViewController that contains view controllers that are instantiated from data fetched over the network. When the user is paging and gets within a few pages of the end, I kick off a new network operation and put the resulting new view controllers onto the end of my UIPageViewControllerDataSource's mutable array of view controllers. In general this all works pretty well.
But if the network is slow (or the user is fast), it's possible to reach the final page before the network operation has completed. Since pageViewController:viewControllerAfterViewController: gets called when the user reaches the last page, it has already returned nil ("to indicate that there is no next view controller").
Is there a good way to force the UIPageViewController to call pageViewController:viewControllerAfterViewController: again (so that when the network operation completes and there are now new pages, I can force this call so the user can page to the new pages)? Or is there a workaround (maybe preventing the user from paging to the last page, but still showing the bounce animation)?
See here: Refresh UIPageViewController - reorder pages and add new pages
When you get your data back from the network, call setViewControllers: direction: animated: completion: and set the current view to the current one again. That forces it to re-call pageViewController:viewControllerAfterViewController:
ssloan's answer was part of the key, but in my case simply calling setViewControllers:direction:animated:completion: in the network operation completion block wasn't enough (due to some edge cases).
My solution was to store the most recent view controller for which pageViewController:viewControllerAfterViewController: returned nil (I called it endPage) and to create a method...
- (void)fixAndResetEndPage {
if (self.endPage && self.endPage == self.currentPage) {
[self.pageController setViewControllers:#[self.currentPage] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
self.endPage = nil;
}
}
...which I called whenever my NSMutableArray of content controllers was mutated (e.g., network operation completion) and in pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted: whenever completed was YES.

Saving iOS Application Scene State

This may be impossible, but I'm trying to save the state of my application between scene transitions, but I can't figure out what to do. Currently I love the way that when you have an application running and hit the home button, you can go back to that application just where you left off, but if you transition between scenes (in a storyboard), once you get back to that scene the application state was not saved.
I only have two different scenes that need to be saved (you transition back and forth from one to the other). How can I go about saving a storyboard scenes state without taking up precise memory?
More Detailed: Here is my entire storyboard. You transition back and forth between scenes using the plus toolbar button. On the second scene the user can tap on the table view cells and a real image will fill the image view (See figure 1.2)
Figure 1.1
In figure 1.2 you see what happens when you tap inside one of the many table view cells (an image view pops up.)
Figure 1.2
THE PROBLEM: When you tap a table view cell, which fills an image view (shown in figure 1.2) it works fine if you stay on that scene or even hit the iPhone home button (if you hit the iPhone home button and then reopen the app the scene's state was saved and the image view filled with a simple image still shows just like we left it), but if I transition (using the plus button) back to the first scene, and then use the plus button on the first scene to get back to the second scene the image view that I created (shown in figure 1.2) disappears and the second scene loads without saving the state and image views we filled.
EDIT: I tried using the same view controller for both scenes, but it didn't solve the problem.
UPDATE: I just found the following code (that I think stores a views state). How could I use this and is this what I've been looking for?
MyViewController *myViewController=[MyViewController alloc] initWithNibName:#"myView" bundle:nil];
[[self navigationController] pushViewController:myViewController animated:YES];
[myViewController release];
I would suggest a combination of two things:
1. Take DBD's advice and make sure that you don't continuously create new views
2. Create a shared class that is the data controller (for the golfers, so that the data is independent of the scene)
The correct way to make the segues would be to have one leading from the view controller on the left to the one on the right. However, to dismiss the one on the right you can use
-(IBAction)buttonPushed:(id)sender
[self dismissModalViewControllerAnimated:YES];
}
This will take you back the the view controller on the left, with the view controller on the left in its original state. The problem now is how to save the data on the right.
To do this, you can create a singleton class. Singleton classes have only one instance, so no matter how many times you go to the view controller on the right, the data will always be the same.
Singleton Class Implementation (Of a class called DataManager) - Header
#interface DataManager : NSObject {
}
+(id)initializeData;
-(id)init;
#end
Singleton Class Implementation (Of a class called DataManager) - Main
static DataManager *sharedDataManager = nil;
#implementation DataManager
+(id)initializeData {
#synchronized(self) {
if (sharedDataManager == nil)
sharedDataManager = [[self alloc] init];
}
return sharedDataManager;
}
-(id)init {
if(self == [super init]) {
}
return self;
}
#end
Then, inside your view controller code you can grab this instance like this
DataManager *sharedDataManager = [DataManager initializeDataManager];
This way you will have the same data no matter how many times you switch views.
Also, you can better adhere to MVC programming by keeping you data and your view controllers separate. (http://en.wikipedia.org/wiki/Model–view–controller)
Figure 1.1 has a fundamental flaw which I believe the basis of your problem.
Segues (the arrows between controllers on the storyboard) create new versions of the UIViewControllers. You have circular segues. So when you go "back" to the original screen through the segue is really taking you forward by creating a new version.
This can create a major problem for memory usage, but it also means you can't maintain state because each newly created item is an empty slate.
Since your are using a UINavigationController and pushViewController:animated: you should "pop" your controller to get rid of it.
On your "second" scene, remove the segue from the + button and create an IBAction on a touchUpInside event. In the IBAction code add the "pop"
- (IBAction)plusButtonTapped {
[self.navigationController popViewControllerAnimated:YES];
}
I see what you mean. This should happen to every application, as when the last view controller in the navigation stack is transitioned away from, it is deallocated and freed. If you need to save values such as text or object positions, a plist may be the way to go. See this related question for how to use a plist.
Apple isn't going to do this for you. You should probably just save the state of each view using NSUserDefaults and each time your application launches re-load your saved data.
If you are storing everything in CoreData you would only need to save the active view and a few object ids, if not you would need to save any data you have.
Don't expect iOS to save anything that you have in memory between launches. Just store it in NSUserDefaults and load it each time.
Store the state of the scene in NSUserDefaults or inside a plist file then when loading up the scene just load it with the settings from there. If the images are loaded from the internet you might also want to save them locally on your iphones hard drive so it runs a bit smoother.
I don't think you should cycle the segues, just use one that connects viewcontroller 1 from viewcontroller 2 should be enough and that way you make sure that no additional viewcontrollers are being made (memory problems maybe?)
However for your particular problem, I believe that you should use core data to save the exact state of your table, view because ios doesn't save the exact state of view at all times. it will require work but you will achieve what you want. You will need to save the exact photo( using a code or enums that will be saved), the location in the table view, the score or well whatever data you need to save that state.
The best of all is that coredata is so efficient that reloading the data when the app is relaucnhed or into foreground it takes no time, and ive used core data to load more than 5k of records until now and works just fine and its not slow at all.
When i get back home ill provide a code you might use to get an idea of what i mean.
The key here is to:
Have some sort of storage for the data that your application needs. This is your application's data model.
Give each view controller access to the model, or at least to the part of the model that it needs to do its job. The view controller can then use the data from the model to configure itself when it's created, or when the view is about to appear.
Have each view controller update the model at appropriate times, such as when the view is about to disappear, or even every time the user makes a change.
There are a lot of ways that you can organize your data in memory, and there are a lot of ways that you can store it on disk (that is, in long term storage). Property lists, Core Data, plain old data files, and keyed archives are all possibilities for writing the data to a file. NSArray, NSDictionary, NSSet, and so on are all classes that you can use to help you organize your data in memory. None of that has anything to do with making your view controllers feel persistent, though. You'll use them, sure, but which one you choose really doesn't matter as far as updating your view controllers goes. The important thing, again, is that you have some sort of model, and that your view controllers have access to it.
Typically, the app delegate sets up the model and then passes it along to the view controllers as necessary.
Something else that may help is that you don't have to let your view controller(s) be deleted when they're popped off the navigation stack. You can set up both view controllers in your app delegate, if you want, so that they stick around. You can then use the ones you've got instead of creating new ones all the time, and in so doing you'll automatically get some degree of persistence.

Retain view state upon reloading

I am developing an iPad application that is essentially a sequence of user instructions to mimic a real life system test, with the ability to make modifications on each view if components were to fail (indicating issues that will need to be resolved).
The problem I am having is that the default behaviour of the views seems to be that as I progress forward through the hierarchy, it retains the state of each view, but if I progress back and then move forward again it will have reset the screen.
What I would like to do is have each view save its state, regardless of how the user leaves that screen, so that they can be confident that their work is preserved even if they need to return to a previous step.
Is there any way of doing this? Or do I need to fundamentally reconsider my design?
You need model objects for your views. These could be as simple as dictionaries or as involved as a custom class for each view.
Each view's controller must update its associated model with the changes made via its interface before the view goes off-screen. When it reappears, the VC will update the display with the information from the model.
This follows the dominant Cocoa paradigm of Model-View-Controller (see also: Cocoa Design Patterns); your views display information, your models store information, and the controllers mediate and translate between the two of them.
How to update a model from the view depends heavily on the design of your model. Here's a mockup that may or may not be helpful. All the things named xField are outlets to UITextFields.
// When the view is taken off screen
- (void) viewWillDisappear {
// Assume that when created, view controller is given a pointer
// to the relevant model object (probably by the previous view
// controller)
[model setNameOfHorse:[[self horseNameField] text]];
NSUInteger newBetValue;
newBetValue = [[dollarValueFormatter
numberFromString:[[self betField] text]]
unsignedIntegerValue];
[model setBet:newBetValue];
[model setNote:[[self noteField] text];
}

Resources