具有模糊背景的模态导航控制器,在推送动画期间无缝?

Modal Navigation Controller with Blurred Background, Seamless During Push Animation?

我以模态方式呈现导航控制器,以 table 视图控制器作为根,并在用户点击单元格时推送更多视图控制器。现在,我想对整个事物应用 blur/vibrancy 效果。

情节提要如下所示:

现在,想要对整个导航控制器应用模糊效果,以便可以在下方看到初始视图控制器中的图像。

通过对 table 视图控制器和详细视图控制器应用相同的模糊效果,我取得了一些成功,如下所示:

Table 视图控制器

class TableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.tableFooterView = UIView(frame: .zero)

        tableView.backgroundColor = UIColor.clear
        let blurEffect = UIBlurEffect(style: .dark)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        tableView.backgroundView = blurEffectView
        tableView.separatorEffect = UIVibrancyEffect(blurEffect: blurEffect)
    }
}

详细视图控制器

class DetailViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .clear
        let blurEffect = UIBlurEffect(style: .dark)
        let blurView = UIVisualEffectView(effect: blurEffect)
        blurView.translatesAutoresizingMaskIntoConstraints = false
        view.insertSubview(blurView, at: 0)
        NSLayoutConstraint.activate([
            blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
            blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
        ])
    }
}

(我也把导航的模态呈现样式设置为.overFullScreen).

但是,在导航过渡到详细视图控制器的最后,在模糊的背景中可以看到一个烦人的工件 :

我觉得这跟iOS7之后pushed view controller不能有透明背景有关,否则pushing view controller会被看到,因为它会在结束时立即被移除过渡。

我尝试将模糊效果应用于导航控制器,并将两个子视图改为透明,但效果也不佳。

无缝地将背景 blur/vibrancy 效果应用到导航控制器的内容的正确方法是什么?

尝试以下步骤。我没试过,但应该可以。

  1. 将window背景图片添加到didFinishLaunchingWithOptions中,默认隐藏。
  2. 将相同的图像添加到呈现模态视图控制器中。
  3. 确保清除视图控制器的背景。
  4. 因此,当您呈现时,它会显示模态视图控制器中的图像。
  5. 完成后取消隐藏 window 图像并隐藏视图控制器图像。
  6. 当您点击后退按钮关闭视图时,首先取消隐藏您的视图控制器图像并隐藏您的 window 图像。所以你的动画看起来很正常。

如果您需要任何帮助,请告诉我。

正如我在问题中发现的那样,自 iOS 7 以来引入的 push/pop 动画过渡(其中导航下方的视图控制器堆栈 "slides out at half the speed",以便它在 pushed/popped) 的下方重叠 使得无法在没有工件的情况下在透明视图控制器之间进行转换。

所以我决定采用协议UINavigationControllerDelegate,更准确地说是方法:navigationController(_:animationControllerFor:from:to:)基本上从头开始复制 UINavigationController 的动画过渡。

这让我可以添加一个 mask view 到在推送操作期间转换离开的视图,以便剪掉重叠部分,并且不会出现可见的伪影。

目前我只实现了push操作(最后还要做pop操作,还有交互过渡form swipe gesture-based pop), 我做了一个自定义的 UINavigationController 子类 put it on GitHub

(目前,它支持动画推送和弹出,但不支持交互。此外,我还没有想出如何复制导航栏标题的 "slide" 动画 - 它只是交叉溶解.)

归结为以下步骤:

  1. 最初,将 .to 视图向右变换,off-screen。在动画期间,逐渐将其转换回身份(屏幕中心)。
  2. white UIView 设置为 mask.from 视图,最初的边界等于 .from 视图(无遮罩) .在动画过程中,逐渐将遮罩的 frame 属性 的宽度减小到其初始值的一半。
  3. 在动画播放期间,逐渐将 from 视图从屏幕中移到左侧。

在代码中:

class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {

    // Super slow for debugging:
    let duration: TimeInterval = 1

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.view(forKey: .from) else {
            return
        }
        guard let toView = transitionContext.view(forKey: .to) else {
            return
        }
        guard let toViewController = transitionContext.viewController(forKey: .to) else {
            return
        }

        //
        // (Code below assumes navigation PUSH operation. For POP, 
        //  use similar code but with view roles and direction 
        //  reversed)
        //

        // Add target view to container:
        transitionContext.containerView.addSubview(toView)

        // Set tagret view frame, centered on screen
        let toViewFinalFrame = transitionContext.finalFrame(for: toViewController)
        toView.frame = toViewFinalFrame
        let containerBounds = transitionContext.containerView.bounds
        toView.center = CGPoint(x: containerBounds.midX, y: containerBounds.midY)

        // But translate it to fully the RIGHT, for now:
        toView.transform = CGAffineTransform(translationX: containerBounds.width, y: 0)

        // We will slide the source view out, to the LEFT, by half as much:
        let fromTransform = CGAffineTransform(translationX: -0.5*containerBounds.width, y: 0)

        // Apply a white UIView as mask to the source view:
        let maskView = UIView(frame: CGRect(origin: .zero, size: fromView.frame.size))
        maskView.backgroundColor = .white
        fromView.mask = maskView

        // The mask will shrink to half the width during the animation:
        let maskViewNewFrame = CGRect(origin: .zero, size: CGSize(width: 0.5*fromView.frame.width, height: fromView.frame.height))

        // Animate:
        UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut], animations: {

            fromView.transform = fromTransform // off-screen to the left (half)

            toView.transform = .identity // Back on screen, fully centered

            maskView.frame = maskViewNewFrame // Mask to half width

        }, completion: {(completed) in

            // Remove mask, or funny things will happen to a 
            // table view or scroll view if the user 
            // "rubber-bands":
            fromView.mask = nil

            transitionContext.completeTransition(completed)
        })
    }

结果:

(为了清晰起见,我在细节视图控制器中添加了一个文本视图)