MackinnonBuck/MauiBlazorPermissionsExample

iOS repeatedly asks for camera permission

Opened this issue · 0 comments

Given this repos example code, iOS will consistently request camera permissions once per app start (tested on iOS 16.4 Simulator). Looks like there are some ways to mitigate this by following some of the steps in this SO post. I implemented a modified version of it, since we want to preserve the existing functionality of the IOSWebViewManager.WebViewUIDelegate used by BlazorWebView if possible (i.e. alert, prompt, confirm dialogs). Just wanted to share it in case others may find it useful.

  • Add within CreateMauiApp()
BlazorWebViewHandler.BlazorWebViewMapper.Add("custom", (handler, view) =>
{
#if IOS || MACCATALYST
        // Add custom UIDelegate's so that we can grant permission requests
        // https://stackoverflow.com/a/75649052/10388359
        var existingDelegate = handler.PlatformView.UIDelegate;
        handler.PlatformView.UIDelegate = new CustomWrapperWebViewDelegate(existingDelegate);
#endif
});
  • Add under Platforms\iOS folder
internal static class IWKUIDelegateExtensions
{
    internal static void GrantDecision(this IWKUIDelegate _, Action<WKPermissionDecision> decisionHandler)
    {
        if (OperatingSystem.IsIOSVersionAtLeast(15, 0))
            decisionHandler(WKPermissionDecision.Grant);
    }
}

/// <summary>Used as a wrapper to automatically grant permissions for <see cref="WebView"/>'s using internal implementations of <see cref="WKUIDelegate"/> such as <see cref="BlazorWebView"/> which uses <see cref="IOSWebViewManager.WebViewUIDelegate"/></summary>
public class CustomWrapperWebViewDelegate : WKUIDelegate
{
    private readonly IWKUIDelegate? existingDelegate;

    public CustomWrapperWebViewDelegate(IWKUIDelegate? existingDelegate)
        => this.existingDelegate = existingDelegate;

    public override void RequestDeviceOrientationAndMotionPermission(WKWebView webView, WKSecurityOrigin origin, WKFrameInfo frame, Action<WKPermissionDecision> decisionHandler)
        => this.GrantDecision(decisionHandler);

    public override void RequestMediaCapturePermission(WKWebView webView, WKSecurityOrigin origin, WKFrameInfo frame, WKMediaCaptureType type, Action<WKPermissionDecision> decisionHandler)
        => this.GrantDecision(decisionHandler);

    public override void CommitPreviewingViewController(WKWebView webView, UIViewController previewingViewController)
        => existingDelegate?.CommitPreviewingViewController(webView, previewingViewController);

    public override void ContextMenuDidEnd(WKWebView webView, WKContextMenuElementInfo elementInfo)
        => existingDelegate?.ContextMenuDidEnd(webView, elementInfo);

    public override void ContextMenuWillPresent(WKWebView webView, WKContextMenuElementInfo elementInfo)
        => existingDelegate?.ContextMenuWillPresent(webView, elementInfo);

    public override WKWebView? CreateWebView(WKWebView webView, WKWebViewConfiguration configuration, WKNavigationAction navigationAction, WKWindowFeatures windowFeatures)
        => existingDelegate?.CreateWebView(webView, configuration, navigationAction, windowFeatures);

    public override void DidClose(WKWebView webView)
        => existingDelegate?.DidClose(webView);

    public override UIViewController? GetPreviewingViewController(WKWebView webView, WKPreviewElementInfo elementInfo, IWKPreviewActionItem[] previewActions)
        => existingDelegate?.GetPreviewingViewController(webView, elementInfo, previewActions);

    public override void RunJavaScriptAlertPanel(WKWebView webView, string message, WKFrameInfo frame, Action completionHandler)
        => existingDelegate?.RunJavaScriptAlertPanel(webView, message, frame, completionHandler);

    public override void RunJavaScriptConfirmPanel(WKWebView webView, string message, WKFrameInfo frame, Action<bool> completionHandler)
        => existingDelegate?.RunJavaScriptConfirmPanel(webView, message, frame, completionHandler);

    public override void RunJavaScriptTextInputPanel(WKWebView webView, string prompt, string? defaultText, WKFrameInfo frame, Action<string> completionHandler)
        => existingDelegate?.RunJavaScriptTextInputPanel(webView, prompt, defaultText, frame, completionHandler);

    public override void SetContextMenuConfiguration(WKWebView webView, WKContextMenuElementInfo elementInfo, Action<UIContextMenuConfiguration> completionHandler)
        => existingDelegate?.SetContextMenuConfiguration(webView, elementInfo, completionHandler);
    public override bool ShouldPreviewElement(WKWebView webView, WKPreviewElementInfo elementInfo)
        => (bool)(existingDelegate?.ShouldPreviewElement(webView, elementInfo));

    public override void ShowLockDownMode(WKWebView webView, string firstUseMessage, Action<WKDialogResult> completionHandler)
        => existingDelegate?.ShowLockDownMode(webView, firstUseMessage, completionHandler);

    public override void WillCommitContextMenu(WKWebView webView, WKContextMenuElementInfo elementInfo, IUIContextMenuInteractionCommitAnimating animator)
        => existingDelegate?.WillCommitContextMenu(webView, elementInfo, animator);
}

Interestingly, for me it doesn't even prompt to allow camera access at all, just allows it w/o prompt, however I haven't tested outside of a simulator or iOS 16.4 yet.

For those of you interested, if you're working with just the default MAUI WebView, we can take a similar approach:

  • Add within CreateMauiApp()
WebViewHandler.Mapper.AppendToMapping(nameof(WKUIDelegate), (handler, view) =>
{
#if IOS || MACCATALYST
        // Add custom UIDelegate's so that we can grant permission requests
        // https://stackoverflow.com/a/75649052/10388359
        if (existingDelegate is MauiWebViewUIDelegate d)
                handler.PlatformView.UIDelegate = new CustomMauiWebViewUIDelegate(handler);
#endif
});
  • Add under Platforms\iOS folder
/// <summary>Used to automatically grant permissions for <see cref="WebView"/>'s using <see cref="MauiWebViewUIDelegate"/></summary>
public class CustomMauiWebViewUIDelegate : MauiWebViewUIDelegate
{
    public CustomMauiWebViewUIDelegate(IWebViewHandler handler) : base(handler) { }

    public override void RequestMediaCapturePermission(WKWebView webView, WKSecurityOrigin origin, WKFrameInfo frame, WKMediaCaptureType type, Action<WKPermissionDecision> decisionHandler)
        => this.GrantDecision(decisionHandler);
}

And just one extra tidbit, if working with the MAUI WebView, you'll need to enable some WKWebViewConfiguration options (primarily AllowsInlineMediaPlayback & MediaTypesRequiringUserActionForPlayback), which will be enabled by default in net8.0, however you can enable them in net7.0 by following the example in this PR (which is what added these options to be default for next version). These same options can be configured for the BlazorWebView within the BlazorWebViewInitializing event, which this example repo already covers:

private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e)
{
e.Configuration.AllowsInlineMediaPlayback = true;
e.Configuration.MediaTypesRequiringUserActionForPlayback = WebKit.WKAudiovisualMediaTypes.None;
}

What a rabbit hole... Hope someone finds this useful!