Why am I getting this error after running my PageViewController? - ios

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"?

Related

How to connect VC throw navController in tabBarController to pass data in swift

I'm trying to change text of label in VC when tabBar item tapped. But after i embedded UINavigationController to VC, i'm having crash when tapping on tabBar (Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value). Does anybody know how to fix that? This is a photo of crash line
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if tabBarController.selectedIndex == 3{
let navVC = tabBarController.viewControllers![3] as? UINavigationController
let bagPageVC = navVC?.topViewController as? bagPage
bagPageVC!.stringCountOfHP = "\(String(bagPageVC!.countArray)) items in bag"
bagPageVC?.tableView.reloadData()
}
}
}
This should help your code from crashing,
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if tabBarController.selectedIndex == 3{
if let navVC = tabBarController.viewControllers?[3] as? UINavigationController,
let bagPageVC = navVC.topViewController as? bagPage {
bagPageVC.countOfHP.text = "\(String(bagPageVC.countArray)) items in bag"
}
}
There might be multiple reasons for crash
1.tabBarController.viewControllers might be returning nil,
tabBarController.viewControllers![3] as! UINavigationController Though there exists a view controller at index 3 it might not be UINavigationController or subclass of UINavigationController
bagPageVC! might be nil and hence force unwrapping might be crashing it because navVC.topViewController is not bagPage
Without taking a proper look into your ViewController set up n hierarchy it wont be possible to help you further than this.
Code above will safely unwrap all the optionals, so it wont crash your code
EDIT 1:
Issue happens for the first time, when you tap on tab bar at index 3, ViewController at that index is not yet visible on screen, so obviously all its IBOutlets are nil, and when you blindly access the implicit optional IBOutlet bagPageVC.countOfHP it crashes.
What can you do?
Two possible solutions I can think of
Solution 1:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if tabBarController.selectedIndex == 3,
let bagPageVC = (viewController as? UINavigationController)?.topViewController as? bagPage {
let _ = bagPageVC.view //this statement will force the view controller to load its view hence `ViewDidLoad` will be called automatically for the first time (which anyway would have called you are just fast tracking it
bagPageVC.countOfHP.text = "\(String(bagPageVC.countArray)) items in bag"
}
}
didSelect viewController will be called everytime user selects a different tab, but let _ = bagPageVC.view will cause ViewDidLoad to be called only once (once view is loaded, when you access .view it will simply return the view)
Though this code will work absolutely fine without any issue, I am not very big fan of meddling with View cycle, hence Solution 2
Solution 2:
Check if the implicit optional IBOutlet is nil, if it is not then simply set the text, else pass the text to bagPageVC and set it in ViewDidLoad
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
if tabBarController.selectedIndex == 3,
let bagPageVC = (viewController as? UINavigationController)?.topViewController as? bagPage {
if let countLabel = bagPageVC.countOfHP {
countLabel.text = "\(String(bagPageVC.countArray)) items in bag"
}
else {
bagPageVC.countInfo = "\(String(bagPageVC.countArray)) items in bag"
}
}
}
Now in your bagPageVC declare a variable named countInfo as String?
var countInfo: String? = nil
And in ViewDidLoad you can simply set
override func viewDidLoad() {
super.viewDidLoad()
countOfHP.text = countInfo
}
ViewDidLoad is called only once in ViewController's life cycle, so you dont have to worry about resetting countInfo in subsequent didSelect viewController call
Which one will you choose I leave it to you :)

Very specific swipe pattern on a UIPageViewController breaks my entire app

So I'm working on an App right now and one of the specs of this app is to have a swipe between views functionality on specific pages kind of like the camera on Instagram.
My current solution is to set the initial view controller as a UIPageViewController which has two view controllers one is the main tab bar controller (first image) for the whole app and the second view controller is the one shown in pink in the image above, and then to enable and disable sliding functionality for the PageViewController depending on whether the current view is meant to have access to the second 'pink' view controller. (PS open to a totally different architecture than this if anyone knows of one, this is just the best I could do given my limited knowledge on iOS)
Normally every thing works fine. However, when I move my finger's in a very particular pattern, the entire app stops functioning. The pattern is shown below:
The pattern is basically:
Swipe slowly to the left a little bit until part of the second 'pink' view controller is showing (image 2 above)
Swipe quickly to the right causing the empty space to the left of the main view controller to show (image 3)
Quickly Let go and let the main view controller fall back into place (image 4)
(*edit - Probably worth noting that if I do this same pattern slowly instead of quickly sliding right and letting go everything works just fine)
(PS if there's a way to upload a screen recording let me know)
Anyways, If I do this just one time, my entire app stops working. Basically every page stops loading data, and any time I click on a button on the main page (eg. the likes button shown in the images) I get an "Unbalanced calls to begin/end appearance transitions for AppName.ViewController" and the new view controller shows up with no data.
Furthermore, as soon as I swipe over to the second page 'pink' view controller and then back to the main app, everything works again.
I don't really know what code is relevant to this problem (spent about 5 hours trying to figure that out with no luck), so I'm just gonna post my UIPageViewController class for now, if you think the problem is coming from somewhere else let me know and I'll post that code.
// MARK: -> Properties
class PageViewController: UIPageViewController {
var pages = [UIViewController]()
}
// MARK: -> Lifecycle
extension PageViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
setViewController(withIdentifier: "MainTabController")
setViewController(withIdentifier: "SecondaryPage") // The 'Pink' screen
setViewControllers([pages.first!], direction: UIPageViewControllerNavigationDirection.forward, animated: false, completion: nil)
}
}
// MARK: -> Helpers
fileprivate extension PageViewController {
func setViewController(withIdentifier storyboardIdentifier: String){
let page: UIViewController! = storyboard?.instantiateViewController(withIdentifier: storyboardIdentifier)
storyboard?.configure(viewController: page)
self.pages.append(page)
}
}
// MARK: -> UIPageController Data Source
extension PageViewController: UIPageViewControllerDataSource {
func presentationIndex(for pageViewController: UIPageViewController)-> Int {
return pages.count
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let cur = pages.index(of: viewController)!
if cur == 0 { return nil }
let prev = abs((cur - 1) % pages.count)
return pages[prev]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let cur = pages.index(of: viewController)!
if cur == (pages.count - 1) { return nil }
let nxt = abs((cur + 1) % pages.count)
return pages[nxt]
}
}
Update
Not sure what exactly this means but I'm sure it's relevant. I added this extension and method to my PageViewController
extension PageViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
print(completed)
}
}
(and then obviously set the delegate to self in viewDidLoad)
This printed true and false as you would expect except when I swiped in the pattern I described above, in which case it never even fired the method and printed nothing at all.

Delegate Returning Incorrect Instance Number of ViewController

I have a ViewController "LandingPageViewController" which has an embedded PageViewController "pageViewController".
I have created a delegate function to write from "pageViewController" to LandingPageViewControlelr". This works fine and the data is sent to the "LandingPageController" however i cant interact with any of its elements because the delegate is creating a new instance.
When the app first launches it goes to "LandingPageViewController" and if I print(self) in viewDidLoad and viewDidAppear this is the result:
Delegate viewDidLoad...<ParseStarterProject_Swift.LandingPageViewController: 0x7fbf29406e60>
Delegate viewDidAppear...<ParseStarterProject_Swift.LandingPageViewController: 0x7fbf29406e60>
Then once i have scrolled over to the 3rd page and the delegate is called i get the following result of print(self)
Delegate function...<ParseStarterProject_Swift.LandingPageViewController: 0x7fbf2940e3a0>
This is the code from my "LandingPageViewController" that is receiving the data:
protocol GetMyIndexDelegate {
func getIndex(index: Int)
}
class LandingPageViewController: UIViewController, GetMyIndexDelegate {
#IBOutlet var createAccountButton: UIButton!
//delegate functionto get index
func getIndex(index: Int) {
if index == 3 {
print("Delegate function...\(self)")
//print(createAccountButton)
}
}
override func viewDidLoad() {
super.viewDidLoad()
print("Delegate viewDidLoad...\(self)")
}
}
and here is the code from the "pageViewController" class:
class pageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
let myHeight = 667
let myCenter = 333.5
// define the delegate for use
var indexDelegate: GetMyIndexDelegate?
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if (completed && finished) {
if let currentVC = pageViewController.viewControllers?.last {
let index = vcArr.index(of: currentVC)
if index == 3 {
// this is the line causing problem
self.indexDelegate = LandingPageViewController()
print("PageView Index Defined As...\(self.indexDelegate)")
indexDelegate?.getIndex(index: index!)
//goToVC()
}
}
}
}
i believe this is the problem here:
self.indexDelegate = LandingPageViewController()
it is defining incorrect instance?????
As a side clue, i had another delegate method going in the other direction, from a viewController to the first page of the "pageViewController" and the issue there was that i had to correctly define the index of the page within the UIPageViewController with the following code:
self.delegate = self.childViewControllers[0] as! pageViewController
im not sure how this would work in the question above as the LandingPageViewController is the parent? of the pageViewController?
The self.indexDelegate = LandingPageViewController() is incorrect because you are instantiating a new view controller programmatically rather than referring to the one that was already instantiated for you by the storyboard.
You have an embed segue, which you are using to embed the page view controller within the other view controller's scene. So, you can use prepareForSegue in the parent view controller to set whatever properties you want in the child view controller.
But if you're simply looking for a reference to the child view controller in the parent, it's childViewControllers.first or childViewControllers[0]. And if the child is looking for a reference to its parent view controller, it's merely parent (or in your case, parent as? GetMyIndexDelegate).

Implementation of UIPageViewController with UICollectionViewControllers (Swift)

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.

Swiping between View Controllers in swift

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.

Resources