UIPageViewController caching issue when waiting for new data - ios

I am using UIPageViewController to show images. If no data for a given image is received from the server I want the user to be able to slide, but to not be able to go to the next view controller / unloaded image. So I return nil in the datasource method pageViewController(_:viewControllerAfter:). The code is something like:
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let nextImageIndex = currentImageIndex + 1
if nextImageIndex >= currentImagesCount && currentImagesCount < totalImagesCount {
return nil
}
return getNewController(forIndex: nextImageIndex)
}
The problem is that the UIPageViewController has some caching. So if the user just continues to slide, they won’t ever see the newly received image.
Is there a good way to handle this?
What I tried so far:
func onNewImage() {
pageViewController.dataSource = nil
pageViewController.dataSource = pageViewController
}
This kind of solves it in a sense that the user will be able to eventually slide to the next page, but leads to many more bugs.
Is there another approach to this? I have tried to preload some images in advance, but on slower internet connection this case still exists.

Related

Is there a better way to do this scrolling animation in Swift?

I'm trying to add a behavior to part of my app's UI whereby the user can swipe left and this will result in a set of UILabels scrolling in sync together off-screen to the left, but then immediately scrolling back in again but from the right, but with new information contained in them.
The effect is meant to give the impression that you're moving from one "set" of info to the next... like, say, choosing a car before starting a race game... but in reality it is the same views being re-used... scrolling offscreen to have their label.text info updated... then scrolling back in again.
I have the swiping all taken care of. The issue I'm having is that my (working) solution:
UIView.animate(withDuration: 0.2, animations: {
// move label off to the left
self.titleLabel.center.x -= self.view.bounds.width
}, completion: {
$0 ; print("I'm halfway done!")
// teleport view to a location off to the right
self.titleLabel.center.x += 2*(self.view.bounds.width)
// reset label's data
self.titleLabel.text = NEW_INFO
// slide label back on screen from the right
UIView.animate(withDuration: 0.2, animations: {
self.titleLabel.center.x -= self.view.bounds.width
}, completion: nil)
})
Feels trashy like wearing someone else's underwear.
The only reason that $0 is there is to make XCode stop saying:
"Cannot convert value of type '() -> ()' to expected argument type '((Bool) -> Void)?'"
And I'm sure the fact that I'm doing the second part of the animation in a completion block will cause headaches down the road.
Is there a smarter way?
PS - I would prefer not to use any pre-made classes like "ScrollView" or anything like that... these views are all individually interactive and respond to other callbacks etc.
Thanks!
A better approach to something like this would be to use a UIPageViewController.
It will take a bit of learning and set up but is much easier than trying to roll it yourself.
The approach to take with a UIPageViewController is something like this...
Create a data model... to use your analogy...
struct Car {
let image: UIImage
let name: String
}
Then create a UIViewController subclass that will display it.
class CarViewController: UIViewController {
var car: Car? {
didSet {
displayCar()
}
}
func displayCar() {
label.text = car?.name
imageView.image = car?.image
}
}
Then you create a UIPageViewController. Inside this you have an array of cars. And in the function func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? you can then create your CarViewController and pass in the correct Car from the array.
This will then do all your scrolling and displaying and everything is still interactive.
For more information about how this works you can look at tutorials like this one from Ray Wenderlich.
You can also use this to display a part of a page (rather than scrolling the entire screen.

Horizontal UIScrollView with infinite loop in swift

How can I make an infinite looping view for horizontal UIScrollView?
Example of what I want
Let's say I have a collection var subViews = [1,2,3,4,5,6]. I want to create UIScrollView with this collection
for subview in subViews {
uiscrollview.addSubview(subview)
}
but I don't want to fill my ScrollView with all items on that collection. It should be only three items always like [1,2,3] or [2,3,4] or [5,6,1]
I have found this library, but its Objective-C.
Thanks in advance.
Upadate:
let page = Int(floor((pageScrollView.contentOffset.x * 2.0 + pageWidth) / (pageWidth * 2.0)))
let firstPage = page - 1
let lastPage = page + 1
for index in 0..<subViews.count{
let subview = subViews[index]
if index != firstPage || index != lastPage {
subview.removeFromSuperview()
subpageViewControlls[index] = nil
}
}
You have a couple of options here. First, something like iCarousel would let you create infinitely looped views very easily – you just tell it how you want each view configured, and you're done. I would suggest that this is the preferred solution for ease and also maintainability.
A second solution is to use UIPageViewController to handle swiping between pages, but that would require you to implement the looping yourself. Thanks to the way UIPageViewController is implemented, this is trivial: just make sure you use modulus or similar in these two:
pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController)
pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController)
Both of these options lazy load so you would never have to load more than three views at a time.
Update: OP has commented that they have lots of code for the scroll view delegate already. Assuming they are unable to replace that code and use something like UIPageViewController, the solution here effectively is to recreate the behaviour of UIPageViewController inside the existing scroll view that is being used. Warning: I would not recommend this solution compared to using something dedicated for this job.
1) Implement scrollViewDidScroll(scrollView: UIScrollView) so you know when the user scrolled.
2) Calculate which "page" they are on using something like let page = floor((scrollView.contentOffset.x - yourPageWidthHere / 2) / yourPageWidthHere) + 1; (written from memory; please check!)
3) Pre-load all pages in your array that are at that page +/- 1. Unload all other pages, replacing them with NSNull in your view controllers array so that the positions don't move.
4) Update your scroll view's contentSize property to allow the user to continue scrolling. If you wanted to jump them back to an earlier point you could do, but I don't think this would have any effect on performance.

