Загрузка файлов в 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;
}
}
}