I am trying to implement a UIPageViewController, where you can slide between two UICollectionViewControllers (much like the iOS 8.4 music App). This is also supposed to use one instance of UINavigationController.
This is what I've tried so far:
Using Storyboard, I've added a new PageViewController, in which I have conformed to the UIPageViewControllerDataSource as well as the UIPageViewControllerDelegate protocols.
Using Storyboard identifiers, I have successfully been able to get this to the point where you can swipe between each view.
Here is some sample code to be more specific:
override func viewDidLoad() {
self.dataSource = self
self.delegate = self
let startingViewController = self.viewControllerAtIndex(index)
let viewControllers: NSArray = [startingViewController]
self.setViewControllers(viewControllers as [AnyObject], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
func viewControllerAtIndex(index: Int) -> UICollectionViewController! {
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
if index == 0 {
return storyBoard.instantiateViewControllerWithIdentifier("AlbumsController") as! UICollectionViewController
}
if index == 1 {
return storyBoard.instantiateViewControllerWithIdentifier("SubscriptionsController") as! UICollectionViewController
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
let index = self.identifiers.indexOfObject(identifier!)
if index == identifiers.count - 1 {
return nil
}
self.index = self.index + 1
return self.viewControllerAtIndex(self.index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
let index = self.identifiers.indexOfObject(identifier!)
if index == 0 {
return nil
}
self.index = self.index - 1
return self.viewControllerAtIndex(self.index)
}
The problem arises in the gestures being lost/overwritten when swiped to the next view. Initially I can scroll the first collection view and tap on the status bar to scroll to the top, but as soon as the pageviewcontroller swipes over to the next collection view, this gesture stops working and now neither collection view responds to the "scroll to top" gesture.
I have also heard using a scrollview is possible, but I wasn't convinced that this implementation would be more appropriate that using a UIPageViewController.
Current research suggests that the gestures are affected by the 'data source' of the PageViewController and must be altered in some way.
I have also embedded the UIPageViewController inside of a UINavigationController to achieve a shared navigation bar for both views. Now of course the navigation bar overlaps the UICollectionViews as they are not themselves embedded within a UINavigationController. To work my way around this problem, I've used this code below:
collectionView.contentInset = UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0)
Would anyone be able to give me some advice on how to properly re-create the 'swiping between two views' effect you get in the new Music App?
With my current implementation, I feel as it is not the correct way to achieve my goal, so I would greatly appreciate any help.
Related
I have a tableview which has load more. I'd like to add scroll to top function when user press tabbar item twice like twitter, instagram.
This is my code when user tap twice to tab bar item.
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if previousController == viewController {
if let navVC = viewController as? UINavigationController, let vc = navVC.viewControllers.first as? AssistantMainViewController {
if vc.isViewLoaded && (vc.view.window != nil) {
// viewController is visible
vc.tableView.setContentOffset(CGPoint(x: 0, y: -173), animated: true)
}
}
}
previousController = viewController
}
It works fine when tableview first load. The problem is when I load more cell to my tableview and scroll to top, tableview not scrolls to top it stuck in middle of somewhere. Other weird thing is if I scroll manually to top after load more, tab bar item scroll tableview properly until load more cells.
In load more action basicly app get more content from server and add them to array and call tableview.reloadData()
adviceDataSource.loadMoreContent(beforeDay: beforeDate, success: {feedCount in
if feedCount == 0 {
self.didGetLastPage = true
}
self.tableView.reloadData()
self.tableView.layoutSubviews()
self.isRunningRequest = false
}, error: { (errorString, statusCode) in
self.isRunningRequest = false
}) { (MoyaError) in
self.isRunningRequest = false
}
Probably the problem is when I load more cell tableview doesn't know its new content size.By the way when tap status bar tableview scroll to top perfectly. If you know the func that called when user tap status bar, it would be perfect for me.
I've been working and searching on it 2 days, any help?
Thank you!
call UITabBarControllerDelegate in your viewcontroller tableView class.
in viewdidload Method call
self.tabBarController?.delegate = self
implement tab bar didselect in your viewcontroller class
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let tabBarIndex = tabBarController.selectedIndex
if tabBarIndex == 0 {
self.tableView.setContentOffset(CGPoint.zero, animated: true)
}
}
Replace tableView.layoutSubviews() with tableView.layoutIfNeeded(), it layout the tableview immediately.
And use the tabbarcontrollerdelegate:
The viewcontroller(which owns the tableview) will listen to it. and use this method
self.primaryTableView.scrollToRow(at: IndexPath, at: UITableViewScrollPosition, animated: Bool)
I want to scroll to the top when i double tap on any BarButtonItem.
I saw a lot of answers on stackOverflow but none of them worked for me.
Maybe I'm using it wrong? Where do i put the code in the AppDelegate or the TableViewControllers i want to add this functionality specifically?
anyway, I'm using Swift 2.3 and Xcode 8 and would love to get some help.
Thank you.
Do you know about scrollsToTop? I think it is what you need. Description from iOS SDK related to scrollsToTop property in UIScrollView:
When the user taps the status bar, the scroll view beneath the touch which is closest to the status bar will be scrolled to top, but only if its scrollsToTop property is YES, its delegate does not return NO from shouldScrollViewScrollToTop, and it is not already at the top.
// On iPhone, we execute this gesture only if there's one on-screen scroll view with scrollsToTop == YES. If more than one is found, none will be scrolled.
At first setup UITabBarControllerDelegate delegate. Delegate can be easily set from Storyboard or via code with UITabBarController's delegate property.
myTabBar.delegate = myNewDelegate // Have to conform to UITabBarControllerDelegate protocol
You can even subclass UITabBarController and implement UITabBarControllerDelegate for it so it can become delegate for itself.
mySubclassedTabBar.delegate = mySubclassedTabBar
When you have delegate you can try out this method.
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool
{
guard let tabBarControllers = tabBarController.viewControllers
else
{
// TabBar have no configured controllers
return true
}
if let newIndex = tabBarControllers.indexOf(viewController) where newIndex == tabBarController.selectedIndex
{
// Index won't change so we can scroll
guard let tableViewController = viewController as? UITableViewController // Or any other condition
else
{
// We are not in UITableViewController
return true
}
tableViewController.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: 0), atScrollPosition: .Top, animated: true)
}
return true
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if tabBarController.viewControllers!.firstIndex(of: viewController) == 0 {
if self.selectedIndex != 0 { return true }
if let navigationController = viewController as? UINavigationController{
if let streamController = navigationController.viewControllers.last as? HomeVC
{
streamController.tableView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
}
}
return true
}
return true
}
I am looking to create a PageViewController very similar to SnapChat's one, whereby you can swipe from the UIImagePickerController to another VC. To do this, I have my initial VC which displays the imagepickercontroller, and a second VC (a caption VC) which I want to come after this initial VC. To encapsulate my PageViewController, I have created another VC class (shown below) which I have now set as my initial VC, and I am trying to handle the PageVC data source.
For whatever reason, it is not working and the error - 'fatal error: unexpectedly found nil while unwrapping an Optional value' occurs. Is this because you can't contain an imagePickerController in a PageVC (doubtful as SnapChat do). I created a simpler template which contained two simple VCs perfectly - why can I not do this here? The other one I did, I contained all the below code in the initial VC that the project starts with, whereas here I created an additional VC and manually changed it to make it the 'initial view controller'.
NB. the project compiles fine without the pageVC so it is nothing to do with any bad code in the other VCs.
I am very stuck and would hugely appreciate some help to this tricky issue. Thanks!
class PageViewController: UIViewController, UIPageViewControllerDataSource {
private var pageViewController: UIPageViewController?
private let VCarray = [ViewController(), CaptionViewController()]
override func viewDidLoad() {
super.viewDidLoad()
createPageViewController()
}
private func createPageViewController() {
let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("PageController") as! UIPageViewController
pageController.dataSource = self
if VCarray.count > 0 {
pageController.setViewControllers([ViewController()], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
pageViewController = pageController
addChildViewController(pageViewController!)
self.view.addSubview(pageViewController!.view)
pageViewController!.didMoveToParentViewController(self)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if viewController.isKindOfClass(CaptionViewController) {
let pvc = self.storyboard?.instantiateViewControllerWithIdentifier("CameraVC")
return pvc
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if viewController.isKindOfClass(ViewController) {
let pvc = self.storyboard?.instantiateViewControllerWithIdentifier("CaptionVC")
return pvc
}
return nil
}
The only optional unwrapping I see is on the first line. Is that where the exception is being thrown? Are you sure your main controller has a storyboard associated with it? If so, are you sure that the storyboard contains a controller named "PageController"?
I have an app in working state in which I have a three screens 1,2,3 each of that screen is associated with a UICollectionView which are created programatically now I want to modify the current implementation & add those collection views to UIPageViewControllers.
I tried to find many tutorials related to PageViewController with CollectionViews but was not able to find anything. Can anyone help me out in implementing this or can give me a reference related to this.
I have also referred this tutorial, but hard luck for me :(
Just follow some of those page view controller tutorials, and when you get to the point of instantiating the child view controllers, make those children collection view controllers. Just as in your example tutorial where it is using UIImageView try replacing it with UICollectionViews & you will achieve what you want.
Do tell if you face any difficulties.
Happy Coding!
You can do this by:
1) Subclassing UIPageViewController and conforming to the UIPageViewControllerDataSource protocol. The data source will tell the pageViewController which view controllers will come next or before the current view controller being presented.
2) In viewDidLoad(), set the dataSource as self so the delegate methods get called. Also, call self.setViewControllers([collectionViewController], direction: .Forward, animated: true, completion: nil). collectionViewController will be an array populated with 1 collection view controller (the first view controller presented).
Example:
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self //so our delegate methods get called
//use tags to reference each controller
let collectionViewOne = TestCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
collectionViewOne.view.tag = 0
let collectionViewTwo = TestCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
collectionViewTwo.view.tag = 1
let collectionViewThree = TestCollectionViewController(collectionViewLayout: UICollectionViewFlowLayout())
collectionViewThree.view.tag = 2
collectionViewControllers = [collectionViewOne, collectionViewTwo, collectionViewThree]
self.setViewControllers([collectionViewOne], direction: .Forward, animated: true, completion: nil)
}
3) Implement the UIPageViewControllerDataSource methods:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var index = viewController.view.tag
if index == 0 {
return nil
}
if index > 0 {
index--
}
return collectionViewControllers[index] as? TestCollectionViewController
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var index = viewController.view.tag
if index == 2 {
return nil
}
if index < collectionViewControllers.count - 1 {
index++
}
return collectionViewControllers[index] as? TestCollectionViewController
}
We return nil if the index will present an out of bounds view controller
Source code of TestCollectionViewController and the UIPageViewController subclass can be found here
I ran into a similar situation recently. I could never get the embedded UICollectionViews to work quite how I wanted them to in the UIPageViewController. I also felt that the implementation was overly complex, requiring a separate view controller for each UICollectionView. I did not feel this was appropriate for my dataset.
My solution was to use a single UIViewController with a horizontally scrolling, paged UICollectionView to handle the paging. Then I added vertically scrolling UICollectionViews for every page to contain my content. I also added an overlay that contained a UIPageControl that was fed scroll-position data to mock the page indicator. This vastly simplified my view controller to data source relationship, was easier to customize, and allowed greater control.
(I would post source but the code belongs to my employer and I used storyboards.)
In Swift, I currently have the following code in my first view controller, yet when I swipe nothing happens? What is wrong with my code? Do I have to implement something else? Thanks alot SO users, appreciate it.
import UIKit
class ViewController: UIViewController, UIPageViewControllerDataSource {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
var myViewControllers = Array(count: 4, repeatedValue:UIViewController())
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
let pvc = segue.destinationViewController as UIPageViewController
pvc.dataSource = self
let storyboard = UIStoryboard(name: "Main", bundle: nil);
var vc0 = storyboard.instantiateViewControllerWithIdentifier("FirstViewController") as UIViewController
var vc1 = storyboard.instantiateViewControllerWithIdentifier("SecondViewController") as UIViewController
var vc2 = storyboard.instantiateViewControllerWithIdentifier("ThirdViewController") as UIViewController
var vc3 = storyboard.instantiateViewControllerWithIdentifier("FourthViewController") as UIViewController
self.myViewControllers = [vc0, vc1, vc2, vc3]
pvc.setViewControllers([myViewControllers[1]], direction:.Forward, animated:false, completion:nil)
println("Loaded")
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
var currentIndex = find(self.myViewControllers, viewController)!+1
if currentIndex >= self.myViewControllers.count {
return nil
}
return self.myViewControllers[currentIndex]
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
var currentIndex = find(self.myViewControllers, viewController)!-1
if currentIndex < 0 {
return nil
}
return self.myViewControllers[currentIndex]
}
}
Sorry, your code is working perfectly for me. I made a trivial test (never having used a UIPageViewController before) with a button to segue from the initial VC to the Page VC, and two disconnected VCs in the storyboard to be constructed programatically with your code (I pasted it direct from above) and given to the Page VC. Each of the 3 visible pages was identified with a label. And they all work fine.
Does your UIPageViewController get instantiated? Do your two delegate routines above get called?
I suspect that you have not set up your storyboard appropriately. Here is mine:
When iOS runs your app, it transfers control to the initial VC. That's the one that you're writing your code in, called ViewController in the above source. That is not a UIPageViewController; it's an ordinary UIViewController, it says so here:
class ViewController: UIViewController, UIPageViewControllerDataSource {
The UIPageViewControllerDataSource bit just says it also provides the routines that tell a UIPageViewController what to display. When you do a segue out of ViewController, the prepareForSegue code runs then, and programs the destination of the segue, which is expected to be a UIPageViewController, with the other 4 VCs. To make the segue work, I dragged a UIButton to the first ViewController in storyboard, and dragged a UIPageViewController onto the right. Then control-drag from the button to the UIPageViewController. You should get a popup asking what kind of segue you want. I chose modal. When you run the app, you should see the button. When you press the button, you should see VC1, and swiping left or right will show the others.