From 4b814c56abe9d05d4931e678e34be7d00d0af33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Caetano?= Date: Mon, 12 Mar 2018 14:11:01 -0300 Subject: [PATCH] Fix: configureWebView gets called multiple times fix #31 --- .../Core/DispatchQueue__Tests.swift | 101 +++++++++++++++--- .../Core/ReCaptchaWebViewManager__Tests.swift | 28 +++++ .../Classes/DispatchQueue+Throttle.swift | 26 +++++ .../Classes/ReCaptchaWebViewManager.swift | 6 +- 4 files changed, 148 insertions(+), 13 deletions(-) diff --git a/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift b/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift index bd63c63..a79cf73 100644 --- a/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/DispatchQueue__Tests.swift @@ -20,6 +20,8 @@ class DispatchQueue__Tests: XCTestCase { super.tearDown() } + // MARK: Throttle + func test__Throttle_Nil_Context() { // Execute closure called once let exp0 = expectation(description: "did call single closure") @@ -31,14 +33,15 @@ class DispatchQueue__Tests: XCTestCase { waitForExpectations(timeout: 1) // Does not execute first closure - let exp1 = expectation(description: "") + let exp1 = expectation(description: "did call last closure") DispatchQueue.main.throttle(deadline: .now() + 0.1) { XCTFail("Shouldn't be called") } - DispatchQueue.main.throttle(deadline: .now() + 0.1) { - exp1.fulfill() - } + DispatchQueue.main.throttle( + deadline: .now() + 0.1, + action: exp1.fulfill + ) waitForExpectations(timeout: 1) } @@ -48,9 +51,11 @@ class DispatchQueue__Tests: XCTestCase { let exp0 = expectation(description: "did call single closure") let c0 = UUID() - DispatchQueue.main.throttle(deadline: .now() + 0.1, context: c0) { - exp0.fulfill() - } + DispatchQueue.main.throttle( + deadline: .now() + 0.1, + context: c0, + action: exp0.fulfill + ) waitForExpectations(timeout: 1) @@ -61,17 +66,89 @@ class DispatchQueue__Tests: XCTestCase { XCTFail("Shouldn't be called") } - DispatchQueue.main.throttle(deadline: .now() + 0.1, context: c1) { - exp1.fulfill() - } + DispatchQueue.main.throttle( + deadline: .now() + 0.1, + context: c1, + action: exp1.fulfill + ) // Execute in a different context let exp2 = expectation(description: "execute on different context") let c2 = UUID() - DispatchQueue.main.throttle(deadline: .now() + 0.1, context: c2) { - exp2.fulfill() + DispatchQueue.main.throttle( + deadline: .now() + 0.1, + context: c2, + action: exp2.fulfill + ) + + waitForExpectations(timeout: 1) + } + + // MARK: Debounce + + func test__Debounce_Nil_Context() { + // Does not execute sequenced closures + let exp0 = expectation(description: "did call first closure") + + DispatchQueue.main.debounce( + interval: 0.1, + action: exp0.fulfill + ) + + DispatchQueue.main.debounce(interval: 0) { + XCTFail("Shouldn't be called") } waitForExpectations(timeout: 1) + + // Executes closure after previous has timed out + let exp1 = expectation(description: "did call closure") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + DispatchQueue.main.debounce( + interval: 0.1, + action: exp1.fulfill + ) + } + + waitForExpectations(timeout: 3) + } + + func test__Debounce_Context() { + // Does not execute sequenced closures + let exp0 = expectation(description: "did call first closure") + let c0 = UUID() + + DispatchQueue.main.debounce( + interval: 0.1, + context: c0, + action: exp0.fulfill + ) + + DispatchQueue.main.debounce(interval: 0, context: c0) { + XCTFail("Shouldn't be called") + } + + // Execute in a different context + let c1 = UUID() + let exp1 = expectation(description: "executes in different context") + DispatchQueue.main.debounce( + interval: 0, + context: c1, + action: exp1.fulfill + ) + + waitForExpectations(timeout: 1) + + // Executes closure after previous has timed out + let exp2 = expectation(description: "did call closure") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + DispatchQueue.main.debounce( + interval: 0.1, + context: c0, + action: exp2.fulfill + ) + } + + waitForExpectations(timeout: 5) } } diff --git a/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift b/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift index ed4fdf8..81a9c84 100644 --- a/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift +++ b/Example/ReCaptcha_Tests/Core/ReCaptchaWebViewManager__Tests.swift @@ -182,6 +182,34 @@ class ReCaptchaWebViewManager__Tests: XCTestCase { waitForExpectations(timeout: 10) } + func test__Configure_Web_View__Called_Once() { + var count = 0 + let exp0 = expectation(description: "configure webview") + + // Configure WebView + let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}") + manager.configureWebView { _ in + if count < 3 { + manager.webView.evaluateJavaScript("execute();") { XCTAssertNil($1) } + } + + count += 1 + exp0.fulfill() + } + + manager.validate(on: presenterView) { _ in + XCTFail("should not call completion") + } + + waitForExpectations(timeout: 10) + + let exp1 = expectation(description: "waiting for extra calls") + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: exp1.fulfill) + waitForExpectations(timeout: 2) + + XCTAssertEqual(count, 1) + } + // MARK: Stop func test__Stop() { diff --git a/ReCaptcha/Classes/DispatchQueue+Throttle.swift b/ReCaptcha/Classes/DispatchQueue+Throttle.swift index a69e21a..c9cf725 100644 --- a/ReCaptcha/Classes/DispatchQueue+Throttle.swift +++ b/ReCaptcha/Classes/DispatchQueue+Throttle.swift @@ -13,6 +13,9 @@ extension DispatchQueue { /// Stores a throttle DispatchWorkItem instance for a given context private static var workItems = [AnyHashable: DispatchWorkItem]() + /// Stores the last call times for a given context + private static var lastDebounceCallTimes = [AnyHashable: DispatchTime]() + /// An object representing a context if none is given private static let nilContext = UUID() @@ -35,4 +38,27 @@ extension DispatchQueue { DispatchQueue.workItems[context]?.cancel() DispatchQueue.workItems[context] = worker } + + /** + - parameters: + - interval: The interval in which new calls will be ignored + - context: The context in which the debounce should be executed + - action: The closure to be executed + + Executes a closure and ensures no other executions will be made during the interval. + */ + func debounce(interval: Double, context: AnyHashable = nilContext, action: @escaping () -> Void) { + let now = DispatchTime.now() + if let last = DispatchQueue.lastDebounceCallTimes[context], last + interval > now { + return + } + + DispatchQueue.lastDebounceCallTimes[context] = now + interval + async(execute: action) + + // Cleanup & release context + throttle(deadline: now + interval) { + DispatchQueue.lastDebounceCallTimes.removeValue(forKey: context) + } + } } diff --git a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift index cd0a37a..62e04c2 100644 --- a/ReCaptcha/Classes/ReCaptchaWebViewManager.swift +++ b/ReCaptcha/Classes/ReCaptchaWebViewManager.swift @@ -271,7 +271,11 @@ fileprivate extension ReCaptchaWebViewManager { } case .showReCaptcha: - configureWebView?(webView) + // Ensures `configureWebView` won't get called multiple times in a short period + DispatchQueue.main.debounce(interval: 1) { [weak self] in + guard let `self` = self else { return } + self.configureWebView?(self.webView) + } case .didLoad: // For testing purposes