Many view controllers - performance issue

I have an IOS app that lets user swipe through weeks of notes. Each week is a UIViewController - the swiping and switching between the view controllers are handled by a UIPageViewController.
On startup all the view controllers are initialised with their data.
When the user swipes I grap a view controller like this:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let currentPageViewController = viewController as? SinglePageViewController {
let currentIndex = currentPageViewController.index
return self.weeks[currentIndex - 1]
}
return nil
}
The app work flawless, until a use has many weeks, and thereby many view controllers. Startup time start to become an issue - and this will of cause only get worse as the weeks go on.
I've played around with initialising the each view controller when the user swipes. Like this:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let currentPageViewController = viewController as? SinglePageViewController {
let currentIndex = currentPageViewController.index
let newVC = SinglePageViewController()
newVC.index = currentIndex - 1
return newVC
}
return nil
}
This approach works and the startup time is great - however, the swiping has now become sluggish and not smooth at all.
Can any one advise on how this issue can be resolved?
The second method (creation on demand) is the correct way to do it. If the swipping gets slow then because you spend to much CPU time in init(), viewDidLoad, viewWillAppear, etc... Look at the initialization and move every CPU intensive task to background threads.
If you depend on data to create the ViewController then you have to preload the data in advance. But it is not needed to preload the data for more then 2 or 3 of them. If it takes to much time and you still run into performane problems then you have to accept that the device is not fast enough for your requirements and you have to present the user an loading indicator. (like UIActivityIndicator)
If you need help in optimizing the initialization then post your code.
I had a similar issue with using too many UIScrollViews inside a UIScrollView. My solution was to monitor where the user was looking, by using the scrollViewDidScroll delegate method, hooked up to my container scrollview, and populate/remove view according to the direction of the user's scroll.
The direction you can get from let direction = scrollView.panGestureRecognizer.translationInView(scrollView.superview!) within the scrollViewDidScroll method.
Would this type of method work for you? I could give you more of my code if you'd like!

Wait for closure result for doing the return

I got a book reader coded in Swift, the first chapter is OK, but when I try to load the second from the webservice (the books come from a server chapter by chapter), the data source method of the pageviewcontroller has to return a viewcontroller, and it doesn't wait to the closure which gets the new chapter, it always return nil. I've tried using dispatch_semaphore_t, dispatch_group_t, etc, and I don't get it. The method is the following:
// MARK: - PageViewControllerDataSource
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if self.currentPageInSpineIndex! + 1 < self.currentChapterPages {
self.currentPageInSpineIndex!++
} else if self.selectedChapter + 1 < self.tale?.chapters.count {
self.selectedChapter++
self.currentPageInSpineIndex = 0
}
self.pageContentViewControllerAtIndex(self.currentPageInSpineIndex!, onCompletePage: { (let pageContentViewController: PageContentViewController) -> Void in
return pageContentViewController
})
return nil
}
The pageContentViewControllerAtIndex method checks into de DB if it has the text of the chapter, and if not, it asks the server for it, then create a pagecontentviewcontroller with the text in it, and then returns it on the onCompletePage block, the thing is, that it always reachs the return nil line before the closure ends, so it doesn't work. I want to wait for the closure, have the pagecontentviewcontroller, and then return that result. Some ideas?
The pageViewController:viewControllerAfterViewController: method expects a UIViewController (or subclass) to be returned, you won't be able to use asynchronous code within this method to create and return a view controller.
Suggestions based on waiting for the asynchronous method to complete are not appropriate. This is because the UIPageViewController delegate methods are invoked on the main thread and introducing any waiting mechanism will block the UI resulting in an unresponsive feel and potentially causing the system to terminate the app.
What I would recommend doing is creating a view controller, return it, and load the content from that view controller. Most likely you will want to display some kind of loading indicator on that view until the content has been retrieved.
Have a look at my Swift library which has a bunch of functions to convert between asynchronous and synchronous functions: https://github.com/Kametrixom/Swift-SyncAsync There's a bunch of examples on the website, as well as a very detailed playground
You'd be able to do something this:
return toSync(pageContentViewControllerAtIndex)(currentPageInSpineIndex!)
EDIT: You're doing a web request -> asynchronous!

