I'm currently implementing automatic state preservation/restoration in an iOS6-only app.
For a restoration of a table view, I added the UIDataSourceModelAssociation protocol to my table view controllers and implemented
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view
and
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
When pressing the home button, the state preservation methods, including modelIdentifierForElementAtIndexPath:iView:, are getting called as expected and return valid identifier strings for the given index paths.
When killing the app and relaunching it, state restoration works more or less. I.e. the app re-opens the correct table view. However, the table view is always scrolled to the top, even when it was scrolled to another position before.
Here's the implementation of the UIDataSourceModelAssociation methods in my table view controller. Nothing fancy going on in there (the NdlFriend::accountUidproperty returns a unique identifier string for a given NdlFriend record):
#pragma mark - UIDataSourceModelAssociation
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view
{
NSString* identifier = nil;
NSArray* content = self.contentArray;
// Sometimes idx might be nil...
if(idx && idx.row<content.count)
{
NdlFriend* friend = content[idx.row];
identifier=friend.accountUid;
}
return identifier;
}
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view
{
NSIndexPath * indexPath=nil;
NSArray* content = self.contentArray;
NSInteger count = content.count;
for(NSInteger i=0;i<count;i++)
{
NdlFriend* friend = content[i];
if([identifier isEqualToString:friend.accountUid])
{
indexPath = [NSIndexPath indexPathForRow:i inSection:0];
break;
}
}
return indexPath;
}
I set break points in both methods.
To test the methods, I opened the table view and scrolled down a little bit. Then, when pressing the home button:
modelIdentifierForElementAtIndexPath:inView: gets called once, with the index path of the top most visible row. The method returns the correct uid for this row.
So far so good.
Then I stop and relaunch the app. Here's what happens (I'm especially puzzled by the first hit break point):
modelIdentifierForElementAtIndexPath:inView: gets called, with nil as index path (the view argument contains the correct pointer of the table view).
indexPathForElementWithModelIdentifier:inView: gets called with a valid identifier string (and a valid index path is returned by the method).
indexPathForElementWithModelIdentifier:inView: gets called again (with the same identifier string).
The table view is refreshed, but scrolled to the very top.
Does someone know, why the restoration of the scroll position fails? Does maybe the call of modelIdentifierForElementAtIndexPath:inView: with nil as indexPath has something to do with it (or is this normal behavior).
There is a bug in iOS 6 regarding state restoration of Table Views in Navigation Controllers.
You can see the open radar here: rdar://13438788
As you can see, it's a duplicate, so Apple are aware of this.
Also, see this next link, the guy who posted that open radar also did this blog post, it has the suggested workarounds that the Apple Engineers told him.
It makes the state preservation/restoration for me not such an enjoyable feature to implement, although remember, this is for your users ! So you should just do the workaround anyway.
Note there are 2 workarounds, one for the table view's view information to restore (scroll offset for example), and another workaround for the use when implementing UIDataSourceModelAssociation which is your case.
http://useyourloaf.com/blog/2013/04/07/bug-table-view-state-not-restored-when-embedded-in-navigation-controller.html
I don't think the problem you're having of the table view's scroll position resetting has to do with the UIDataSourceModelAssociation methods.
I believe there's a bug with a table view controller embedded in a nav controller, that causes it to reset its scroll position after restoration. As I understand it, if the cells in your table view don't reorder, you don't need to implement the UIDataSourceModelAssociation methods and you should get the scroll position "for free" (i.e. as long as you've opted into state preservation and set the restorations IDs). I can't really confirm this explicitly from the documentation, except to point out that UITableView descends from UIScrollView, which saves its scroll position. I've tested that if you set the table view controller to be the root controller, or embed a table view controller in a tab bar controller, then the scroll position is restored.
I've filed a bug report, you should too, if you haven't already.
I agree with Aky and this might indeed be a bug in iOS 6. See my answer on a related question: https://stackoverflow.com/a/14567551/322548
Make sure that you're not performing an asynchronous fetch on your data. If you are fetching your data from viewDidLoad, make sure that you use a [myManagedObjectContext performBlockAndWait:^{}] call instead of a [myManagedObjectContext performBlock:^{}].
It may be that you have a race condition where self.contentArray is empty when indexPathForElementWithModelIdentifier gets called.
Related
My UIPageViewController was working fine in iOS 5. But when iOS 6 came along, I wanted to use the new scroll transition style (UIPageViewControllerTransitionStyleScroll) instead of the page curl style. This caused my UIPageViewController to break.
It works fine except right after I've called setViewControllers:direction:animated:completion:. After that, the next time the user scrolls manually by one page, we get the wrong page. What's wrong here?
My workaround of this bug was to create a block when finished that was setting the same viewcontroller but without animation
__weak YourSelfClass *blocksafeSelf = self;
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){
if(finished)
{
dispatch_async(dispatch_get_main_queue(), ^{
[blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller
});
}
}];
This is actually a bug in UIPageViewController. It occurs only with the scroll style (UIPageViewControllerTransitionStyleScroll) and only after calling setViewControllers:direction:animated:completion: with animated:YES. Thus there are two workarounds:
Don't use UIPageViewControllerTransitionStyleScroll.
Or, if you call setViewControllers:direction:animated:completion:, use only animated:NO.
To see the bug clearly, call setViewControllers:direction:animated:completion: and then, in the interface (as user), navigate left (back) to the preceding page manually. You will navigate back to the wrong page: not the preceding page at all, but the page you were on when setViewControllers:direction:animated:completion: was called.
The reason for the bug appears to be that, when using the scroll style, UIPageViewController does some sort of internal caching. Thus, after the call to setViewControllers:direction:animated:completion:, it fails to clear its internal cache. It thinks it knows what the preceding page is. Thus, when the user navigates leftward to the preceding page, UIPageViewController fails to call the dataSource method pageViewController:viewControllerBeforeViewController:, or calls it with the wrong current view controller.
I have posted a movie that clearly demonstrates how to see the bug:
http://www.apeth.com/PageViewControllerBug.mov
EDIT This bug will probably be fixed in iOS 8.
EDIT For another interesting workaround for this bug, see this answer: https://stackoverflow.com/a/21624169/341994
Here is a "rough" gist I put together. It contains a UIPageViewController alternative that suffers from Alzheimer (ie: it doesn't have the internal caching of the Apple implementation).
This class isn't complete but it works in my situation (namely: horizontal scroll).
As of iOS 12 the problem described in the original question seems to be almost fixed. I came to this question because I experienced it in my particular setup, in which it does still happen, hence the word "almost" here.
The setup I experienced this issue was:
1) the app was opened via a deep link
2) based on the link the app had to switch to a particular tab and open a given item there via push
3) described issue happened only when the target tab was not previously selected by user (so that UIPageViewController was supposed to animate to that tab) and only when setViewControllers:direction:animated:completion: had animated = true
4) after the push returning back to the view controller containing the UIPageViewController, the latter was found to be a big mess - it was presenting completely wrong view controllers, even though debugging showed everything was fine on the logic level
I supposed that the root of the problem was that I was pushing view controller very quick after setViewControllers:direction:animated:completion: called, so that the UIPageViewController had no chance to finish something (maybe animation, or caching, or something else).
Simply giving UIPageViewController some spare time by delaying my programmatic navigation in UI via
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { ... }
fixed the issue for me. And it also made the programmatic opening of the linked item more user friendly visually.
Hope this helps someone in similar situation.
Because pageviewVC call multi childVC when swipe it. But we just need last page that visible.
In my case, I need to change index for segmented control when change pageView.
Hope this help someone :)
extension ViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard let pageView = pageViewController.viewControllers?.first as? ChildViewController else { return }
segmentedControl.set(pageView.index)
}
}
This bug still exists in iOS9. I am using the same workaround that George Tsifrikas posted above, but a Swift version:
pageViewController.setViewControllers([page], direction: direction, animated: true) { done in
if done {
dispatch_async(dispatch_get_main_queue()) {
self.pageViewController.setViewControllers([page], direction: direction, animated: false, completion: {done in })
}
}
}
Another simple workaround in Swift: Just reset the UIPageViewController's datasource. This apparently clears its cache and works around the bug. Here's a method to go directly to a page without breaking subsequent swipes. In the following, m_pages is an array of your view controllers. I show how to find currPage (the index of the current page) below.
func goToPage(_ index: Int, animated: Bool)
{
if m_pages.count > 0 && index >= 0 && index < m_pages.count && index != currPage
{
var dir: UIPageViewController.NavigationDirection
if index < currPage
{
dir = UIPageViewController.NavigationDirection.reverse
}
else
{
dir = UIPageViewController.NavigationDirection.forward
}
m_pageViewController.setViewControllers([m_pages[index]], direction: dir, animated: animated, completion: nil)
delegate?.tabDisplayed(sender: self, index: index)
m_pageViewController.dataSource = self;
}
}
How to find the current page:
var currPage: Int
{
get
{
if let currController = m_pageViewController.viewControllers?[0]
{
return m_pages.index(of: currController as! AtomViewController) ?? 0
}
return 0
}
}
STATEMENT:
It seems that Apple has spotted that developers are using UIPageViewController in very different applications that go way beyond the
originally intended ones Apple based their design-choices on in the first place. Rather than using it in a gesture driven linear fashion
PVC is often used to programmatically jump to random
positions within a structured environment. So they have enhanced their implementation of UIPageViewController and the class is now calling both DataSource
callbacks
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
after setting a new contentViewController on UIPageViewController with
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
even if an animated turn of pages rather suggests a linear progress in an e.g. page hierarchy like a book or PDF with consecutive pages. Although - I doubt that Apple from a HIG standpoint
is very fond of seeing PVC being used this way, but - it doesn't break backwards compatibility, it was an easy fix, so - they eventually did it. Actually it is just one more call of one of the two DataSource methods that is absolutely unnecessary in a linear environment where pages (ViewControllers) have already been cashed for later use.
However, even if this enhancement might come in very handy for certain use-cases the initial behavior of the class is NOT to be considered a bug. The fact that a lot of developers do - also in other
posts on SO that accuse UIPageViewController of misbehavior - rather emphasizes a widely spread misconception of its design, purpose and functionality.
Without trying to offend any of my fellow developers here in this great facility I nonetheless decided not to remove my initial 'disquisition' that clearly explains to the OP the mechanics of PVC and why his assumption is wrong that he has to deal with a bug here.
This might also be of use for any other fellow developer too who struggles with some intricacies in the implementation of UIPageViewController!
ORIGINAL ANSWER:
After having read all the answers over and over again - included the
accepted one - there is just one more thing left to say...
The design of UIPageViewController is absolutely FLAWLESS and all the
hacks you submit in order to circumvent an alleged bug is nothing but
remedies for your own faulty assumptions because you goofed it up in the
first place!!!
THERE IS NO BUG AT ALL! You are just fighting the framework. I'll explain why!
There is so much talk about page numbers and indices! These are concepts the
controller knows NOTHING about! The only thing it knows is - it is showing
some content (btw. provided by you as a dataViewController) and that it can
do something like a right/left animation in order to imitate a page turn.
CURL or SCROLL...!!!
In the pageViewController's world there only exists a current SPACE (let's call
it just this way to avoid confusion with pages and indices).
When you initially set a pageViewController it only minds about this very SPACE.
Only when you start panning its view it starts asking its DataSource what it
eventually should display in case a left/right flip should happen. When you start
panning to the left the PVC asks first for the BEFORE-SPACE and then for the
AFTER-SPACE, in case you start to the right it does it the other way round.
After the completed animation (a new SPACE is displayed by the PVC's view) the
PVC considers this SPACE as its new center of the universe and while it is at it, it
asks the DataSource about the one it still does not know anything about. In case of
a completed turn to the right it wants to know about the new AFTER space and in
case of a completed turn to the left it asks for a new BEFORE space.
The old BEFORE space (from before the animation) is in case of a completed turn to
the right completely obsolete and gets deallocated as soon as possible. The old center
is now the new BEFORE and the former AFTER is the new center. Everything just
shifted one step to the right.
So - no talk of 'which page' or 'whatever index' - just simply - is there a BEFORE or
an AFTER space. If you return NIL to one of the DataSource callbacks the PVC just
assumes it is at one extreme of your range of SPACES. If you return NIL to both
callbacks it assumes it is showing the one and only SPACE there is and will never
ever again call a DataSource callback anymore! The logic is up to you! You define
pages and indices in your code! Not the PVC!!!
For the user of the class there are two means of interacting with the PVC.
A pan-gesture that indicates whether a turn to the BEFORE/AFTER space is desired
A method - namely setViewControllers:direction:animated:completion:
This method does exactly the same than the pan gesture is doing. You are indicating the
direction (e.g. UIPageViewControllerNavigationDirectionBackward/Forward)
for the animation - if there is one intended - which in other words just means -> going to
BEFORE or AFTER...
Again - no mentioning of indices, page-numbers etc....!!!
It is just a programmatically way of achieving the same a gesture would!
And the PVC is doing right by showing the old content again when moving back
to the left after having moved to the right in the first place. Remember
- it is just showing content (that you provide) in a structured way - which is a 'single page turn' by design!!!
That is the concept of a page turn - or BOOK, if you like that term better!
Just because you goof it up by submitting PAGE 8 after PAGE 1 doesn't mean the PVC
cares at all about your twisted opinion of how a book should work. And the user of your
apps neither. Flipping to the right and back to the left should definitely result in reaching
the original page - IF done with an animation. And it is up to YOU to correct the goof by
finding a solution for the disaster. Don't blame it on the UIPageViewController. It is doing
its job perfectly!
Just ask yourself - would you do the same thing with a PAGE-CURL animation? NO ?
Well, neither should you with a SCROLL animation!!! An animated page turn is a page turn and only a page turn!
In either mode!
And if you decide to tear out PAGE 2 to PAGE 7 of your BOOK that's perfectly fine!
But just don't expect UIPageViewController to invent a non-existing PAGE 7 when turning back to the recent page unless YOU tell it that things have changed...
If you really want to achieve an uncoordinated jump to elsewhere, well - do it without an
animation! In most cases this will not be very elegant but - it's possible... -
And the PVC even plays nicely along! When jumping to a new SPACE without animation
it will ask you further down the road for both - the BEFORE and AFTER controller. So your application-logic can keep up with the PVC...
But with an animation you are always conveying - move to the previous/next space (BEFORE -
AFTER). So logically there is no need at all for the PVC to ask again about a space it already
knows about when animating page turns!!!
If you wanna see PAGE 7 when flipping back to the left after having animated from PAGE 1
to the right - well, I would say - that's definitely your very own problem!
And just in case you are looking for a better solution than the 'completion-block' hack from
the accepted answer (because with it you are doing work beforehand for something that might
possibly not even get used further down the road) use the gesture recognizer delegate:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
Set your PVC's DataViewController here (without animation) if you really intend to go back
left to PAGE 7 and the DataSource will be asked for BEFORE and AFTER and you can submit
whatever page you like! With a flag or ivar that you should have stashed away when doing your uncontrolled
jump from PAGE 1 to 8 this should be no problem...
And when people keep on complaining about a bug in the PVC - doing 2 page turns when it is
supposed to do 1 turn only - point them to this article.
Same problem - triggering an un-animated setViewControllers: method within the transition gesture
will cause exactly the same havoc. You think you set the new center - the DataSource is asked
for the new BEFORE - AFTER dataController - you reset your index count... - Well, that seems OK...
But - after all that business the PVC ends its transition/animation and wants to know about the
next (still unknown to it) dataViewController (BEFORE or AFTER) and also triggers the DataSource. That's totally justified ! It needs to know where in its small BEFORE - CENTER - AFTER
world it is and be prepared for the next turn.
But your program-logic adds another index++ count to its logic and suddenly got 2 page turns !!!
And that is one off from where you think you are.
And YOU have to account for that! Not UIPageViewController !!!
That is exactly the point of the DataSourceProtocol only having two methods! It wants to be as generic as possible - leaving you the space and freedom to define your own logic and not being stuck with somebody else's special ideas and use-cases! The logic is completely up to you. And only because you find functions like
- (DataViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard position:(GSPositionOfDataViewController)position;
- (NSUInteger)indexOfViewController:(DataViewController *)viewController;
in all the copy/pasted sample applications in the cloud doesn't necessarily mean that you have to eat that pre-cook food! Extend them any way you like! Just look above - in my signature you will find a 'position:' argument! I extended this to know later on if a completed page turn was a right or a left turn. Because the delegate unfortunately just tells you whether your turn completed or not! It doesn't tell you about the direction! But this sometimes matters for index-counting, depending on your application's need...
Go crazy - they are your's...
HAPPY CODING !!!
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
I have an app with a simple "drill in" pattern. I present a list of items in either map mode or list mode. Clicking on one dives into a more detailed "edit" view controller. My edit controller has
#property (assign, nonatomic) BOOL showMap;
And in source implementation setter/getter methods:
#pragma mark - Properties
- (BOOL) showMap {
return self.viewModeSegments.selectedSegmentIndex == 1;
}
- (void) setShowMap: (BOOL) showMap {
self.view; // had to add this, it's a hack
self.viewModeSegments.selectedSegmentIndex = showMap ? 1 : 0;
}
I set this property in the initiating controller with:
#pragma mark - Navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString: #"ValveEditSegue"]) {
ValveEditController* controller = segue.destinationViewController;
controller.valve = self.selections.anyObject;
controller.showMap = ShowMap;
}
}
What I discovered, is that the showMap setter happens before the view has been populated, so the viewModeSegments was still nil. I experimented with the hack shown in the setter, that forces the access to self.view to make sure it's loaded. But that seems like a bad idea. What I don't know, is what pattern I should use instead.
I could make the showMap a normal property with backing, and then mirror that state into the widget at viewDidLoad time, but that seems silly to have a property/state just for that one time trampoline like effect.
That is the other option really...
The underlying issue is that you are using the view (or a subview) as a container of knowledge that is really shouldn't be used for. A view is for displaying knowledge, not owning that knowledge.
More broadly, it's often attractive to abuse views and the properties they offer, such as view tags, cell selection state, etc, because it seems simple and wasteful to 'additionally' store that information somewhere else as well - but that doesn't make it correct to do so.
It is the responsibility of the controller to maintain the state information about what is being shown, so you should store that information in a property on the controller and maintain it there (use it to update the view, and don't query the view about its state).
I think you understand the problem fully. You might reduce the hacky-ness a little by forcing the view load in the from vc's prepareForSegue...
(void)controller.view;
But I think the answer you should take is your own, using a regular property. I don't think it's silly, since the default mode of a two-mode vc is a legit property. (think of UIViewController -hidesBottomBarWhenPushed)
Ok StackOverflow People...I've got a very interesting problem that I've been trying to solve for days and can't figure out so I need some major help. This will most likely be a very lengthy description but please bear with me and thank you deeply in advance for reading all of this because the more words I have, the clearer I can describe the full picture to you all. I will do my absolute best to be as terse and coherent as I can possibly be. Please let me know wherever I fall short.
Here's the context of my problem: I'm using Storyboards for my iOS app and for a particular nav tab in my app, I had to create two separate scenes for both the Portrait and Landscape orientations. The reason for doing this (instead of say, using Autolayout), is because within this said tab, there are visual elements (table views, web views, etc.) that are laid out differently depending on the orientation and it was a lot easier to create a separate orientation scene to handle this change in the UI instead of doing it programmatically -- (it's also just a lot easier to understand and cleaner code-wise). So the take away to keep in mind from all of this is that these two separate Portrait and Landscape scenes represent the SAME TAB in my app. (Side Note: these scenes were made in the IB of course)
Now the visual elements that I mentioned in the UI earlier -- going deeper, they are all containers for different UIViewControllers. I sandboxed everything in the app and pretty much have a 1-to-1 relationship for all things so these containers will map to my subclassed UIViewControllers that I've created for their specific purposes -- but it's here that the first caveat of my problem arises. Here's a practical example for a clearer picture, I have one UIViewController that contains a UITableView called MXSAnnouncementsViewController and this same view controller exists in both the Landscape and Portrait scenes. I did not create an explicit Portrait or Landscape VERSION of that view controller but instead, have the controller keep track of two IBOutlet properties (tableViewLandscape and tableViewPortrait) that point to the orientation-specific UITableViews -- and this approach works perfectly fine. Moreover in my MXSAnnouncementsViewController, I have a local property called tableView that abstracts the orientation-specific table views. It gets set within viewDidLoad which you can see below:
- (void)viewDidLoad
{
[super viewDidLoad];
if (self.tableViewPortrait) {
self.tableView = self.tableViewPortrait;
} else {
self.tableView = self.tableViewLandscape;
}
[self.tableView setDelegate:self];
[self.tableView setDataSource:self];
if (![MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements) {
[MXSAnnouncementManager loadModel:#"MXSAnnouncementGroupAllAnnouncements" withBlock:^(id model, NSError *error) {
if (!error) {
self.arrayLatestAnnouncements = [MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements;
[self.tableView reloadData];
} else {
// show some error msg
}
}];
} else {
self.arrayLatestAnnouncements = [MXSAnnouncementManager sharedAnnouncementManager].latestAnnouncements;
}
[self setupPullToRefresh];
}
Whenever I'm in the tab, one of the two orientation-specific IBOutlets is always active and has an address in memory while the other is nil. Whenever I rotate, the roles reverse -- whatever had an address in memory previously is now nil and the other has been initialized and allocated which is why I do what I did with the tableView property in the snippet above. Here is where caveat #2 comes into the picture and it's a doozy -- it has to do with the view lifecycle. Here's a practical example for clarity sake: Say I load the app up in Landscape orientation. When I do, my tableViewLandscape outlet has an address in memory and my tableViewPortrait outlet is nil. That's the expected and desired behavior. Now, when I rotate the app, the crazy stuff begins. Here's one place where I need clarity from all of you with regards to instances of UIViewControllers and what's normal vs. what's not so read the following VERY slowly and carefully.
Rotating the app immediately causes the opposite orientation scene (another INSTANCE of MXSAnnouncementsViewController???) to call its viewDidLoad method (in this example, we're in Landscape so the Portrait scene invokes that method). In that method, my local tableView property gets set to the currently active table view for that orientation (see snippet above). When that method finishes, the previous LANDSCAPE instance of MXSAnnouncementsViewController invokes its viewWillDisappear method which is then followed by the PORTRAIT instance's invocation of its viewWillAppear method which then lastly ends with the LANDSCAPE instance calling its willRotateToInterfaceOrientation callback -- that's the order of operation that I'm seeing from the breakpoints. I really do hope you got all of that because my mind just blew up from it all.
If you're still with me at this point, thank you because we're finally at the home stretch. As the title of this post suggests, the problem I'm trying to solve is my app freezing on rotation. If you haven't noticed on the viewDidLoad snippet, the last instruction to get executed is the setupPullToRefresh method which is the following:
- (void)setupPullToRefresh
{
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[refreshControl addTarget:self action:#selector(refreshTableView:) forControlEvents:UIControlEventValueChanged];
[self.tableView addSubview:refreshControl];
}
Since I already explained the whole view lifecycle order of operations on rotation earlier, to make a very long story short, if I comment out that last setupPullToRefresh instruction at the end of viewDidLoad for MXSAnnouncementsViewController, my app works fine. If I include that instruction, my app becomes totally unresponsive on the first rotation and I cannot for the life of me figure out why. Not sure if I'm dealing with an edge case here or something. Any and all insights are welcome and THANK YOU SO MUCH for reading all of this!
Your best approach is probably to abandon your current design of having two separate controllers for portrait and landscape. On iOS, you should always relayout the views for the orientation you want to be in, not destroying and recreating everything. By trying to handle it by recreating everything, you're just going to get yourself in trouble I think.
You can use auto layout to do complex reorderings of views upon rotation if you know it well, but probably your best bet is to scrap your current code to do landscape, and write code to simply rearrange the views yourself upon rotating. You'll have far fewer issues down the road, and your code will be easier for others to understand and maintain as well.
When you remove that one bit of code, your app may appear to be working just fine, but there is probably something going on behind the scenes that isn't quite correct that could come back to bite you in the future. That's probably why adding the line of code breaks it.
Try to add it after rotation
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
[self setupPullToRefresh];
}
If that doesn't help, create UIRefreshControl only once and set it to the right table on rotation.
If that doesn't help too, follow the first given answer (#Gavin's answer) and create only 1 table on viewDidLoad and relayout things in -(void)viewWillLayoutSubviews
What I am trying to achieve is simple, from first thinking though. I found it hard to handle at last.
I would like to push a table view as a selection list, user select one cell and the cell string was sent to the previous view as selected string, simple huh??
See two pictures first
what bothers me is that:
I would like to provide (at least) two buttons, one on the left is back button auto-generated by navigation controller, and the right one is for editing. And the navigation controller is defaulted to have two buttons (from my knowledge). So there is no place for "Done" button, which is supposed for user to tap and then confirm and pop to the previous view.
So, when the user tap a cell, "Wearing" for example, I would like the following to happen, automatically and visually SEEable for user:
user can SEE that "Housing" cell is unmarked
then user can SEE that "Wearing" cell is marked
then after a little time gap (say 0.2 second), pop to the previous view and update the selection, automatically.
At first I thought it's easy but it's definitely not. Here is my code for doing it, but working wired
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
dispatch_queue_t queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0ul);
dispatch_async(queue, ^{
//unmark previous cell
if (selectedIndexPath!=nil) {
[[self.tableView cellForRowAtIndexPath:selectedIndexPath]setAccessoryType:UITableViewCellAccessoryNone];
}
selectedIndexPath=indexPath;
//get the selected cell to mark
UITableViewCell *cell=[self.tableView cellForRowAtIndexPath:indexPath];
[cell setAccessoryType:UITableViewCellAccessoryCheckmark];
dispatch_sync(dispatch_get_main_queue(), ^{
//wait a little
[NSThread sleepForTimeInterval:0.2];
//return to previous view
NSLog(#"here.........");
if ([objectToUpdateCategory respondsToSelector:#selector(updateCategoryTo:withSelectedIndexPath:)]) {
NSLog(#"sending.......... update info");
[objectToUpdateCategory updateCategoryTo:cell.textLabel.text withSelectedIndexPath:selectedIndexPath];
NSLog(#"sent update info");
}
[self.navigationController popViewControllerAnimated:YES];
});
});
The tricky thing is that if I put [self.navigationController popViewControllerAnimated:YES]; to the last, the view will not visually update the unmark and mark step and go back to the previous view immediately. At first, when I didn't consider the unmark thing, the “queue" stuff in code can do the mark step visually before popping back, but sometimes not working. I don't know if my code is correct, actually I don't quite understand this queue tech from apple. But I'm pretty sure it has something to do with NSThread / queue or else that handle concurrency. I've checking Apple documents for a whole day and found no direct answer.
Hope someone could help me on this, thanks in advance :)
To "after a little time gap (say 0.2 second), pop to the previous view", use the performSelector:withObject:afterDelay: methods or one of its variants, e.g.:
[self performSelector:#selector(delayedPop) withObject:nil afterDelay:0.2];
and put the popViewControllerAnimated in the delayedPop method, e.g.:
-(void)delayedPop{
[self.navigationController popViewControllerAnimated:YES];
}
First of all, as I wrote in my comment, you shouldn't update the UI on a background thread. This will cause a lot of problems, including the UI not being updated immediately. In your case you don't need to use dispatch_async or dispatch_sync at all. What I would do is create a property in the view controller that displays the categories table view:
#property (nonatomic, weak) id<CategoryControllerDelegate> delegate;
When you push the category controller on the stack you set your expense controller as the delegate. Then, when the user makes a selection in the category controller, you call a method on the delegate (defined in a protocol), for example the one in your code sample:
#protocol CategoryControllerDelegate<NSObject>
#optional
- (void) updateCategoryTo: (NSString*) category withSelectedIndexPath: (NSIndexPath*) path;
#end
After that you pop the current view controller off the stack.