[webview_flutter_wkwebview] Adds the isInspectable to WebKitWebViewController. (#3984)

Fixes: flutter/flutter#126899

Starting from iOS version 16.4, it is now possible to change the value of the isInspectable property in WKWebView.

https://developer.apple.com/documentation/webkit/wkwebview/4111163-isinspectable

Since iOS 16.4, the Web Inspector feature is disabled by default. 
Therefore, to use Web Inspector, need to change the value of isInspectable.

Here is a blog post from WebKit that discusses this
https://webkit.org/blog/13936/enabling-the-inspection-of-web-content-in-apps/

Apple Developer Forums thread (iOS 16.4 webview can not debug in safari web inspector) 
https://developer.apple.com/forums/thread/727049
This commit is contained in:
GwonHyeok
2023-06-27 02:25:59 +09:00
committed by GitHub
parent 05efbb2470
commit 96c8e2d7f8
17 changed files with 233 additions and 2 deletions

View File

@ -1,3 +1,8 @@
## 3.6.0
* Adds support to enable debugging of web contents on the latest versions of WebKit. See
`WebKitWebViewController.setInspectable`.
## 3.5.0
* Adds support to limit navigation to pages within the apps domain. See

View File

@ -464,4 +464,20 @@ static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; }
XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom));
XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100)));
}
- (void)testSetInspectable API_AVAILABLE(ios(16.4), macos(13.3), tvos(16.4)) {
FWFWebView *mockWebView = OCMClassMock([FWFWebView class]);
FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init];
[instanceManager addDartCreatedInstance:mockWebView withIdentifier:0];
FWFWebViewHostApiImpl *hostAPI = [[FWFWebViewHostApiImpl alloc]
initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger))
instanceManager:instanceManager];
FlutterError *error;
[hostAPI setInspectableForWebViewWithIdentifier:@0 inspectable:@YES error:&error];
OCMVerify([mockWebView setInspectable:YES]);
XCTAssertNil(error);
}
@end

View File

