Add CommonKit and Activity

Signed-off-by: Andy Uhnak <andyuhnak@gmail.com>
This commit is contained in:
Andy Uhnak
2022-02-15 10:55:06 +00:00
parent 002cf8b5fb
commit e1f87f67f6
17 changed files with 646 additions and 1 deletions

28
CommonKit/Common.xcconfig Normal file
View File

@@ -0,0 +1,28 @@
//
// Copyright 2021 Vector Creations 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.
//
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
#include "Config/AppIdentifiers.xcconfig"
#include "Config/AppVersion.xcconfig"
PRODUCT_NAME = CommonKit
PRODUCT_BUNDLE_IDENTIFIER = $(BASE_BUNDLE_IDENTIFIER).commonkit
INFOPLIST_FILE = CommonKit/Info.plist
SKIP_INSTALL = YES

27
CommonKit/CommonKit.h Normal file
View File

@@ -0,0 +1,27 @@
//
// 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/Foundation.h>
//! Project version number for CommonKit.
FOUNDATION_EXPORT double CommonKitVersionNumber;
//! Project version string for CommonKit.
FOUNDATION_EXPORT const unsigned char CommonKitVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <CommonKit/PublicHeader.h>

20
CommonKit/Debug.xcconfig Normal file
View File

@@ -0,0 +1,20 @@
//
// Copyright 2021 Vector Creations 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.
//
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
#include "Common.xcconfig"

22
CommonKit/Info.plist Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
</dict>
</plist>

View File

@@ -0,0 +1,20 @@
//
// Copyright 2021 Vector Creations 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.
//
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
#include "Common.xcconfig"

View File

@@ -0,0 +1,94 @@
//
// 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
/// An `Activity` represents the state of a temporary visual indicator, such as activity indicator, 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 `Activity` may be requested by the system at the same time (e.g. global syncing vs local refresh),
/// and the `ActivityCenter` will ensure that only one activity is shown at a given time, putting the other in a pending queue.
///
/// A client that requests an activity can specify a default timeout after which the activity is dismissed, or it has to be manually
/// responsible for dismissing it via `cancel` method, or by deallocating itself.
public class Activity {
enum State {
case pending
case executing
case completed
}
private let request: ActivityRequest
private let completion: () -> Void
private(set) var state: State
public init(request: ActivityRequest, completion: @escaping () -> Void) {
self.request = request
self.completion = completion
state = .pending
}
deinit {
cancel()
}
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 activity, triggering any dismissal action / animation
///
/// Note: clients can call this method directly, if they have access to the `Activity`.
/// Once cancelled, `ActivityCenter` will automatically start the next `Activity` in the queue.
func cancel() {
complete()
}
private func complete() {
guard state != .completed else {
return
}
if state == .executing {
request.presenter.dismiss()
}
state = .completed
completion()
}
}
public extension Activity {
func store<C>(in collection: inout C) where C: RangeReplaceableCollection, C.Element == Activity {
collection.append(self)
}
}

View File

@@ -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 shared activity center with a single FIFO queue which will ensure only one activity is shown at a given time.
///
/// `ActivityCenter` offers a `shared` center that can be used by any clients, but clients are also allowed
/// to create local `ActivityCenter` if the context requres multiple simultaneous activities.
public class ActivityCenter {
private class Weak<T: AnyObject> {
weak var element: T?
init(_ element: T) {
self.element = element
}
}
public static let shared = ActivityCenter()
private var queue = [Weak<Activity>]()
/// Add a new activity to the queue by providing a request.
///
/// The queue will start the activity right away, if there are no currently running activities,
/// otherwise the activity will be put on hold.
public func add(_ request: ActivityRequest) -> Activity {
let activity = Activity(request: request) { [weak self] in
self?.startNextIfIdle()
}
queue.append(Weak(activity))
startNextIfIdle()
return activity
}
private func startNextIfIdle() {
cleanup()
if let activity = queue.first?.element, activity.state == .pending {
activity.start()
}
}
private func cleanup() {
queue.removeAll {
$0.element == nil || $0.element?.state == .completed
}
}
}

View File

@@ -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 an `Activity` can be dismissed
public enum ActivityDismissal {
/// The `Activity` will not manage the dismissal, but will expect the calling client to do so manually
case manual
/// The `Activity` will be automatically dismissed after `TimeInterval`
case timeout(TimeInterval)
}

View File

@@ -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 an `Activity`, and responsible for the underlying view shown on the screen.
public protocol ActivityPresentable {
/// Called when the `Activity` is started (manually or by the `ActivityCenter`)
func present()
/// Called when the `Activity` is manually cancelled or completed
func dismiss()
}

View File

@@ -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 `Activity`, allowing clients to only specify the visual aspects of an activity.
public struct ActivityRequest {
internal let presenter: ActivityPresentable
internal let dismissal: ActivityDismissal
public init(presenter: ActivityPresentable, dismissal: ActivityDismissal) {
self.presenter = presenter
self.dismissal = dismissal
}
}

View File

