mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Update UserIndicators with newer implementation from Element (modified to work here)
This commit is contained in:
committed by
Stefan Ceriu
parent
32d40eec47
commit
4b728f3d09
@@ -19,9 +19,10 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
private let authenticationCoordinator: AuthenticationCoordinator!
|
||||
|
||||
private let memberDetailProviderManager: MemberDetailProviderManager
|
||||
|
||||
private var loadingActivity: Activity?
|
||||
private var errorActivity: Activity?
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
private var errorIndicator: UserIndicator?
|
||||
|
||||
var childCoordinators: [Coordinator] = []
|
||||
|
||||
@@ -35,6 +36,8 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
|
||||
memberDetailProviderManager = MemberDetailProviderManager()
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: mainNavigationController)
|
||||
|
||||
guard let bundleIdentifier = Bundle.main.bundleIdentifier else {
|
||||
fatalError("Should have a valid bundle identifier at this point")
|
||||
}
|
||||
@@ -143,30 +146,14 @@ class AppCoordinator: AuthenticationCoordinatorDelegate, Coordinator {
|
||||
}
|
||||
|
||||
private func showLoadingIndicator() {
|
||||
let presenter = FullscreenLoadingActivityPresenter(label: "Loading",
|
||||
on: mainNavigationController)
|
||||
|
||||
let request = ActivityRequest(
|
||||
presenter: presenter,
|
||||
dismissal: .manual
|
||||
)
|
||||
|
||||
loadingActivity = ActivityCenter.shared.add(request)
|
||||
loadingIndicator = indicatorPresenter.present(.loading(label: "Loading", isInteractionBlocking: true))
|
||||
}
|
||||
|
||||
private func hideLoadingIndicator() {
|
||||
loadingActivity = nil
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
private func showLoginErrorToast() {
|
||||
let presenter = ToastActivityPresenter(viewState: .init(style: .success, label: "Failed logging in"),
|
||||
navigationController: mainNavigationController)
|
||||
|
||||
let request = ActivityRequest(
|
||||
presenter: presenter,
|
||||
dismissal: .timeout(3.0)
|
||||
)
|
||||
|
||||
errorActivity = ActivityCenter.shared.add(request)
|
||||
errorIndicator = indicatorPresenter.present(.success(label: "Failed logging in"))
|
||||
}
|
||||
}
|
||||
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
//
|
||||
// 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 `ActivityPresenter` responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls.
|
||||
/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes.
|
||||
class FullscreenLoadingActivityPresenter: ActivityPresentable {
|
||||
private let label: String
|
||||
private weak var viewController: UIViewController?
|
||||
private weak var view: UIView?
|
||||
|
||||
init(label: String, on viewController: UIViewController) {
|
||||
self.label = label
|
||||
self.viewController = viewController
|
||||
}
|
||||
|
||||
func present() {
|
||||
let view = LabelledActivityIndicatorView(text: label)
|
||||
self.view = view
|
||||
|
||||
guard let window = viewController?.view.window else {
|
||||
return
|
||||
}
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
window.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.topAnchor.constraint(equalTo: window.topAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: window.bottomAnchor),
|
||||
view.leadingAnchor.constraint(equalTo: window.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: window.trailingAnchor)
|
||||
])
|
||||
|
||||
view.alpha = 0
|
||||
CATransaction.commit()
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
view.alpha = 1
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
guard let view = view, view.superview != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) {
|
||||
view.alpha = 0
|
||||
} completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
//
|
||||
// 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 `ActivityPresenter` responsible for showing / hiding a toast view for activity indicators or success messages.
|
||||
/// It is managed by an `Activity`, meaning the `present` and `dismiss` methods will be called when the parent `Activity` starts or completes.
|
||||
class ToastActivityPresenter: ActivityPresentable {
|
||||
private let viewState: RoundedToastView.ViewState
|
||||
private weak var navigationController: UINavigationController?
|
||||
private weak var view: UIView?
|
||||
|
||||
init(viewState: RoundedToastView.ViewState, navigationController: UINavigationController) {
|
||||
self.viewState = viewState
|
||||
self.navigationController = navigationController
|
||||
}
|
||||
|
||||
func present() {
|
||||
guard let navigationController = navigationController,
|
||||
let window = navigationController.view.window else {
|
||||
return
|
||||
}
|
||||
|
||||
let view = RoundedToastView(viewState: viewState)
|
||||
self.view = view
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
window.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.centerXAnchor.constraint(equalTo: navigationController.navigationBar.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: navigationController.navigationBar.bottomAnchor)
|
||||
])
|
||||
|
||||
view.alpha = 0
|
||||
CATransaction.flush()
|
||||
view.transform = .init(translationX: 0, y: 5)
|
||||
|
||||
UIView.animate(withDuration: 0.2) {
|
||||
view.alpha = 1
|
||||
view.transform = .identity
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
guard let view = view, view.superview != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState) {
|
||||
view.alpha = 0
|
||||
view.transform = .init(translationX: 0, y: -5)
|
||||
} completion: { _ in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
Copyright 2019 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
|
||||
|
||||
/// Used to present activity indicator on a view
|
||||
final class ActivityIndicatorPresenter: ActivityIndicatorPresenterType {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private enum Constants {
|
||||
static let animationDuration: TimeInterval = 0.3
|
||||
static let backgroundOverlayColor = UIColor.clear
|
||||
static let backgroundOverlayAlpha: CGFloat = 1.0
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private weak var backgroundOverlayView: UIView?
|
||||
private weak var activityIndicatorView: ActivityIndicatorView?
|
||||
private weak var presentingView: UIView?
|
||||
|
||||
var isPresenting: Bool {
|
||||
return self.activityIndicatorView != nil
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
if self.presentingView != nil {
|
||||
if let completion = completion {
|
||||
completion()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.presentingView = view
|
||||
|
||||
view.isUserInteractionEnabled = false
|
||||
|
||||
let backgroundOverlayView = self.createBackgroundOverlayView(with: view.frame)
|
||||
|
||||
let activityIndicatorView = ActivityIndicatorView()
|
||||
|
||||
// Add activityIndicatorView on backgroundOverlayView centered
|
||||
backgroundOverlayView.addSubview(activityIndicatorView)
|
||||
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
|
||||
activityIndicatorView.centerXAnchor.constraint(equalTo: backgroundOverlayView.centerXAnchor).isActive = true
|
||||
activityIndicatorView.centerYAnchor.constraint(equalTo: backgroundOverlayView.centerYAnchor).isActive = true
|
||||
|
||||
activityIndicatorView.startAnimating()
|
||||
|
||||
backgroundOverlayView.alpha = 0
|
||||
backgroundOverlayView.isHidden = false
|
||||
|
||||
view.vc_addSubViewMatchingParent(backgroundOverlayView)
|
||||
|
||||
self.backgroundOverlayView = backgroundOverlayView
|
||||
self.activityIndicatorView = activityIndicatorView
|
||||
|
||||
let animationInstructions = {
|
||||
backgroundOverlayView.alpha = Constants.backgroundOverlayAlpha
|
||||
}
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: Constants.animationDuration, animations: {
|
||||
animationInstructions()
|
||||
}, completion: { _ in
|
||||
completion?()
|
||||
})
|
||||
} else {
|
||||
animationInstructions()
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
guard let presentingView = self.presentingView,
|
||||
let backgroundOverlayView = self.backgroundOverlayView,
|
||||
let activityIndicatorView = self.activityIndicatorView else {
|
||||
return
|
||||
}
|
||||
|
||||
presentingView.isUserInteractionEnabled = true
|
||||
self.presentingView = nil
|
||||
|
||||
let animationInstructions = {
|
||||
activityIndicatorView.alpha = 0
|
||||
}
|
||||
|
||||
let animationCompletionInstructions = {
|
||||
activityIndicatorView.stopAnimating()
|
||||
backgroundOverlayView.isHidden = true
|
||||
backgroundOverlayView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: Constants.animationDuration, animations: {
|
||||
animationInstructions()
|
||||
}, completion: { _ in
|
||||
animationCompletionInstructions()
|
||||
})
|
||||
} else {
|
||||
animationInstructions()
|
||||
animationCompletionInstructions()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func createBackgroundOverlayView(with frame: CGRect = CGRect.zero) -> UIView {
|
||||
let backgroundOverlayView = UIView(frame: frame)
|
||||
backgroundOverlayView.backgroundColor = Constants.backgroundOverlayColor
|
||||
backgroundOverlayView.alpha = Constants.backgroundOverlayAlpha
|
||||
return backgroundOverlayView
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIView {
|
||||
|
||||
/// Add a subview matching parent view using autolayout
|
||||
@objc func vc_addSubViewMatchingParent(_ subView: UIView) {
|
||||
self.addSubview(subView)
|
||||
subView.translatesAutoresizingMaskIntoConstraints = false
|
||||
let views = ["view": subView]
|
||||
["H:|[view]|", "V:|[view]|"].forEach { vfl in
|
||||
let constraints = NSLayoutConstraint.constraints(withVisualFormat: vfl,
|
||||
options: [],
|
||||
metrics: nil,
|
||||
views: views)
|
||||
constraints.forEach { $0.isActive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2019 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 UIKit
|
||||
|
||||
/// Protocol used to present activity indicator on a view
|
||||
protocol ActivityIndicatorPresenterType {
|
||||
func presentActivityIndicator(on view: UIView, animated: Bool, completion: (() -> Void)?)
|
||||
func removeCurrentActivityIndicator(animated: Bool, completion: (() -> Void)?)
|
||||
}
|
||||
|
||||
// `ActivityIndicatorPresenterType` default implementation
|
||||
extension ActivityIndicatorPresenterType {
|
||||
func presentActivityIndicator(on view: UIView, animated: Bool) {
|
||||
self.presentActivityIndicator(on: view, animated: animated, completion: nil)
|
||||
}
|
||||
|
||||
func removeCurrentActivityIndicator(animated: Bool) {
|
||||
self.removeCurrentActivityIndicator(animated: animated, completion: nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2019 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 UIKit
|
||||
|
||||
final class ActivityIndicatorView: UIView {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
private enum Constants {
|
||||
static let cornerRadius: CGFloat = 5.0
|
||||
static let activityIndicatorMargin = CGSize(width: 30.0, height: 30.0)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Outlets
|
||||
|
||||
@IBOutlet private weak var activityIndicatorView: UIActivityIndicatorView!
|
||||
@IBOutlet private weak var activityIndicatorBackgroundView: UIView!
|
||||
|
||||
// MARK: Public
|
||||
|
||||
var color: UIColor? {
|
||||
get {
|
||||
return activityIndicatorView.color
|
||||
}
|
||||
set {
|
||||
activityIndicatorView.color = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func commonInit() {
|
||||
self.activityIndicatorBackgroundView.layer.masksToBounds = true
|
||||
}
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: CGRect.zero)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
self.loadNibContent()
|
||||
self.commonInit()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.loadNibContent()
|
||||
self.commonInit()
|
||||
}
|
||||
|
||||
// MARK: - Overrides
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: self.activityIndicatorView.intrinsicContentSize.width + Constants.activityIndicatorMargin.width,
|
||||
height: self.activityIndicatorView.intrinsicContentSize.height + Constants.activityIndicatorMargin.height)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.activityIndicatorBackgroundView.layer.cornerRadius = Constants.cornerRadius
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func startAnimating() {
|
||||
self.activityIndicatorView.startAnimating()
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
self.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIView {
|
||||
static var nib: UINib {
|
||||
return UINib(nibName: String(describing: self), bundle: Bundle(for: self))
|
||||
}
|
||||
|
||||
func loadNibContent() {
|
||||
let layoutAttributes: [NSLayoutConstraint.Attribute] = [.top, .leading, .bottom, .trailing]
|
||||
for case let view as UIView in type(of: self).nib.instantiate(withOwner: self, options: nil) {
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.addSubview(view)
|
||||
NSLayoutConstraint.activate(layoutAttributes.map { attribute in
|
||||
NSLayoutConstraint(
|
||||
item: view, attribute: attribute,
|
||||
relatedBy: .equal,
|
||||
toItem: self, attribute: attribute,
|
||||
multiplier: 1, constant: 0.0
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ActivityIndicatorView" customModule="Riot" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="activityIndicatorBackgroundView" destination="2qY-Ui-WjQ" id="obw-ml-Ppb"/>
|
||||
<outlet property="activityIndicatorView" destination="32q-cl-Ru4" id="38r-gn-BJg"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="DuQ-Nt-i8B">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="204"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="2qY-Ui-WjQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="204"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="32q-cl-Ru4">
|
||||
<rect key="frame" x="15" y="15" width="345" height="174"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.80000000000000004" green="0.80000000000000004" blue="0.80000000000000004" alpha="0.89787029109589045" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="32q-cl-Ru4" firstAttribute="top" secondItem="2qY-Ui-WjQ" secondAttribute="top" constant="15" id="hLL-Ao-h6a"/>
|
||||
<constraint firstItem="32q-cl-Ru4" firstAttribute="centerX" secondItem="2qY-Ui-WjQ" secondAttribute="centerX" id="n1z-Rb-ce3"/>
|
||||
<constraint firstItem="32q-cl-Ru4" firstAttribute="leading" secondItem="2qY-Ui-WjQ" secondAttribute="leading" constant="15" id="tTB-ZQ-Xrm"/>
|
||||
<constraint firstItem="32q-cl-Ru4" firstAttribute="centerY" secondItem="2qY-Ui-WjQ" secondAttribute="centerY" id="ty6-5W-zen"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Zud-2y-G9d" firstAttribute="bottom" secondItem="2qY-Ui-WjQ" secondAttribute="bottom" id="F2N-Wt-CCH"/>
|
||||
<constraint firstItem="2qY-Ui-WjQ" firstAttribute="top" secondItem="Zud-2y-G9d" secondAttribute="top" id="V7A-HL-11f"/>
|
||||
<constraint firstItem="2qY-Ui-WjQ" firstAttribute="leading" secondItem="Zud-2y-G9d" secondAttribute="leading" id="jbB-4Q-tNb"/>
|
||||
<constraint firstItem="Zud-2y-G9d" firstAttribute="trailing" secondItem="2qY-Ui-WjQ" secondAttribute="trailing" id="psU-2p-PBH"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<viewLayoutGuide key="safeArea" id="Zud-2y-G9d"/>
|
||||
<point key="canvasLocation" x="55" y="-605"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// 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 UIKit
|
||||
|
||||
/// A presenter responsible for showing / hiding a full-screen loading view that obscures (and thus disables) all other controls.
|
||||
/// It is managed by a `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes.
|
||||
class FullscreenLoadingViewPresenter: UserIndicatorViewPresentable {
|
||||
private let label: String
|
||||
private let presentationContext: UserIndicatorPresentationContext
|
||||
private weak var view: UIView?
|
||||
private var animator: UIViewPropertyAnimator?
|
||||
|
||||
init(label: String, presentationContext: UserIndicatorPresentationContext) {
|
||||
self.label = label
|
||||
self.presentationContext = presentationContext
|
||||
}
|
||||
|
||||
func present() {
|
||||
// Find the current top navigation controller
|
||||
var presentingController: UIViewController? = presentationContext.indicatorPresentingViewController
|
||||
while presentingController?.navigationController != nil {
|
||||
presentingController = presentingController?.navigationController
|
||||
}
|
||||
guard let presentingController = presentingController else {
|
||||
return
|
||||
}
|
||||
|
||||
let view = LabelledActivityIndicatorView(text: label)
|
||||
self.view = view
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
presentingController.view.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.topAnchor.constraint(equalTo: presentingController.view.topAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: presentingController.view.bottomAnchor),
|
||||
view.leadingAnchor.constraint(equalTo: presentingController.view.leadingAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: presentingController.view.trailingAnchor)
|
||||
])
|
||||
|
||||
view.alpha = 0
|
||||
animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
|
||||
view.alpha = 1
|
||||
}
|
||||
animator?.startAnimation()
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
guard let view = view, view.superview != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
animator?.stopAnimation(true)
|
||||
animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) {
|
||||
view.alpha = 0
|
||||
}
|
||||
animator?.addCompletion { _ in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
animator?.startAnimation()
|
||||
}
|
||||
}
|
||||
+14
-29
@@ -14,17 +14,21 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class RoundedToastView: UIView {
|
||||
private struct ShadowStyle {
|
||||
let offset: CGSize
|
||||
let radius: CGFloat
|
||||
let opacity: Float
|
||||
}
|
||||
|
||||
private struct Constants {
|
||||
static let padding = UIEdgeInsets(top: 10, left: 12, bottom: 10, right: 12)
|
||||
static let activityIndicatorScale = CGFloat(0.75)
|
||||
static let imageViewSize = CGFloat(15)
|
||||
static let shadowOffset = CGSize(width: 0, height: 4)
|
||||
static let shadowRadius = CGFloat(12)
|
||||
static let shadowOpacity = Float(0.1)
|
||||
static let lightShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 12, opacity: 0.1)
|
||||
static let darkShadow = ShadowStyle(offset: .init(width: 0, height: 4), radius: 4, opacity: 0.2)
|
||||
}
|
||||
|
||||
private lazy var activityIndicator: UIActivityIndicatorView = {
|
||||
@@ -34,7 +38,7 @@ class RoundedToastView: UIView {
|
||||
return indicator
|
||||
}()
|
||||
|
||||
private lazy var imagView: UIImageView = {
|
||||
private lazy var imageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
@@ -57,7 +61,7 @@ class RoundedToastView: UIView {
|
||||
return UILabel()
|
||||
}()
|
||||
|
||||
init(viewState: ViewState) {
|
||||
init(viewState: ToastViewState) {
|
||||
super.init(frame: .zero)
|
||||
setup(viewState: viewState)
|
||||
}
|
||||
@@ -66,11 +70,10 @@ class RoundedToastView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setup(viewState: ViewState) {
|
||||
private func setup(viewState: ToastViewState) {
|
||||
|
||||
backgroundColor = .gray.withAlphaComponent(0.75)
|
||||
|
||||
setupLayer()
|
||||
setupStackView()
|
||||
stackView.addArrangedSubview(toastView(for: viewState.style))
|
||||
stackView.addArrangedSubview(label)
|
||||
@@ -88,36 +91,18 @@ class RoundedToastView: UIView {
|
||||
])
|
||||
}
|
||||
|
||||
private func setupLayer() {
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOffset = Constants.shadowOffset
|
||||
layer.shadowRadius = Constants.shadowRadius
|
||||
layer.shadowOpacity = Constants.shadowOpacity
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
layer.cornerRadius = layer.frame.height / 2
|
||||
}
|
||||
|
||||
private func toastView(for style: Style) -> UIView {
|
||||
private func toastView(for style: ToastViewState.Style) -> UIView {
|
||||
switch style {
|
||||
case .loading:
|
||||
return activityIndicator
|
||||
case .success:
|
||||
imagView.image = UIImage(systemName: "checkmark.circle")
|
||||
return imagView
|
||||
imageView.image = UIImage(systemName: "checkmark.circle")
|
||||
return imageView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoundedToastView {
|
||||
enum Style {
|
||||
case loading
|
||||
case success
|
||||
}
|
||||
struct ViewState {
|
||||
let style: Style
|
||||
let label: String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// 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 UIKit
|
||||
|
||||
/// A presenter responsible for showing / hiding a toast view for loading spinners or success messages.
|
||||
/// It is managed by an `UserIndicator`, meaning the `present` and `dismiss` methods will be called when the parent `UserIndicator` starts or completes.
|
||||
class ToastViewPresenter: UserIndicatorViewPresentable {
|
||||
struct Constants {
|
||||
static let navigationBarPatting = CGFloat(10)
|
||||
}
|
||||
|
||||
private let viewState: ToastViewState
|
||||
private let presentationContext: UserIndicatorPresentationContext
|
||||
private weak var view: UIView?
|
||||
private var animator: UIViewPropertyAnimator?
|
||||
|
||||
init(viewState: ToastViewState, presentationContext: UserIndicatorPresentationContext) {
|
||||
self.viewState = viewState
|
||||
self.presentationContext = presentationContext
|
||||
}
|
||||
|
||||
func present() {
|
||||
guard let viewController = presentationContext.indicatorPresentingViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
let view = RoundedToastView(viewState: viewState)
|
||||
self.view = view
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
if let navigation = viewController.topNavigationController {
|
||||
navigation.view.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.centerXAnchor.constraint(equalTo: navigation.view.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: navigation.navigationBar.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.navigationBarPatting)
|
||||
])
|
||||
} else {
|
||||
viewController.view.addSubview(view)
|
||||
NSLayoutConstraint.activate([
|
||||
view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor),
|
||||
view.topAnchor.constraint(equalTo: viewController.view.topAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
view.alpha = 0
|
||||
view.transform = .init(translationX: 0, y: 5)
|
||||
animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
|
||||
view.alpha = 1
|
||||
view.transform = .identity
|
||||
}
|
||||
animator?.startAnimation()
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
guard let view = view, view.superview != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
animator?.stopAnimation(true)
|
||||
animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) {
|
||||
view.alpha = 0
|
||||
view.transform = .init(translationX: 0, y: -5)
|
||||
}
|
||||
animator?.addCompletion { _ in
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
animator?.startAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private extension UIViewController {
|
||||
var topNavigationController: UINavigationController? {
|
||||
var controller: UINavigationController? = self as? UINavigationController ?? navigationController
|
||||
while controller?.navigationController != nil {
|
||||
controller = controller?.navigationController
|
||||
}
|
||||
return controller
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
struct ToastViewState {
|
||||
enum Style {
|
||||
case loading
|
||||
case success
|
||||
}
|
||||
|
||||
let style: Style
|
||||
let label: String
|
||||
}
|
||||
+14
-13
@@ -17,27 +17,27 @@
|
||||
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`
|
||||
/// 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 `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.
|
||||
/// 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 activity can specify a default timeout after which the activity is dismissed, or it has to be manually
|
||||
/// 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 Activity {
|
||||
public class UserIndicator {
|
||||
public enum State {
|
||||
case pending
|
||||
case executing
|
||||
case completed
|
||||
}
|
||||
|
||||
private let request: ActivityRequest
|
||||
private let request: UserIndicatorRequest
|
||||
private let completion: () -> Void
|
||||
|
||||
public private(set) var state: State
|
||||
|
||||
public init(request: ActivityRequest, completion: @escaping () -> Void) {
|
||||
public init(request: UserIndicatorRequest, completion: @escaping () -> Void) {
|
||||
self.request = request
|
||||
self.completion = completion
|
||||
|
||||
@@ -66,10 +66,11 @@ public class Activity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the activity, triggering any dismissal action / animation
|
||||
/// Cancel the indicator, 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.
|
||||
/// Note: clients can call this method directly, if they have access to the `UserIndicator`. Alternatively
|
||||
/// deallocating the `UserIndicator` will call `cancel` automatically.
|
||||
/// Once cancelled, `UserIndicatorQueue` will automatically start the next `UserIndicator` in the queue.
|
||||
public func cancel() {
|
||||
complete()
|
||||
}
|
||||
@@ -87,13 +88,13 @@ public class Activity {
|
||||
}
|
||||
}
|
||||
|
||||
public extension Activity {
|
||||
func store<C>(in collection: inout C) where C: RangeReplaceableCollection, C.Element == Activity {
|
||||
public extension UserIndicator {
|
||||
func store<C>(in collection: inout C) where C: RangeReplaceableCollection, C.Element == UserIndicator {
|
||||
collection.append(self)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Collection where Element == Activity {
|
||||
public extension Collection where Element == UserIndicator {
|
||||
func cancelAll() {
|
||||
forEach {
|
||||
$0.cancel()
|
||||
+4
-4
@@ -16,10 +16,10 @@
|
||||
|
||||
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
|
||||
/// 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 `Activity` will be automatically dismissed after `TimeInterval`
|
||||
/// The `UserIndicator` will be automatically dismissed after `TimeInterval`
|
||||
case timeout(TimeInterval)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2022 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
|
||||
|
||||
/// The presentation context is used by `UserIndicatorViewPresentable`s to display content
|
||||
/// on the screen and it serves two primary purposes:
|
||||
///
|
||||
/// - abstraction on top of UIKit (passing context instead of view controllers)
|
||||
/// - immutable context passed at init with variable presenting view controller
|
||||
/// (e.g. depending on collapsed / uncollapsed iPad presentation that changes
|
||||
/// at runtime)
|
||||
public protocol UserIndicatorPresentationContext {
|
||||
var indicatorPresentingViewController: UIViewController? { get }
|
||||
}
|
||||
|
||||
/// A simple implementation of `UserIndicatorPresentationContext` that uses a weak reference
|
||||
/// to the passed-in view controller as the presentation context.
|
||||
public class StaticUserIndicatorPresentationContext: UserIndicatorPresentationContext {
|
||||
// The presenting view controller will be the parent of the user indicator,
|
||||
// and the indicator holds a strong reference to the context, so the view controller
|
||||
// must be decleared `weak` to avoid a retain cycle
|
||||
public private(set) weak var indicatorPresentingViewController: UIViewController?
|
||||
|
||||
public init(viewController: UIViewController) {
|
||||
self.indicatorPresentingViewController = viewController
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// 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 UIKit
|
||||
|
||||
/// A set of user interactors commonly used across the app
|
||||
enum UserIndicatorType {
|
||||
case loading(label: String, isInteractionBlocking: Bool)
|
||||
case success(label: String)
|
||||
}
|
||||
|
||||
/// A presenter which can handle `UserIndicatorType` by creating the underlying `UserIndicator`
|
||||
/// and adding it to its `UserIndicatorQueue`
|
||||
protocol UserIndicatorTypePresenterProtocol {
|
||||
/// Present a new type of user indicator, such as loading spinner or success message.
|
||||
///
|
||||
/// The presenter will internally convert the type into a `UserIndicator` and add it to its internal queue
|
||||
/// of other indicators.
|
||||
///
|
||||
/// If the queue is empty, the indicator will be displayed immediately, otherwise it will be pending
|
||||
/// until the previously added indicators have completed / been cancelled.
|
||||
///
|
||||
/// To remove an indicator, `cancel` or deallocate the returned `UserIndicator`
|
||||
func present(_ type: UserIndicatorType) -> UserIndicator
|
||||
|
||||
/// The queue of user indicators owned by the presenter
|
||||
///
|
||||
/// Clients can access the queue to add custom `UserIndicatorRequest`s
|
||||
/// above and beyond those defined by `UserIndicatorType`
|
||||
var queue: UserIndicatorQueue { get }
|
||||
}
|
||||
|
||||
class UserIndicatorTypePresenter: UserIndicatorTypePresenterProtocol {
|
||||
private let presentationContext: UserIndicatorPresentationContext
|
||||
let queue: UserIndicatorQueue
|
||||
|
||||
init(presentationContext: UserIndicatorPresentationContext) {
|
||||
self.presentationContext = presentationContext
|
||||
self.queue = UserIndicatorQueue()
|
||||
}
|
||||
|
||||
convenience init(presentingViewController: UIViewController) {
|
||||
let context = StaticUserIndicatorPresentationContext(viewController: presentingViewController)
|
||||
self.init(presentationContext: context)
|
||||
}
|
||||
|
||||
func present(_ type: UserIndicatorType) -> UserIndicator {
|
||||
let request = userIndicatorRequest(for: type)
|
||||
return queue.add(request)
|
||||
}
|
||||
|
||||
private func userIndicatorRequest(for type: UserIndicatorType) -> UserIndicatorRequest {
|
||||
switch type {
|
||||
case .loading(let label, let isInteractionBlocking):
|
||||
if isInteractionBlocking {
|
||||
return fullScreenLoadingRequest(label: label)
|
||||
} else {
|
||||
return loadingRequest(label: label)
|
||||
}
|
||||
case .success(let label):
|
||||
return successRequest(label: label)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadingRequest(label: String) -> UserIndicatorRequest {
|
||||
let presenter = ToastViewPresenter(
|
||||
viewState: .init(
|
||||
style: .loading,
|
||||
label: label
|
||||
),
|
||||
presentationContext: presentationContext
|
||||
)
|
||||
return UserIndicatorRequest(
|
||||
presenter: presenter,
|
||||
dismissal: .manual
|
||||
)
|
||||
}
|
||||
|
||||
private func fullScreenLoadingRequest(label: String) -> UserIndicatorRequest {
|
||||
let presenter = FullscreenLoadingViewPresenter(
|
||||
label: label,
|
||||
presentationContext: presentationContext
|
||||
)
|
||||
return UserIndicatorRequest(
|
||||
presenter: presenter,
|
||||
dismissal: .manual
|
||||
)
|
||||
}
|
||||
|
||||
private func successRequest(label: String) -> UserIndicatorRequest {
|
||||
let presenter = ToastViewPresenter(
|
||||
viewState: .init(
|
||||
style: .success,
|
||||
label: label
|
||||
),
|
||||
presentationContext: presentationContext
|
||||
)
|
||||
return UserIndicatorRequest(
|
||||
presenter: presenter,
|
||||
dismissal: .timeout(1.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
+18
-15
@@ -16,11 +16,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A shared activity center with a single FIFO queue which will ensure only one activity is shown at a given time.
|
||||
/// A FIFO queue which will ensure only one user indicator 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 {
|
||||
/// `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) {
|
||||
@@ -28,27 +28,30 @@ public class ActivityCenter {
|
||||
}
|
||||
}
|
||||
|
||||
public static let shared = ActivityCenter()
|
||||
private var queue = [Weak<Activity>]()
|
||||
private var queue: [Weak<UserIndicator>]
|
||||
|
||||
/// Add a new activity to the queue by providing a request.
|
||||
public init() {
|
||||
queue = []
|
||||
}
|
||||
|
||||
/// Add a new indicator 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
|
||||
/// 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(activity))
|
||||
queue.append(Weak(indicator))
|
||||
startNextIfIdle()
|
||||
return activity
|
||||
return indicator
|
||||
}
|
||||
|
||||
private func startNextIfIdle() {
|
||||
cleanup()
|
||||
if let activity = queue.first?.element, activity.state == .pending {
|
||||
activity.start()
|
||||
if let indicator = queue.first?.element, indicator.state == .pending {
|
||||
indicator.start()
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -16,12 +16,12 @@
|
||||
|
||||
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
|
||||
/// 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: UserIndicatorViewPresentable
|
||||
internal let dismissal: UserIndicatorDismissal
|
||||
|
||||
public init(presenter: ActivityPresentable, dismissal: ActivityDismissal) {
|
||||
public init(presenter: UserIndicatorViewPresentable, dismissal: UserIndicatorDismissal) {
|
||||
self.presenter = presenter
|
||||
self.dismissal = dismissal
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Copyright 2022 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
|
||||
|
||||
typealias UserIndicatorCancel = () -> Void
|
||||
|
||||
/// An abstraction on top of `UserIndicatorTypePresenterProtocol` which manages and stores the individual user indicators.
|
||||
/// When used to present an indicator the `UserIndicatorStore` will instead returns a simple callback function to the clients
|
||||
/// letting them cancel the indicators without worrying about memory.
|
||||
@objc final class UserIndicatorStore: NSObject {
|
||||
private let presenter: UserIndicatorTypePresenterProtocol
|
||||
private var indicators: [UserIndicator]
|
||||
|
||||
init(presenter: UserIndicatorTypePresenterProtocol) {
|
||||
self.presenter = presenter
|
||||
self.indicators = []
|
||||
}
|
||||
|
||||
/// Present a new type of user indicator, such as loading spinner or success message.
|
||||
/// To remove an indicator, call the returned `UserIndicatorCancel` function
|
||||
func present(type: UserIndicatorType) -> UserIndicatorCancel {
|
||||
let indicator = presenter.present(type)
|
||||
indicators.append(indicator)
|
||||
return {
|
||||
indicator.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/// Present a loading indicator.
|
||||
/// To remove the indicator call the returned `UserIndicatorCancel` function
|
||||
///
|
||||
/// Note: This is a convenience function callable by objective-c code
|
||||
@objc func presentLoading(label: String, isInteractionBlocking: Bool) -> UserIndicatorCancel {
|
||||
present(
|
||||
type: .loading(
|
||||
label: label,
|
||||
isInteractionBlocking: isInteractionBlocking
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Present a success message that will be automatically dismissed after a few seconds.
|
||||
///
|
||||
/// Note: This is a convenience function callable by objective-c code
|
||||
@objc func presentSuccess(label: String) {
|
||||
let indicator = presenter.present(.success(label: label))
|
||||
indicators.append(indicator)
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -16,10 +16,10 @@
|
||||
|
||||
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`)
|
||||
/// A presenter associated with and called by a `UserIndicator`, and responsible for the underlying view shown on the screen.
|
||||
public protocol UserIndicatorViewPresentable {
|
||||
/// Called when the `UserIndicator` is started (manually or by the `UserIndicatorQueue`)
|
||||
func present()
|
||||
/// Called when the `Activity` is manually cancelled or completed
|
||||
/// Called when the `UserIndicator` is manually cancelled or completed
|
||||
func dismiss()
|
||||
}
|
||||
Reference in New Issue
Block a user