How I can create an UIPageViewController where are all screens different and has own ViewControllers?

I want to use the cool swipe animation between screens - UIPageViewController (yes, you know the style from the intro screen), but all the code I found on the Internet and Github was useless for me.
I found demos with just one UIViewController in the Storyboard interface and almost all the apps showed how to change an image source from an array. I read the Apple reference, but I do not understand it.
I need a few ViewControllers on my Storyboard (I want to design all the screens in the UIPageViewController differently, they will be connected to own ViewControllers classes) who will be presented in the UIPageViewController.
Or of course if you know a better way how do that please say so! But I need the feature that if you swipe, the screen moves with you.
Does someone know how to do that?
There's nothing in UIPageViewController that requires the various view controllers to be same class. So, just implement viewControllerBeforeViewController and viewControllerAfterViewController that return different types of view controllers. If you want to reference child view controllers from the storyboard, just give those scenes unique storyboard ids, and then you can use instantiateViewControllerWithIdentifier. You might, for example, have an array of storyboard identifiers, and use that to determine which type of scene is "before" and "after" the current one.
There are tons of ways of implementing this, but you could do something like:
class ViewController: UIPageViewController, UIPageViewControllerDataSource {
let identifiers = ["A", "B", "C", "D"] // the storyboard ids for the four child view controllers
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
setViewControllers([viewControllerForPage(0)!], direction: .Forward, animated: false, completion: nil)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var page = (viewController as PageDelegate).pageNumber + 1
return viewControllerForPage(page)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var page = (viewController as PageDelegate).pageNumber - 1
return viewControllerForPage(page)
}
func viewControllerForPage(page: Int) -> UIViewController? {
if page >= 0 && page < identifiers.count {
let controller = storyboard?.instantiateViewControllerWithIdentifier(identifiers[page]) as? UIViewController
(controller as PageDelegate).pageNumber = page
return controller
}
return nil
}
}
Clearly, if you wanted to be more elegant here, you could maintain a cache of previously instantiated view controllers, but make sure you respond to memory pressure and purge that cache if necessary. But hopefully this illustrates the fact that there's nothing about page view controllers that dictates that the children are a particular class of controller.
By the way, the above assumes that each of the child view controllers conforms to a protocol to keep track of the page number.
/// Page delegate protocol
///
/// This is a protocol implemented by all of the child view controllers. I'm using it
/// just to keep track of the page number. In practical usage, you might also pass a
/// reference to a model object, too.
#objc protocol PageDelegate {
var pageNumber: Int { get set }
}
If you want to go a completely different direction, another approach is to use standard storyboard where you have segues that present one view controller after another, and then for each view controller implement swipe gesture recognizers, where swiping from the right performs the segue to transition to the next scene (e.g. an IBAction that does performSegueWithIdentifier), and another swipe gesture recognizer (left to right) will dismiss the view controller.
Finally, if you want these gesture recognizers to be interactive (e.g. to follow along with the user's finger), you could use custom transitions, combined with interactive transitions. For more information, see WWDC 2013 video Custom Transitions Using View Controllers or WWDC 2014 videos View Controller Advancements in iOS 8 (which, about 20 minutes into the video, describes how custom transitions have been enhanced in iOS 8 with presentation controllers) and A Look Inside Presentation Controllers.
I think you might take advantage of View Controller Containment.
We are dealing with 4 elements at play here.
Main View Controller
scroll view
UIPage control
Detail View Controllers
You would add the scroll view and the page control as the main view controller's properties. The main controller would handle the scrolling logic, basically syncing the horizontal scrolling between the scrollview and the page control.
The contents of the scroll view would be constituted by root views of all the detail view controllers.

Resources