Commit 306ef6ba authored by Ali Juma's avatar Ali Juma Committed by Chromium LUCI CQ

[iOS] Remove EarlGrey dependencies from CWTChromeDriver

EarlGrey is not compatible with ASan, so in order to adapt
CWTChromeDriver for fuzzing, all dependencies on EarlGrey need to
be removed.

CWTChromeDriver doesn't do any actual EarlGrey UI testing.
Instead, it only used EarlGrey as a convenient way of setting up
a test process and app process, with EDO communication between
processes.

This CL removes the EarlGrey dependencies, instead using XCUITest
and EDO directly.

Bug: 1158540
Change-Id: Iec0534d668413e0630010c7d470bac4e98db43bb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2622239Reviewed-by: default avatarJustin Cohen <justincohen@chromium.org>
Commit-Queue: Ali Juma <ajuma@chromium.org>
Cr-Commit-Position: refs/heads/master@{#843008}
parent 9f4f84b5
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
import("//build/apple/tweak_info_plist.gni")
import("//build/config/ios/ios_sdk.gni") import("//build/config/ios/ios_sdk.gni")
import("//build/config/ios/rules.gni") import("//build/config/ios/rules.gni")
import("//ios/chrome/test/earl_grey2/chrome_ios_eg2_test.gni") import("//ios/build/chrome_build.gni")
import("//ios/public/provider/chrome/browser/build_config.gni")
group("all_tests") { group("all_tests") {
testonly = true testonly = true
...@@ -14,15 +16,50 @@ group("all_tests") { ...@@ -14,15 +16,50 @@ group("all_tests") {
] ]
} }
chrome_ios_eg2_test_app_host("ios_cwt_chromedriver_tests") { tweak_info_plist("chrome_app_plist") {
deps = [ ":eg_app_support+eg2" ] info_plists = [
"//ios/chrome/app/resources/Info.plist",
"//ios/chrome/app/resources/ChromeAddition+Info.plist",
"//ios/chrome/app/resources/MultiWindowEnabled+Info.plist",
]
}
ios_app_bundle("ios_cwt_chromedriver_tests") {
testonly = true
deps = [
":app_support",
"//ios/chrome/app:main",
]
bundle_deps = [
"//ios/chrome/app/resources",
"//ios/third_party/gtx:gtx+bundle",
ios_application_icons_target,
]
info_plist_target = ":chrome_app_plist"
extra_substitutions = [
"CHROMIUM_HANDOFF_ID=$chromium_handoff_id",
"CHROMIUM_SHORT_NAME=$target_name",
"CHROMIUM_URL_CHANNEL_SCHEME=$url_channel_scheme",
"CHROMIUM_URL_SCHEME_1=$url_unsecure_scheme",
"CHROMIUM_URL_SCHEME_2=$url_secure_scheme",
"CHROMIUM_URL_SCHEME_3=$url_x_callback_scheme",
"CHROMIUM_BUNDLE_ID=gtest.$target_name",
"CONTENT_WIDGET_EXTENSION_BUNDLE_ID=$chromium_bundle_id.ContentTodayExtension",
"IOS_MOVE_TAB_ACTIVITY_TYPE=$ios_move_tab_activity_type",
"SSOAUTH_URL_SCHEME=$url_ssoauth_scheme",
]
} }
chrome_ios_eg2_test("ios_cwt_chromedriver_tests_module") { ios_xcuitest_test("ios_cwt_chromedriver_tests_module") {
xcode_test_application_name = "ios_cwt_chromedriver_tests" xcode_test_application_name = "ios_cwt_chromedriver_tests"
deps = [ ":cwt_chromedriver_tests" ] deps = [ ":cwt_chromedriver_tests" ]
data_deps = [ ":ios_cwt_chromedriver_tests" ] data_deps = [ ":ios_cwt_chromedriver_tests" ]
bundle_deps = [
"//ios/chrome/app/resources",
"//ios/third_party/gtx:gtx+bundle",
"//third_party/icu:icudata",
]
} }
source_set("shared_helper_headers") { source_set("shared_helper_headers") {
...@@ -30,25 +67,36 @@ source_set("shared_helper_headers") { ...@@ -30,25 +67,36 @@ source_set("shared_helper_headers") {
sources = [ "cwt_webdriver_app_interface.h" ] sources = [ "cwt_webdriver_app_interface.h" ]
} }
source_set("eg_app_support+eg2") { source_set("cwt_constants") {
defines = [ "CHROME_EARL_GREY_2" ] testonly = true
sources = [
"cwt_constants.cc",
"cwt_constants.h",
]
}
source_set("app_support") {
testonly = true testonly = true
configs += [ "//build/config/compiler:enable_arc" ] configs += [ "//build/config/compiler:enable_arc" ]
sources = [ "cwt_webdriver_app_interface.mm" ] sources = [
"cwt_tests_hook.mm",
"cwt_webdriver_app_interface.mm",
]
deps = [ deps = [
":cwt_constants",
"//base", "//base",
"//base/test:test_support", "//base/test:test_support",
"//ios/chrome/app:app_internal", "//ios/chrome/app:app_internal",
"//ios/chrome/app:tests_hook",
"//ios/chrome/browser/main:public", "//ios/chrome/browser/main:public",
"//ios/chrome/browser/tabs:tabs", "//ios/chrome/browser/tabs:tabs",
"//ios/chrome/browser/web:tab_id_tab_helper", "//ios/chrome/browser/web:tab_id_tab_helper",
"//ios/chrome/browser/web_state_list", "//ios/chrome/browser/web_state_list",
"//ios/chrome/test/app:test_support", "//ios/chrome/test/app:test_support",
"//ios/testing:nserror_support", "//ios/testing:nserror_support",
"//ios/testing/earl_grey:eg_app_support+eg2", "//ios/third_party/edo",
"//ios/third_party/earl_grey2:app_framework+link",
"//ios/web/public/test", "//ios/web/public/test",
"//ui/gfx", "//ui/gfx",
] ]
...@@ -57,7 +105,6 @@ source_set("eg_app_support+eg2") { ...@@ -57,7 +105,6 @@ source_set("eg_app_support+eg2") {
} }
source_set("cwt_chromedriver_tests") { source_set("cwt_chromedriver_tests") {
defines = [ "CHROME_EARL_GREY_2" ]
testonly = true testonly = true
configs += [ configs += [
"//build/config/compiler:enable_arc", "//build/config/compiler:enable_arc",
...@@ -71,10 +118,10 @@ source_set("cwt_chromedriver_tests") { ...@@ -71,10 +118,10 @@ source_set("cwt_chromedriver_tests") {
] ]
deps = [ deps = [
":cwt_constants",
":shared_helper_headers", ":shared_helper_headers",
"//components/version_info:version_info", "//components/version_info:version_info",
"//ios/testing/earl_grey:eg_test_support+eg2", "//ios/third_party/edo",
"//ios/third_party/earl_grey2:test_lib",
"//net:test_support", "//net:test_support",
] ]
......
...@@ -2,14 +2,12 @@ ...@@ -2,14 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#import <TestLib/EarlGreyImpl/EarlGrey.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <XCTest/XCTest.h> #import <XCTest/XCTest.h>
#include "base/logging.h" #include "base/logging.h"
#include "base/strings/sys_string_conversions.h" #include "base/strings/sys_string_conversions.h"
#import "ios/chrome/test/wpt/cwt_request_handler.h" #import "ios/chrome/test/wpt/cwt_request_handler.h"
#import "ios/testing/earl_grey/base_earl_grey_test_case.h"
#include "net/base/port_util.h" #include "net/base/port_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h" #include "net/test/embedded_test_server/embedded_test_server.h"
#include "url/url_constants.h" #include "url/url_constants.h"
...@@ -28,17 +26,20 @@ const int kDefaultPort = 8123; ...@@ -28,17 +26,20 @@ const int kDefaultPort = 8123;
// Dummy test case that hosts CWTChromeDriver. CWTChromeDriver implements a // Dummy test case that hosts CWTChromeDriver. CWTChromeDriver implements a
// minimal subset of the WebDriver protocol needed to run most Web Platform // minimal subset of the WebDriver protocol needed to run most Web Platform
// Tests. CWTChromeDriverTestCase launches a test server that listens for // Tests. CWTChromeDriverTestCase launches a test server that listens for
// WebDriver commands, and then uses EarlGrey2's eDistantObject protocol to pass // WebDriver commands, and then uses the eDistantObject protocol to pass on
// on corresponding messages to the app process. Each CWTChromeDriver launches a // corresponding messages to the app process. Each CWTChromeDriver launches a
// single instance of Chrome, but mulitple instances of CWTChromeDriver can be // single instance of Chrome, but multiple instances of CWTChromeDriver can be
// run in parallel in order to use multiple instances of Chrome. // run in parallel in order to use multiple instances of Chrome.
@interface CWTChromeDriverTestCase : BaseEarlGreyTestCase @interface CWTChromeDriverTestCase : XCTestCase
@end @end
@implementation CWTChromeDriverTestCase @implementation CWTChromeDriverTestCase
// Dummy test that keeps the test app alive. // Dummy test that keeps the test app alive.
- (void)testRunCWTChromeDriver { - (void)testRunCWTChromeDriver {
XCUIApplication* application = [[XCUIApplication alloc] init];
[application launch];
int port = kDefaultPort; int port = kDefaultPort;
NSArray* arguments = NSProcessInfo.processInfo.arguments; NSArray* arguments = NSProcessInfo.processInfo.arguments;
......
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/test/wpt/cwt_constants.h"
extern const int kCwtEdoPortNumber = 56800;
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_CHROME_TEST_WPT_CWT_CONSTANTS_H_
#define IOS_CHROME_TEST_WPT_CWT_CONSTANTS_H_
// The port that the app process listens on for EDO communication with the
// the test process.
extern const int kCwtEdoPortNumber;
#endif // IOS_CHROME_TEST_WPT_CWT_CONSTANTS_H_
...@@ -10,15 +10,16 @@ ...@@ -10,15 +10,16 @@
#include "base/json/json_writer.h" #include "base/json/json_writer.h"
#include "base/strings/sys_string_conversions.h" #include "base/strings/sys_string_conversions.h"
#include "components/version_info/version_info.h" #include "components/version_info/version_info.h"
#import "ios/chrome/test/wpt/cwt_constants.h"
#import "ios/chrome/test/wpt/cwt_webdriver_app_interface.h" #import "ios/chrome/test/wpt/cwt_webdriver_app_interface.h"
#import "ios/testing/earl_grey/earl_grey_test.h" #import "ios/third_party/edo/src/Service/Sources/EDOClientService.h"
#include "net/http/http_status_code.h" #include "net/http/http_status_code.h"
#if !defined(__has_feature) || !__has_feature(objc_arc) #if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support." #error "This file requires ARC support."
#endif #endif
GREY_STUB_CLASS_IN_APP_BACKGROUND_QUEUE(CWTWebDriverAppInterface) EDO_STUB_CLASS(CWTWebDriverAppInterface, kCwtEdoPortNumber)
using net::test_server::HttpRequest; using net::test_server::HttpRequest;
using net::test_server::HttpResponse; using net::test_server::HttpResponse;
......
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/test/wpt/cwt_constants.h"
#import "ios/chrome/test/wpt/cwt_webdriver_app_interface.h"
#import "ios/third_party/edo/src/Service/Sources/EDOHostService.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace tests_hook {
bool DisableAppGroupAccess() {
return true;
}
bool DisableContentSuggestions() {
return true;
}
bool DisableFirstRun() {
return true;
}
bool DisableGeolocation() {
return true;
}
bool DisableSigninRecallPromo() {
return true;
}
bool DisableUpdateService() {
return true;
}
bool DisableMainThreadFreezeDetection() {
return true;
}
policy::ConfigurationPolicyProvider* GetOverriddenPlatformPolicyProvider() {
return nullptr;
}
void SetUpTestsIfPresent() {
CWTWebDriverAppInterface* appInterface =
[[CWTWebDriverAppInterface alloc] init];
[EDOHostService serviceWithPort:kCwtEdoPortNumber
rootObject:appInterface
queue:[appInterface executingQueue]];
}
void RunTestsIfPresent() {}
} // namespace tests_hook
...@@ -13,6 +13,9 @@ ...@@ -13,6 +13,9 @@
// avoid deadlock while waiting for actions to complete on the main thread. // avoid deadlock while waiting for actions to complete on the main thread.
@interface CWTWebDriverAppInterface : NSObject @interface CWTWebDriverAppInterface : NSObject
// The background thread where this class' methods are run.
@property(nonatomic, readonly) dispatch_queue_t executingQueue;
// Loads the given URL in the tab identified by |tabID|. Returns an error if the // Loads the given URL in the tab identified by |tabID|. Returns an error if the
// page fails to load within |timeout| seconds or if no such tab exists. // page fails to load within |timeout| seconds or if no such tab exists.
+ (NSError*)loadURL:(NSString*)URL + (NSError*)loadURL:(NSString*)URL
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
#import "ios/chrome/test/app/chrome_test_util.h" #import "ios/chrome/test/app/chrome_test_util.h"
#import "ios/chrome/test/app/settings_test_util.h" #import "ios/chrome/test/app/settings_test_util.h"
#import "ios/chrome/test/app/tab_test_util.h" #import "ios/chrome/test/app/tab_test_util.h"
#import "ios/testing/earl_grey/earl_grey_app.h"
#import "ios/testing/nserror_util.h" #import "ios/testing/nserror_util.h"
#import "ios/web/public/test/navigation_test_util.h" #import "ios/web/public/test/navigation_test_util.h"
#import "ios/web/public/ui/crw_web_view_proxy.h" #import "ios/web/public/ui/crw_web_view_proxy.h"
...@@ -59,15 +58,40 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -59,15 +58,40 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
return web_state_list->GetIndexOfWebState(GetWebStateWithId(tab_id)); return web_state_list->GetIndexOfWebState(GetWebStateWithId(tab_id));
} }
void DispatchSyncOnMainThread(void (^block)(void)) {
if ([NSThread isMainThread]) {
block();
} else {
dispatch_semaphore_t waitForBlock = dispatch_semaphore_create(0);
CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{
block();
dispatch_semaphore_signal(waitForBlock);
});
// CFRunLoopPerformBlock does not wake up the main queue.
CFRunLoopWakeUp(CFRunLoopGetMain());
// Waits until block is executed and semaphore is signalled.
dispatch_semaphore_wait(waitForBlock, DISPATCH_TIME_FOREVER);
}
}
} // namespace } // namespace
@implementation CWTWebDriverAppInterface @implementation CWTWebDriverAppInterface
- (instancetype)init {
self = [super init];
if (self) {
_executingQueue = dispatch_queue_create("com.google.chrome.cwt.background",
DISPATCH_QUEUE_SERIAL);
}
return self;
}
+ (NSError*)loadURL:(NSString*)URL + (NSError*)loadURL:(NSString*)URL
inTab:(NSString*)tabID inTab:(NSString*)tabID
timeoutInSeconds:(NSTimeInterval)timeout { timeoutInSeconds:(NSTimeInterval)timeout {
__block web::WebState* webState = nullptr; __block web::WebState* webState = nullptr;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
webState = GetWebStateWithId(tabID); webState = GetWebStateWithId(tabID);
if (webState) if (webState)
web::test::LoadUrl(webState, GURL(base::SysNSStringToUTF8(URL))); web::test::LoadUrl(webState, GURL(base::SysNSStringToUTF8(URL)));
...@@ -78,7 +102,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -78,7 +102,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
bool success = WaitUntilConditionOrTimeout(timeout, ^bool { bool success = WaitUntilConditionOrTimeout(timeout, ^bool {
__block BOOL isLoading = NO; __block BOOL isLoading = NO;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
isLoading = webState->IsLoading(); isLoading = webState->IsLoading();
}); });
return !isLoading; return !isLoading;
...@@ -92,7 +116,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -92,7 +116,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
+ (NSString*)currentTabID { + (NSString*)currentTabID {
__block NSString* tabID = nil; __block NSString* tabID = nil;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
web::WebState* webState = chrome_test_util::GetCurrentWebState(); web::WebState* webState = chrome_test_util::GetCurrentWebState();
if (webState) if (webState)
tabID = GetIdForWebState(webState); tabID = GetIdForWebState(webState);
...@@ -103,7 +127,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -103,7 +127,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
+ (NSArray*)tabIDs { + (NSArray*)tabIDs {
__block NSMutableArray* tabIDs; __block NSMutableArray* tabIDs;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
DCHECK(!chrome_test_util::IsIncognitoMode()); DCHECK(!chrome_test_util::IsIncognitoMode());
WebStateList* webStateList = GetCurrentWebStateList(); WebStateList* webStateList = GetCurrentWebStateList();
tabIDs = [NSMutableArray arrayWithCapacity:webStateList->count()]; tabIDs = [NSMutableArray arrayWithCapacity:webStateList->count()];
...@@ -119,7 +143,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -119,7 +143,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
+ (NSError*)closeTabWithID:(NSString*)ID { + (NSError*)closeTabWithID:(NSString*)ID {
__block NSError* error = nil; __block NSError* error = nil;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
int webStateIndex = GetIndexOfWebStateWithId(ID); int webStateIndex = GetIndexOfWebStateWithId(ID);
if (webStateIndex != WebStateList::kInvalidIndex) { if (webStateIndex != WebStateList::kInvalidIndex) {
WebStateList* webStateList = GetCurrentWebStateList(); WebStateList* webStateList = GetCurrentWebStateList();
...@@ -135,7 +159,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -135,7 +159,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
+ (NSError*)switchToTabWithID:(NSString*)ID { + (NSError*)switchToTabWithID:(NSString*)ID {
__block NSError* error = nil; __block NSError* error = nil;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
DCHECK(!chrome_test_util::IsIncognitoMode()); DCHECK(!chrome_test_util::IsIncognitoMode());
int webStateIndex = GetIndexOfWebStateWithId(ID); int webStateIndex = GetIndexOfWebStateWithId(ID);
if (webStateIndex != WebStateList::kInvalidIndex) { if (webStateIndex != WebStateList::kInvalidIndex) {
...@@ -193,7 +217,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -193,7 +217,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
__block BOOL webStateFound = NO; __block BOOL webStateFound = NO;
__block base::CallbackListSubscription subscription; __block base::CallbackListSubscription subscription;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
web::WebState* webState = GetWebStateWithId(tabID); web::WebState* webState = GetWebStateWithId(tabID);
if (!webState) if (!webState)
return; return;
...@@ -208,7 +232,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -208,7 +232,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
bool success = WaitUntilConditionOrTimeout(timeout, ^bool { bool success = WaitUntilConditionOrTimeout(timeout, ^bool {
__block BOOL scriptExecutionComplete = NO; __block BOOL scriptExecutionComplete = NO;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
scriptExecutionComplete = messageValue.has_value(); scriptExecutionComplete = messageValue.has_value();
}); });
return scriptExecutionComplete; return scriptExecutionComplete;
...@@ -223,14 +247,14 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -223,14 +247,14 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
} }
+ (void)enablePopups { + (void)enablePopups {
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
chrome_test_util::SetContentSettingsBlockPopups(CONTENT_SETTING_ALLOW); chrome_test_util::SetContentSettingsBlockPopups(CONTENT_SETTING_ALLOW);
}); });
} }
+ (NSString*)takeSnapshotOfTabWithID:(NSString*)ID { + (NSString*)takeSnapshotOfTabWithID:(NSString*)ID {
__block web::WebState* webState; __block web::WebState* webState;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
webState = GetWebStateWithId(ID); webState = GetWebStateWithId(ID);
}); });
...@@ -238,7 +262,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -238,7 +262,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
return nil; return nil;
__block UIImage* snapshot = nil; __block UIImage* snapshot = nil;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
CGRect bounds = webState->GetWebViewProxy().bounds; CGRect bounds = webState->GetWebViewProxy().bounds;
UIEdgeInsets insets = webState->GetWebViewProxy().contentInset; UIEdgeInsets insets = webState->GetWebViewProxy().contentInset;
CGRect adjustedBounds = UIEdgeInsetsInsetRect(bounds, insets); CGRect adjustedBounds = UIEdgeInsetsInsetRect(bounds, insets);
...@@ -252,7 +276,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) { ...@@ -252,7 +276,7 @@ int GetIndexOfWebStateWithId(NSString* tab_id) {
const NSTimeInterval kSnapshotTimeoutSeconds = 100; const NSTimeInterval kSnapshotTimeoutSeconds = 100;
bool success = WaitUntilConditionOrTimeout(kSnapshotTimeoutSeconds, ^bool { bool success = WaitUntilConditionOrTimeout(kSnapshotTimeoutSeconds, ^bool {
__block BOOL snapshotComplete = NO; __block BOOL snapshotComplete = NO;
grey_dispatch_sync_on_main_thread(^{ DispatchSyncOnMainThread(^{
if (snapshot != nil) if (snapshot != nil)
snapshotComplete = YES; snapshotComplete = YES;
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment