簡體   English   中英

調用文件選擇器時,帶有新 iOS13 模式的 WKWebview 崩潰

[英]WKWebview with the new iOS13 modal crash when a file picker is invoked

我在 iOS13 上的模態視圖 controller 中有一個 webview。 當用戶嘗試將圖像上傳到 webview 時,它會崩潰。

這是我得到的例外:

2019-09-30 17:50:10.676940+0900 Engage[988:157733] * 由於未捕獲的異常“NSGenericException”而終止應用程序,原因:“您的應用程序已呈現 UIDocumentMenuViewController ()。 在其當前的 trait 環境中,具有此樣式的 UIDocumentMenuViewController 的 modalPresentationStyle 是 UIModalPresentationPopover。 您必須通過視圖控制器的 popoverPresentationController 提供此彈出框的位置信息。 您必須提供 sourceView 和 sourceRect 或 barButtonItem。 如果在您呈現視圖 controller 時不知道此信息,您可以在 UIPopoverPresentationControllerDelegate 方法 -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填寫了錯誤報告

正如@jshapy8 正確指出的那樣,您需要覆蓋present()方法並手動設置.sourceView / .sourceFrame / .barButtonItem But you need to keep in mind that in case the UIViewController that holds the WkWebView is presented by a UINavigationController , the UINavigationController is responsible for presenting other UIViewController .

除非您使用的是 iPad。

因此,實際上您需要覆蓋UINavigationController以及包含WkWebViewUIViewController中的present()方法。

在下面的示例中,保存UIViewControllerWkWebView稱為WebVC

在您的UINavigationController ,您需要添加:

  override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
    if let webVC = viewControllers.filter({ $0 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 上的問題,而不是 iPad 上的問題(剛剛在 iPad 上用 iOS 12 和 13 進行了測試。看起來蘋果工程師只是忘記了這一點,以防萬一WKWebView以其新的模態演示樣式呈現, UIDocumentMenuViewControllerUIModalPresentationPopover樣式呈現,即使在手機上也是如此,直到 iOS 13 根本不是這種情況。

我更新了我的代碼,所以現在它只為手機類型設置.sourceView / .sourceFrame / .barButtonItem ,因為平板電腦類型將由 iOS 它自己正確處理。

我也遇到過類似的崩潰。

您可以通過將 modalPresentationStyle 設置為.fullScreen來修復它。

細節

  • Swift 5.1,Xcode 11.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)
    }
}

用法

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

對於導航控制器

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

完整樣本

不要忘記在此處粘貼解決方案代碼

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

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

結果

在此處輸入圖像描述

在此處輸入圖像描述

正如錯誤消息所述,UIKit 決定UIDocumentMenuController應該是一個彈出框,因此它希望設置該彈出框的sourceViewsourceFramebarButtonItem (崩潰正是因為尚未設置)。

如果您想保持 iOS 13 模態演示樣式而不必恢復為僅使用.fullScreen ,則在嵌入WKWebView的視圖 controller 中,您可以覆蓋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)
    }
}

我在這里找到了解決方案: https://medium.com/swlh/popover-menu-over-cards- contains-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
    }

然后你必須設置你的popover的position。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM