Goal: to make a viewcontroller have multiple pages and can be swapped through a segmented controller, pages content are scrollable vertically
details:
I made a pagviewcontroller and embedded it as a subview to main viewcontroller
//add pageviewcontroller as subview to viewcontroller
if let vc = storyboard?.instantiateViewControllerWithIdentifier("ProfileEditController"){
self.addChildViewController(vc)
self.view.addSubview(vc.view)
EditTabs = vc as! UIPageViewController
EditTabs.dataSource = self
EditTabs.delegate = self
//define First page
EditTabs.setViewControllers([pagesAtIndexPath(0)!], direction:.Forward, animated: true, completion: nil)
EditTabs.didMoveToParentViewController(self)
//bring segmented view buttons to front of pageViews
self.view.bringSubviewToFront(self.topTabs)
}
I called pageViewController functions, and I am adding pages through restoration Identifiers
I managed segmented view controller by getting pageindex and setting viewcontroller like this:
EditTabs.setViewControllers([pagesAtIndexPath(0)!], direction:.Reverse, animated: true, completion: nil)
in story board the sub pages has scroll view inside to hold the content
I tested subpages scroll view by calling it through segue and its working fine
Case:
everything work fine Only scroll view of subpages are not working at all
How to solve this issue?
your guidelines will be much appreciated
Thanks,
I have created a view controller with PageViewController with three View Controllers that have scroll view in it. It works fine.
import UIKit
class ViewController: UIViewController, UIPageViewControllerDataSource {
var viewControllers : [UIViewController]?
override func viewDidLoad() {
super.viewDidLoad()
CreatePageView()
}
func CreatePageView() {
SetupViewControllers()
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
pageViewController.dataSource = self
pageViewController.setViewControllers([(viewControllers?[0])!] , direction: .forward, animated: false, completion: nil)
pageViewController.view.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height);
pageViewController.view.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height);
addChildViewController(pageViewController)
view.addSubview(pageViewController.view)
pageViewController.didMove(toParentViewController: self)
pageViewController.view.backgroundColor = UIColor.blue
}
func SetupViewControllers() {
let firstVC = UIViewController()
firstVC.view.tag = 100
firstVC.view.backgroundColor = UIColor.red
AddScrollView(bgView: firstVC.view)
let secondVC = UIViewController()
secondVC.view.tag = 101
secondVC.view.backgroundColor = UIColor.brown
AddScrollView(bgView: secondVC.view)
let thirdVC = UIViewController()
thirdVC.view.tag = 102
thirdVC.view.backgroundColor = UIColor.purple
AddScrollView(bgView: thirdVC.view)
viewControllers = [firstVC,secondVC,thirdVC]
}
func AddScrollView(bgView: UIView) {
let scrollView = UIScrollView()
scrollView.frame = CGRect.init(x: 10, y: 10, width: bgView.frame.width-20, height: bgView.frame.height-20)
scrollView.backgroundColor = UIColor.init(red: 0.34, green: 0.45, blue: 0.35, alpha: 0.9)
bgView.addSubview(scrollView)
scrollView.contentSize = CGSize.init(width: scrollView.frame.size.width, height: scrollView.frame.size.height+200)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if viewController.view.tag == 101 {
return viewControllers?[0]
}
else if viewController.view.tag == 102{
return viewControllers?[1]
}
else{
return viewControllers?[2]
}
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if viewController.view.tag == 101 {
return viewControllers?[0]
}
else if viewController.view.tag == 102{
return viewControllers?[1]
}
else{
return viewControllers?[2]
}
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return (viewControllers?.count)!
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
}
Similar issues are raised fairly often on Stack. For my own issue in very similar circumstances, I too was able to use the scrollview without issue in it's own view, but when used as a subview in another subview, I needed to set the scrollview dimensions programatically.
Swift 2
scrollView.setFrame(CGRectMake(0, 0, DEVICE_WIDTH, DEVICE_HEIGHT))
or
Swift 3
scrollView.setFrame(CGRect(x: 0, y: 0, width: DEVICE_WIDTH, height: DEVICE_HEIGHT))
A further (and often viewed as a better) suggestion, automatically set the size based on contents.
var contentRect = CGRectZero
for view in self.scrollView.subviews {
contentRect = CGRectUnion(contentRect, view.frame)
}
self.scrollView.contentSize = contentRect.size
Hope this helps.
Related
So I have a UIPageViewController. It houses 5 UIHostingViewControllers (Swift UI)
I add the hosting view controller as a child then setup views as per recommended approach
Everything works and I am able to swipe between controllers
Now I need to show highlighted circles to give users an idea of which page they are viewing as they scroll. This is where the problem occurs
I add these circles as a subview on the UIPageViewController's view. Everything works fine but the first controller of the page view controller alone has a bug with the view. The view's frame does not fill the page view's bounds
This is what I get. As you can see in the below image, all other controllers are able to fit the view except the first one. This behaviour only happens when I try to add those indicator circles as a subview of self.view or UIPageViewController.view
Here is the code:
class TutorialController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate{
var controllers = [UIViewController]()
var circleStackView: UIStackView = {
let sv = UIStackView()
sv.axis = .horizontal
sv.spacing = 16
return sv
}()
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
dataSource = self
setupViewControllers()
}
fileprivate func setupViewControllers(){
let tutorialPage1 = UIHostingController(rootView: TutorialPage1())
let sampleViewController1 = UIViewController()
sampleViewController1.addChild(tutorialPage1)
tutorialPage1.view.translatesAutoresizingMaskIntoConstraints = false
sampleViewController1.view.addSubview(tutorialPage1.view)
tutorialPage1.didMove(toParent: sampleViewController1)
tutorialPage1.view.frame = sampleViewController1.view.bounds
controllers.append(sampleViewController1)
let tutorialPage2 = UIHostingController(rootView: TutorialPage2())
let sampleViewController2 = UIViewController()
sampleViewController2.addChild(tutorialPage2)
tutorialPage2.view.translatesAutoresizingMaskIntoConstraints = false
sampleViewController2.view.addSubview(tutorialPage2.view)
tutorialPage2.didMove(toParent: sampleViewController2)
tutorialPage2.view.frame = sampleViewController2.view.bounds
controllers.append(sampleViewController2)
setViewControllers([controllers.first!], direction: .forward, animated: false)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
controllers.forEach { (_) in
let circleView = UIView()
circleView.anchor(top: nil, leading: nil, bottom: nil, trailing: nil, size: .init(width: 24, height: 24))
circleView.layer.cornerRadius = 12
circleView.clipsToBounds = true
circleView.backgroundColor = UIColor.white.withAlphaComponent(0.8)
self.circleStackView.addArrangedSubview(circleView)
self.circleStackView.bringSubviewToFront(view)
}
view.addSubview(circleStackView)
circleStackView.anchor(top: nil, leading: nil, bottom: view.safeAreaLayoutGuide.bottomAnchor, trailing: nil, padding: .init(top: 0, left: 0, bottom: 16, right: 0))
circleStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let index = controllers.firstIndex(where: {$0 == viewController}) ?? 0
if index == 0{
return nil
}
return controllers[index - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let index = controllers.firstIndex(where: {$0 == viewController}) ?? 0
if index == controllers.count - 1{
return nil
}
return controllers[index + 1]
}
}
I would try moving your call to setupViewControllers() from viewDidLoad() to viewDidLayoutSubviews() before you set up your circleView controllers.
Please try removing
tutorialPage1.view.translatesAutoresizingMaskIntoConstraints = false
tutorialPage2.view.translatesAutoresizingMaskIntoConstraints = false
The reason might be that ios is expecting at least one of the auto-resizing or the auto-layout.
The frame-based layout seems to have some issues with swift UI.
This question already has an answer here:
Pass touches through a UIViewController
(1 answer)
Closed 7 days ago.
I have a side navigation controller and present it via a UIButton. When I make this NC the root view controller directly by [self presentviewcontroller: NC animated: YES completion: nil], some reason the menu side of the NC is blocked by a UITransitionView that I cannot get to disappear.
I have tried the following:
UIWindow *window = [(AppDelegate *)[[UIApplication sharedApplication] delegate] window];
window.backgroundColor = kmain;
CATransition* transition = [CATransition animation];
transition.duration = .5;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromTop;
[nc.view.layer addAnimation:transition forKey:kCATransition];
[UIView transitionWithView:window
duration:0.5
options:UIViewAnimationOptionTransitionNone
animations:^{ window.rootViewController = nc; }
completion:^(BOOL finished) {
for (UIView *subview in window.subviews) {
if ([subview isKindOfClass:NSClassFromString(#"UITransitionView")]) {
[subview removeFromSuperview];
}
}
}];
But it is very hacky, and as the rootviewcontroller of the window changes during the transition, it's a little choppy and part of the navigationcontroller and the top right corner turn black. It looks very bad.
To get tap events through the UITransitionView, set the containerView's userInteractionEnabled to false. This is if you're doing a custom transition animation by using UIViewControllerAnimatedTransitioning.
Example, in your animateTransition(_:):
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
containerView.isUserInteractionEnabled = false
...
}
In my situation I needed a halfSize view controller. I followed this answer which worked great until I realized I needed to still be able to interact with the presenting vc (the vc behind the halfSizeVC).
The key is that you have to set both of these frames with the same CGRect values:
halfSizeVC.frame = CGRect(x: 0, y: UIScreen.main.bounds.height / 2, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
containerView = CGRect(x: 0, y: UIScreen.main.bounds.height / 2, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
Here is the code to go from ViewController to HalfSizeController and make HalfSizeController 1/2 the screen size. Even with halfSizeVC on screen you will still be able to interact with the top half of the vc behind it.
You also have to make a PassthroughView class if you want to be able to touch something inside the halfSizeVC. I included it at the bottom.
The presenting vc is white with a purple button at the bottom. Tapping the purple button will bring up the red halfSizeVC.
vc/presentingVC:
import UIKit
class ViewController: UIViewController {
lazy var purpleButton: UIButton = {
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Tap to Present HalfSizeVC", for: .normal)
button.setTitleColor(UIColor.white, for: .normal)
button.backgroundColor = UIColor.systemPurple
button.addTarget(self, action: #selector(purpleButtonPressed), for: .touchUpInside)
button.layer.cornerRadius = 7
button.layer.masksToBounds = true
return button
}()
var halfSizeVC: HalfSizeController?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// tap gesture on vc will dismiss HalfSizeVC
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissHalfSizeVC))
view.addGestureRecognizer(tapGesture)
}
// tapping the purple button presents HalfSizeVC
#objc func purpleButtonPressed() {
halfSizeVC = HalfSizeController()
// *** IMPORTANT ***
halfSizeVC!.view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height / 2)
halfSizeVC!.modalPresentationStyle = .custom
present(halfSizeVC!, animated: true, completion: nil)
}
// dismiss HalfSizeVC by tapping anywhere on the white background
#objc func dismissHalfSizeVC() {
halfSizeVC?.dismissVC()
}
}
halfSizeVC/presentedVC
import UIKit
class HalfSizeController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = self
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var topHalfDummyView: PassthroughView = {
let view = PassthroughView()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .clear
view.isUserInteractionEnabled = true
return view
}()
var isPresenting = false
let halfScreenHeight = UIScreen.main.bounds.height / 2
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
setAnchors()
}
private func setAnchors() {
view.addSubview(topHalfDummyView)
topHalfDummyView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
topHalfDummyView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
topHalfDummyView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
topHalfDummyView.heightAnchor.constraint(equalToConstant: halfScreenHeight).isActive = true
}
public func dismissVC() {
dismiss(animated: true, completion: nil)
}
}
extension HalfSizeController: UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
// *** IMPORTANT ***
containerView.frame = CGRect(x: 0, y: halfScreenHeight, width: UIScreen.main.bounds.width, height: halfScreenHeight)
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
guard let toVC = toViewController else { return }
isPresenting = !isPresenting
if isPresenting == true {
containerView.addSubview(toVC.view)
topHalfDummyView.frame.origin.y += halfScreenHeight
UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
self.topHalfDummyView.frame.origin.y -= self.halfScreenHeight
}, completion: { (finished) in
transitionContext.completeTransition(true)
})
} else {
UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
}, completion: { (finished) in
self.topHalfDummyView.frame.origin.y += self.halfScreenHeight
transitionContext.completeTransition(true)
})
}
}
}
PassthroughView needed for the topHalfDummyView in HalfSizeVC
import UIKit
class PassthroughView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("Passing all touches to the next view (if any), in the view stack.")
return false
}
}
Before purple button is pressed:
After purple button is pressed:
If you press the white background the red color will get dismissed
You can just c+p all 3 files and run your project
I had a similar issue where a UITransitionView kept blocking my views, preventing any user interaction.
In my case this was due to an uncompleted custom animated UIViewController transition.
I forgot to properly complete my transition with:
TransitionContext.completeTransition(transitionContext.transitionWasCancelled)
or
TransitionContext.completeTransition(!transitionContext.transitionWasCancelled)
In the
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}
from the UIViewControllerAnimatedTransitioning protocol
I had the same issue but in a little different scenario, I ended up doing something very similar to find the view but instead of removing the view which can be more problematic I disabled the user interaction so any touch events just go throw it and any other objects can handle to user's interaction.
In my case this was only present after updating the app to iOS 10, the same code running in iOS 9 didn't fall into this.
I was facing the same issue, and this solved issue for me,
navigationController.setNavigationBarHidden(true, animated: false)
This worked for me as I am having custom view as navigation bar in view controllers.
Ive had this issue when I was setting accessibilityElements on a popover view controller. I fix it by removing assigning an array of elements.
I have the following code in my iOS app:
class BannerTableViewCell: UITableViewCell, UIPageViewControllerDataSource {
private var pageViewController: UIPageViewController!
private var pages: [UIViewController] = []
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clearColor()
self.backgroundView?.backgroundColor = UIColor.clearColor()
self.contentView.backgroundColor = UIColor.clearColor()
self.selectionStyle = UITableViewCellSelectionStyle.None
pageViewController = UIPageViewController();
self.pageViewController.dataSource = self
pageViewController.view.frame = CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: 130)
self.addSubview(pageViewController.view)
//Example data
let v1 = UIViewController()
v1.view.frame = CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: 130);
v1.view.backgroundColor = UIColor.blueColor()
let v2 = UIViewController()
v2.view.frame = CGRect(x: 0, y: 0, width: UIScreen.mainScreen().bounds.width, height: 130);
v2.view.backgroundColor = UIColor.greenColor()
pages.append(v1)
pages.append(v2)
}
func pageViewController(pageViewController: UIPageViewController,
viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = pages.indexOf(viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
guard pages.count > previousIndex else {
return nil
}
return pages[previousIndex]
}
func pageViewController(pageViewController: UIPageViewController,
viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = pages.indexOf(viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = pages.count
guard orderedViewControllersCount != nextIndex else {
return nil
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return pages[nextIndex]
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return pages.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
}
I instantiate this cell in the table view, however page view controller in this cell is always empty, and the data source methods of pageViewController are not called. Do you have any idea why they are not called?
You should use the documented initializer to instantiate the UIPageViewController:
public init(transitionStyle style: UIPageViewControllerTransitionStyle, navigationOrientation: UIPageViewControllerNavigationOrientation, options: [String : AnyObject]?)
Also the ViewControllers you create at the end of awakeFromNib can be placed into the pageViewController right away.
pageViewController.setViewControllers([v1, v2], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: nil)
A page indicator will be visible if both methods are implemented, transition style is 'UIPageViewControllerTransitionStyleScroll', and navigation orientation is 'UIPageViewControllerNavigationOrientationHorizontal'.
Both methods are called in response to a 'setViewControllers:...' call, but the presentation index is updated automatically in the case of gesture-driven navigation.
It could also be because the Page View Controller is not your Root View Controller.
You could add the following code in your segue:
let x = (UIApplication.shared.delegate as! AppDelegate).window!
x.rootViewController = yourViewController()
I am using a UISplitViewController, with a MasterViewController and DetailViewController, without UINavigationControllers.
Currenly the segue animation for master->detail, triggered by
performSegueWithIdentifier("showDetail", sender: self)
consists in the DetailViewController showing up from the bottom upwards.
How can I make that animation showing left-wards?
I've recently needed to have more control of how the segues are being performed, so I made my custom segue classes which all perform the transition in different directions. Here's one of the implementations:
Swift 2.x
override func perform() {
//credits to http://www.appcoda.com/custom-segue-animations/
let firstClassView = self.sourceViewController.view
let secondClassView = self.destinationViewController.view
let screenWidth = UIScreen.mainScreen().bounds.size.width
let screenHeight = UIScreen.mainScreen().bounds.size.height
secondClassView.frame = CGRectMake(screenWidth, 0, screenWidth, screenHeight)
if let window = UIApplication.sharedApplication().keyWindow {
window.insertSubview(secondClassView, aboveSubview: firstClassView)
UIView.animateWithDuration(0.4, animations: { () -> Void in
firstClassView.frame = CGRectOffset(firstClassView.frame, -screenWidth, 0)
secondClassView.frame = CGRectOffset(secondClassView.frame, -screenWidth, 0)
}) {(Finished) -> Void in
self.sourceViewController.navigationController?.pushViewController(self.destinationViewController, animated: false)
}
}
}
This one will have a "right to left" transition. You can modify this function for your needs by simply changing the initial and ending positions of the source and destination view controller.
Also don't forget that you need to mark your segue as "custom segue", and to assign the new class to it.
UPDATE: Added Swift 3 version
Swift 3
override func perform() {
//credits to http://www.appcoda.com/custom-segue-animations/
let firstClassView = self.source.view
let secondClassView = self.destination.view
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
secondClassView?.frame = CGRect(x: screenWidth, y: 0, width: screenWidth, height: screenHeight)
if let window = UIApplication.shared.keyWindow {
window.insertSubview(secondClassView!, aboveSubview: firstClassView!)
UIView.animate(withDuration: 0.4, animations: { () -> Void in
firstClassView?.frame = (firstClassView?.frame.offsetBy(dx: -screenWidth, dy: 0))!
secondClassView?.frame = (secondClassView?.frame.offsetBy(dx: -screenWidth, dy: 0))!
}, completion: {(Finished) -> Void in
self.source.navigationController?.pushViewController(self.destination, animated: false)
})
}
}
Embed your view controllers in UINavigationControllers.
Per the SplitViewController template:
On smaller devices it's going to have to use the Master's navigationController.
Furthermore this question has been answered here and here and here
More from the View Controller Programming Guide:
There are two ways to display a view controller onscreen: embed it in
a container view controller or present it. Container view controllers
provide an app’s primary navigation….
In storyboards it's not that difficult to embed something in a navigation controller. Click on the view controller you want to embed, then Editor->embed in->navigation controller.
--Swift 3.0--
Armin's solution adapted for swift 3.
New -> File -> Cocoa Touch Class -> Class: ... (Subclass: UIStoryboardSegue).
import UIKit
class SlideHorSegue: UIStoryboardSegue {
override func perform() {
//credits to http://www.appcoda.com/custom-segue-animations/
let firstClassView = self.source.view
let secondClassView = self.destination.view
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
secondClassView?.frame = CGRect(x: screenWidth, y: 0, width: screenWidth, height: screenHeight)
if let window = UIApplication.shared.keyWindow {
window.insertSubview(secondClassView!, aboveSubview: firstClassView!)
UIView.animate(withDuration: 0.4, animations: { () -> Void in
firstClassView?.frame = (firstClassView?.frame)!.offsetBy(dx: -screenWidth, dy: 0)
secondClassView?.frame = (secondClassView?.frame)!.offsetBy(dx: -screenWidth, dy: 0)
}) {(Finished) -> Void in
self.source.navigationController?.pushViewController(self.destination, animated: false)
}
}
}
}
In storyboard: mark your segue as "custom segue", and to assign the new class to it.
Note: If you have a UIScrollView in your detailesVC, this won't work.
It sounds as though the new view controller is presenting modally. If you embed the detailViewController into a UINavigationController and push the new controller it will animate from right to left and should show a back button too by default.
When you have a compact-width screen,"Show Detail" segue fall back to modal segue automatically.So your DetailViewController will appear vertically as the default modal segue.
You can use a UIViewControllerTransitioningDelegate to custom the animation of a modal segue.
Here is an example to achieve the horizontal animation:
1. set the transitioningDelegate and it's delegate method
class MasterViewController: UITableViewController,UIViewControllerTransitioningDelegate {
//prepare for segue
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "showDetail" {
let detailVC = segue.destinationViewController as! DetailViewController
detailVC.transitioningDelegate = self
// detailVC.detailItem = object//Configure detailVC
}
}
//UIViewControllerTransitioningDelegate
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return LeftTransition()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let leftTransiton = LeftTransition()
leftTransiton.dismiss = true
return leftTransiton
}
}
2: a custom UIViewControllerAnimatedTransitioning : LeftTransition
import UIKit
class LeftTransition: NSObject ,UIViewControllerAnimatedTransitioning {
var dismiss = false
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 2.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning){
// Get the two view controllers
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
let containerView = transitionContext.containerView()!
var originRect = containerView.bounds
originRect.origin = CGPointMake(CGRectGetWidth(originRect), 0)
containerView.addSubview(fromVC.view)
containerView.addSubview(toVC.view)
if dismiss{
containerView.bringSubviewToFront(fromVC.view)
UIView.animateWithDuration(transitionDuration(transitionContext), animations: { () -> Void in
fromVC.view.frame = originRect
}, completion: { (_ ) -> Void in
fromVC.view.removeFromSuperview()
transitionContext.completeTransition(true )
})
}else{
toVC.view.frame = originRect
UIView.animateWithDuration(transitionDuration(transitionContext),
animations: { () -> Void in
toVC.view.center = containerView.center
}) { (_) -> Void in
fromVC.view.removeFromSuperview()
transitionContext.completeTransition(true )
}
}
}
}
I have a simple UIPageViewController which displays the default UIPageControl at the bottom of the pages. I wonder if it's possible to modify the position of the UIPageControl, e.g. to be on top of the screen instead of the bottom.
I've been looking around and only found old discussions that say I need to create my own UIPageControl.
Is this thing simpler with iOS8 and 9?
Thanks.
Yes, you can add custom page controller for that.
self.pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height - 50, self.view.frame.size.width, 50)]; // your position
[self.view addSubview: self.pageControl];
then remove
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
and
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
Then add another delegate method:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers
{
PageContentViewController *pageContentView = (PageContentViewController*) pendingViewControllers[0];
self.pageControl.currentPage = pageContentView.pageIndex;
}
Just lookup PageControl in PageViewController subclass and set frame, location or whatever you want
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subView in view.subviews {
if subView is UIPageControl {
subView.frame.origin.y = self.view.frame.size.height - 164
}
}
}
Override the viewDidLayoutSubviews() of the pageviewcontroller and use this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// get pageControl and scroll view from view's subviews
let pageControl = view.subviews.filter{ $0 is UIPageControl }.first! as! UIPageControl
let scrollView = view.subviews.filter{ $0 is UIScrollView }.first! as! UIScrollView
// remove all constraint from view that are tied to pagecontrol
let const = view.constraints.filter { $0.firstItem as? NSObject == pageControl || $0.secondItem as? NSObject == pageControl }
view.removeConstraints(const)
// customize pagecontroll
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.addConstraint(pageControl.heightAnchor.constraintEqualToConstant(35))
pageControl.backgroundColor = view.backgroundColor
// create constraints for pagecontrol
let leading = pageControl.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor)
let trailing = pageControl.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor)
let bottom = pageControl.bottomAnchor.constraintEqualToAnchor(scrollView.topAnchor, constant:8) // add to scrollview not view
// pagecontrol constraint to view
view.addConstraints([leading, trailing, bottom])
view.bounds.origin.y -= pageControl.bounds.maxY
}
The Shameerjan answer is very good, but it needs one more thing to work properly, and that is implementation of another delegate method:
func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
// If user bailed our early from the gesture,
// we have to revert page control to previous position
if !completed {
let pageContentView = previousViewControllers[0] as! PageContentViewController;
self.pageControl.currentPage = pageContentView.pageIndex;
}
}
This is because if you don't, if you move the page control just so slightly, it will go back to previous position - but the page control will show different page.
Hope it helps!
Swift 4 version of #Jan's answer with fixed bug when the user cancels transition:
First, you create custom pageControl:
let pageControl = UIPageControl()
You add it to the view and position it as you want:
self.view.addSubview(self.pageControl)
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.pageControl.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 43),
self.pageControl.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -33)
])
Then you need to initialize the pageControl:
self.pageControl.numberOfPages = self.dataSource.controllers.count
Finally, you need to implement UIPageViewControllerDelegate, and its method pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:):
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
// this will get you currently presented view controller
guard let selectedVC = pageViewController.viewControllers?.first else { return }
// and its index in the dataSource's controllers (I'm using force unwrap, since in my case pageViewController contains only view controllers from my dataSource)
let selectedIndex = self.dataSource.controllers.index(of: selectedVC)!
// and we update the current page in pageControl
self.pageControl.currentPage = selectedIndex
}
Now in comparison with #Jan's answer, we update self.pageControl.currentPage using pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) (shown above), instead of pageViewController(_:willTransitionTo:). This overcomes the problem of cancelled transition - pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) is called always when a transition was completed (it also better mimics the behavior of standard page control).
Finally, to remove the standard page control, be sure to remove implementation of presentationCount(for:) and presentationIndex(for:) methods of the UIPageViewControllerDataSource - if the methods are implemented, the standard page control will be presented.
So, you do NOT want to have this in your code:
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return self.dataSource.controllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
For swift:
self.pageController = UIPageControl(
frame: CGRect(
x: 0,
y: self.view.frame.size.height - 50,
width: self.view.frame.size.width,
height: 50
)
)
self.view.addSubview(pageController)
remember use pageController.numberOfPages and delegate the pageView
then remove
func presentationCountForPageViewController(
pageViewController: UIPageViewController
) -> Int
and
func presentationIndexForPageViewController(
pageViewController: UIPageViewController
) -> Int
Then add another delegate method:
func pageViewController(
pageViewController: UIPageViewController,
willTransitionToViewControllers pendingViewControllers:[UIViewController]){
if let itemController = pendingViewControllers[0] as? PageContentViewController {
self.pageController.currentPage = itemController.pageIndex
}
}
}
Yes, the Shameerjan answer is very good, but instead of adding another page control you can use default page indicator:
- (UIPageControl*)pageControl {
for (UIView* view in self.view.subviews) {
if ([view isKindOfClass:[UIPageControl class]]) {
//set new pageControl position
view.frame = CGRectMake( 100, 200, width, height);
return (id)view;
}
}
return nil;
}
and then extend the size of the UIPageViewController to cover up the bottom gap:
//somewhere in your code
self.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height+40);
here s a very effective way to change the position of the default PageControl for the PageViewController without having the need to create a new one ...
extension UIPageViewController {
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subV in self.view.subviews {
if type(of: subV).description() == "UIPageControl" {
let pos = CGPoint(x: subV.frame.origin.x, y: subV.frame.origin.y - 75 * 2)
subV.frame = CGRect(origin: pos, size: subV.frame.size)
}
}
}
}
Here's my take. This version only changes de indicator when the animation is finished, i.e. when you get to the destination page.
/** Each page you add to the UIPageViewController holds its index */
class Page: UIViewController {
var pageIndex = 0
}
class MyPageController : UIPageViewController {
private let pc = UIPageControl()
private var pendingPageIndex = 0
// ... add pc to your view, and add Pages ...
/** Will transition, so keep the page where it goes */
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController])
{
let controller = pendingViewControllers[0] as! Page
pendingPageIndex = controller.pageIndex
}
/** Did finish the transition, so set the page where was going */
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)
{
if (completed) {
pc.currentPage = pendingPageIndex
}
}
}
Swift 5 & iOS 13.5 (with Manual Layout)
The example below uses a custom UIPageControl, laid out at the bottom center position of the UIPageViewController. To use the code, replace MemberProfilePhotoViewController with the type of view controller you are using as pages.
Quick Notes
You must set the numberOfPages property on pageControl.
To size pageControl you can use pageControl.size(forNumberOfPages: count).
Make sure you delete presentationCount(...) and presentationIndex(...) to remove the default page control.
Using UIPageViewControllerDelegate's didFinishAnimating seems to be better timing for updating pageControl.currentPage.
The Code
import Foundation
import UIKit
class MemberProfilePhotosViewController: UIPageViewController {
private let profilePhotoURLs: [URL]
private let profilePhotoViewControllers: [MemberProfilePhotoViewController]
private var pageControl: UIPageControl?
// MARK: - Initialization
init(profilePhotoURLs: [URL]) {
self.profilePhotoURLs = profilePhotoURLs
profilePhotoViewControllers = profilePhotoURLs.map { (profilePhotoURL) -> MemberProfilePhotoViewController in
MemberProfilePhotoViewController(profilePhotoURL: profilePhotoURL)
}
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
// MARK: UIViewController
override func loadView() {
super.loadView()
pageControl = UIPageControl(frame: CGRect.zero)
pageControl!.numberOfPages = profilePhotoViewControllers.count
self.view.addSubview(pageControl!)
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
if let firstViewController = profilePhotoViewControllers.first {
setViewControllers([firstViewController],
direction: .forward,
animated: true,
completion: nil)
}
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let pageControl = pageControl {
let pageControlSize = pageControl.size(forNumberOfPages: profilePhotoViewControllers.count)
pageControl.frame = CGRect(
origin: CGPoint(x: view.frame.midX - pageControlSize.width / 2, y: view.frame.maxY - pageControlSize.height),
size: pageControlSize
)
}
}
// MARK: Private Helpers
private func indexOf(_ viewController: UIViewController) -> Int? {
return profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController)
}
}
extension MemberProfilePhotosViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
guard let selectedViewController = pageViewController.viewControllers?.first else { return }
if let indexOfSelectViewController = indexOf(selectedViewController) {
pageControl?.currentPage = indexOfSelectViewController
}
}
}
extension MemberProfilePhotosViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
guard profilePhotoViewControllers.count > previousIndex else {
return nil
}
return profilePhotoViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let profilePhotoViewControllersCount = profilePhotoViewControllers.count
guard profilePhotoViewControllersCount != nextIndex else {
return nil
}
guard profilePhotoViewControllersCount > nextIndex else {
return nil
}
return profilePhotoViewControllers[nextIndex]
}
}
This is good, and with more change
var pendingIndex = 0;
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed {
pageControl.currentPage = pendingIndex
}
}
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
let itemController = pendingViewControllers.first as! IntroPageItemViewController
pendingIndex = itemController.itemIndex
}
Make sure pageControl is added as subview.
Then in
-(void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGRect frame = self.pageControl.frame;
frame.origin.x = self.view.frame.size.width/2 -
frame.size.width/2;
frame.origin.y = self.view.frame.size.height - 100 ;
self.pageControl.numberOfPages = self.count;
self.pageControl.currentPage = self.currentIndex;
self.pageControl.frame = frame;
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews{
if view is UIScrollView{
view.frame = UIScreen.main.bounds
}
else if view is UIPageControl {
view.backgroundColor = UIColor.clear
view.frame.origin.y = self.view.frame.size.height - 75
}
}
}
Just get the first view controller and find out the index in didFinishAnimating:
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
pageControl.currentPage = onboardingViewControllers.index(of: pageViewController.viewControllers!.first!)!
}