Skip to content

Commit

Permalink
task: Implement toast based activity indicator #1600
Browse files Browse the repository at this point in the history
- Replaces old implementation of indicator
  • Loading branch information
priyonto committed Dec 3, 2023
1 parent cd810a5 commit d04b5e1
Show file tree
Hide file tree
Showing 35 changed files with 307 additions and 216 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

public enum ActivityPresenterPosition {
case top
case bottom
case center
}
Original file line number Diff line number Diff line change
@@ -1,74 +1,48 @@
import UIKit

public final class ActivityPresenterRuuviLogo: ActivityPresenter {
var counter = 0 {
didSet {
switch counter {
case 0:
hide()
case 1:
show()
default:
return
}
}
}
let minAnimationTime: CFTimeInterval = 0.75
let minAnimationTime: CFTimeInterval = 1.0
var startTime: CFTimeInterval?
let window = UIWindow(frame: UIScreen.main.bounds)
let hudViewController: ActivityRuuviLogoViewController
let activityPresenterViewProvider: ActivityPresenterViewProvider
let activityPresenterViewController: UIViewController
weak var appWindow: UIWindow?

public init() {
hudViewController = ActivityRuuviLogoViewController()
activityPresenterViewProvider = ActivityPresenterViewProvider()
activityPresenterViewController = ActivityPresenterViewProvider.makeViewController()
activityPresenterViewController.view.backgroundColor = .clear
window.windowLevel = .normal
hudViewController.view.translatesAutoresizingMaskIntoConstraints = false
window.rootViewController = hudViewController
}

public func increment() {
counter += 1
hideMessageLabel()
}

public func increment(with message: String) {
counter += 1
showMessageLabel(with: message)
activityPresenterViewController.view.translatesAutoresizingMaskIntoConstraints = false
window.rootViewController = activityPresenterViewController
}
}

