iOS - Dismiss presented view controller touching outside its View - ios

I have a CustomPresentationController which animates in and out with custom animations;
This specific controller gets presented, more less at 50% of the screen size, and when I present it, I add a shadow-gray view to the presentingViewController so it adds some depth.
I can only dismiss the presentedViewController if I tap the cancel button in the NavBar which I call the default dismiss(:) method.
What I'm trying to accomplish is to detect a tap outside the presentedViewController, maybe inside the gray zone, so I can dismiss the presentedViewController, somehow like dismissing an ActionSheet but I've failed to do it. Let me explain what I've tried so far.
I tried to add a UITapGestureRecognizer to the shadow-gray view but since I'm presenting a different controller, the app-engine might think that since the shadow view isn't on the top hierarchy view it might not be accessible so it 'blocks' the recognizer - whenever I tap it, the gesture handles doesn't fire.
I'm implementing now in addition a swipe down to dismiss, which I can make it easily, but I really wanted the tap-outside feature to work as well.
Any hint on how can I approach this?
The apps image is the following:

My solution:
In presenting view controller (aka ViewControllerA):
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vcb = storyboard.instantiateViewController(withIdentifier: "ViewControllerB") as! ViewControllerB // ViewControllerB is the presented view controller
vcb.modalPresentationStyle = .custom
vcb.transitioningDelegate = self
modalRatio = Float(0.5) // modalRatio is an object property
self.present(pvc, animated: true)
ViewControllerA shall also implement Transitioning delegate:
extension ViewControllerA: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PartialSizePresentController(presentedViewController: presented, presenting: presenting, withRatio: modalRatio ?? 0.5) // modal ratio is configurable using modalRatio property
}
}
Then, implement the presentation controller (aka PartialSizePresentController), so that it also handles tap gesture:
class PartialSizePresentController: UIPresentationController {
let heightRatio : CGFloat
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, withRatio ratio: Float = 0.5) {
heightRatio = CGFloat(ratio)
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
guard let cv = containerView else { fatalError("No container view available") }
return CGRect(x: 0, y: cv.bounds.height * (1 - heightRatio), width: cv.bounds.width, height: cv.bounds.height * heightRatio)
}
override func presentationTransitionWillBegin() {
let bdView = UIView(frame: containerView!.bounds)
bdView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
containerView?.addSubview(bdView)
bdView.addSubview(presentedView!)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(PartialSizePresentController.handleTap(_:)))
bdView.addGestureRecognizer(tapGesture)
}
#objc func handleTap(_ sender: UITapGestureRecognizer) {
presentedViewController.dismiss(animated: true, completion: nil)
}
}

I just had to implement this in one of my app.
I made it worked by adding a button that covers the entire view and this button, once tapped triggers the VC to be dismissed.
Once the button is added you can add your custom View on top.
So far it looks like it's working pretty well.
My code below (I do everything programmatically, no storyboard)
//—————————————————————————————
// MARK: View Life Cycle
//—————————————————————————————
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.clear //VC view background transparent
setupUI()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//Animate blackView opacity to 1 to give some depth
UIView.animate(withDuration: 0.4, delay: 0.2, options: .curveEaseInOut, animations: {
self.blackView.alpha = 1
})
}
//————————————————
// MARK: Setup UI
//————————————————
let blackView: UIView = {
let view = UIView()
view.alpha = 0.0
view.backgroundColor = UIColor.black.withAlphaComponent(0.6)
return view
}()
//Invisible button which covers the entire view that can be tapped
lazy var dismissLayerBtn: UIButton = {
let btn = UIButton()
btn.addTarget(self, action: #selector(tapToDismiss), for: .touchUpInside)
return btn
}()
#objc func tapToDismiss() {
print("tapToDimiss")
self.dismiss(animated: true, completion: nil)
}
let milestonePickerView: MilestonePickerView = {
let view = MilestonePickerView(frame: .zero)
return view
}()
func setupUI() {
view.addSubview(blackView)
view.addSubview(dismissLayerBtn)
view.addSubview(milestonePickerView) //Important to add the customView after the button.
blackView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
dismissLayerBtn.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
milestonePickerView.anchor(top: nil, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor, paddingTop: 0, paddingLeft: 20, paddingBottom: 40, paddingRight: 20, width: 0, height: 400)
//I'm using a custom extension to setup constraints (anchors)
}
If you're using storyboard, make sure you put the invisible button under the custom view.
I hope this helps.

Try with my below code:
You need to implement this method inside you presented controller which you are working with as a popup.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Here write down you logic to dismiss controller
}
Hope this will work. :D

You were on the right track with the UITapGestureRecognizer. Just make sure you implement the shouldRecognizeSimultaneouslyWith as such:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
This should allow the gesture to fire correctly.

// it works for me
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
var touch: UITouch? = touches.first
if touch?.view == yourView {
navigationController?.popViewController(animated: true)
dismiss(animated: true, completion: nil)
}
}

Related

Tapping a UIImage while it's being animated

I've been trying to be able to tap a UIImage as it animates to the top of my screen and print("Image Tapped"), yet to no success.
override func viewDidLoad() {
super.viewDidLoad()
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
UIView.animate(withDuration: 5, delay: 0, options: UIImageView.AnimationOptions.allowUserInteraction, animations: {
self.redBalloon.frame = CGRect(x: Int(self.xEnding), y: -192, width: 166, height: 192)
}, completion: {(finished:Bool) in
self.endGame()
})
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
}
#objc func imageTapped(_ sender: UITapGestureRecognizer) {
// do something when image tapped
print("image tapped")
}
The problem is that the image view is not in the spot where you see it during animation (it's at the endpoint of the animation). So you are not tapping on the image view at the point where it is, and thus the tap is not detected.
Therefore, either you must hit-test the presentation layer or, if you don't want to do that, you must use a UIViewPropertyAnimator instead of calling UIView.animate.
As an example of the first approach, I'll subclass UIImageView. Make your UIImageView an instance of this subclass:
class TouchableImageView : UIImageView {
override func hitTest(_ point: CGPoint, with e: UIEvent?) -> UIView? {
let pres = self.layer.presentation()!
let suppt = self.convert(point, to: self.superview!)
let prespt = self.superview!.layer.convert(suppt, to: pres)
return super.hitTest(prespt, with: e)
}
}
However, personally I think it's a lot simpler to use UIViewPropertyAnimator. In that case, do not make your UIImageView a TouchableImageView! You don't want to do extra hit-test munging. Just let the property animator do all the work:
redBalloon.image = UIImage(named: "redBalloon")
redBalloon.contentMode = .scaleAspectFit
redBalloon.frame = CGRect(x: Int(xOrigin), y: 667, width: Int(redBalloon.frame.size.width), height: Int(redBalloon.frame.size.height))
let anim = UIViewPropertyAnimator(duration: 5, timingParameters: UICubicTimingParameters(animationCurve: .easeInOut))
anim.addAnimations {
self.redBalloon.frame = CGRect(x: Int(xEnding), y: -192, width: 166, height: 192)
}
anim.addCompletion { _ in
self.endGame()
}
let imageTap = UITapGestureRecognizer(target: self, action: #selector(imageTapped))
redBalloon.isUserInteractionEnabled = true
redBalloon.addGestureRecognizer(imageTap)
anim.startAnimation()

How to build a context menu like Facebook / Slack on iOS?

I was just looking at Context menu of Facebook and or slack and wanted to create something similar in my App.
I have tried two methods.
First method. Having a in View Table View and sliding it from bottom to create as if it is animated on to the view. But the problem with this is that The navigation controller and Tab bar controller are not hidden and a white patch is shown over the Black (Alpha 30 %).
The second method I tried was showing a new View controller over the current view controller and presenting as a Modal presentation.
let vc = CustomActionTableViewController(nibName: "CustomActionTableViewController", bundle: nil)
vc.modalPresentationStyle = .overFullScreen
self.present(vc, animated: false, completion: nil)
This works okay but the method is too slow as I have to work with lot of Notifications (To send selected index to my main View and then perform action). It is painfully slow.
Could anyone help me with how I can improve the implementation so that I can get the Action sheet similar to Facebook which is smooth and very very fluid
Check this example : Bottom pop Up
Currently I am using this in my app and it's work fine.
Since you mentioned Slack, they actually have open sourced their bottom sheet implementation, PanModal.
Using UIPresentationController and UIPanGestureRecognizer
1- create BottomMenu presentation Controller which will handle the height of your View Controller and blur
class BottomMenuPresentationController: UIPresentationController {
// MARK: - Properties
var blurEffectView: UIVisualEffectView?
var tapGestureRecognizer = UITapGestureRecognizer()
private var topHeightRatio: Float
private var bottomHeightRatio: Float
init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?, topHeightRatio: Float, bottomHeightRatio: Float) {
let blurEffect = UIBlurEffect(style: .systemThickMaterialDark)
blurEffectView = UIVisualEffectView(effect: blurEffect)
self.topHeightRatio = topHeightRatio
self.bottomHeightRatio = bottomHeightRatio
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
blurEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
self.blurEffectView?.isUserInteractionEnabled = true
self.blurEffectView?.addGestureRecognizer(tapGestureRecognizer)
}
override var frameOfPresentedViewInContainerView: CGRect {
CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * CGFloat(topHeightRatio)),
size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height * CGFloat(bottomHeightRatio)))
}
override func presentationTransitionWillBegin() {
self.blurEffectView?.alpha = 0
if let blurEffectView = blurEffectView {
self.containerView?.addSubview(blurEffectView)
}
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (_) in
self.blurEffectView?.alpha = 0.66
}, completion: { (_) in })
}
override func dismissalTransitionWillBegin() {
self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (_) in
self.blurEffectView?.alpha = 0
}, completion: { (_) in
self.blurEffectView?.removeFromSuperview()
})
}
override func containerViewWillLayoutSubviews() {
super.containerViewWillLayoutSubviews()
presentedView!.roundCorners([.topLeft, .topRight], radius: 14)
}
override func containerViewDidLayoutSubviews() {
super.containerViewDidLayoutSubviews()
presentedView?.frame = frameOfPresentedViewInContainerView
blurEffectView?.frame = containerView!.bounds
}
#objc func dismissController() {
self.presentedViewController.dismiss(animated: true, completion: nil)
}
}
2- create Your ViewController
class BottomMenuVC: UIViewController {
// MARK: - Instances
var hasSetPointOrigin = false
var pointOrigin: CGPoint?
// MARK: - Properties
let topDarkLine: UIView = {
let view = UIView()
view.backgroundColor = UIColor(hexString: "#E1E1E1")
view.layer.cornerRadius = 2
return view
}()
let cancelButn: UIButton = {
let button = UIButton(type: .custom)
button.setAttributedTitle(NSAttributedString(string: "Cancel", attributes: [NSAttributedString.Key.font: UIFont.LatoMedium(size: 17),
NSAttributedString.Key.foregroundColor: UIColor(hexString: "#515151")
]), for: .normal)
button.backgroundColor = UIColor(hexString: "#F1F3F4")
button.layer.cornerRadius = 5.0
button.addTarget(self, action: #selector(cancelButnPressed), for: .touchUpInside)
return button
}()
// MARK: - viewLifeCycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.isUserInteractionEnabled = true
setupMenuView()
}
override func viewDidLayoutSubviews() {
if !hasSetPointOrigin {
hasSetPointOrigin = true
pointOrigin = self.view.frame.origin
}
}
// MARK: - SetupView
func setupMenuView() {
self.view.addSubview(topDarkLine)
self.view.addSubview(cancelButn)
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction(_:)))
view.addGestureRecognizer(panGesture)
topDarkLine.constrainHeight(constant: 4)
topDarkLine.constrainWidth(constant: view.frame.size.width * 0.10)
topDarkLine.centerXInSuperview()
topDarkLine.anchor(top: view.topAnchor, leading: nil, bottom: nil, trailing: nil, padding: .init(top: 8, left: 0, bottom: 0, right: 0))
cancelButn.anchor(top:view.topAnchor, leading: view.leadingAnchor, bottom: nil, trailing: view.trailingAnchor,
padding: .init(top: 16, left: 16, bottom: 0, right: 16))
cancelButn.constrainHeight(constant: 44)
}
// MARK: - Actions
#objc func panGestureRecognizerAction(_ sender: UIPanGestureRecognizer) {
let translation = sender.translation(in: view)
// Not allowing the user to drag the view upward
guard translation.y >= 0 else { return }
// setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down in the y-axis
view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)
if sender.state == .ended {
let dragVelocity = sender.velocity(in: view)
if dragVelocity.y >= 1300 {
// Velocity fast enough to dismiss the uiview
self.dismiss(animated: true, completion: nil)
} else {
// If the dragging isn’t too fast, resetting the view back to it’s original point
UIView.animate(withDuration: 0.3) {
self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
}
}
}
}
#objc func cancelButnPressed() {
dismiss(animated: true, completion: nil)
}
}
3- make the viewController that contain the button that will present your menu conforms to UIViewControllerTransitioningDelegate
extension viewController: UIViewControllerTransitioningDelegate {
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
BottomMenuPresentationController(presentedViewController: presented, presenting: presenting, topHeightRatio: 0.6, bottomHeightRatio: 0.4)
}
}
4- set the transitioning delegate to self and present your custom presentation Controller
func showBottomMenu() {
let menu = BottomMenuVC()
menu.coordinator = self
menu.modalPresentationStyle = .custom
menu.transitioningDelegate = self
present(menu, animated: true, completion: nil)
}
check this PanGesture Slidable View article

