如何防止 UIScrollView 在激活 iOS 13 上下文菜单时放大一吨?
How do I prevent UIScrollView from zooming in a ton when activating an iOS 13 context menu?
如果你有一个可以放大的UIScrollView
,并且你添加了一个iOS 13上下文菜单交互到滚动视图内的视图(例如:UIImageView
) ,当您执行交互时,它会奇怪地暂时放大图像,然后将其缩小以显示上下文菜单,然后在退出此上下文菜单时,它会使图像放大得非常远。它似乎超出了 UIImageView 的范围。
Whosebug 似乎不支持嵌入 videos/GIFs,所以这是 Imgur 上的一段视频,展示了我的意思:https://imgur.com/mAzWlJA
有没有办法防止这种行为?例如,在 WKWebView
(UIScrollView
子类)中,长按图像不会出现此行为。
如果你想在一个简单的新 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 { [=11=].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
我想我已经解决了。解决方案的要点是 不是 将交互添加到图像视图本身,就像您直观地认为的那样,而是将其添加到外部视图,然后将上下文菜单预览聚焦到矩形上使用 UITargetPreview
API 的图像视图。这样你们就可以一起避免触摸出现错误的图像视图,而是转到它的 parent 而只是 "crop in" 到子视图,这使子视图保持快乐。 :)
这是我最终得到的代码:
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 { [=10=].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)
}
}
一些注意事项:
- 不幸的是,当视图放大时,这不能很好地工作(我没有做任何更改)。无论出于何种原因,iOS 仍然试图弄乱滚动视图,这次将其缩小,但是不渲染它周围的区域,导致不完整的图像视图周围有大片白色区域。叹。在这一点上我已经完成了这个,你可能会尝试在内部与一些试图反驳 iOS 级别变化的
UIScrollView
子类进行斗争,但是是的,我已经花了差不多正如我所愿,所以我只是将 scrollView 的 zoomScale 重置为完全缩小,一旦它要求上下文菜单(注意你必须在这里,在 willPresent
上下文菜单 API 中这样做太晚了)。它并没有那么糟糕并且完全解决了它,只是有点烦人地重置了用户的缩放级别。但是,如果我收到一封支持电子邮件,我将 link 他们发送到此 post.
- 13.0 的角半径与 iOS 默认值匹配。唯一要注意的是,这不会像 iOS 那样将角半径从 0 动画化到圆角半径,它有点跳跃,但几乎不会引起注意。我确定有办法解决这个问题,上下文菜单 API 的 headers 在某种程度上提到了动画,但是文档确实很缺乏,我不想花很多时间去弄清楚怎么样。
- 在这个例子中,我在视图控制器视图中使用
wrapperView
。这可能特定于我的用例,在您的用例中可能没有必要。本质上,您可以将它附加到 scrollView
本身,但我的有一些自定义插入,以使其始终在安全区域插入方面居中,如果我使用滚动视图进行 interaction/targeted 预览它跳了一点,看起来不太好。您也不想直接使用视图控制器的视图作为交互,因为它在执行动画时将其屏蔽掉,因此媒体 viewer/scroll 视图的黑色背景完全消失,这看起来不太好。因此,顶层的包装器视图可以很好地防止这两种情况。
如果你有一个可以放大的UIScrollView
,并且你添加了一个iOS 13上下文菜单交互到滚动视图内的视图(例如:UIImageView
) ,当您执行交互时,它会奇怪地暂时放大图像,然后将其缩小以显示上下文菜单,然后在退出此上下文菜单时,它会使图像放大得非常远。它似乎超出了 UIImageView 的范围。
Whosebug 似乎不支持嵌入 videos/GIFs,所以这是 Imgur 上的一段视频,展示了我的意思:https://imgur.com/mAzWlJA
有没有办法防止这种行为?例如,在 WKWebView
(UIScrollView
子类)中,长按图像不会出现此行为。
如果你想在一个简单的新 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 { [=11=].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
我想我已经解决了。解决方案的要点是 不是 将交互添加到图像视图本身,就像您直观地认为的那样,而是将其添加到外部视图,然后将上下文菜单预览聚焦到矩形上使用 UITargetPreview
API 的图像视图。这样你们就可以一起避免触摸出现错误的图像视图,而是转到它的 parent 而只是 "crop in" 到子视图,这使子视图保持快乐。 :)
这是我最终得到的代码:
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 { [=10=].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)
}
}
一些注意事项:
- 不幸的是,当视图放大时,这不能很好地工作(我没有做任何更改)。无论出于何种原因,iOS 仍然试图弄乱滚动视图,这次将其缩小,但是不渲染它周围的区域,导致不完整的图像视图周围有大片白色区域。叹。在这一点上我已经完成了这个,你可能会尝试在内部与一些试图反驳 iOS 级别变化的
UIScrollView
子类进行斗争,但是是的,我已经花了差不多正如我所愿,所以我只是将 scrollView 的 zoomScale 重置为完全缩小,一旦它要求上下文菜单(注意你必须在这里,在willPresent
上下文菜单 API 中这样做太晚了)。它并没有那么糟糕并且完全解决了它,只是有点烦人地重置了用户的缩放级别。但是,如果我收到一封支持电子邮件,我将 link 他们发送到此 post. - 13.0 的角半径与 iOS 默认值匹配。唯一要注意的是,这不会像 iOS 那样将角半径从 0 动画化到圆角半径,它有点跳跃,但几乎不会引起注意。我确定有办法解决这个问题,上下文菜单 API 的 headers 在某种程度上提到了动画,但是文档确实很缺乏,我不想花很多时间去弄清楚怎么样。
- 在这个例子中,我在视图控制器视图中使用
wrapperView
。这可能特定于我的用例,在您的用例中可能没有必要。本质上,您可以将它附加到scrollView
本身,但我的有一些自定义插入,以使其始终在安全区域插入方面居中,如果我使用滚动视图进行 interaction/targeted 预览它跳了一点,看起来不太好。您也不想直接使用视图控制器的视图作为交互,因为它在执行动画时将其屏蔽掉,因此媒体 viewer/scroll 视图的黑色背景完全消失,这看起来不太好。因此,顶层的包装器视图可以很好地防止这两种情况。