Detect a touch outside of presented view to dismiss it - ios

So I have a side menu that is presented when a button is clicked and I would like to know if u guys could help me find how I can detect if a click occurred outside of that side menu view so I can dismiss it.
I have looked around for this and all I see are deprecated things and with errors, and I can't use any.
Here is my animation code :
import UIKit
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else {return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width * 0.8
let finalHeight = toViewController.view.bounds.height
if isPresenting{
containerView.addSubview(toViewController.view)
toViewController.view.frame = CGRect(x: -finalWidth, y: 0, width: finalWidth, height: finalHeight)
}
let transform = {
toViewController.view.transform = CGAffineTransform(translationX: finalWidth, y: 0)
}
let identity = {
fromViewController.view.transform = .identity
}
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}){(_) in
transitionContext.completeTransition(!isCancelled)
}
}
}

I actually have something like this in my app. What you can do is add a UIView() that covers your whole view. Make sure this view is in front of everything but the menu! Set the UIView() userInteraction to false. When the menu is shown, simply set the view to intractable. Then put a touch recognizer so that when its touched the menu goes away!
Something I also like to do with this is set the views background to black, with an alpha of like 0.25! Then when the menu is hidden, alpha is zero, when it shows, animate it to 0.25. it dims the background when the menu is shown so it'll be functional and design nice.
class BackGroundView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
SetUpView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func SetUpView(){
backgroundColor = .black
alpha = 0
isUserInteractionEnabled = false
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Here's where you would hide the menu
}
func MenuIsShown(menuWillShow: Bool)
{
if(menuWillShow){
isUserInteractionEnabled = true
UIView.animate(withDuration: 0.2) {
alpha = 0.45
}
} else{
isUserInteractionEnabled = false
UIView.animate(withDuration: 0.2) {
alpha = 0
}
}
}
func AddViewToScene(view: UIView){
view.addSubview(self)
translatesAutoresizingMaskIntoConstraints = false
topAnchor.constraint(equalTo: view.topAnchor).isActive = true
bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
}
}
then you can call it doing something like:
class ViewController: UIViewController{
override func viewDidLoad() {
super.viewDidLoad()
let dimView = BackGroundView()
dimView.AddViewToScene(view: view)
}
}

Related

Touch delegates not called on view when z-index is set to non zero value

I am implementing the chat bubble feature. I want the chat bubble to be on top of all the view controller and shouldn't be affected by screen transition. So I decided to add it as subview to UIWindow so far it looks like
This works great, but if I present view controller instead of push then chat head goes behind the presented view controller. To avoid it I set ZIndex property of chat head layer
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let window = UIApplication.shared.keyWindow {
window.addSubview(chatHead)
chatHead.layer.zPosition = 1
}
}
But now though chat head appears on top of presented view controller, its touch delegates like touchesBegan, touchesMoved, touchesEnded nothing gets called when chat head is above presented ViewController
Adding code of chat head (lot of code might be unnecessary to this question, but just posting it so if someone would like to try it)
class ChatHeadView: UIView {
let headerWidth: CGFloat = 80.0
let headerHeight: CGFloat = 80.0
let sideInset: CGFloat = 10.0
let bottomInset: CGFloat = 10.0
var cancellationWindowShown = false
lazy var cancellationContainerRect: CGRect = {
var rect = self.cancelChatView.convert(self.cancelChatView.frame, to: nil)
rect.origin.x = rect.origin.x + 30
rect.size.height = 100
return rect
}()
lazy var cancellationThreshold: CGFloat = {
return self.keywindow.frame.maxY - (self.keywindow.frame.maxY / 3)
}()
lazy var keywindow: UIWindow = {
let window = UIApplication.shared.keyWindow ?? UIWindow()
return window
}()
lazy var cancelChatView: CancelChatView = {
if let cancelView = Bundle.main.loadNibNamed("CancelChatView", owner: nil, options: [:])?.first as? CancelChatView {
return cancelView
}
fatalError("Couldnt load")
}()
init() {
super.init(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
self.layer.masksToBounds = true
self.layer.cornerRadius = 40
self.layer.backgroundColor = UIColor.red.cgColor
self.cancelChatView.frame = CGRect(x: 0, y: self.keywindow.frame.maxY - 140, width: self.keywindow.frame.width, height: 140)
self.cancelChatView.layer.opacity = 0.0
self.keywindow.addSubview(self.cancelChatView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
debugPrint("Touch started")
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self.keywindow)
if location.y >= cancellationThreshold {
self.showCancellationWindow()
}
else {
removeCancellationView()
debugPrint("Removing Cancellation window")
}
self.center = location
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self.keywindow)
self.center = location
self.snap(to: location)
self.removeCancellationView()
}
}
private func snap(to point: CGPoint) {
var finalPoint:CGPoint = self.frame.origin
if self.cancellationContainerRect.intersects(self.convert(self.frame, to: nil)) {
self.removeFromSuperview()
}
else {
if finalPoint.x <= self.keywindow.center.x {
finalPoint.x = self.keywindow.frame.minX + sideInset
}
else if finalPoint.x > self.keywindow.center.x {
finalPoint.x = self.keywindow.frame.maxX - (sideInset + self.headerWidth)
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.6, options: .curveEaseInOut, animations: {
self.frame.origin = finalPoint
}, completion: nil)
}
}
private func showCancellationWindow() {
if cancellationWindowShown == false {
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
animation.fromValue = 0.0
animation.toValue = 0.6
animation.duration = 0.1
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.cancelChatView.layer.add(animation, forKey: "reveal")
self.cancellationWindowShown = true
}
}
private func removeCancellationView() {
if cancellationWindowShown == true {
let animation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
animation.toValue = 0.0
animation.duration = 0.1
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
self.cancelChatView.layer.add(animation, forKey: "hide")
cancellationWindowShown = false
}
}
}
Issue Gif (touch not working):
As matt has asked for details of how I am presenting ViewController, adding screenshot of storyboard segue configuration

View not presenting properly on iPhone plus models

In iPhone 7 simulator, I have a view called cartView which looks like so...
On pressing the next button on the cartView at the bottom, another view is presented like so...
This presented view is called presentedView.
The code written on the press of the next button on the cartView is this...
let vc = PresentedUserDetailsViewController()
vc.modalPresentationStyle = .custom
present(vc, animated: true, completion: nil)
In the presentedView, these have been declared before the viewDidLoad...
lazy var backdropView: UIView = {
let bdView = UIView(frame: self.view.bounds)
bdView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
return bdView
}()
let menuHeight = UIScreen.main.bounds.height / 2
var isPresenting = false
init() {
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .custom
transitioningDelegate = self
}
Finally, at the end, an extension is also given like so...
extension PresentedUserDetailsViewController: 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
let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
guard let toVC = toViewController else { return }
isPresenting = !isPresenting
if isPresenting == true {
containerView.addSubview(toVC.view)
menuView.frame.origin.y += menuHeight
backdropView.alpha = 0
UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
self.menuView.frame.origin.y -= self.menuHeight
self.backdropView.alpha = 1
}, completion: { (finished) in
transitionContext.completeTransition(true)
})
} else {
UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
self.menuView.frame.origin.y += self.menuHeight
self.backdropView.alpha = 0
}, completion: { (finished) in
transitionContext.completeTransition(true)
})
}
}
}
But when I run the app in a plus type iPhone model (say iPhone 6 plus or iPhone 7 plus) and press the next button on the cartView this is what I'm getting...
Here, I've just colored the presentedView to make it distinct. In this case, the presentedView not only does not fill the entire screen size but part of the cartView is also seen behind including the next button of the cartView.
How can I make the presentedView to appear properly on a plus type iPhone model like they appear in the iPhone 7 model (the 2nd screenshot from top)
EDIT 1: THE SCREENSHOT ON iPhone 6 plus AFTER MAKING CHANGES..
EDIT: 2 As seen in iPad Air 2...
you need to set constraints when you add subview, in this case.
containerView.addSubview(toVC.view)
use autolayout, to set constraints
**UPDATED: **
extension UIView{
public func attachTo(view : UIView, animated : Bool = true){
if(animated){
self.alpha = 0
}
else{
self.alpha = 1
}
view.addSubview(self)
// self.frame = view.bounds
self.translatesAutoresizingMaskIntoConstraints = false
self.topAnchor.constraint(equalTo: self.superview!.topAnchor).isActive = true
self.bottomAnchor.constraint(equalTo: self.superview!.bottomAnchor).isActive = true
self.leadingAnchor.constraint(equalTo: self.superview!.leadingAnchor, constant: 0).isActive = true
self.trailingAnchor.constraint(equalTo: self.superview!.trailingAnchor, constant: 0).isActive = true
if(animated){
UIView.animate(withDuration: 0.3, animations: {
self.alpha = 1
})
}
}
}
now, remove
containerView.addSubview(toVC.view)
and add this :
toVC.view.attachTo(view: containerView)
I think maybe you get wrong frame in let bdView = UIView(frame: self.view.bounds). try to use autolayout
After containerView.addSubview(toVC.view) add following:
toVC.view.autoresizingMasks = [.flexibleWidth, .flexibleHeight]

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.

Retrieving the right position of a scaled UIView

I'm having trouble retrieving the current position of a view, absolutely in the iPhone screen, when the view is scaled.
Here is a minimal example of what I've done.
I have a single elliptical view on screen. When I tap, it grows and start to shiver to show that it is selected. Then I can drag the view all over the screen.
Here is the code:
ViewController
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// The View
let toDrag = MyView(frame: CGRect(x: 0, y: 0, width: 50, height: 40))
toDrag.center = CGPoint(x:100, y:100)
// The gesture recognizer
let panGestRec = UIPanGestureRecognizer(target: self, action:#selector(self.panView(sender:)))
toDrag.addGestureRecognizer(panGestRec)
self.view.addSubview(toDrag)
}
func panView(sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
if sender.state == .recognized { print("Pan Recognized", terminator:"")}
if sender.state == .began { print("Pan began", terminator: "") }
if sender.state == .changed { print("Pan changed", terminator: "") }
if sender.state == .ended { print("Pan ended", terminator: "") }
let point = senderView.convert(senderView.center, to: nil)
print (" point \(point)")
let translation = sender.translation(in: self.view)
senderView.center = CGPoint(x: senderView.center.x + translation.x, y: senderView.center.y + translation.y)
sender.setTranslation(CGPoint.zero, in: self.view)
}
}
MyView
import UIKit
class MyView: UIView {
var isAnimated: Bool = false
override init(frame: CGRect) {
super.init(frame: frame) // calls designated initializer
self.isOpaque = false
self.backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.isOpaque = false
self.backgroundColor = UIColor.clear
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = true
rotate1()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = false
endRotation()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
isAnimated = false
endRotation()
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
context.addEllipse(in: rect)
context.setLineWidth(1.0)
context.setStrokeColor(UIColor.black.cgColor)
context.setFillColor(UIColor.red.cgColor)
context.drawPath(using: .fillStroke)
}
func rotate1() {
guard isAnimated else { return }
UIView.animate(withDuration: 0.05, delay:0.0, options: [], animations: {
self.transform = CGAffineTransform.init(rotationAngle:.pi/16).scaledBy(x: 1.5, y: 1.5)
}, completion: { _ in
self.rotate2()
})
}
func rotate2() {
guard isAnimated else { return }
UIView.animate(withDuration: 0.05, delay:0.0, options: [], animations: {
self.transform = CGAffineTransform.init(rotationAngle:.pi/(-16)).scaledBy(x: 1.5, y: 1.5)
}, completion: { _ in
self.rotate1()
})
}
func endRotation () {
// End existing animation
UIView.animate(withDuration: 0.5, delay:0.0, options: .beginFromCurrentState, animations: {
self.transform = CGAffineTransform.init(rotationAngle:0 ).scaledBy(x: 1, y: 1)
}, completion: nil)
}
}
With this version, here are some displays of the position
Pan began point (233.749182687299, 195.746572421573)
Pan changed point (175.265022520054, 181.332358181369)
Pan changed point (177.265022520054, 184.332358181369)
Pan changed point (177.265022520054, 184.332358181369)
Pan changed point (178.265022520054, 185.332358181369)
Pan changed point (179.265022520054, 186.332358181369)
Pan changed point (180.265022520054, 187.332358181369)
Pan changed point (180.265022520054, 188.332358181369)
Pan changed point (181.265022520054, 188.332358181369)
Pan RecognizedPan ended point (181.265022520054, 189.332358181369)
You can see that the first position (began point) isn't correct: the view was animated at that moment. The other positions are OK, when dragging, the view don't animate.
If I comment the code in rotate1 and rotate2, all the positions are correct, so I assume that the sizing and eventually the rotation of the view are interfering in the result. My question is: how can I retrieve the correct position of the view, when it is scaled? Obviously, the line
senderView.convert(senderView.center, to: nil)
didn't make what I thought : converting the coordinate to the fixed size screen coordinates?
I tried changing the animation from View animation to Core Animation (layer), and the result was the same.
So I tried to imagine a formula, with size, bounds and frame values, that could result in the correct position, but I did not succeed...
The only think that worked, was to stop immediately the animation with a transform, just before trying to retrieve the center, like that in panView
func panView(sender: UIPanGestureRecognizer) {
guard let senderView = sender.view else { return }
// Stop the transformation now!
senderView.transform = CGAffineTransform.init(rotationAngle:0 ).scaledBy(x: 1, y: 1)
if sender.state == .recognized { print("Pan Recognized", terminator:"")}
...
It isn't the solution I wanted, but I'll go with this for now...
In fact, I had another problem with view convert (Retrieve the right position of a subView with auto-layout)
and my problem was the same, so the solution is here...
I didn't use the method correctly. If I change using beninho85's solution (and even more cleaner cosyn's method to use just one view), it works! I can have the right coordinates even if the view is animated.
I just had to replace, in ViewController :
let point = senderView.convert(senderView.center, to: nil)
with
let point = senderView.convert(CGPoint(x:senderView.bounds.midX, y:senderView.bounds.midY), to: nil)

Searchbar behind Navigationbar

Given 3 Controllers: A,B,C
A has a hidden navigationbar. Calls Controller B via a StoryboardReference.
Controller B shows Navigationbar on viewDidLoad. It has a searchbar and a collectionView. See screenshot A of my storyboard. Calls controller C if a cell is clicked.
Problem:
If A calls B the searchbar is behind the navigationbar (Screenshot B). It appears with the transition from C to B (Screenshot C).
Navigationbar is already translucent. Any ideas?
EDIT
I realized that my animated transitioning is causing my problem.
Perhaps you can spot the error?
class ZoomInCircleViewTransition: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
var transitionContext: UIViewControllerContextTransitioning?
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.6
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
self.transitionContext = transitionContext
guard let toViewController: UIViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
return
}
guard let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else { return
}
guard let fromViewTransitionFromView = fromViewController as? TransitionFromViewProtocol else {
return
}
let imageViewSnapshot = fromViewTransitionFromView.getViewForTransition()
let endFrame = CGRectMake(-CGRectGetWidth(toViewController.view.frame)/2, -CGRectGetHeight(toViewController.view.frame)/2, CGRectGetWidth(toViewController.view.frame)*2, CGRectGetHeight(toViewController.view.frame)*2)
if let containerView = transitionContext.containerView(){
containerView.addSubview(fromViewController.view)
containerView.addSubview(toViewController.view)
containerView.addSubview(imageViewSnapshot)
}
let maskPath = UIBezierPath(ovalInRect: imageViewSnapshot.frame)
let maskLayer = CAShapeLayer()
maskLayer.frame = toViewController.view.frame
maskLayer.path = maskPath.CGPath
toViewController.view.layer.mask = maskLayer
let quadraticEndFrame = CGRect(x: endFrame.origin.x - (endFrame.height - endFrame.width)/2, y: endFrame.origin.y, width: endFrame.height, height: endFrame.height)
let bigCirclePath = UIBezierPath(ovalInRect: quadraticEndFrame)
let pathAnimation = CABasicAnimation(keyPath: "path")
pathAnimation.delegate = self
pathAnimation.fromValue = maskPath.CGPath
pathAnimation.toValue = bigCirclePath
pathAnimation.duration = transitionDuration(transitionContext)
maskLayer.path = bigCirclePath.CGPath
maskLayer.addAnimation(pathAnimation, forKey: "pathAnimation")
let hideImageViewAnimation = {
imageViewSnapshot.alpha = 0.0
}
UIView.animateWithDuration(0.2, delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: hideImageViewAnimation) { (completed) -> Void in
}
let scaleImageViewAnimation = {
imageViewSnapshot.frame = quadraticEndFrame
}
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, options: UIViewAnimationOptions.CurveLinear, animations: scaleImageViewAnimation) { (completed) -> Void in
// After the complete animations have endet
imageViewSnapshot.removeFromSuperview()
toViewController.view.layer.mask = nil
}
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let transitionContext = self.transitionContext {
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
I believe I was running in to some similar problems in my app, but running in to difficulties because of all the things you don't have control over. My solution was to put a search icon in the navigationbar, then have the search controller slide down over the navigationbar, keeping it out of the table/scroll view. Here is my implementation (should be complete)
import UIKit
class tvc: UITableViewController, UISearchBarDelegate, UISearchControllerDelegate {
var searchController:UISearchController!
#IBAction func startSearch() {
self.navigationController?.presentViewController(self.searchController, animated: true, completion: {})
}
override func viewDidDisappear(animated: Bool) {
cancelSearch(self)
}
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.delegate = self
searchController.hidesNavigationBarDuringPresentation = false
searchController.loadViewIfNeeded() /* Fixes bug in iOS http://stackoverflow.com/questions/32675001/uisearchcontroller-warning-attempting-to-load-the-view-of-a-view-controller */
definesPresentationContext = true
tableView.sectionIndexBackgroundColor = UIColor.clearColor()
tableView.sectionIndexTrackingBackgroundColor = UIColor.clearColor()
}
extension tvc: UISearchResultsUpdating {
// MARK: - UISearchResultsUpdating Delegate
func updateSearchResultsForSearchController(searchController: UISearchController) {
filterContentForSearchText(searchController.searchBar.text!)
}
}
func cancelSearch(sender: AnyObject?) {
if sender!.searchController.active == true {
sender?.searchController.resignFirstResponder()
sender!.navigationController!!.dismissViewControllerAnimated(false, completion: {})
sender!.searchController.searchBar.text = ""
sender!.searchController.active = false
}
}
I think problem is your are either you are not setting frame for imageViewSnapshot or setting wrong frame. As imageViewSnapshot includes the search bar, your have to set the frame such are it goes behind the navigation bar. or imageViewSnapshot should contain only visible area of the fromViewTransitionFromView.

Resources