@ -751,6 +751,9 @@ NSObject<FlutterMessageCodec> *FWFWKWebViewHostApiGetCodec(void);
javaScriptString:(NSString *)javaScriptString
completion:(void (^)(id _Nullable,
FlutterError *_Nullable))completion;
- (void)setInspectableForWebViewWithIdentifier:(NSNumber *)identifier
inspectable:(NSNumber *)inspectable
error:(FlutterError *_Nullable *_Nonnull)error;
@end
extern void FWFWKWebViewHostApiSetup(id<FlutterBinaryMessenger> binaryMessenger,

View File

@ -2481,6 +2481,31 @@ void FWFWKWebViewHostApiSetup(id<FlutterBinaryMessenger> binaryMessenger,
[channel setMessageHandler:nil];
}
}
{
FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc]
initWithName:@"dev.flutter.pigeon.WKWebViewHostApi.setInspectable"
binaryMessenger:binaryMessenger
codec:FWFWKWebViewHostApiGetCodec()];
if (api) {
NSCAssert([api respondsToSelector:@selector(setInspectableForWebViewWithIdentifier:
inspectable:error:)],
@"FWFWKWebViewHostApi api (%@) doesn't respond to "
@"@selector(setInspectableForWebViewWithIdentifier:inspectable:error:)",
api);
[channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
NSArray *args = message;
NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0);
NSNumber *arg_inspectable = GetNullableObjectAtIndex(args, 1);
FlutterError *error;
[api setInspectableForWebViewWithIdentifier:arg_identifier
inspectable:arg_inspectable
error:&error];
callback(wrapResult(nil, error));
}];
} else {
[channel setMessageHandler:nil];
}
}
}
NSObject<FlutterMessageCodec> *FWFWKUIDelegateHostApiGetCodec(void) {
static FlutterStandardMessageCodec *sSharedObject = nil;

View File

@ -197,6 +197,18 @@
}];
}
- (void)setInspectableForWebViewWithIdentifier:(NSNumber *)identifier
inspectable:(NSNumber *)inspectable
error:(FlutterError *_Nullable *_Nonnull)error {
if (@available(macOS 13.3, iOS 16.4, tvOS 16.4, *)) {
[[self webViewForIdentifier:identifier] setInspectable:inspectable.boolValue];
} else {
*error = [FlutterError errorWithCode:@"FWFUnsupportedVersionError"
message:@"setInspectable is only supported on versions 16.4+."
details:nil];
}
}
- (void)goBackForWebViewWithIdentifier:(nonnull NSNumber *)identifier
error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error {
[[self webViewForIdentifier:identifier] goBack];

View File

@ -675,6 +675,7 @@ class ObjectOrIdentifier {
class _WKWebsiteDataStoreHostApiCodec extends StandardMessageCodec {
const _WKWebsiteDataStoreHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is WKWebsiteDataTypeEnumData) {
@ -961,6 +962,7 @@ class UIScrollViewHostApi {
class _WKWebViewConfigurationHostApiCodec extends StandardMessageCodec {
const _WKWebViewConfigurationHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is WKAudiovisualMediaTypeEnumData) {
@ -1150,6 +1152,7 @@ abstract class WKWebViewConfigurationFlutterApi {
class _WKUserContentControllerHostApiCodec extends StandardMessageCodec {
const _WKUserContentControllerHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is WKUserScriptData) {
@ -1435,6 +1438,7 @@ class WKScriptMessageHandlerHostApi {
class _WKScriptMessageHandlerFlutterApiCodec extends StandardMessageCodec {
const _WKScriptMessageHandlerFlutterApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is WKScriptMessageData) {
@ -1537,6 +1541,7 @@ class WKNavigationDelegateHostApi {
class _WKNavigationDelegateFlutterApiCodec extends StandardMessageCodec {
const _WKNavigationDelegateFlutterApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSErrorData) {
@ -1768,6 +1773,7 @@ abstract class WKNavigationDelegateFlutterApi {
class _NSObjectHostApiCodec extends StandardMessageCodec {
const _NSObjectHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSKeyValueObservingOptionsEnumData) {
@ -1881,6 +1887,7 @@ class NSObjectHostApi {
class _NSObjectFlutterApiCodec extends StandardMessageCodec {
const _NSObjectFlutterApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSKeyValueChangeKeyEnumData) {
@ -1982,6 +1989,7 @@ abstract class NSObjectFlutterApi {
class _WKWebViewHostApiCodec extends StandardMessageCodec {
const _WKWebViewHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSErrorData) {
@ -2527,6 +2535,28 @@ class WKWebViewHostApi {
return replyList[0];
}
}
Future<void> setInspectable(int arg_identifier, bool arg_inspectable) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.WKWebViewHostApi.setInspectable', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList = await channel
.send(<Object?>[arg_identifier, arg_inspectable]) as List<Object?>?;
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel.',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else {
return;
}
}
}
/// Mirror of WKUIDelegate.
@ -2567,6 +2597,7 @@ class WKUIDelegateHostApi {
class _WKUIDelegateFlutterApiCodec extends StandardMessageCodec {
const _WKUIDelegateFlutterApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSUrlRequestData) {
@ -2703,6 +2734,7 @@ abstract class WKUIDelegateFlutterApi {
class _WKHttpCookieStoreHostApiCodec extends StandardMessageCodec {
const _WKHttpCookieStoreHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSHttpCookieData) {

View File

@ -1107,6 +1107,24 @@ class WKWebView extends UIView {
);
}
/// Enables debugging of web contents (HTML / CSS / JavaScript) in the
/// underlying WebView.
///
/// This flag can be enabled in order to facilitate debugging of web layouts
/// and JavaScript code running inside WebViews. Please refer to [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview?language=objc).
/// documentation for the debugging guide.
///
/// Starting from macOS version 13.3, iOS version 16.4, and tvOS version 16.4,
/// the default value is set to false.
///
/// Defaults to true in previous versions.
Future<void> setInspectable(bool inspectable) {
return _webViewApi.setInspectableForInstances(
this,
inspectable,
);
}
@override
WKWebView copy() {
return WKWebView.detached(

View File

@ -1067,6 +1067,17 @@ class WKWebViewHostApiImpl extends WKWebViewHostApi {
}
}
/// Calls [setInspectable] with the ids of the provided object instances.
Future<void> setInspectableForInstances(
WKWebView instance,
bool inspectable,
) async {
return setInspectable(
instanceManager.getIdentifier(instance)!,
inspectable,
);
}
/// Calls [setNavigationDelegate] with the ids of the provided object instances.
Future<void> setNavigationDelegateForInstances(
WKWebView instance,

View File

@ -544,6 +544,18 @@ class WebKitWebViewController extends PlatformWebViewController {
) async {
_onPermissionRequestCallback = onPermissionRequest;
}
/// Whether to enable tools for debugging the current WKWebView content.
///
/// It needs to be activated in each WKWebView where you want to enable it.
///
/// Starting from macOS version 13.3, iOS version 16.4, and tvOS version 16.4,
/// the default value is set to false.
///
/// Defaults to true in previous versions.
Future<void> setInspectable(bool inspectable) {
return _webView.setInspectable(inspectable);
}
}
/// An implementation of [JavaScriptChannelParams] with the WebKit api.

View File

@ -694,6 +694,9 @@ abstract class WKWebViewHostApi {
@ObjCSelector('evaluateJavaScriptForWebViewWithIdentifier:javaScriptString:')
@async
Object? evaluateJavaScript(int identifier, String javaScriptString);
@ObjCSelector('setInspectableForWebViewWithIdentifier:inspectable:')
void setInspectable(int identifier, bool inspectable);
}
/// Mirror of WKUIDelegate.

View File

@ -2,7 +2,7 @@ name: webview_flutter_wkwebview
description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control.
repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22
version: 3.5.0
version: 3.6.0
environment:
sdk: ">=2.18.0 <4.0.0"

View File

@ -641,6 +641,15 @@ class MockWKWebView extends _i1.Mock implements _i4.WKWebView {
returnValue: _i5.Future<Object?>.value(),
) as _i5.Future<Object?>);
@override
_i5.Future<void> setInspectable(bool? inspectable) => (super.noSuchMethod(
Invocation.method(
#setInspectable,
[inspectable],
),
returnValue: _i5.Future<void>.value(),
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override
_i4.WKWebView copy() => (super.noSuchMethod(
Invocation.method(
#copy,

View File

@ -7,14 +7,15 @@
// ignore_for_file: avoid_relative_lib_imports
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart';
class _TestWKWebsiteDataStoreHostApiCodec extends StandardMessageCodec {
const _TestWKWebsiteDataStoreHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is WKWebsiteDataTypeEnumData) {
@ -333,6 +334,7 @@ abstract class TestUIScrollViewHostApi {
class _TestWKWebViewConfigurationHostApiCodec extends StandardMessageCodec {
const _TestWKWebViewConfigurationHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is WKAudiovisualMediaTypeEnumData) {
@ -511,6 +513,7 @@ abstract class TestWKWebViewConfigurationHostApi {
class _TestWKUserContentControllerHostApiCodec extends StandardMessageCodec {
const _TestWKUserContentControllerHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is WKUserScriptData) {
@ -867,6 +870,7 @@ abstract class TestWKNavigationDelegateHostApi {
class _TestNSObjectHostApiCodec extends StandardMessageCodec {
const _TestNSObjectHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSKeyValueObservingOptionsEnumData) {
@ -995,6 +999,7 @@ abstract class TestNSObjectHostApi {
class _TestWKWebViewHostApiCodec extends StandardMessageCodec {
const _TestWKWebViewHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSErrorData) {
@ -1145,6 +1150,8 @@ abstract class TestWKWebViewHostApi {
Future<Object?> evaluateJavaScript(int identifier, String javaScriptString);
void setInspectable(int identifier, bool inspectable);
static void setup(TestWKWebViewHostApi? api,
{BinaryMessenger? binaryMessenger}) {
{
@ -1575,6 +1582,31 @@ abstract class TestWKWebViewHostApi {
});
}
}
{
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.WKWebViewHostApi.setInspectable', codec,
binaryMessenger: binaryMessenger);
if (api == null) {
_testBinaryMessengerBinding!.defaultBinaryMessenger
.setMockDecodedMessageHandler<Object?>(channel, null);
} else {
_testBinaryMessengerBinding!.defaultBinaryMessenger
.setMockDecodedMessageHandler<Object?>(channel,
(Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.WKWebViewHostApi.setInspectable was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_identifier = (args[0] as int?);
assert(arg_identifier != null,
'Argument for dev.flutter.pigeon.WKWebViewHostApi.setInspectable was null, expected non-null int.');
final bool? arg_inspectable = (args[1] as bool?);
assert(arg_inspectable != null,
'Argument for dev.flutter.pigeon.WKWebViewHostApi.setInspectable was null, expected non-null bool.');
api.setInspectable(arg_identifier!, arg_inspectable!);
return <Object?>[];
});
}
}
}
}
@ -1617,6 +1649,7 @@ abstract class TestWKUIDelegateHostApi {
class _TestWKHttpCookieStoreHostApiCodec extends StandardMessageCodec {
const _TestWKHttpCookieStoreHostApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is NSHttpCookieData) {

View File

@ -321,6 +321,21 @@ class MockTestWKWebViewHostApi extends _i1.Mock
),
returnValue: _i4.Future<Object?>.value(),
) as _i4.Future<Object?>);
@override
void setInspectable(
int? identifier,
bool? inspectable,
) =>
super.noSuchMethod(
Invocation.method(
#setInspectable,
[
identifier,
inspectable,
],
),
returnValueForMissingStub: null,
);
}
/// A class which mocks [TestUIScrollViewHostApi].

View File

@ -550,6 +550,21 @@ class MockTestWKWebViewHostApi extends _i1.Mock
),
returnValue: _i3.Future<Object?>.value(),
) as _i3.Future<Object?>);
@override
void setInspectable(
int? identifier,
bool? inspectable,
) =>
super.noSuchMethod(
Invocation.method(
#setInspectable,
[
identifier,
inspectable,
],
),
returnValueForMissingStub: null,
);
}
/// A class which mocks [TestWKWebsiteDataStoreHostApi].

View File

@ -1153,6 +1153,17 @@ void main() {
]);
expect(decision, WKPermissionDecision.grant);
});
test('inspectable', () async {
final MockWKWebView mockWebView = MockWKWebView();
final WebKitWebViewController controller = createControllerWithMocks(
createMockWebView: (_, {dynamic observeValue}) => mockWebView,
);
await controller.setInspectable(true);
verify(mockWebView.setInspectable(true));
});
});
group('WebKitJavaScriptChannelParams', () {
@ -1204,6 +1215,7 @@ class CapturingNavigationDelegate extends WKNavigationDelegate {
}) : super.detached() {
lastCreatedDelegate = this;
}
static CapturingNavigationDelegate lastCreatedDelegate =
CapturingNavigationDelegate();
}
@ -1217,5 +1229,6 @@ class CapturingUIDelegate extends WKUIDelegate {
}) : super.detached() {
lastCreatedDelegate = this;
}
static CapturingUIDelegate lastCreatedDelegate = CapturingUIDelegate();
}

View File

@ -738,6 +738,15 @@ class MockWKWebView extends _i1.Mock implements _i5.WKWebView {
returnValue: _i6.Future<Object?>.value(),
) as _i6.Future<Object?>);
@override
_i6.Future<void> setInspectable(bool? inspectable) => (super.noSuchMethod(
Invocation.method(
#setInspectable,
[inspectable],
),
returnValue: _i6.Future<void>.value(),
returnValueForMissingStub: _i6.Future<void>.value(),
) as _i6.Future<void>);
@override
_i5.WKWebView copy() => (super.noSuchMethod(
Invocation.method(
#copy,