mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Copied over loading indicator classes and added a modal one for the login screen.
This commit is contained in:
@@ -44,8 +44,6 @@ class AuthenticationCoordinator: Coordinator {
|
||||
|
||||
func start() {
|
||||
|
||||
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||
|
||||
let availableRestoreTokens = keychainController.restoreTokens()
|
||||
|
||||
guard let usernameTokenTuple = availableRestoreTokens.first else {
|
||||
@@ -116,6 +114,9 @@ class AuthenticationCoordinator: Coordinator {
|
||||
}
|
||||
|
||||
private func login(username: String, password: String, completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||
|
||||
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
@@ -136,6 +137,9 @@ class AuthenticationCoordinator: Coordinator {
|
||||
}
|
||||
|
||||
private func restorePreviousLogin(_ usernameTokenTuple: (username: String, token: String), completion: @escaping (Result<Void, AuthenticationCoordinatorError>) -> Void) {
|
||||
|
||||
delegate?.authenticationCoordinatorDidStartLoading(self)
|
||||
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ struct LoginScreen: View {
|
||||
.padding(.horizontal, 8.0)
|
||||
.navigationTitle("Login")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ struct HomeScreen: View {
|
||||
.listStyle(.plain)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Logout") {
|
||||
@@ -66,6 +65,7 @@ struct HomeScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
/// 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 {
|
||||
public enum State {
|
||||
case pending
|
||||
case executing
|
||||
case completed
|
||||
}
|
||||
|
||||
private let request: ActivityRequest
|
||||
private let completion: () -> Void
|
||||
|
||||
public private(set) var state: State
|
||||
|
||||
public init(request: ActivityRequest, 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 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.
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Collection where Element == Activity {
|
||||
func cancelAll() {
|
||||
forEach {
|
||||
$0.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// 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 else {
|
||||
return
|
||||
}
|
||||
|
||||
let view = RoundedToastView(viewState: viewState)
|
||||
self.view = view
|
||||
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationController.view.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,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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
final class LabelledActivityIndicatorView: UIView {
|
||||
private enum Constants {
|
||||
static let padding = UIEdgeInsets(top: 20, left: 40, bottom: 15, right: 40)
|
||||
static let activityIndicatorScale = CGFloat(1.5)
|
||||
static let cornerRadius: CGFloat = 12.0
|
||||
static let stackBackgroundOpacity: CGFloat = 0.9
|
||||
static let stackSpacing: CGFloat = 15
|
||||
static let backgroundOpacity: CGFloat = 0.5
|
||||
}
|
||||
|
||||
private let stackBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.cornerRadius = Constants.cornerRadius
|
||||
view.alpha = Constants.stackBackgroundOpacity
|
||||
view.backgroundColor = .gray.withAlphaComponent(0.75)
|
||||
return view
|
||||
}()
|
||||
|
||||
private let stackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.distribution = .fill
|
||||
stack.alignment = .center
|
||||
stack.spacing = Constants.stackSpacing
|
||||
return stack
|
||||
}()
|
||||
|
||||
private let activityIndicator: UIActivityIndicatorView = {
|
||||
let view = UIActivityIndicatorView()
|
||||
view.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale)
|
||||
view.startAnimating()
|
||||
return view
|
||||
}()
|
||||
|
||||
private let label: UILabel = {
|
||||
return UILabel()
|
||||
}()
|
||||
|
||||
init(text: String) {
|
||||
super.init(frame: .zero)
|
||||
setup(text: text)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setup(text: String) {
|
||||
setupStackView()
|
||||
label.text = text
|
||||
}
|
||||
|
||||
private func setupStackView() {
|
||||
addSubview(stackView)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
stackView.centerYAnchor.constraint(equalTo: centerYAnchor)
|
||||
])
|
||||
|
||||
stackView.addArrangedSubview(activityIndicator)
|
||||
stackView.addArrangedSubview(label)
|
||||
|
||||
insertSubview(stackBackgroundView, belowSubview: stackView)
|
||||
stackBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stackBackgroundView.topAnchor.constraint(equalTo: stackView.topAnchor, constant: -Constants.padding.top),
|
||||
stackBackgroundView.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: Constants.padding.bottom),
|
||||
stackBackgroundView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: -Constants.padding.left),
|
||||
stackBackgroundView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: Constants.padding.right)
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
class RectangleToastView: UIView {
|
||||
|
||||
private enum Constants {
|
||||
static let padding: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
static let cornerRadius: CGFloat = 8.0
|
||||
}
|
||||
|
||||
private lazy var imageView: UIImageView = {
|
||||
let view = UIImageView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.backgroundColor = .clear
|
||||
return view
|
||||
}()
|
||||
|
||||
private lazy var messageLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.backgroundColor = .clear
|
||||
label.numberOfLines = 0
|
||||
label.textAlignment = .left
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
return label
|
||||
}()
|
||||
|
||||
private lazy var stackView: UIStackView = {
|
||||
let result = UIStackView()
|
||||
result.axis = .horizontal
|
||||
result.distribution = .fill
|
||||
result.alignment = .center
|
||||
result.spacing = 8.0
|
||||
result.backgroundColor = .clear
|
||||
|
||||
addSubview(result)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
result.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding.left),
|
||||
result.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding.top),
|
||||
result.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding.right),
|
||||
result.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding.bottom)
|
||||
])
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
init(withMessage message: String?,
|
||||
image: UIImage? = nil) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
if let image = image {
|
||||
imageView.image = image
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.widthAnchor.constraint(equalToConstant: image.size.width),
|
||||
imageView.heightAnchor.constraint(equalToConstant: image.size.height)
|
||||
])
|
||||
stackView.addArrangedSubview(imageView)
|
||||
}
|
||||
|
||||
messageLabel.text = message
|
||||
stackView.addArrangedSubview(messageLabel)
|
||||
|
||||
stackView.layoutIfNeeded()
|
||||
layer.cornerRadius = Constants.cornerRadius
|
||||
layer.masksToBounds = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
class RoundedToastView: UIView {
|
||||
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)
|
||||
}
|
||||
|
||||
private lazy var activityIndicator: UIActivityIndicatorView = {
|
||||
let indicator = UIActivityIndicatorView()
|
||||
indicator.transform = .init(scaleX: Constants.activityIndicatorScale, y: Constants.activityIndicatorScale)
|
||||
indicator.startAnimating()
|
||||
return indicator
|
||||
}()
|
||||
|
||||
private lazy var imagView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.widthAnchor.constraint(equalToConstant: Constants.imageViewSize),
|
||||
imageView.heightAnchor.constraint(equalToConstant: Constants.imageViewSize)
|
||||
])
|
||||
return imageView
|
||||
}()
|
||||
|
||||
private let stackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.spacing = 5
|
||||
return stack
|
||||
}()
|
||||
|
||||
private let label: UILabel = {
|
||||
return UILabel()
|
||||
}()
|
||||
|
||||
init(viewState: ViewState) {
|
||||
super.init(frame: .zero)
|
||||
setup(viewState: viewState)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setup(viewState: ViewState) {
|
||||
setupLayer()
|
||||
setupStackView()
|
||||
stackView.addArrangedSubview(toastView(for: viewState.style))
|
||||
stackView.addArrangedSubview(label)
|
||||
label.text = viewState.label
|
||||
}
|
||||
|
||||
private func setupStackView() {
|
||||
addSubview(stackView)
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.padding.top),
|
||||
stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.padding.bottom),
|
||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Constants.padding.left),
|
||||
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Constants.padding.right)
|
||||
])
|
||||
}
|
||||
|
||||
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 {
|
||||
switch style {
|
||||
case .loading:
|
||||
return activityIndicator
|
||||
case .success:
|
||||
imagView.image = UIImage(systemName: "checkmark.circle")
|
||||
return imagView
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RoundedToastView {
|
||||
enum Style {
|
||||
case loading
|
||||
case success
|
||||
}
|
||||
struct ViewState {
|
||||
let style: Style
|
||||
let label: String
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user