Here's what I am tring to do:
Implement a side nav that will slide in with custom animation from left to right
I am refering to this: https://www.thorntech.com/2016/03/ios-tutorial-make-interactive-slide-menu-swift/
But this and all other tutorials that implement this functionality use segues to present the view.
So their code goes here:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?){
if let destinationViewController = segue.destinationViewController as? MenuViewController {
destinationViewController.transitioningDelegate = self
}
}
But this is not an option for me!
My entire view controller is created by code. Right now it's a dummy view controller and I am just trying to get it to slide in from left to right.
import UIKit
class SideNavVC: UIViewController {
static let sideNav = SideNavVC()
override func viewDidLoad() {
super.viewDidLoad()
let dummyView = UIView(frame: CGRect(x: 10, y: 200, width: 100, height: 100))
dummyView.backgroundColor = accentColor
view.addSubview(dummyView)
let closeBtn = UIButton(frame: CGRect(x: 4, y: 30, width: 200, height: 20))
closeBtn.addTarget(self, action: #selector(closeViewTapped), for: .touchUpInside)
closeBtn.setTitle("Close", for: .normal)
view.addSubview(closeBtn)
view.backgroundColor = confirmGreen
}
#objc func closeViewTapped(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
And I am stuck on this step of the tutorial: https://www.thorntech.com/2016/03/ios-tutorial-make-interactive-slide-menu-swift/#four
This is how I am trying to set the delegate to self:
func addSlideGesture() {
let edgeSlide = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(presentSideNav))
edgeSlide.edges = .left
view.addGestureRecognizer(edgeSlide)
}
#objc func presentSideNav() {
if presentedViewController != SideNavVC.sideNav {
SideNavVC.sideNav.transitioningDelegate = self
SideNavVC.sideNav.modalPresentationStyle = .custom
self.present(SideNavVC.sideNav, animated: true, completion: nil)
}
}
This is where I was implementing my delegate (MainVC is from where the user will slide right)
extension MainVC: UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("Source: \(source)")
print("Destination \(presenting)")
if source == self && presenting == SideNavVC.sideNav {
print("PRESENTING SIDE NAV")
return RevealSideNav()
}
return nil
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
//Added a break point here. It is hitting.
return nil
}
}
Since my delegate was not calling, I decided to add presentationController(forPresented,presenting,source) and put a break point in there which was hitting. So I suspect that I am missing some part of the code that needs to go in there.
When I googled it, I found this UIPresentationController tutorial https://www.raywenderlich.com/915-uipresentationcontroller-tutorial-getting-started
But as I went through it, the more and more it started confusing me.
I am sure there is some small part that I am missing. Because if it works when the delegte is set in prepare for segue, then it should also work when calling present(_,animated,completion)
Can someone point me in right direction? How do I get the custom animations to trigger and the delegate methods to call? Does anyone know a tutorial that teaches how to do this using just code and no storyboard?
I referred to answer by #Womble
"UIViewControllerTransitioningDelegate method not called in iOS 7"
Turns out the method that I was using was wrong and Xcode didn't warn of this. Kinda odd.
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController)
-> UIViewControllerAnimatedTransitioning?
{
print("Source: \(source)")
print("Destination \(presenting)")
// ... return your cached controller object here.
return RevealSideNav()
}
Related
I want all of the functionality of a pageSheet UIModalPresentationStyle segue but I only want the presented ViewController to show half the screen (see the example in the image below).
I am presenting it modally using the pageSheet modalPresentationStyle but it always presents it at 100% height.
I haven't been able to figure out how to limit or modify a ViewController's height. I tried the following in my SecondViewController but it didn't work:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.preferredContentSize = CGSize(width: self.view.frame.width, height: 400)
}
}
I'm initiating the segue with Storyboard Segues, and a button that presents it modally:
I figured out a way to do it, which I find to be pretty simple:
import UIKit
class SecondViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let newView = UIView(frame: CGRect(x: 0, y: 500, width: self.view.frame.width, height: 400))
newView.backgroundColor = .yellow
newView.layer.cornerRadius = 20
self.view = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
// self.view is now a transparent view, so now I add newView to it and can size it however, I like.
self.view.addSubview(newView)
// works without the tap gesture just fine (only dragging), but I also wanted to be able to tap anywhere and dismiss it, so I added the gesture below
let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
self.view.addGestureRecognizer(tap)
}
#objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
dismiss(animated: true, completion: nil)
}
}
In order to achieve you will need to subclass UIPresentationController and implement the protocol UIViewControllerTransitioningDelegate in the presenting controller and set transitioningDelegate and modalPresentationStyle of presented view controller as self(presenting view controller) and .custom respectively. Implement an optional function of UIViewControllerTransitioningDelegate:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source _: UIViewController) -> UIPresentationController?
and return the custom presentationController which sets the height of presented controller as per your requirement.
Basic code that might help:
class CustomPresentationController: UIPresentationController {
var presentedViewHeight: CGFloat
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, presentedViewHeight: CGFloat) {
self.presentedViewHeight = presentedViewHeight
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
var frame: CGRect = .zero
frame.size = CGSize(width: containerView!.bounds.width, height: presentedViewHeight)
frame.origin.y = containerView!.frame.height - presentedViewHeight
return frame
}
}
Implementation of optional function:
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source _: UIViewController) -> UIPresentationController? {
let presentationController = CustomPresentationController(presentedViewController: presented, presenting: presenting, presentedViewHeight: 100)
return presentationController
}
You can also play with other optional functions and adding some other functionalities to CustomPresentationController like adding blur background, adding tap functionality and swipe gesture.
We can add our view in UIActivityController and remove UIActivityController's default view and if you add navigation controller so you will get navigation also, so you can do half your controller by this:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func actionPresent(_ sender: UIBarButtonItem) {
let vc1 = storyboard?.instantiateViewController(withIdentifier: "ViewControllerCopy")
let vc = ActivityViewController(controller: vc1!)
self.present(vc, animated: true, completion: nil)
}
}
class ActivityViewController: UIActivityViewController {
private let controller: UIViewController!
required init(controller: UIViewController) {
self.controller = controller
super.init(activityItems: [], applicationActivities: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let subViews = self.view.subviews
for view in subViews {
view.removeFromSuperview()
}
self.addChild(controller)
self.view.addSubview(controller.view)
}
}
for example you can check this repo:
https://github.com/SomuYadav/HalfViewControllerTransition
How can I get a presenting modal to be of custom size? Tried lots of different solutions, many which seem obsolete
This is how I instantiate the modal view from the parent view controller:
self.definesPresentationContext = true
let vc = (storyboard?.instantiateViewController(withIdentifier: "modalViewController"))!
vc.modalPresentationStyle = .overCurrentContext
vc.preferredContentSize = CGSize(width: 100, height: 100)
present(vc, animated: true, completion: nil)
But, the modal view covers the full screen instead of just occupying 100 * 100.
You need to implement UIViewControllerTransitioningDelegate methods and UIViewControllerAnimatedTransitioning methods for customizing the presented UIViewController size.
To know how to implement custom animator,
Refer to: https://github.com/pgpt10/Custom-Animator
Edit:
class ViewController: UIViewController
{
//MARK: Private Properties
fileprivate let animator = Animator()
//MARK: View Lifecycle Methods
override func viewDidLoad()
{
super.viewDidLoad()
}
override func awakeFromNib()
{
super.awakeFromNib()
self.transitioningDelegate = self
self.modalPresentationStyle = .custom
}
//MARK: Button Action Methods
#IBAction func dismissController(_ sender: UIButton)
{
self.dismiss(animated: true, completion: nil)
}
}
// MARK: - UIViewControllerTransitioningDelegate Methods
extension ViewController : UIViewControllerTransitioningDelegate
{
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
self.animator.transitionType = .zoom
self.animator.size = CGSize(width: 100, height: 100)
return self.animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
return self.animator
}
}
I have an iMessage extension and I'm having some issues with the top layout guide. I have an MSMessagesAppViewController that handles changes between presentation styles. In my extension I have a button. When it is clicked I transition to expanded presentation style and then present a view controller modally. Here's the problem: my UI in the second VC is getting hidden behind the top navigation bar. I thought this was strange as I set my constraints to the top layout guide. So I dug through my code and started debugging the top layout guide. I noticed that after I transition to expanded presentation style, topLayoutGuide.length = 86. That's how it should be. But when I present the second view controller modally, the top layout guide is reset to 0. Why isn't it 86 as it should be? Here is my code:
In my main viewController:
#IBAction func addStickerButtonPressed(_ sender: AnyObject) {
shouldPerformCreateSegue = true
theSender = sender
requestPresentationStyle(.expanded)
}
override func didTransition(to presentationStyle: MSMessagesAppPresentationStyle) {
if presentationStyle == .expanded {
if shouldPerformCreateSegue == true {
shouldPerformCreateSegue = false
performSegue(withIdentifier: "CreateStickerSegue", sender: theSender)//here is where I present the new viewController
} else {
searchBar.becomeFirstResponder()
searchBar.placeholder = nil
searchBar.showsCancelButton = true
searchBar.tintColor = UIColor.white
}
} else {
searchBar.showsCancelButton = false
}
print(topLayoutGuide.length) //This prints out 86
}
In the other modally presented view controller:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.view.addConstraint(navBar.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor))
print(topLayoutGuide.length) //This prints out 0
}
As a workaround I use UIPresentationController, which shifts the modal view controller by topLayoutGuide.length points:
class MyViewController: MSMessagesAppViewController {
private func presentModalViewController() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.modalPresentationStyle = .custom
imagePicker.transitioningDelegate = self
present(imagePicker, animated: true, completion: nil)
}
}
// MARK: - UIViewControllerTransitioningDelegate
extension MyViewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
let vc = PresentationController(presentedViewController: presented, presenting: presenting)
// I really don't want to hardcode the value of topLayoutGuideLength here, but when the extension is in compact mode, topLayoutGuide.length returns 172.0.
vc.topLayoutGuideLength = topLayoutGuide.length > 100 ? 86.0 : topLayoutGuide.length
return vc
}
}
class PresentationController: UIPresentationController {
var topLayoutGuideLength: CGFloat = 0.0
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerView = containerView else {
return super.frameOfPresentedViewInContainerView
}
return CGRect(x: 0, y: topLayoutGuideLength, width: containerView.bounds.width, height: containerView.bounds.height - topLayoutGuideLength)
}
}
The only problem is when you're calling presentModalViewController from compact mode, topLayoutGuide.length is 172.0 for unknown reason. So I had to hardcode a value for that case.
I believe this was known bug on previous iOS 10 beta. I had same issue and top and bottom layout guide works as I expect after I upgraded iOS version to latest.
I used a slightly varied version of Andrey's
class MyViewController: MSMessagesAppViewController {
private func presentModalViewController() {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = .savedPhotosAlbum
imagePicker.modalPresentationStyle = .custom
imagePicker.transitioningDelegate = self
present(
imagePicker,
animated: true,
completion: nil
)
}
}
extension MyViewController: UIViewControllerTransitioningDelegate {
func presentationController(
forPresented presented: UIViewController,
presenting: UIViewController?,
source: UIViewController
) -> UIPresentationController? {
let vc = PresentationController(
presentedViewController: presented,
presenting: presenting
)
vc.framePresented = modalBoundaries.frame
return vc
}
}
class PresentationController: UIPresentationController {
var framePresented = CGRect.zero
override var frameOfPresentedViewInContainerView: CGRect {
return framePresented
}
}
modalBoundaries being a dummy UIView constrained (via XIB in my case) to respect any TopLayoutGuide length.
I encountered a strange bug. I am just using iOS's custom transitioning method for UIViewControllers using UIViewControllerTransitioningDelegate together with an implementation of UIViewControllerAnimatedTransitioning. It all seems to work fine, until I do exactly the following:
open the app
present another view controller with my custom transition
rotate to landscape
dismiss the just presented view controller
That's all! What happens now is the following: I see a large black bar on the right side of the initial view controller (as if that controller's view wasn't rotated to landscape).
The funny thing is this only goes wrong in iOS 9, in iOS 8 everything seems to work just fine. Did anything change with custom transition API I don't know of? Or is this simply a really nasty iOS 9 bug? If anyone can tell me what I did wrong or if anyone can provide me with a workaround I would really appreciate that!
These classes reproduce the problem:
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
let controller = ModalViewController()
controller.transitioningDelegate = self
presentViewController(controller, animated: true, completion: nil)
}
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController,
sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transitioning()
}
}
The presented view controller:
import UIKit
class ModalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.redColor()
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "tap")
view.addGestureRecognizer(tapGestureRecognizer)
}
func tap() {
dismissViewControllerAnimated(true, completion: nil)
}
}
And finally the UIViewControllerAnimatedTransitioning implementation:
import UIKit
class Transitioning: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
let containerView = transitionContext.containerView()
if let fromView = fromView, toView = toView {
containerView?.addSubview(fromView)
containerView?.addSubview(toView)
toView.alpha = 0
UIView.animateWithDuration(0.5, animations: {
toView.alpha = 1
}, completion: {
finished in
transitionContext.completeTransition(true)
})
}
}
}
I generally use the following in animateTransition:
toView.frame = fromView.frame
FYI, you don't have to add fromView to the hierarchy, as it's already there.
I wish to create a small popover about 50x50px from a UIButton. I have seen methods using adaptive segue's but I have my size classes turn of thus meaning I can not use this features!
How else can I create this popover? Can I create it with code inside my button IBACtion? Or is there still a way I can do this with storyboards?
You can do one of the following two options :
Create an action for the UIButton in your UIViewController and inside present the ViewController you want like a Popover and your UIViewController has to implement the protocol UIPopoverPresentationControllerDelegate, take a look in the following code :
#IBAction func showPopover(sender: AnyObject) {
var popoverContent = self.storyboard?.instantiateViewControllerWithIdentifier("StoryboardIdentifier") as! UIViewController
popoverContent.modalPresentationStyle = .Popover
var popover = popoverContent.popoverPresentationController
if let popover = popoverContent.popoverPresentationController {
let viewForSource = sender as! UIView
popover.sourceView = viewForSource
// the position of the popover where it's showed
popover.sourceRect = viewForSource.bounds
// the size you want to display
popoverContent.preferredContentSize = CGSizeMake(200,500)
popover.delegate = self
}
self.presentViewController(popoverContent, animated: true, completion: nil)
}
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
According to the book of #matt Programming iOS 8:
A popover presentation controller, in iOS 8, is a presentation controller (UIPresentationController), and presentation controllers are adaptive. This means that, by default, in a horizontally compact environment (i.e. on an iPhone), the .Popover modal presentation style will be treated as .FullScreen. What appears as a popover on the iPad will appear as a fullscreen presented view on the iPhone, completely replacing the interface.
To avoid this behavior in the iPhone you need to implement the delegate method adaptivePresentationStyleForPresentationController inside your UIViewController to display the Popover correctly.
The other way in my opinion is more easy to do, and is using Interface Builder, just arrange from the UIButton to create a segue to the ViewController you want and in the segue select the Popover segue.
I hope this help you.
Swift 4 Here is fully working code. So here you will see popup window with size of 250x250:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var button: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// in case if you don't want to make it via IBAction
button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
}
#objc
private func tapped() {
guard let popVC = storyboard?.instantiateViewController(withIdentifier: "popVC") else { return }
popVC.modalPresentationStyle = .popover
let popOverVC = popVC.popoverPresentationController
popOverVC?.delegate = self
popOverVC?.sourceView = self.button
popOverVC?.sourceRect = CGRect(x: self.button.bounds.midX, y: self.button.bounds.minY, width: 0, height: 0)
popVC.preferredContentSize = CGSize(width: 250, height: 250)
self.present(popVC, animated: true)
}
}
// This is we need to make it looks as a popup window on iPhone
extension ViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
Take into attention that you have to provide popVC identifier to one viewController you want to present as a popup.
Hope that helps!
Here you can present a popover on button click.
func addCategory( _ sender : UIButton) {
var popoverContent = self.storyboard?.instantiateViewControllerWithIdentifier("NewCategory") as UIViewController
var nav = UINavigationController(rootViewController: popoverContent)
nav.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = nav.popoverPresentationController
popoverContent.preferredContentSize = CGSizeMake(50,50)
popover.delegate = self
popover.sourceView = sender
popover.sourceRect = sender.bounds
self.presentViewController(nav, animated: true, completion: nil)
}
Swift 4 Version
Doing most work from the storyboard
I added a ViewController, went to it's attribute inspector and ticked the "Use Preferred Explicit size". After that I changed the Width and Height values to 50 each.
Once this was done I ctrl clicked and dragged from the Button to the ViewController I added choosing "Present as Popover" and naming the segue Identifier as "pop"
Went to the ViewController where I had my Button and added the following code:
class FirstViewController: UIViewController, UIPopoverPresentationControllerDelegate {
#IBOutlet weak var popoverButton: UIButton! // the button
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "pop" {
let popoverViewController = segue.destination
popoverViewController.modalPresentationStyle = .popover
popoverViewController.presentationController?.delegate = self
popoverViewController.popoverPresentationController?.sourceView = popoverButton
popoverViewController.popoverPresentationController?.sourceRect = CGRect(x: 0, y: 0, width: popoverButton.frame.size.width, height: popoverButton.frame.size.height)
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
override func viewDidLoad() {
super.viewDidLoad()
}
}