Загрузка файлов в MAUI iOS WebView

У меня есть WebView в приложении MAUI, которое обычно работает, но всякий раз, когда я нажимаю ссылку на iOS, которая должна загрузить файл (ссылка возвращает заголовок Content-Disposition), она открывается в WebView. Вместо этого я бы хотел, чтобы его загрузили (и открыли в приложении iOS по умолчанию).

Как это должно быть реализовано? По-видимому, существуетIWKDownloadDelegateинтерфейс сDecideDestination()метод, но я не могу найти примеры того, как все это подключить в MAUI. Я заставил его работать на Android, написав код, специфичный для платформы, и думаю, что-то подобное можно сделать и для iOS.

      <WebView
    x:Name="WebView"
    Source=".." />
      public partial class WebClientPage : ContentPage
{
    public WebClientPage()
    {
        InitializeComponent();
    }

    protected override void OnHandlerChanged()
    {
        base.OnHandlerChanged();

#if IOS
        var iosWebView = WebView.Handler.PlatformView as WebKit.WKWebView;

        // Otherwise swiping doesn't work
        iosWebView.AllowsBackForwardNavigationGestures = true;
#endif
    }
}

Связанный вопрос для Android: загрузка файлов в MAUI Android WebView

2 ответа

Для всех, кому интересно, вот мое полное решение для загрузки файла из WebView в MAUI на iOS и отображения диалогового окна, чтобы пользователь мог выбрать, что с ним делать.

Для меня самой большой проблемой было то, что загрузки должны были открываться в новом окне, что в веб-представлении не обрабатывалось так, как я ожидал. Поэтому я проверяюTargetFrameвсех навигационных действий и переопределив их при необходимости.

Он также работает с пользовательским WebViewHandler, но для этого решения требуется больше кода. С другой стороны, его можно повторно использовать для нескольких веб-представлений.

      public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    protected override void OnHandlerChanged()
    {
        base.OnHandlerChanged();

#if IOS
        var iosWebView = WebView.Handler.PlatformView as WebKit.WKWebView;

        // Otherwise swiping doesn't work
        iosWebView.AllowsBackForwardNavigationGestures = true;

        // Custom navigation delegate for iOS
        iosWebView.NavigationDelegate = new MyNavigationDelegate();
#endif
    }
}

Платформы\iOS\MyNavigationDelegate.cs:

      using Foundation;
using System.Text.RegularExpressions;
using WebKit;

public class MyNavigationDelegate : WKNavigationDelegate
{
    private static readonly Regex _fileNameRegex = new("filename\\*?=['\"]?(?:UTF-\\d['\"]*)?([^;\\r\\n\"']*)['\"]?;?", RegexOptions.Compiled);

    public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler)
    {
        // Can't navigate away from the main window
        if (navigationAction.TargetFrame?.MainFrame != true)
        {
            // Cancel the original action and load the same request in the web view
            decisionHandler?.Invoke(WKNavigationActionPolicy.Cancel);
            webView.LoadRequest(navigationAction.Request);
            return;
        }

        decisionHandler?.Invoke(WKNavigationActionPolicy.Allow);
    }

    public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, WKWebpagePreferences preferences, Action<WKNavigationActionPolicy, WKWebpagePreferences> decisionHandler)
    {
        // Can't navigate away from the main window
        if (navigationAction.TargetFrame?.MainFrame != true)
        {
            // Cancel the original action and load the same request in the web view
            decisionHandler?.Invoke(WKNavigationActionPolicy.Cancel, preferences);
            webView.LoadRequest(navigationAction.Request);
            return;
        }

        decisionHandler?.Invoke(WKNavigationActionPolicy.Allow, preferences);
    }

    public override void DecidePolicy(WKWebView webView, WKNavigationResponse navigationResponse, Action<WKNavigationResponsePolicy> decisionHandler)
    {
        // Determine whether to treat it as a download
        if (navigationResponse.Response is NSHttpUrlResponse response
            && response.AllHeaderFields.TryGetValue(new NSString("Content-Disposition"), out var headerValue))
        {
            // Handle it as a download and prevent further navigation
            StartDownload(headerValue.ToString(), navigationResponse.Response.Url);
            decisionHandler?.Invoke(WKNavigationResponsePolicy.Cancel);
            return;
        }

        decisionHandler?.Invoke(WKNavigationResponsePolicy.Allow);
    }

    private void StartDownload(string contentDispositionHeader, NSUrl url)
    {
        try
        {
            var message = TryGetFileNameFromContentDisposition(contentDispositionHeader, out var fileName)
                ? $"Downloading {fileName}..."
                : "Downloading...";

            // TODO: Show toast message

            NSUrlSession
                .FromConfiguration(NSUrlSessionConfiguration.DefaultSessionConfiguration, new MyDownloadDelegate(), null)
                .CreateDownloadTask(url)
                .Resume();
        }
        catch (NSErrorException ex)
        {
            // TODO: Show toast message
        }
    }

    private bool TryGetFileNameFromContentDisposition(string contentDisposition, out string fileName)
    {
        if (string.IsNullOrEmpty(contentDisposition))
        {
            fileName = null;
            return false;
        }

        var match = _fileNameRegex.Match(contentDisposition);
        if (!match.Success)
        {
            fileName = null;
            return false;
        }

        // Use first match even though there could be several matched file names
        fileName = match.Groups[1].Value;
        return true;
    }
}

Платформы\iOS\MyDownloadDelegate.cs:

      using CoreFoundation;
using Foundation;
using UIKit;
using UniformTypeIdentifiers;

