调用文件选择器时,WKWebview 与新的 iOS13 模式崩溃
WKWebview with the new iOS13 modal crash when a file picker is invoked
我在 iOS13 上的模式视图控制器中有一个 webview。
当用户尝试将图像上传到 webview 时,它崩溃了。
这是我遇到的异常:
2019-09-30 17:50:10.676940+0900 Engage[988:157733] * Terminating app
due to uncaught exception 'NSGenericException', reason: 'Your
application has presented a UIDocumentMenuViewController
(). In its current trait
environment, the modalPresentationStyle of a
UIDocumentMenuViewController with this style is
UIModalPresentationPopover. You must provide location information for
this popover through the view controller's
popoverPresentationController. You must provide either a sourceView
and sourceRect or a barButtonItem. If this information is not known
when you present the view controller, you may provide it in the
UIPopoverPresentationControllerDelegate method
-prepareForPopoverPresentation.'
* First throw call stack: (0x18926c98c 0x188f950a4 0x18cb898a8 0x18cb939b4 0x18cb914f8 0x18d283b98 0x18d2737c0 0x18d2a3594
0x1891e9c48 0x1891e4b34 0x1891e5100 0x1891e48bc 0x193050328
0x18d27a6d4 0x1002e6de4 0x18906f460) libc++abi.dylib: terminating with
uncaught exception of type NSException
我不确定我可以在哪里设置这个委托...
我做了一个示例项目:https://github.com/ntnmrndn/WKUploadFormCrash
并向 Apple 提交错误报告
我也遇到过类似的崩溃。
您可以通过将 modalPresentationStyle 设置为 .fullScreen
来修复它。
如错误消息所述,UIKit 决定 UIDocumentMenuController
应该是弹出窗口,因此它期望 sourceView
和 sourceFrame
或 barButtonItem
要设置的弹出窗口(崩溃正是因为那些还没有被设置)。
如果你想保留 iOS 13 模态呈现风格而不必恢复到仅使用 .fullScreen
,在嵌入 WKWebView
的视图控制器中你可以重写 present
获取对 UIDocumentMenuViewController
的引用并在其 popoverPresentationController
上设置属性:
class WebViewController: UIViewController {
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if #available(iOS 13, *), viewControllerToPresent is UIDocumentMenuViewController {
viewControllerToPresent.popoverPresentationController?.barButtonItem = // reference to a bar button item you want to present the popover from
// OR
viewControllerToPresent.popoverPresentationController?.sourceView = // whatever view you want to present the popover from
viewControllerToPresent.popoverPresentationController?.sourceFrame = // that view's frame
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
正如@jshapy8 正确指出的那样,您需要覆盖 present()
方法并手动设置 .sourceView
/.sourceFrame
/.barButtonItem
。
但是你需要记住,如果持有 WkWebView
的 UIViewController
由 UINavigationController
呈现,则 UINavigationController
负责呈现其他 UIViewController
.
除非你在 iPad.
所以实际上你需要重写 UINavigationController
中的 present()
方法以及包含 WkWebView
.
的 UIViewController
中的方法
在下面的示例中,包含 WkWebView
的 UIViewController
称为 WebVC
。
在您的 UINavigationController
中您需要添加:
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let webVC = viewControllers.filter({ [=10=] is WebVC }).first as? WebVC {
webVC.setUIDocumentMenuViewControllerSoureViewsIfNeeded(viewControllerToPresent)
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
并且在您的 WebVC
中您需要添加:
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
setUIDocumentMenuViewControllerSoureViewsIfNeeded(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
func setUIDocumentMenuViewControllerSoureViewsIfNeeded(_ viewControllerToPresent: UIViewController) {
if #available(iOS 13, *), viewControllerToPresent is UIDocumentMenuViewController && UIDevice.current.userInterfaceIdiom == .phone {
// Prevent the app from crashing if the WKWebView decides to present a UIDocumentMenuViewController while it self is presented modally.
viewControllerToPresent.popoverPresentationController?.sourceView = webView
viewControllerToPresent.popoverPresentationController?.sourceRect = CGRect(x: webView.center.x, y: webView.center.y, width: 1, height: 1)
}
}
因此您可以使用新的 iOS 13 模式呈现样式并上传文件而不会崩溃
编辑:
这种崩溃行为似乎是(另一个)iOS 13 错误,因为这只是 iPhone 上的问题,而不是 iPads(刚刚在 iPads 上用 iOS 测试过12 和 13。
看起来苹果工程师只是忘记了如果 WKWebView
以他们新的模式呈现样式呈现,UIDocumentMenuViewController
以 UIModalPresentationPopover
样式呈现,即使在 phone s,直到 iOS 13 根本不是这样。
我更新了我的代码,现在它只为 phone 类型设置 .sourceView
/.sourceFrame
/.barButtonItem
,因为平板电脑类型将由 iOS 它自己正确。
我在这里找到了解决方案:https://medium.com/swlh/popover-menu-over-cards-containing-webkit-views-on-ios-13-a16705aff8af。感谢作者
我只是添加了这两个函数来避免崩溃。
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if #available(iOS 13, *), viewControllerToPresent is UIDocumentMenuViewController {
viewControllerToPresent.popoverPresentationController?.delegate = self
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) {
popoverPresentationController.sourceView = self.view
}
然后你必须设置弹出窗口的位置。
详情
- Swift5.1,Xcode11.2.1
解决方案
import UIKit
import WebKit
protocol WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { get }
}
extension WebViewTapGestureRecognizable where Self: UIViewController {
func prepareForPresent(_ viewControllerToPresent: UIViewController) {
if viewControllerToPresent is UIDocumentMenuViewController || viewControllerToPresent is UIDocumentPickerViewController,
UIDevice.current.userInterfaceIdiom == .phone,
let popoverPresentationController = viewControllerToPresent.popoverPresentationController {
popoverPresentationController.sourceView = view
let origin = (self as WebViewTapGestureRecognizable).lastWebViewTapPosition
popoverPresentationController.sourceRect = CGRect(origin: origin, size: CGSize(width: 1, height: 1))
}
}
}
class WebView: WKWebView {
private(set) var lastWebViewTapPosition: CGPoint = CGPoint(x: 0, y: 0)
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
setupGestureRecognizer()
}
required init?(coder: NSCoder) { super.init(coder: coder) }
}
extension WebView: WebViewTapGestureRecognizable, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc func webViewTapped(_ sender: UITapGestureRecognizer) {
lastWebViewTapPosition = sender.location(in: superview ?? self)
}
private func setupGestureRecognizer() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(webViewTapped(_:)) )
tapGesture.delegate = self
addGestureRecognizer(tapGesture)
}
}
用法
For UIViewController
import UIKit
class ViewController: UIViewController {
private weak var webView: WebView!
}
extension ViewController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return webView.lastWebViewTapPosition }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
For NavigationController
import UIKit
class NavigationController: UINavigationController {}
extension NavigationController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return (visibleViewController as? WebViewTapGestureRecognizable)?.lastWebViewTapPosition ?? .zero }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
完整样本
Do not forget to paste here the solution code
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let stackView = UIStackView()
stackView.axis = .vertical
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
var button = UIButton()
button.setTitle("Present VC", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(buttonTouchedUpInside1), for: .touchUpInside)
stackView.addArrangedSubview(button)
button = UIButton()
button.setTitle("Present NavVC", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(buttonTouchedUpInside2), for: .touchUpInside)
stackView.addArrangedSubview(button)
}
@objc func buttonTouchedUpInside1() {
present(ViewController(), animated: true, completion: nil)
}
@objc func buttonTouchedUpInside2() {
present(NavigationController(rootViewController: ViewController()), animated: true, completion: nil)
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class ViewController: UIViewController {
private weak var webView: WebView!
override func viewDidLoad() {
super.viewDidLoad()
createWebView()
}
private func createWebView() {
let webView = WebView(frame: .zero,
configuration: WKWebViewConfiguration())
view.addSubview(webView)
self.webView = webView
//webView.navigationDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: webView.bottomAnchor).isActive = true
view.rightAnchor.constraint(equalTo: webView.rightAnchor).isActive = true
let urlString = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_type_file"
webView.load(URLRequest(url: URL(string: urlString)!))
}
}
extension ViewController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return webView.lastWebViewTapPosition }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
import UIKit
class NavigationController: UINavigationController {}
extension NavigationController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return (visibleViewController as? WebViewTapGestureRecognizable)?.lastWebViewTapPosition ?? .zero }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
结果
我在 iOS13 上的模式视图控制器中有一个 webview。 当用户尝试将图像上传到 webview 时,它崩溃了。
这是我遇到的异常:
2019-09-30 17:50:10.676940+0900 Engage[988:157733] * Terminating app due to uncaught exception 'NSGenericException', reason: 'Your application has presented a UIDocumentMenuViewController (). In its current trait environment, the modalPresentationStyle of a UIDocumentMenuViewController with this style is UIModalPresentationPopover. You must provide location information for this popover through the view controller's popoverPresentationController. You must provide either a sourceView and sourceRect or a barButtonItem. If this information is not known when you present the view controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation.' * First throw call stack: (0x18926c98c 0x188f950a4 0x18cb898a8 0x18cb939b4 0x18cb914f8 0x18d283b98 0x18d2737c0 0x18d2a3594 0x1891e9c48 0x1891e4b34 0x1891e5100 0x1891e48bc 0x193050328 0x18d27a6d4 0x1002e6de4 0x18906f460) libc++abi.dylib: terminating with uncaught exception of type NSException
我不确定我可以在哪里设置这个委托...
我做了一个示例项目:https://github.com/ntnmrndn/WKUploadFormCrash 并向 Apple 提交错误报告
我也遇到过类似的崩溃。
您可以通过将 modalPresentationStyle 设置为 .fullScreen
来修复它。
如错误消息所述,UIKit 决定 UIDocumentMenuController
应该是弹出窗口,因此它期望 sourceView
和 sourceFrame
或 barButtonItem
要设置的弹出窗口(崩溃正是因为那些还没有被设置)。
如果你想保留 iOS 13 模态呈现风格而不必恢复到仅使用 .fullScreen
,在嵌入 WKWebView
的视图控制器中你可以重写 present
获取对 UIDocumentMenuViewController
的引用并在其 popoverPresentationController
上设置属性:
class WebViewController: UIViewController {
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if #available(iOS 13, *), viewControllerToPresent is UIDocumentMenuViewController {
viewControllerToPresent.popoverPresentationController?.barButtonItem = // reference to a bar button item you want to present the popover from
// OR
viewControllerToPresent.popoverPresentationController?.sourceView = // whatever view you want to present the popover from
viewControllerToPresent.popoverPresentationController?.sourceFrame = // that view's frame
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
正如@jshapy8 正确指出的那样,您需要覆盖 present()
方法并手动设置 .sourceView
/.sourceFrame
/.barButtonItem
。
但是你需要记住,如果持有 WkWebView
的 UIViewController
由 UINavigationController
呈现,则 UINavigationController
负责呈现其他 UIViewController
.
除非你在 iPad.
所以实际上你需要重写 UINavigationController
中的 present()
方法以及包含 WkWebView
.
UIViewController
中的方法
在下面的示例中,包含 WkWebView
的 UIViewController
称为 WebVC
。
在您的 UINavigationController
中您需要添加:
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let webVC = viewControllers.filter({ [=10=] is WebVC }).first as? WebVC {
webVC.setUIDocumentMenuViewControllerSoureViewsIfNeeded(viewControllerToPresent)
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
并且在您的 WebVC
中您需要添加:
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
setUIDocumentMenuViewControllerSoureViewsIfNeeded(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
func setUIDocumentMenuViewControllerSoureViewsIfNeeded(_ viewControllerToPresent: UIViewController) {
if #available(iOS 13, *), viewControllerToPresent is UIDocumentMenuViewController && UIDevice.current.userInterfaceIdiom == .phone {
// Prevent the app from crashing if the WKWebView decides to present a UIDocumentMenuViewController while it self is presented modally.
viewControllerToPresent.popoverPresentationController?.sourceView = webView
viewControllerToPresent.popoverPresentationController?.sourceRect = CGRect(x: webView.center.x, y: webView.center.y, width: 1, height: 1)
}
}
因此您可以使用新的 iOS 13 模式呈现样式并上传文件而不会崩溃
编辑:
这种崩溃行为似乎是(另一个)iOS 13 错误,因为这只是 iPhone 上的问题,而不是 iPads(刚刚在 iPads 上用 iOS 测试过12 和 13。
看起来苹果工程师只是忘记了如果 WKWebView
以他们新的模式呈现样式呈现,UIDocumentMenuViewController
以 UIModalPresentationPopover
样式呈现,即使在 phone s,直到 iOS 13 根本不是这样。
我更新了我的代码,现在它只为 phone 类型设置 .sourceView
/.sourceFrame
/.barButtonItem
,因为平板电脑类型将由 iOS 它自己正确。
我在这里找到了解决方案:https://medium.com/swlh/popover-menu-over-cards-containing-webkit-views-on-ios-13-a16705aff8af。感谢作者
我只是添加了这两个函数来避免崩溃。
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if #available(iOS 13, *), viewControllerToPresent is UIDocumentMenuViewController {
viewControllerToPresent.popoverPresentationController?.delegate = self
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
func prepareForPopoverPresentation(_ popoverPresentationController: UIPopoverPresentationController) {
popoverPresentationController.sourceView = self.view
}
然后你必须设置弹出窗口的位置。
详情
- Swift5.1,Xcode11.2.1
解决方案
import UIKit
import WebKit
protocol WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { get }
}
extension WebViewTapGestureRecognizable where Self: UIViewController {
func prepareForPresent(_ viewControllerToPresent: UIViewController) {
if viewControllerToPresent is UIDocumentMenuViewController || viewControllerToPresent is UIDocumentPickerViewController,
UIDevice.current.userInterfaceIdiom == .phone,
let popoverPresentationController = viewControllerToPresent.popoverPresentationController {
popoverPresentationController.sourceView = view
let origin = (self as WebViewTapGestureRecognizable).lastWebViewTapPosition
popoverPresentationController.sourceRect = CGRect(origin: origin, size: CGSize(width: 1, height: 1))
}
}
}
class WebView: WKWebView {
private(set) var lastWebViewTapPosition: CGPoint = CGPoint(x: 0, y: 0)
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
setupGestureRecognizer()
}
required init?(coder: NSCoder) { super.init(coder: coder) }
}
extension WebView: WebViewTapGestureRecognizable, UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
@objc func webViewTapped(_ sender: UITapGestureRecognizer) {
lastWebViewTapPosition = sender.location(in: superview ?? self)
}
private func setupGestureRecognizer() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(webViewTapped(_:)) )
tapGesture.delegate = self
addGestureRecognizer(tapGesture)
}
}
用法
For UIViewController
import UIKit
class ViewController: UIViewController {
private weak var webView: WebView!
}
extension ViewController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return webView.lastWebViewTapPosition }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
For NavigationController
import UIKit
class NavigationController: UINavigationController {}
extension NavigationController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return (visibleViewController as? WebViewTapGestureRecognizable)?.lastWebViewTapPosition ?? .zero }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
完整样本
Do not forget to paste here the solution code
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let stackView = UIStackView()
stackView.axis = .vertical
view.addSubview(stackView)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
var button = UIButton()
button.setTitle("Present VC", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(buttonTouchedUpInside1), for: .touchUpInside)
stackView.addArrangedSubview(button)
button = UIButton()
button.setTitle("Present NavVC", for: .normal)
button.setTitleColor(.blue, for: .normal)
button.addTarget(self, action: #selector(buttonTouchedUpInside2), for: .touchUpInside)
stackView.addArrangedSubview(button)
}
@objc func buttonTouchedUpInside1() {
present(ViewController(), animated: true, completion: nil)
}
@objc func buttonTouchedUpInside2() {
present(NavigationController(rootViewController: ViewController()), animated: true, completion: nil)
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
class ViewController: UIViewController {
private weak var webView: WebView!
override func viewDidLoad() {
super.viewDidLoad()
createWebView()
}
private func createWebView() {
let webView = WebView(frame: .zero,
configuration: WKWebViewConfiguration())
view.addSubview(webView)
self.webView = webView
//webView.navigationDelegate = self
webView.translatesAutoresizingMaskIntoConstraints = false
webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
webView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
view.bottomAnchor.constraint(equalTo: webView.bottomAnchor).isActive = true
view.rightAnchor.constraint(equalTo: webView.rightAnchor).isActive = true
let urlString = "https://www.w3schools.com/tags/tryit.asp?filename=tryhtml5_input_type_file"
webView.load(URLRequest(url: URL(string: urlString)!))
}
}
extension ViewController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return webView.lastWebViewTapPosition }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
import UIKit
class NavigationController: UINavigationController {}
extension NavigationController: WebViewTapGestureRecognizable {
var lastWebViewTapPosition: CGPoint { return (visibleViewController as? WebViewTapGestureRecognizable)?.lastWebViewTapPosition ?? .zero }
override open func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
self.prepareForPresent(viewControllerToPresent)
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
}