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!
Related
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.
I'm trying to make a custom ContainerViewController, but due to lots of difficulties with the ViewController transitions and making everything interactive, I've decided to mimic that functionality myself.
What I basically want to do, is have a paginated UIScrollView (the HeaderView) on the top control different another UIScrollView (the ControllersView) below that contains ViewControllers as pages so that as you swipe to a new page on the HeaderView, it also swipes to the next viewcontroller on the ControllersView. This is what the setup would look like.
My question is, is there anything wrong with having the aforementioned setup? All I'll do to add the view controllers to the ControllersView is just something like: controllersView.addSubview(pagecontroller1.view).
Some posts online seem to say that "the appropriate ViewController functions won't be called" or whatever. What do I seem to be missing here? I'm guessing there's a lot of dismissing and admitting of ViewControllers that I need to call every time a ViewController is out of frame right?
To clarify the question: Is it ok/efficient to do this? Should I be calling some viewWillAppear/disapper functions when the VC's get in and out of frame? If so, what should I call? I'm realizing that if I were to set things up this way, I need to manage a lot of things that are usually handled automatically, but as I mentioned before, custom ContainerViewControllers have failed me and I'm going with this.
PS. If you seem to still be lost on how this will look like, see my previous question here where I originally wanted to use a Container ViewController. There's a much better mockup there.
You can add and remove VC In Container Views
For - Is it ok/efficient to do this? Should I be calling some viewWillAppear/disapper functions when the VC's get in and out of frame? If so, what should I call?
As, We need to call WillAppear and Disappear Func when Adding and removing a VC , Thus Try using below Functions That will Handle these Responses
I use the Two specific Functions to add and remove Controller in ContainerView/UIView/SubView in ScrollView inside a UIView
To Add
private func add(asChildViewController viewController: UIViewController)
{
// Configure Child View
viewController.view.frame = CGRect(x: 0, y: 0, width: self.firstContainer.frame.size.width, height: self.firstContainer.frame.size.height)
// Add Child View Controller
addChildViewController(viewController)
viewController.view.translatesAutoresizingMaskIntoConstraints = true
// Add Child View as Subview
firstContainer.addSubview(viewController.view)
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
To Remove
private func remove(asChildViewController viewController: UIViewController)
{
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
secondContainer.willRemoveSubview(viewController.view)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
Creating Object
private lazy var FirstObject: firstVC =
{
// Instantiate View Controller
let viewController = self.storyboard?.instantiateViewController(withIdentifier: "firstVC") as! firstVC
// Add View Controller as Child View Controller
self.addChildViewController(viewController)
return viewController
}()
For - controllersView.addSubview(pagecontroller1.view)
Answer - Yes Approbate func wont be called if pagecontroller1 is not loaded in to memory stack, to load that you need to notify pagecontroller1 that it is going to be added to memory stack as Child View , Just as We initiate a Controller and basically notifies the Controller to get its component loaded to memory stack to get some memory allocations
For Question - Is it fine to nest a UIViewController within another without using addChildViewController?
Check apple Documentation - https://developer.apple.com/documentation/uikit/uiviewcontroller/1621394-addchildviewcontroller
This is necessary just as to notify the controller who is going to be added in Another Parent View as Child
Sample Project
https://github.com/RockinGarg/Container_Views.git
Or
https://github.com/RockinGarg/ContainerView-TabBar.git
If Question is Still not answered Please Tell me what Func Exactly you want to handle by yourself
The below function is how I display different views when a user swipes right where
self.viewControllerAtIndex() is my own custom function that returns a view. The problem is that the first swipe outputs "---------swipe Right before 0" twice. And then works perfectly like expected afterwards.
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
print("---------swipe Right before " + String(index))
index += 1
print("---------swipe Right " + String(index))
if index == (products!.count) {
index = 0
}
return self.viewControllerAtIndex(index:index)
}
=====CONSOLE OUTPUT=====
---------swipe Right before 0
---------swipe Right 1
---------swipe Right before 0
---------swipe Right 1
func viewControllerAtIndex(index: Int) -> ViewController{
return ViewController.init(id: index)
}
For some reason, every other swipe after the first works as expected. The initial swipe is what causes the console output above. This causes my view sequence to look like below (2 views)
First Swipe
View1
Second Swipe
View1
Third Swipe
View2
Fourth Swipe
View1
Fifth Swipe
View2
I'm also initiating my uiPageViewController like so
let array = [ViewController](repeating: ViewController.init(videos: (self.videos![self.products![self.index].id]!)), count: 1)
self.UIViewPageController?.setViewControllers(array, direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
So I'm creating a new ViewController on viewDidLoad and then when a user swipes, I'm creating new ViewControllers
I think you should avoid doing index state management in this method. Apple does not guarantee the circumstances under which this method is called. If I had to paraphrase, I would say that while the framework is asking you for the view controller that should come after the provided one, you are answering a different question: Which view controller comes after the one you previously told us about?
To get around this, you need to use your original array of view controllers, find the index of the passed-in view controller in that array, and return the view controller that comes next (or nil if you are at the end of the array).
Too long for a comment ...
Chris Trahey's is the correct answer.
I had the same problem, based on the misunderstanding that the viewControllerBefore/viewControllerAfter methods meant "What should happen if the user swipes right/left". Like you said, these methods only answer the question: "Which view controller comes before/after a given view controller?" They should only answer that question, without any side effects.
A solution for some scenarios would be to add an index property to the view controller class, and set that index on creation. That's useful if you're not working directly with an array of view controllers, but create them on the fly. Then, instead of saying array.index(of: vc), you can say vc.index.
See here: https://stackoverflow.com/questions/25795093/swift-uipageviewcontroller-always-repeats-second-viewcontroller#=
EDIT:
Oh, and if you want to actually react to a page turn, you can do that in pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:).
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.
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.