@@ -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 ActivityCenterTests: XCTestCase {
var activities: [Activity]!
var center: ActivityCenter!
override func setUp() {
activities = []
center = ActivityCenter()
}
func makeRequest() -> ActivityRequest {
return ActivityRequest(
presenter: ActivityPresenterSpy(),
dismissal: .manual
)
}
func testStartsActivityWhenAdded() {
let activity = center.add(makeRequest())
XCTAssertEqual(activity.state, .executing)
}
func testSecondActivityIsPending() {
center.add(makeRequest()).store(in: &activities)
let activity = center.add(makeRequest())
XCTAssertEqual(activity.state, .pending)
}
func testSecondActivityIsExecutingWhenFirstCompleted() {
let first = center.add(makeRequest())
let second = center.add(makeRequest())
first.cancel()
XCTAssertEqual(second.state, .executing)
}
}

View File

@@ -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 ActivityPresenterSpy: ActivityPresentable {
var intel = [String]()
func present() {
intel.append(#function)
}
func dismiss() {
intel.append(#function)
}
}

View File

@@ -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 ActivityTests: XCTestCase {
var presenter: ActivityPresenterSpy!
override func setUp() {
super.setUp()
presenter = ActivityPresenterSpy()
}
func makeActivity(dismissal: ActivityDismissal = .manual, callback: @escaping () -> Void = {}) -> Activity {
let request = ActivityRequest(
presenter: presenter,
dismissal: dismissal
)
return Activity(
request: request,
completion: callback
)
}
// MARK: - State
func testNewActivityIsPending() {
let activity = makeActivity()
XCTAssertEqual(activity.state, .pending)
}
func testStartedActivityIsExecuting() {
let activity = makeActivity()
activity.start()
XCTAssertEqual(activity.state, .executing)
}
func testCancelledActivityIsCompleted() {
let activity = makeActivity()
activity.cancel()
XCTAssertEqual(activity.state, .completed)
}
// MARK: - Presenter
func testStartingActivityPresentsUI() {
let activity = makeActivity()
activity.start()
XCTAssertEqual(presenter.intel, ["present()"])
}
func testAllowStartingOnlyOnce() {
let activity = makeActivity()
activity.start()
presenter.intel = []
activity.start()
XCTAssertEqual(presenter.intel, [])
}
func testCancellingActivityDismissesUI() {
let activity = makeActivity()
activity.start()
presenter.intel = []
activity.cancel()
XCTAssertEqual(presenter.intel, ["dismiss()"])
}
func testAllowCancellingOnlyOnce() {
let activity = makeActivity()
activity.start()
activity.cancel()
presenter.intel = []
activity.cancel()
XCTAssertEqual(presenter.intel, [])
}
// MARK: - Dismissal
func testDismissAfterTimeout() {
let interval: TimeInterval = 0.01
let activity = makeActivity(dismissal: .timeout(interval))
activity.start()
let exp = expectation(description: "")
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
exp.fulfill()
}
waitForExpectations(timeout: 1)
XCTAssertEqual(activity.state, .completed)
}
// MARK: - Completion callback
func testTriggersCallbackWhenCompleted() {
var didComplete = false
let activity = makeActivity {
didComplete = true
}
activity.start()
activity.cancel()
XCTAssertTrue(didComplete)
}
}

40
CommonKit/target.yml Normal file
View File

@@ -0,0 +1,40 @@
name: CommonKit
schemes:
CommonKit:
analyze:
config: Debug
archive:
config: Release
build:
targets:
CommonKit:
- running
- testing
- profiling
- analyzing
- archiving
profile:
config: Release
run:
config: Debug
disableMainThreadChecker: true
test:
config: Debug
disableMainThreadChecker: true
targets:
- CommonKitUnitTests
targets:
CommonKit:
type: framework
platform: iOS
configFiles:
Debug: Debug.xcconfig
Release: Release.xcconfig
sources:
- path: .
excludes:
- "**/Tests/**"

View File

@@ -0,0 +1,42 @@
name: CommonKitUnitTests
schemes:
CommonKitUnitTests:
analyze:
config: Debug
archive:
config: Release
build:
targets:
CommonKitUnitTests:
- running
- testing
- profiling
- analyzing
- archiving
profile:
config: Release
run:
config: Debug
disableMainThreadChecker: true
test:
config: Debug
disableMainThreadChecker: true
targets:
- CommonKitUnitTests
targets:
CommonKitUnitTests:
type: bundle.unit-test
platform: iOS
dependencies:
- target: CommonKit
configFiles:
Debug: Debug.xcconfig
Release: Release.xcconfig
sources:
- path: .

View File

@@ -35,6 +35,7 @@ targets:
- target: SiriIntents
- target: RiotNSE
- target: DesignKit
- target: CommonKit
- package: Mapbox
configFiles:

View File

@@ -35,9 +35,11 @@ include:
- path: RiotSwiftUI/target.yml
- path: RiotSwiftUI/targetUnitTests.yml
- path: RiotSwiftUI/targetUITests.yml
- path: CommonKit/target.yml
- path: CommonKit/targetUnitTests.yml
packages:
Mapbox:
url: https://github.com/maplibre/maplibre-gl-native-distribution
minVersion: 5.12.2
maxVersion: 5.13.0
maxVersion: 5.13.0