public func decrement() {
guard counter > 0 else {
return
}
counter -= 1
extension ActivityPresenterRuuviLogo {
public func setPosition(_ position: ActivityPresenterPosition) {
activityPresenterViewProvider.updatePosition(position)
}

private func show() {
public func show(with state: ActivityPresenterState) {
startTime = CFAbsoluteTimeGetCurrent()
appWindow = UIWindow.key
window.makeKeyAndVisible()
hudViewController.spinnerView.animate()
window.layoutIfNeeded()
activityPresenterViewProvider.updateState(state)
}

private func showMessageLabel(with message: String) {
hudViewController.messageLabel.alpha = 1
hudViewController.messageLabel.text = message
public func update(with state: ActivityPresenterState) {
activityPresenterViewProvider.updateState(state)
}

private func hide() {
public func dismiss() {
let executionTime = CFAbsoluteTimeGetCurrent() - (startTime ?? 0)
let additionalWaitTime = executionTime < minAnimationTime ? (minAnimationTime - executionTime) : 0
DispatchQueue.main.asyncAfter(deadline: .now() + additionalWaitTime) {
self.appWindow?.makeKeyAndVisible()
self.appWindow = nil
self.window.isHidden = true
self.hudViewController.spinnerView.stopAnimating()
self.hideMessageLabel()
DispatchQueue.main.asyncAfter(deadline: .now() + additionalWaitTime) { [weak self] in
self?.activityPresenterViewProvider.updateState(.dismiss)
self?.appWindow?.makeKeyAndVisible()
self?.appWindow = nil
self?.window.isHidden = true
}
}

private func hideMessageLabel() {
hudViewController.messageLabel.alpha = 0
hudViewController.messageLabel.text = nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

public enum ActivityPresenterState: Equatable {
case loading(message: String?)
case success(message: String?)
case failed(message: String?)
case dismiss

public static func == (
lhs: ActivityPresenterState,
rhs: ActivityPresenterState
) -> Bool {
switch (lhs, rhs) {
case (.loading(let lhsMessage), .loading(let rhsMessage)):
return lhsMessage == rhsMessage
case (.success(let lhsMessage), .success(let rhsMessage)):
return lhsMessage == rhsMessage
case (.failed(let lhsMessage), .failed(let rhsMessage)):
return lhsMessage == rhsMessage
case (.dismiss, .dismiss):
return true
default:
return false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import UIKit
import SwiftUI

private struct ActivityPresenterAssets {
static let activityOngoingDefault = "activity_ongoing_generic"
static let activitySuccessDefault = "activity_success_generic"
static let activityFailedDefault = "activity_failed_generic"

static let activityLogoRuuvi = "ruuvi_activity_presenter_logo"
}

public struct ActivityPresenterView: View {
@EnvironmentObject var stateHolder: ActivityPresenterStateHolder

public var body: some View {
VStack {
if stateHolder.position == .bottom || stateHolder.position == .center {
Spacer()
}

ActivityPresenterContentView(state: stateHolder.state)
.padding([.leading, .trailing],
stateHolder.state == .dismiss ? 0 : 12)
.padding([.top, .bottom],
stateHolder.state == .dismiss ? 0 : 12)
.background(Color.black.opacity(0.8))
.cornerRadius(8)
.foregroundColor(.white)
.transition(.scale.combined(with: .opacity))
.opacity(stateHolder.state == .dismiss ? 0 : 1)

if stateHolder.position == .top || stateHolder.position == .center {
Spacer()
}
}
}
}

struct ActivityPresenterContentView: View {
let state: ActivityPresenterState

var body: some View {
HStack(spacing: 8) {
if case .loading = state {
ZStack {
contentImage?
.resizable()
.frame(width: 24, height: 24)
ActivitySpinnerViewRepresentable()
.frame(width: 30, height: 30)
}
} else {
contentImage?
.resizable()
.frame(width: 12, height: 12)
}
Text(message)
}
}

private var contentImage: Image? {
switch state {
case .loading:
return Image(
ActivityPresenterAssets.activityLogoRuuvi,
bundle: .pod(ActivityPresenterViewProvider.self)
)
case .success:
return Image(systemName: "checkmark")
case .failed:
return Image(systemName: "xmark")
default:
return nil
}
}

private var message: String {
switch state {
case .loading(let message):
if let message = message {
return message
} else {
return ActivityPresenterAssets
.activityOngoingDefault
.localized(for: ActivityPresenterViewProvider.self)
}
case .success(let message):
if let message = message {
return message
} else {
return ActivityPresenterAssets
.activitySuccessDefault
.localized(for: ActivityPresenterViewProvider.self)
}
case .failed(let message):
if let message = message {
return message
} else {
return ActivityPresenterAssets
.activityFailedDefault
.localized(for: ActivityPresenterViewProvider.self)
}
case .dismiss:
return "" // Placeholder for dismiss state
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import UIKit
import SwiftUI

public class ActivityPresenterStateHolder: ObservableObject {
@Published var state: ActivityPresenterState = .dismiss
@Published var position: ActivityPresenterPosition = .bottom
}

public class ActivityPresenterViewProvider: NSObject {
private static let stateHolder = ActivityPresenterStateHolder()

public static func makeViewController() -> UIViewController {
return UIHostingController(
rootView: ActivityPresenterView().environmentObject(stateHolder)
)
}

func updateState(_ newState: ActivityPresenterState) {
Self.stateHolder.state = newState
}

func updatePosition(_ position: ActivityPresenterPosition) {
Self.stateHolder.position = position
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import UIKit
import SwiftUI

// UIViewRepresentable wrapper for ActivitySpinnerView
struct ActivitySpinnerViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> ActivitySpinnerView {
let spinnerView = ActivitySpinnerView()
spinnerView.animate()
return spinnerView
}

func updateUIView(_ uiView: ActivitySpinnerView, context: Context) {
// No op
}
}

@IBDesignable
class ActivitySpinnerView: UIView {

private var strokeColor = UIColor(red: 0.21, green: 0.68, blue: 0.62, alpha: 1.00)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import Foundation

public protocol ActivityPresenter {
func increment()
func increment(with message: String)
func decrement()
func setPosition(_ position: ActivityPresenterPosition)
func show(with state: ActivityPresenterState)
func update(with state: ActivityPresenterState)
func dismiss()
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,9 @@ public final class ErrorPresenterAlert: ErrorPresenter {
let group = DispatchGroup()
DispatchQueue.main.async {
group.enter()
let topViewController = UIApplication.shared.topViewController()
var fireAfter: DispatchTimeInterval = .milliseconds(0)
if topViewController is ActivityRuuviLogoViewController {
fireAfter = .milliseconds(750)
}
group.leave()
group.notify(queue: .main) {
DispatchQueue.main.asyncAfter(deadline: .now() + fireAfter) {
DispatchQueue.main.async {
let feedback = UINotificationFeedbackGenerator()
feedback.notificationOccurred(.error)
feedback.prepare()
Expand Down
Loading

0 comments on commit d04b5e1

Please sign in to comment.