My project is an eBook-reader (ePub) with UIPageViewController holding webViews to show the pages. The pages are computed by loading a webView with a chapterFile and counting the pages the webView needs to display in webViewDidFinishLoad. This will be done for all chapterFiles in a delegate called trough webViewDidFinishLoad, where the next chapter will be loaded in a webView to compute the pages. On certain conditions, like turning from portrait to landscape with new textSize in portrait, the pageViewController is demanding a second page faster than the paging needs to finish.
PageViewController is calling
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
In this method I am calling another method "goToNextPage" to look if the next Page is a shift of the webViews viewPort or an new chapterFile to load. What I want is "goToNextPage" to wait for the paging to be done.
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController{
...
//paging is already in progress at this point (if needed )
//I want goToNextPage to wait for paging to be finished
[self goToNextPage];`
...
//loading newViewController with right page
return newViewController;
}
I don't know how to achieve this because of the delegate calls to wait for.
I have worked on a similar project. We found that it was necessary to calculate pages behind the scenes for performance. We have a web view sized the same as it would be in the UIPageViewController placed behind the UIPageViewController so that it is not visible (setting the view to hidden causes it to not lay out the web page). Then we cycle through each spine item calculating the page count and save the spine item index, current font size, and the page count for use later. In our case it was possible to provide some amount of navigation as pages are calculated (similar to iBooks) without preventing all navigation.
Once page counts are calculated, you have all of the information necessary to display any page at any point even if the current page is not yet complete in loading.
If font size or view size changes, page counts will need to be recalculated.
Another option could be to prevent page changes until the current spine item has finished calculating pages. You can accomplish that by returning nil from pageViewController:viewControllerAfterViewController: and pageViewController:viewControllerBeforeViewController: until your page count comes back. Pages will likely calculate fast enough that it will just appear that the page turn is rate limited.
One of the benefits to calculating the size of all spine items before-hand is that navigation by more than one page at a time becomes possible.
Either way you go, I do recommend rate limiting the page turn -- I have run into some issues with the web view crashing in setDeviceScaleFactor if pages are turned too quickly when in a UIPageViewController. The UIPageViewController will attempt to ask for new view controllers much faster than the device can keep up with creating and rendering UIWebViews if a user continually turns pages as fast as possible.
Related
We need to build a horizontal swipe-able page, with number of page range from 2 to infinity. User has the ability to add and remove any page.
We really do not want to create all and keep all UIViewController in memory. That is highly inefficient.
We hope, UIPageViewController can have mechanism, similar to UICollectionView cell reusing mechanism.
Since I'm pretty familiar how Android handling this case. Let me describe it below
When you want to have 100 swipe-able pages, Android will only create 2 (This figure is configurable) view hierarchy. If the view is not visible, the view hierarchy will be destroyed, and a new view heirarhy for the newly visible page will be created. All the view hierarchy recreation, is done through Fragment's onCreateView (Android Fragment is similar to iOS ViewController)
When you swipe from page 1 direcly to page 4, then swipe back again to page 1, the view for page 1 will be re-created via Fragment's onCreateView. However, how does Android restore multiple UI states in the page? If there is a RecyclerView (Similar to iOS UICollectionView) in page 1 being re-created, RecyclerView's previous scroll position will be restored via onRestoreInstanceState.
In Android, the mechanism 1 & 2, is pretty much offered by the Android framework out of the box. Not much work required at developer side.
I was wondering, how does iOS able to achieve the same thing?
I have seen a technique, described in https://stackoverflow.com/a/36876103/72437
However, it isn' clear, on how the UI state restoration state work? For instance, if I have an UICollectionView in page 1. How can I restore the UI state (scroll position) of UICollectionView, when I swipe back to page 1?
Use a UICollectionView.
If you need to have controllers for each cell, load the controllers in the re-useable cell on cell initialization.
UICollectionViewCells are UIViews with reuse functionality built on top; they are only de-initialized when the UICollectionView is de-initialized. When your cell is created for the first time, also create the view controller inside (which holds the actual view you want to reuse). Use the cell reuse and configuration calls (e.g. cellForRowAt(indexPath) and prepareForReuse()) to setup your vc/view inside the cell. Dependency injection is great in this flow. Prefetch if you need to load a lot of data or have slow response times.
iOS will also optimize the number of cells being reused at a time (really should never be more than say 20, there can only be so many on the screen), so you wont have to worry about maintaining a set or tracking how many instances you have.
Use a page view indicator if you need that, or make your own.
If you want to develop your own reuse system, study that of UICollectionView(Cell) first because they do this very well imo.
I have a set of UIViews and their controllers representing different pages in a "document." All are under the control of Apple's UIPageViewController.
The pages do not necessarily have static graphic content. The content can change based on user actions when viewing any given page. When that happens, my code invalidates the UIViews for all the appropriate pages using setNeedsDisplay.
But the view drawing update doesn't occur until a page is displayed >after< the UIPageViewController does all its magic animating a page turn.
The problem is that during that animation, UIPageViewController appears to use a cached version of the pixels of the old page. The user sees the old content up until the page-turning animation is finished, at which time, there's one last update (because of the earlier setNeedsDisplay call) and then the final correct version of the page is displayed. But the user sees this (flashing) change, which makes no visual sense, and violates the GUI illusion that what you see represents what you imagine.
I can't call each view's drawRect: method to force the pixels to change; drawRect: is reserved for the system to call.
So how does one get the UIPageViewController to update or toss a cached page to prevent this visually confusing situation?
I have to implement a view controller (on iPhone, portrait only, full screen view) where the upper part of the view must have an horinzontal, paged scrolling behavior, potentially infinite.
I already used for similar purposes UIPageViewControllers, to take advantage of the datasource and delegate protocols, which are very helpul for manage memory and other stuff (keeping only 3 view controllers in memory, providing delegates to handle actions exactly when a transition is done and so on): so I think that in this case too this component is the best choice.
But here comes my problem. In the view I'm realizing, I have to let the user understand that he can swipe left and right to move to another view: a page control is not a good choice, since the scroll could be potentially infinite, so I would like to let a small portion of the views of the left and right view controllers to be visible.
Something like that:
link to the image (sorry I cannot include images in my posts yet)
Up to now I have not been able to figure out how to realize this. In the options during initialization, UIPageViewControllerOptionSpineLocationKey can be specified to set (from documentation) "Space between pages, in points": but this seems to work only with positive value, so that the space increases, while it ignores negative values that could reduce the space.
I hope there might be a solution using page view controllers, since at the same time I need to refresh a table view in the lower part of the screen when a transition is complete, and the delegate method of page controllers is ideal for this aim.
Maybe a collection view can be an alternate solution, but it is more complicated and I'm not sure how to obtain a behavior like the one I described to refresh the table view.
If needed I can attach some code and a screenshot of the prototype
Ok, I understand that this is not possible and why it is.
The datasource methods load, when needed, the view controllers that are before and after the current one. Making these view controllers' views always visible, as I desired, will require that the datasource loads more than one view controllers after (or before, depends on the direction of scrolling) the current one, to be ready for the pan actions (potentially, before the animation is ended by the user lifting up its finger, two view controllers "after" or "before" could become visible it my desired configuration), and this is not intended by UIPageViewController on iPhone, especially in portrait mode.
So actually, the only way to achieve that more than one view is visible in an horizontal-scrolling component at any time, is to implement a UIScrollview with horizontal paging and calculate the contentSize and other sizes accordingly.
My original plan was to use a split view controller to have a table view and a generic view beside it for an iPad app. Then I realized I could not use a split view controller as a child controller. So, I've been experimenting with using a table view controller instead, and I've got pretty good results so far. Essentially, I'm using the tableview's header view as a container for the various views I'll be using depending on which row is selected. The only unresolved issue is that images for a website are not displayed in the UIWebView the first time it is presented. If I tap another table row that displays a web view, it displays fine. And if I tap the original one again, it displays fine, too.
It doesn't matter which one I tap first. It's always the first one that doesn't display images. Instead of displaying the images, it displays the names of the image files.
When I tried the analogous thing with a split view controller, I didn't have this problem.
Any ideas what I should be looking for? Or is there something I need to be doing that I must not be doing?
If it matters, the page being loaded is just a thumbnail page on a remote server, created with Irfanview's HTML thumbnail page generator.
P.S. After further testing, it is really the very first presentation only that doesn't work as expected. By very first, I mean the first per app session. The UIWebview is presented as a subview of the table header. The tableview controller is presented as a popover from a button of another controller that is presented when a user taps an image. Even if I dismiss all these layers of controllers, etc., if I instantiate them again, the UIWebView displays properly.
This suggests possibly loading a dummy UIWebView before starting, but that seems kludgy.
I'm getting exactly one call to webViewDidStartLoad: and exactly one call to webViewDidFinishLoad: and no calls to webView:didFailLoadWithError:.
KLUDGE:
The kludge works. Here it is:
In my table view controller, I create a boolean property URLHasBeenRequested and set it to NO in viewDidLoad. Then I ask for a reload thus:
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
if (!self.URLHasBeenRequested) {
//On first pass, try a second request.
[webView loadRequest:webView.request];
self.URLHasBeenRequested = YES;//Set this flag so a duplicate request happens only the first time.
}
}
I suspect this has more to do with the way the UITableView is loading the header and trying to allocate memory during that process, than with the UIWebView itself. For whatever reason, the UIWebView's loading seems to be getting interrupted while the rendering of the UITableView is still finishing ( just a guess ).
One thing you might try would be to go ahead and setup your table header by adding the UIWebView subview, but not calling "loadRequest" right away. You could, for example, wait until the rest of the visible cells of the table have been loaded, and then load the header view last. You can detect when that occurs like this:
-(void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if([indexPath row] == ((NSIndexPath*)[[tableView indexPathsForVisibleRows] lastObject]).row){
[webView loadRequest:webView.request];
}
}
This is, admittedly, also a somewhat kludgy workaround, but at least you're not having the UIWebView load the content twice. I also haven't tested this myself, and without seeing your actual table loading code, it's hard to say if the root of the problem lies somewhere else, but let me know if this works!
My UIViewController has a UIPageViewController embedded in it. The pager can contain anywhere from 5-38 pages (each page is an instance of a UIViewController subclass) depending on the situation. I've noticed that depending how complicated I make the UI elements on each page, the app slows down considerably, and is very slow when swiping to go to the next page.
Here's the thing - the view on each page is identical, except for the values of a few UITextViews. I am building the view in each page's viewDidLoad method each time viewControllerAtIndex is called for a new page. I feel like there must be a way to re-use the same view for each page, and just swap the text values that are supposed to be different. Can anyone describe a strategy to do this?
Like I said, every page has an identical view hierarchy except the values for some of the text, so I'm really just looking for a way to maintain one basic view controller per page, but cache the view hierarchy to be re-used on each page, and swap out some simple text values depending on page number.
EDIT
Something I forgot to mention in the original post is that I'm building my view programmatically because the number of elements on the page is dependent on choices the user made on previous screens. i.e. there may be 5 TextViews or 10 TextViews, etc. depending on what the user selected on a previous screen (before coming to the UIPageViewController). With that said, I do not believe an xib based approach will work because the initial layout is dynamic.
Thanks in advance!
Have you tried using a xib based view, which you could pull out of the xib once, and keep a strong reference to it. In your controller's viewDidLoad, you could set its view to this xib base view, then populate the text view(s) with the proper text.