UIPageViewController initial swipe called twice - ios

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:).

Related

Pass data between three viewController, all in navigationController, popToRootView

The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.

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!

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.

Swift: Perform a function on ViewController after dismissing modal

A user is in a view controller which calls a modal. When self.dismissViewController is called on the modal, a function needs to be run on the initial view controller. This function also requires a variable passed from the modal.
This modal can be displayed from a number of view controllers, so the function cannot be directly called in a viewDidDisappear on the modal view.
How can this be accomplished in swift?
How about delegate?
Or you can make a ViewController like this:
typealias Action = (x: AnyObject) -> () // replace AnyObject to what you need
class ViewController: UIViewController {
func modalAction() -> Action {
return { [unowned self] x in
// the x is what you want to passed by the modal viewcontroller
// now you got it
}
}
}
And in modal:
class ModalViewController: UIViewController {
var callbackAction: Action?
override func viewDidDisappear(_ animated: Bool) {
let x = … // the x is what you pass to ViewController
callbackAction?(x)
}
}
Of course, when you show ModalViewController need to set callbackAction like this modal.callbackAction = modalAction() in ViewController
The answer supplied and chosen by the question asker (Michael Voccola) didn't work for me, so I wanted to supply another answer option. His answer didn't work for me because viewDidAppear does not appear to run when I dismiss the modal view.
I have a table and a modal VC that appears and takes some table input. I had no trouble sending the initial VC the modal's new variable info. However, I was having trouble getting the table to automatically run a tableView.reloadData function upon dismissing the modal view.
The answer that worked for me was in the comments above:
You likely want to do this using an unwind segue on the modal, that
way you can set up a function on the parent that gets called when it
unwinds. stackoverflow.com/questions/12561735/… – porglezomp Dec 15
'14 at 3:41
And if you're only unwinding one step (VC2 to VC1), you only need a snippet of the given answer:
Step 1: Insert method in VC1 code
When you perform an unwind segue, you need to specify an action, which
is an action method of the view controller you want to unwind to:
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
//Insert function to be run upon dismiss of VC2
}
Step 2: In storyboard, in the presented VC2, drag from the button to the exit icon and select "unwindToThisViewController"
After the action method has been added, you can define the unwind
segue in the storyboard by control-dragging to the Exit icon.
And that's it. Those two steps worked for me. Now when my modal view is dismissed, my table updates. Just figured I'd add this, in case anyone else's issue wasn't solved by the chosen answer.
I was able to achieve the desired result by setting a Global Variable as a boolean value from the modal view controller. The variable is initiated and made available from a struct in a separate class.
When the modal is dismissed, the viewDidAppear method on the initial view controller responds accordingly to the value of the global variable and, if needed, flips the value on the global variable.
I am not sure if this is the most efficient way from a performance perspective, but it works perfectly in my scenario.

Resources