Swift: Changing (translate) a UIView position through a pan gesture in its superview window

Introduction
Context:
In my main ViewController I have a scrollView with a few objects inside (which are UIViews). When one of the UIViews are tapped/selected I animate forward a UITextView in a UIView to go with the selected object. (only one UIView can appear at a time)
This UIView that appears on object selection is separated into a separate class called AdjunctiveTextView.
Issue/goal:
(the example code provided below will clear make this clear, I've also commented where the issue lies in the code)
When an object has been tapped and has an adjacent UIView with a text I want to have that adjacent UIView to follow with the scrollView.
I'm using a UIPanGestureRecognizer to attempt to do this. But I can't figure out how to make it work when the user drags in the scrollview. It only work if the user drags on the actual adjunctiveTextView.
Everything works as expected except that the adjunctiveTextView does not change its position during the panGesture.
I would like (if possible) to have the AdjunctiveTextView as a separate class. My ViewController file is getting rather big.
Question:
Why doesn't the UIPanGestureRecognizer work as expected? What is needed in order for it to translate the backView correctly?
Code
My attempt: (as shown below)
My attempt simply makes the backView itself "dragable" around through the panGesture. Nothing happens to it when I scroll the scrollView.
(I have only included relevant portions of my code)
class ViewController: UIViewController {
let adjunctiveTextView = AdjunctiveTextView()
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
** adjunctiveTextView.scrollView = scrollView // **Edited! (scrollView is the name of the scrollView in this class too)
adjunctiveTextView.showView(passInObject: AvailableObject)
}
}
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
}
lazy var textView: UITextView = {
//textView setup
}
//additional init and setup
** weak var scrollView : UIScrollView! // **Edited!
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// window.addGestureRecognizer(panG)
** scrollView.addGestureRecognizer(panG) // **Edited!
window.addSubview(backView)
textView.text = passInObject.information
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width, y: passInObject.minY, width: window.frame.width - passInObject.maxX - 6, height: textView.bounds.height + 5)
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window) //Have tried setting this to scrollView also
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window) //Have tried setting this to sccrollView also
break
case .ended:
break
default:
break
}
}
}
}
Thanks for reading my question.
I just add a weak reference to your scrollView and then add the pan gesture to scrollView. It works as you want. You may consider add another pan gesture to the back view if you want your original behavior.
class AdjunctiveTextView: NSObject {
lazy var backView: UIView = {
//backView setup
return UIView.init()
}()
lazy var textView: UITextView = {
//textView setup
return UITextView.init(frame: CGRect.init(x: 0, y: 0, width: 300, height: 100))
}()
weak var scrollView: UIScrollView!
//additional init and setup
func showView(passInObject: AvailableObject) {
if let window = UIApplication.shared.keyWindow {
// the issue must either be here in the PanGesture setup
let panG = UIPanGestureRecognizer(target: self, action: #selector(translateView(sender:)))
panG.cancelsTouchesInView = false
// passInObject.addGestureRecognizer(panG)
scrollView.addGestureRecognizer(panG)
window.addSubview(backView)
textView.text = passInObject.information
textView.backgroundColor = UIColor.blue
backView.addSubview(textView)
backView.frame = CGRect(x: passInObject.frame.minX, y: passInObject.frame.minY, width: window.frame.width - passInObject.frame.maxX - 6, height: textView.bounds.height + 5)
backView.alpha = 0
//it animates a change of the backViews x position and alpha.
UIView.animate(withDuration: 0.42, delay: 0, options: .curveEaseInOut, animations: {
self.backView.alpha = 1
self.backView.frame = CGRect(x: passInObject.frame.minX + passInObject.frame.width , y: passInObject.frame.minY , width: window.frame.width - passInObject.frame.maxX - 6, height: self.textView.bounds.height + 5)
self.backView.backgroundColor = UIColor.red
}, completion: nil)
}
}
// or the issue is here in the handle function for the PanGesture.
#objc private func translateView(sender: UIPanGestureRecognizer) {
if let window = UIApplication.shared.keyWindow {
let translation = sender.translation(in: window)
switch sender.state {
case .began, .changed:
backView.center = CGPoint(x: backView.center.x, y: backView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: window)
break
case .ended:
break
default:
break
}
}
}
}
class ObjectScrollView: UIScrollView{
}
class AvailableObject: UIView{
var information: String!
}
class MySCNViewController: UIViewController {
#IBOutlet weak var oScrollView: ObjectScrollView!
// this is a delegate method which gets called when an object is tapped in the scrollView
func scrollViewObjectIsTapped(_ objectScrollView: ObjectScrollView, object: AvailableObject) {
adjunctiveTextView.showView(passInObject: object)
}
let adjunctiveTextView = AdjunctiveTextView()
let ao = AvailableObject.init(frame: CGRect.init(x: 0, y: 0, width: 200, height: 200))
override func viewDidLoad() {
super.viewDidLoad()
ao.information = "test"
adjunctiveTextView.scrollView = oScrollView
ao.backgroundColor = UIColor.yellow
}
#IBAction func tap(_ sender: Any?){
scrollViewObjectIsTapped(oScrollView, object: ao)}
}

