Как предотвратить увеличение UIScrollView на тонну при активации контекстного меню iOS 13?

Если у тебя есть UIScrollView что вы можете увеличить, и вы добавляете взаимодействие с контекстным меню iOS 13 в представление внутри представления прокрутки (например: UIImageView), когда вы выполняете взаимодействие, он на мгновение странным образом увеличивает изображение, затем уменьшает его, чтобы отобразить контекстное меню, а при выходе из этого контекстного меню он оставляет изображение очень сильно увеличенным. Кажется, что он выходит за пределы UIImageView.

Stackru, похоже, не поддерживает встраивание видео /GIF, поэтому вот видео об этом на Imgur, показывающее, что я имею в виду: https://imgur.com/mAzWlJA

Есть ли способ предотвратить такое поведение? ВWKWebViewUIScrollView подкласс), например, долгое нажатие на изображение не демонстрирует этого поведения.

Вот простой код, чтобы показать его образец, если вы хотите протестировать его в простом новом проекте Xcode:

import UIKit

class RootViewController: UIViewController, UIScrollViewDelegate, UIContextMenuInteractionDelegate {
    let scrollView = UIScrollView()
    let imageView = UIImageView(image: UIImage(named: "cat.jpg")!)

    override func viewDidLoad() {
        super.viewDidLoad()

        [view, scrollView].forEach { $0.backgroundColor = .black }

        scrollView.delegate = self
        scrollView.frame = view.bounds
        scrollView.addSubview(imageView)
        scrollView.contentSize = imageView.frame.size
        view.addSubview(scrollView)

        // Set zoom scale
        let scaleToFit = min(scrollView.bounds.width / imageView.bounds.width, scrollView.bounds.height / imageView.bounds.height)
        scrollView.maximumZoomScale = max(1.0, scaleToFit)
        scrollView.minimumZoomScale = scaleToFit < 1.0 ? scaleToFit : 1.0
        scrollView.zoomScale = scaleToFit

        // Add context menu support
        imageView.isUserInteractionEnabled = true
        imageView.addInteraction(UIContextMenuInteraction(delegate: self))
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        scrollView.frame = view.bounds
    }

    // MARK: - UIScrollView

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }

    // MARK: - Context Menus

    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
            return nil
        }) { (suggestedElements) -> UIMenu? in
            var children: [UIAction] = []

            children.append(UIAction(title: "Upvote", image: UIImage(systemName: "arrow.up")) { (action) in
            })

            children.append(UIAction(title: "Downvote", image: UIImage(systemName: "arrow.down")) { (action) in
            })

            return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children)
        }
    }
}

А вот cat.jpgесли вы тоже этого хотите: https://imgur.com/hTTZaw4

1 ответ

Решение

Think I solved it. The gist of the solution is to not add the interaction to the image view itself as you would intuitively think to, but add it to an outer view and then focus the context menu preview onto the rect of the image view using the UITargetPreview APIs. This way you all together avoid touching the image view that bugs out, and go to its parent instead and just "crop in" to the subview, which keeps the subview happy.:)

Here's the code I ended up with:

import UIKit

class RootViewController: UIViewController, UIScrollViewDelegate, UIContextMenuInteractionDelegate {
    let wrapperView = UIView()
    let scrollView = UIScrollView()
    let imageView = UIImageView(image: UIImage(named: "cat.jpg")!)

    override func viewDidLoad() {
        super.viewDidLoad()

        wrapperView.frame = view.bounds
        view.addSubview(wrapperView)

        [view, wrapperView, scrollView].forEach { $0.backgroundColor = .black }

        scrollView.delegate = self
        scrollView.frame = view.bounds
        scrollView.addSubview(imageView)
        scrollView.contentSize = imageView.frame.size
        wrapperView.addSubview(scrollView)

        // Set zoom scale
        let scaleToFit = min(scrollView.bounds.width / imageView.bounds.width, scrollView.bounds.height / imageView.bounds.height)
        scrollView.maximumZoomScale = max(1.0, scaleToFit)
        scrollView.minimumZoomScale = scaleToFit < 1.0 ? scaleToFit : 1.0
        scrollView.zoomScale = scaleToFit

        // Add context menu support
        wrapperView.addInteraction(UIContextMenuInteraction(delegate: self))
    }

    // MARK: - UIScrollView

    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return imageView
    }

    // MARK: - Context Menus

    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        scrollView.zoomScale = scrollView.minimumZoomScale

        return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
            return nil
        }) { (suggestedElements) -> UIMenu? in
            var children: [UIAction] = []

            children.append(UIAction(title: "Upvote", image: UIImage(systemName: "arrow.up")) { (action) in
            })

            children.append(UIAction(title: "Downvote", image: UIImage(systemName: "arrow.down")) { (action) in
            })

            return UIMenu(title: "", image: nil, identifier: nil, options: [], children: children)
        }
    }

    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
        let parameters = UIPreviewParameters()

        let rect = imageView.convert(imageView.bounds, to: wrapperView)
        parameters.visiblePath = UIBezierPath(roundedRect: rect, cornerRadius: 13.0)

        return UITargetedPreview(view: wrapperView, parameters: parameters)
    }

    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
        let parameters = UIPreviewParameters()

        let rect = imageView.convert(imageView.bounds, to: wrapperView)
        parameters.visiblePath = UIBezierPath(roundedRect: rect, cornerRadius: 0.0)

        return UITargetedPreview(view: wrapperView, parameters: parameters)
    }
}

Some notes:

  • This unfortunately doesn't work well (without one change I made) when the view is zoomed in. For whatever reason iOS still tries to mess with the scroll view and this time zooms it out, but doesn't render the area around it, causing there to be large white areas around an incomplete image view. Sigh. At this point I'm kind of done with this, you could probably try to fight it internally with some UIScrollView subclass that attempts to rebuke the iOS level changes, but yeah I've spent about as much time on this as I'd like to, so I'm just resetting the scrollView's zoomScale to be completely zoomed out once it asks for the context menu (note you have to do it here, in the willPresent context menu APIs it's too late). It's not that bad and solves it completely, just resets the user's zoom level somewhat annoyingly. But if I get a support email I'll just link them to this post.
  • Corner radius of 13.0 matches the iOS default one. Only catch is that this doesn't animate the corner radius from 0 to the rounded corner radius like the iOS one does, it kinda jumps, but it's barely noticeable. I'm sure there's a way to fix this, the headers for the context menu APIs mention animations in some capacity, but the documentation is really lacking and I don't want to spend a ton of time trying to figure out how.
  • In this example I use wrapperView inside the view controllers view. This is probably specific to my use case and might not be necessary in yours. Essentially you could attach it to the scrollView itself, but mine has some custom insetting to keep it always centered within notched iPhones with regard to safe area insets, and if I use the scroll view for the interaction/targeted preview it jumps it around a bit, which doesn't look great. You also don't want to use the view controller's view directly as the interaction, as it masks it off when doing the animation, so the black background of the media viewer/scroll view disappears completely, which doesn't look great. So a wrapper view at the top level prevents both of these nicely.
Другие вопросы по тегам