Rename Activity to UserIndicator

This commit is contained in:
Andy Uhnak
2022-02-22 15:26:51 +00:00
parent b80e3bc89e
commit 5bb32d4971
17 changed files with 152 additions and 151 deletions
@@ -0,0 +1,29 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
class UserIndicatorPresenterSpy: UserIndicatorPresentable {
var intel = [String]()
func present() {
intel.append(#function)
}
func dismiss() {
intel.append(#function)
}
}
@@ -0,0 +1,55 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import XCTest
class UserIndicatorQueueTests: XCTestCase {
var indicators: [UserIndicator]!
var center: UserIndicatorQueue!
override func setUp() {
indicators = []
center = UserIndicatorQueue()
}
func makeRequest() -> UserIndicatorRequest {
return UserIndicatorRequest(
presenter: UserIndicatorPresenterSpy(),
dismissal: .manual
)
}
func testStartsIndicatorWhenAdded() {
let indicator = center.add(makeRequest())
XCTAssertEqual(indicator.state, .executing)
}
func testSecondIndicatorIsPending() {
center.add(makeRequest()).store(in: &indicators)
let indicator = center.add(makeRequest())
XCTAssertEqual(indicator.state, .pending)
}
func testSecondIndicatorIsExecutingWhenFirstCompleted() {
let first = center.add(makeRequest())
let second = center.add(makeRequest())
first.cancel()
XCTAssertEqual(second.state, .executing)
}
}
@@ -0,0 +1,127 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import XCTest
class UserIndicatorTests: XCTestCase {
var presenter: UserIndicatorPresenterSpy!
override func setUp() {
super.setUp()
presenter = UserIndicatorPresenterSpy()
}
func makeIndicator(dismissal: UserIndicatorDismissal = .manual, callback: @escaping () -> Void = {}) -> UserIndicator {
let request = UserIndicatorRequest(
presenter: presenter,
dismissal: dismissal
)
return UserIndicator(
request: request,
completion: callback
)
}
// MARK: - State
func testNewIndicatorIsPending() {
let indicator = makeIndicator()
XCTAssertEqual(indicator.state, .pending)
}
func testStartedIndicatorIsExecuting() {
let indicator = makeIndicator()
indicator.start()
XCTAssertEqual(indicator.state, .executing)
}
func testCancelledIndicatorIsCompleted() {
let indicator = makeIndicator()
indicator.cancel()
XCTAssertEqual(indicator.state, .completed)
}
// MARK: - Presenter
func testStartingIndicatorPresentsUI() {
let indicator = makeIndicator()
indicator.start()
XCTAssertEqual(presenter.intel, ["present()"])
}
func testAllowStartingOnlyOnce() {
let indicator = makeIndicator()
indicator.start()
presenter.intel = []
indicator.start()
XCTAssertEqual(presenter.intel, [])
}
func testCancellingIndicatorDismissesUI() {
let indicator = makeIndicator()
indicator.start()
presenter.intel = []
indicator.cancel()
XCTAssertEqual(presenter.intel, ["dismiss()"])
}
func testAllowCancellingOnlyOnce() {
let indicator = makeIndicator()
indicator.start()
indicator.cancel()
presenter.intel = []
indicator.cancel()
XCTAssertEqual(presenter.intel, [])
}
// MARK: - Dismissal
func testDismissAfterTimeout() {
let interval: TimeInterval = 0.01
let indicator = makeIndicator(dismissal: .timeout(interval))
indicator.start()
let exp = expectation(description: "")
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
exp.fulfill()
}
waitForExpectations(timeout: 1)
XCTAssertEqual(indicator.state, .completed)
}
// MARK: - Completion callback
func testTriggersCallbackWhenCompleted() {
var didComplete = false
let indicator = makeIndicator {
didComplete = true
}
indicator.start()
indicator.cancel()
XCTAssertTrue(didComplete)
}
}
@@ -0,0 +1,102 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import UIKit
/// A `UserIndicator` represents the state of a temporary visual indicator, such as loading spinner, success notification or an error message. It does not directly manage the UI, instead it delegates to a `presenter`
/// whenever the UI should be shown or hidden.
///
/// More than one `UserIndicator` may be requested by the system at the same time (e.g. global syncing vs local refresh),
/// and the `UserIndicatorQueue` will ensure that only one indicator is shown at a given time, putting the other in a pending queue.
///
/// A client that requests an indicator can specify a default timeout after which the indicator is dismissed, or it has to be manually
/// responsible for dismissing it via `cancel` method, or by deallocating itself.
public class UserIndicator {
public enum State {
case pending
case executing
case completed
}
private let request: UserIndicatorRequest
private let completion: () -> Void
public private(set) var state: State
public init(request: UserIndicatorRequest, completion: @escaping () -> Void) {
self.request = request
self.completion = completion
state = .pending
}
deinit {
complete()
}
internal func start() {
guard state == .pending else {
return
}
state = .executing
request.presenter.present()
switch request.dismissal {
case .manual:
break
case .timeout(let interval):
Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
self?.complete()
}
}
}
/// Cancel the indicator, triggering any dismissal action / animation
///
/// Note: clients can call this method directly, if they have access to the `UserIndicator`.
/// Once cancelled, `UserIndicatorQueue` will automatically start the next `UserIndicator` in the queue.
public func cancel() {
complete()
}
private func complete() {
guard state != .completed else {
return
}
if state == .executing {
request.presenter.dismiss()
}
state = .completed
completion()
}
}
public extension UserIndicator {
func store<C>(in collection: inout C) where C: RangeReplaceableCollection, C.Element == UserIndicator {
collection.append(self)
}
}
public extension Collection where Element == UserIndicator {
func cancelAll() {
forEach {
$0.cancel()
}
}
}
@@ -0,0 +1,25 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Different ways in which a `UserIndicator` can be dismissed
public enum UserIndicatorDismissal {
/// The `UserIndicator` will not manage the dismissal, but will expect the calling client to do so manually
case manual
/// The `UserIndicator` will be automatically dismissed after `TimeInterval`
case timeout(TimeInterval)
}
@@ -0,0 +1,25 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// A presenter associated with and called by a `UserIndicator`, and responsible for the underlying view shown on the screen.
public protocol UserIndicatorPresentable {
/// Called when the `UserIndicator` is started (manually or by the `UserIndicatorQueue`)
func present()
/// Called when the `UserIndicator` is manually cancelled or completed
func dismiss()
}
@@ -0,0 +1,60 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// A FIFO queue which will ensure only one user indicator is shown at a given time.
///
/// `UserIndicatorQueue` offers a `shared` queue that can be used by any clients app-wide, but clients are also allowed
/// to create local `UserIndicatorQueue` if the context requres multiple simultaneous indicators.
public class UserIndicatorQueue {
private class Weak<T: AnyObject> {
weak var element: T?
init(_ element: T) {
self.element = element
}
}
public static let shared = UserIndicatorQueue()
private var queue = [Weak<UserIndicator>]()
/// Add a new indicator to the queue by providing a request.
///
/// The queue will start the indicator right away, if there are no currently running indicators,
/// otherwise the indicator will be put on hold.
public func add(_ request: UserIndicatorRequest) -> UserIndicator {
let indicator = UserIndicator(request: request) { [weak self] in
self?.startNextIfIdle()
}
queue.append(Weak(indicator))
startNextIfIdle()
return indicator
}
private func startNextIfIdle() {
cleanup()
if let indicator = queue.first?.element, indicator.state == .pending {
indicator.start()
}
}
private func cleanup() {
queue.removeAll {
$0.element == nil || $0.element?.state == .completed
}
}
}
@@ -0,0 +1,28 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// A request used to create an underlying `UserIndicator`, allowing clients to only specify the visual aspects of an indicator.
public struct UserIndicatorRequest {
internal let presenter: UserIndicatorPresentable
internal let dismissal: UserIndicatorDismissal
public init(presenter: UserIndicatorPresentable, dismissal: UserIndicatorDismissal) {
self.presenter = presenter
self.dismissal = dismissal
}
}