iOS - 点击视图外面以解除呈现的视图控制器

6
我有一个CustomPresentationController,它使用自定义动画进行进出动画;
这个特定的控制器被呈现时,大小大约为屏幕的50%,当我呈现它时,我会在presentingViewController中添加一个阴影灰色视图,以增加一些深度。
只有在我点击NavBar中的cancel按钮时,才能解除呈现的presentedViewController,我调用默认的dismiss(:)方法。
我想要实现的是检测是否在presentedViewController之外点击了tap,也许在灰色区域内,以便我可以解除呈现的presentedViewController,就像解除ActionSheet一样,但我尝试过了。让我解释一下我到目前为止尝试过的内容。
我尝试向阴影灰色视图添加UITapGestureRecognizer,但由于我正在呈现不同的控制器,应用程序引擎可能会认为由于阴影视图不在顶级层次结构视图上,因此可能无法访问它,因此会“阻止”识别器-每当我点击它时,手势处理程序都不会触发。

我现在正在实现下滑关闭功能,这很容易实现,但我真的希望点击外部区域也能起到关闭功能。

你有什么建议吗?

应用程序的图像如下:

screenshot


不必添加一个不可见的按钮来覆盖外部视图,有一种更优雅的方法可以实现:https://dev59.com/j18e5IYBdhLWcg3wja1I#25846330 - Neimsz
5个回答

7

我的解决方案:

在展示视图控制器(也称为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还需实现转场代理:

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
        
    }
    
}

然后,实现演示控制器(也称为PartialSizePresentController),使其还可以处理轻击手势:

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)
    }
}

完美。解释得非常清楚。 - Shikhil

3

我刚刚需要在我的一个应用程序中实现这个功能。

我通过添加一个覆盖整个视图的按钮来使其工作,当点击此按钮时,触发VC被关闭。

添加按钮后,您可以在其上方添加自定义视图。

目前看起来它运行得相当不错。

以下是我的代码(我一切都是以编程方式完成的,没有使用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) 
   } 

如果您正在使用故事板,请确保将不可见按钮放置在自定义视图下方。

希望这可以帮到您。


3

请使用我提供的代码:

您需要在弹出窗口所在的控制器中实现此方法。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
   // Here write down you logic to dismiss controller     
}

希望这个能够起作用。 :D

它没有起作用。 由于我的弹出视图是UICollectionView类型,我必须设置collectionView?.isUserInteractionEnabled = false来检测其中的触摸,但即使它检测到触摸,它也无法检测到触摸是否在阴影中。如果我触摸阴影区域,它不会触发touches(:)方法。 - Ivan Cantarino
这样是行不通的,我和 OP 遇到了同样的问题,棘手的事情是我们使用的是 viewController 而不是 view,touchesBegan 无法检测到当前 VC 外的触摸。 - Zhou Haibo

1
你使用UITapGestureRecognizer是正确的。只需确保按照以下方式实现shouldRecognizeSimultaneouslyWith:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
    return true
}

这应该可以使手势正确触发。

嗯...我从未使用过那个属性,这可能就是为什么我无法弄清楚的原因 :D 我会尝试一下并给你反馈。 - Ivan Cantarino
它没有起作用。以某种方式设置该属性,手势识别器从未触发。 它可能认为该视图对用户不可用,因为它不在顶部视图层次结构中。 应该有一种方法可以独立于呈现的视图在屏幕上的任何位置检测触摸,然后我会检查触摸位置是否在呈现的弹出窗口上方,然后我就知道何时关闭。 - Ivan Cantarino

1

// 这对我有效

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)
    }
}

这个方法不起作用,touchesBegan 只会检测当前视图控制器内的触摸事件,而且 OP 明确表示它是一个视图控制器而不是一个视图。 - Zhou Haibo
你能再解释一下吗?这样我就可以帮助你了。 - keshav
当然,请查看我的问题 - Zhou Haibo
你想要关闭底部视图吗? - keshav

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接