public class MyDownloadDelegate : NSUrlSessionDownloadDelegate
{
    public override void DidFinishDownloading(NSUrlSession session, NSUrlSessionDownloadTask downloadTask, NSUrl location)
    {
        try
        {
            if (downloadTask.Response == null)
            {
                return;
            }

            // Determine the cache folder
            var fileManager = NSFileManager.DefaultManager;
            var tempDir = fileManager.GetUrls(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomain.User).FirstOrDefault();
            if (tempDir == null)
            {
                return;
            }

            var contentType = UTType.CreateFromMimeType(downloadTask.Response.MimeType);
            if (contentType == null)
            {
                return;
            }

            // Determine the file name in the cache folder
            var destinationPath = tempDir.AppendPathComponent(downloadTask.Response.SuggestedFilename, contentType);
            if (destinationPath == null || string.IsNullOrEmpty(destinationPath.Path))
            {
                return;
            }

            // Remove any existing files with the same name
            if (fileManager.FileExists(destinationPath.Path) && !fileManager.Remove(destinationPath, out var removeError))
            {
                return;
            }

            // Copy the downloaded file from the OS temp folder to our cache folder
            if (!fileManager.Copy(location, destinationPath, out var copyError))
            {
                return;
            }

            DispatchQueue.MainQueue.DispatchAsync(() =>
            {
                ShowFileOpenDialog(destinationPath);
            });
        }
        catch (NSErrorException ex)
        {
            // TODO: Show toast message
        }
    }

    private void ShowFileOpenDialog(NSUrl fileUrl)
    {
        try
        {
            var window = UIApplication.SharedApplication.Windows.Last(x => x.IsKeyWindow);

            var viewController = window.RootViewController;
            if (viewController == null || viewController.View == null)
            {
                return;
            }

            // TODO: Apps sometimes cannot open the file
            var documentController = UIDocumentInteractionController.FromUrl(fileUrl);
            documentController.PresentOpenInMenu(viewController.View.Frame, viewController.View, true);
        }
        catch (NSErrorException ex)
        {
            // TODO: Show toast message
        }
    }
}

Вы можете загружать файлы на iOS, обрабатываяDecideDestinationмероприятиеWKDownloadDelegate. Также вам необходимо реализоватьIWKDownloadDelegateиIWKNavigationDelegateи переопределить ответ по умолчанию, чтобы отобразить загруженный файл. Вот решение , предоставленное Тимом для справки:

      using ObjCRuntime;
using WebKit;

namespace WebviewTestCatalyst;

[Register("AppDelegate")]
public class AppDelegate : UIApplicationDelegate
{
    public override UIWindow? Window { get; set; }
    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
    {
        // create a new window instance based on the screen size
        Window = new UIWindow(UIScreen.MainScreen.Bounds);

        // create a UIViewController with a single UILabel
        var vc = new UIViewController();
        var webview = new TestDownloadWebView(Window!.Frame, new WebKit.WKWebViewConfiguration())
        {
            AutoresizingMask = UIViewAutoresizing.All
        };

        vc.View!.AddSubview(webview);
        Window.RootViewController = vc;
        // make the window visible
        Window.MakeKeyAndVisible();
        webview.LoadRequest(new NSUrlRequest(
            new NSUrl("https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/")));
        return true;
    }

    public class TestDownloadWebView : WKWebView, IWKDownloadDelegate, IWKNavigationDelegate
    {
        public TestDownloadWebView(CGRect frame, WKWebViewConfiguration configuration) : base(frame, configuration)
        {
            this.NavigationDelegate = this;
        }
        public void DecideDestination(WKDownload download, NSUrlResponse response, string suggestedFilename,
            Action<NSUrl> completionHandler)
        {
            var destinationURL = GetDestinationURL();
            completionHandler?.Invoke(destinationURL);
        }

        [Export("webView:decidePolicyForNavigationResponse:decisionHandler:")]
        public void DecidePolicy(WKWebView webView, WKNavigationResponse navigationResponse, Action<WKNavigationResponsePolicy> decisionHandler)
        {
            var url = navigationResponse.Response.Url;
            var mimeType = navigationResponse.Response.MimeType;
            Console.WriteLine($"Content-Type: {mimeType}");
            // Perform any actions based on the content type
            if (mimeType == "application/pdf")
            {
                // Download the PDF file separately instead of loading it in the WKWebView
                DownloadPDF(url);

                decisionHandler?.Invoke(WKNavigationResponsePolicy.Cancel);
            }
            else
            {
                decisionHandler?.Invoke(WKNavigationResponsePolicy.Allow);
            }
           
        }

        private void DownloadPDF(NSUrl url)
        {
            var downloadTask = NSUrlSession.SharedSession.CreateDownloadTask(url, (location, _, error) =>
            {
                if (location is NSUrl sourceURL && error == null)
                {
                    var destinationURL = GetDestinationURL();

                    try
                    {
                        NSFileManager.DefaultManager.Move(sourceURL, destinationURL, out error);
                        Console.WriteLine($"PDF file downloaded and saved at: {destinationURL.Path}");

                        // Perform any additional actions with the downloaded file
                    }
                    catch (Exception ex)
                    {
                        // Handle file moving error
                    }
                }
                else
                {
                    // Handle download error
                }
            });

            downloadTask.Resume();
        }

        private NSUrl GetDestinationURL()
        {
            // Customize the destination URL as desired
            var documentsURL =
                NSFileManager.DefaultManager.GetUrls(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomain.User)
                    [0];
            var destinationURL = documentsURL.Append("downloaded_file.pdf", false);

            return destinationURL;
        }
    }
}

Другие вопросы по тегам