Trying to mimic the Mail.app compose animation keeping a layer in view

I have been trying for a while but I cannot figure out how to create a Compose animation seen in the iOS 10+ when you can drag the new composed email down, then it stays on the bottom and the rest of the app is normally accessed, then when you tap it, it re-shows.
I have created a sample project in which I have a UIViewController that presents another UIViewController which has a UIPanGestureRecognizer in it's UINavigationController that fires the pangesture state analyzer.
I can indeed drag to dismiss it , but I cannot find a way to keep it frame.
Bellow there's a print screen of what I'm trying to accomplish and then my used code to where I'm stuck at.
UIViewController that is the presentingViewController class
//
// ViewController.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
#objc let interactor = Interactor()
lazy var presentButton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("Present", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(didTapPresentButton), for: .touchUpInside)
return b
}()
lazy var testbutton: UIButton = {
let b = UIButton(type: .custom)
b.setTitle("test", for: .normal)
b.setTitleColor(.black, for: .normal)
b.addTarget(self, action: #selector(test), for: .touchUpInside)
return b
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .white
view.addSubview(presentButton)
presentButton.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 100, height: 100)
presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
view.addSubview(testbutton)
testbutton.anchor(top: nil, left: nil, bottom: presentButton.topAnchor, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 100, paddingRight: 0, width: 100, height: 100)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#objc func didTapPresentButton() {
let presentedVC = PresentedViewController()
let navController = UINavigationController(rootViewController: presentedVC)
navController.transitioningDelegate = self
presentedVC.interactor = interactor // new
navController.modalPresentationStyle = .custom
navController.view.layer.masksToBounds = true
present(navController, animated: true, completion: nil)
}
#objc func test() {
print("test")
}
// Handles the presenting animation
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForPresentor()
}
// Handles the dismissing animation
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomAnimationForDismisser()
}
// interaction controller, only for dismissing the view;
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactor.hasStarted ? interactor : nil
}
// delegate do custom modal presentation style
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
UIViewController 2 that is the presentedViewController
import Foundation
import UIKit
class PresentedViewController: UIViewController, UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate {
#objc var interactor: Interactor? = nil
#objc var panGr = UIPanGestureRecognizer()
#objc var panTapRecon = UITapGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
let leftB = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel))
navigationItem.leftBarButtonItem = leftB
panGr = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
navigationController?.navigationBar.addGestureRecognizer(panGr)
panTapRecon = UITapGestureRecognizer(target: self, action: #selector(handleNavControllerTapGR))
navigationController?.navigationBar.addGestureRecognizer(panTapRecon)
}
#objc func didTapCancel() {
guard let interactor = interactor else { return }
interactorFinish(interactor: interactor)
dismiss(animated: true, completion: nil)
}
#objc func handleNavControllerTapGR(_ sender: UITapGestureRecognizer) {
print("tap detected")
}
// Swipe gesture recognizer handler
#objc func handleGesture(_ sender: UIPanGestureRecognizer) {
//percentThreshold: This variable sets how far down the user has to drag
//in order to trigger the modal dismissal. In this case, it’s set to 40%.
let percentThreshold:CGFloat = 0.30
// convert y-position to downward pull progress (percentage)
let translation = sender.translation(in: view)
let verticalMovement = translation.y / view.bounds.height
let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
let downwardMovementPercent = fminf(downwardMovement, 1.0)
let progress = CGFloat(downwardMovementPercent)
guard let interactor = interactor else { return }
switch sender.state {
case .began:
interactor.hasStarted = true
self.dismiss(animated: true, completion: nil)
case .changed:
// alterar se o tamanho do presentigViewController (MainTabBarController) for alterado no background
let scaleX = 0.95 + (progress * (1 - 0.95))
let scaleY = 0.95 + (progress * (1 - 0.95))
// Não deixa ultrapassar os 100% de scale (tamanho original)
if (scaleX > 1 && scaleY > 1) { return }
presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: scaleX, y: scaleY);
presentingViewController?.view.layer.masksToBounds = true
interactor.shouldFinish = progress > percentThreshold
interactor.update(progress)
case .cancelled:
interactor.hasStarted = false
interactor.cancel()
case .ended:
interactor.hasStarted = false
if (interactor.shouldFinish) {
interactorFinish(interactor: interactor)
} else {
// repõe o MainTabBarController na posição dele atrás do NewPostController
UIView.animate(withDuration: 0.5, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95);
self.presentingViewController?.view.layer.masksToBounds = true
let c = UIColor.black.withAlphaComponent(0.4)
let shadowView = self.presentingViewController?.view.viewWithTag(999)
shadowView?.backgroundColor = c
})
interactor.cancel()
}
default: break
}
}
#objc func interactorFinish(interactor: Interactor) {
removeShadow()
interactor.finish()
}
// remove a shadow view
#objc func removeShadow() {
UIView.animate(withDuration: 0.2, animations: {
self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 1.0, y: 1.0);
self.presentingViewController?.view.layer.masksToBounds = true
}) { _ in
}
}
}
Here's an Helper file that has the custom presentations:
//
// Helper.swift
// dismissLayerTest
//
// Created by Ivan Cantarino on 27/09/17.
// Copyright © 2017 Ivan Cantarino. All rights reserved.
//
import Foundation
import UIKit
class Interactor: UIPercentDrivenInteractiveTransition {
#objc var hasStarted = false
#objc var shouldFinish = false
}
extension UIView {
#objc func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddinfLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddinfLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if width != 0 {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if height != 0 {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
#objc func roundCorners(corners:UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
}
}
class CustomAnimationForDismisser: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.27
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
print("Returning animateTransition VC")
return
}
// from view só existe no dismiss
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
print("Failed to instantiate fromView: CustomAnimationForDismisser()")
return
}
// Set up some variables for the animation.
let containerFrame: CGRect = containerView.frame
var fromViewFinalFrame: CGRect = transitionContext.finalFrame(for: fromVC)
fromViewFinalFrame = CGRect(x: 0, y: containerFrame.size.height, width: containerFrame.size.width, height: containerFrame.size.height)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: {
fromView.frame = fromViewFinalFrame
}) { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
}
}
}
class CustomAnimationForPresentor: NSObject, UIViewControllerAnimatedTransitioning {
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
// obtém os VCs para não o perder na apresentação (default desaparece por trás)
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {//, let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
print("Returning animateTransition VC")
return
}
// gets the view of the presented object
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
// Set up animation parameters.
toView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height)
// Always add the "to" view to the container.
containerView.addSubview(toView)
// Animate using the animator's own duration value.
UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseOut, animations: {
// Zooms out da MainTabBarController - o VC
fromVC.view.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
// propriedades declaradas no CustomPresentationController() // Anima o presented view
toView.transform = .identity
}, completion: { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// So it avoids view stacks and overlap issues
if (!success) { toView.removeFromSuperview() }
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
})
}
}
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
// Tamanho desejado para o NewPostController
override var frameOfPresentedViewInContainerView: CGRect {
guard let containerBounds = containerView?.bounds else {
print("Failed to instantiate container bounds: CustomPresentationController")
return .zero
}
return CGRect(x: 0.0, y: 0.0, width: containerBounds.width, height: containerBounds.height)
}
// Garante que o frame do view controller a mostrar, se mantém conforme desenhado na função frameOfPresentedViewInContainerView
override func containerViewWillLayoutSubviews() {
presentedView?.frame = frameOfPresentedViewInContainerView
}
}
This desired effect can also be seen in other apps, such like Music app, Stack Exchange/Overflow iOS App
Does anyone have a hint on how can this be accomplished? I feel like I'm really close to achieve it, but I can't find a way to keep the dismissed view with a layer on screen.
The project above can be found here
Thank you very much.
Regards.
I would suggest that Apple (in the animated screen gif you have so helpfully provided) is not using a presented view controller. If it were, the presenting view controller would not be able to shrink its view — and on dismissal, the presented view controller's view would completely disappear.
I would say that underlying this interface is a parent view controller with multiple child view controllers (or maybe just a normal view controller with two child views). Thus, we can display the two child views wherever and however we like. Your animated gif shows two possible arrangements of the two child views: overlapping, and one above the other with the second view just barely visible from the bottom of the screen.

Darkened overlay while user is typing

When the user taps on a text field, I wanted to darken the rest of the screen (everything below the text box, above the keyboard) to make it clear what they should be doing. I believe it involved putting a transparent UI view down and adding a gesture recognizer to it, but I'm not quite sure how to do that.
I've got the following code for when the user arrives on the screen. Is this where I would add the new UI View?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
textField.becomeFirstResponder()
}
Thanks!
I created a subclass of UIView for two reasons:
you can add a gesture recogniser to it
you can add a delegate to it
Since the last subview added to a view is on top I first add the dark layer and then call bringSubviewToFront(textview) this will put the dark layer between the textview in question and everything else.
I created a protocol with one function. This function get's called by the gesture recogniser and returns the instance of DarkView to the delegate. The delegate (your ViewController) can then remove it from it's superview.
This you can do without a delegate function, but you also have to call resignFirstResponder() on the textfield.
Don't forget to set up the delegate of the DarkView in your ViewController.
Just a VC with some textfields.
class FirstViewController: UIViewController, DarkViewDelegate, UITextFieldDelegate {
var masterView : UIView!
override func viewDidLoad() {
super.viewDidLoad()
masterView = UIView(frame: self.view.frame)
masterView.backgroundColor = UIColor.whiteColor()
let textField1 = UITextField(frame: CGRect(x: 50, y: 20, width: 300, height: 20))
let textField2 = UITextField(frame: CGRect(x: 50, y: 60, width: 300, height: 20))
let textField3 = UITextField(frame: CGRect(x: 50, y: 100, width: 300, height: 20))
let textField4 = UITextField(frame: CGRect(x: 50, y: 140, width: 300, height: 20))
func styleTextField(field : UITextField) {
field.layer.borderColor = UIColor.blackColor().CGColor
field.layer.borderWidth = 2
field.layer.cornerRadius = field.frame.size.height / 2
field.backgroundColor = UIColor.whiteColor()
field.delegate = self
masterView.addSubview(field)
}
styleTextField(textField1)
styleTextField(textField2)
styleTextField(textField3)
styleTextField(textField4)
self.view.addSubview(masterView)
// Do any additional setup after loading the view, typically from a nib.
}
// delegate function of a textfield
func textFieldDidBeginEditing(textField: UITextField) {
focusUserOn(textField) // darken everything else
}
// delegate function of DarkView undarken everything
func tappedDark(view: DarkView) {
guard let superV = view.superview else {
return
}
if let textField = superV.subviews.last as? UITextField {
textField.resignFirstResponder() // also stop editing
}
view.removeFromSuperview()
}
func focusUserOn(textfield: UITextField) {
guard let superV = textfield.superview else {
return
}
let darkArea = DarkView(frame: superV.bounds)
darkArea.delegate = self
superV.addSubview(darkArea)// add DarkView (everything is dark now)
superV.bringSubviewToFront(textfield) // bring the textview back to the front.
}
}
simple subclass of UIView with a gesture recogniser
class DarkView : UIView, UIGestureRecognizerDelegate {
weak var delegate : DarkViewDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.4)
let tap = UITapGestureRecognizer(target: self, action: Selector("tapped"))
tap.delegate = self
self.addGestureRecognizer(tap)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
print("deinit dark")
}
func tapped() {
guard let del = self.delegate else {
return
}
del.tappedDark(self)
}
}
Protocol to pass the DarkView when it is tapped to a VC
protocol DarkViewDelegate : class {
func tappedDark(view:DarkView)
}
After thought
func textFieldDidEndEditing(textField: UITextField) {
guard let superV = view.superview else {
return
}
for subview in superV.subviews {
if subview is DarkView {
subview.removeFromSuperview()
}
}
}
Yes, you have the right idea. Add your UIView (let's call it darkeningView) to your storyboard and set it's background color to 50% opaque black. Position it where you want it and add constraints that position it there.
You can also attach a tap gesture recognizer to the darkeningView in IB and set up it's delegate. (You will probably need to set userInteractionEnabled = true on the darkeningView so that it responds to taps.
Add an IBOutlet to your view. In IB, set it to hidden = true.
In your code, when you activate the text field for editing, also set your darkeningView.hidden = false.

Resources