Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

task: Implement toast based activity indicator #1600 #1747

Merged
merged 1 commit into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,51 @@
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
let stateHolder = ActivityPresenterStateHolder()

weak var appWindow: UIWindow?

public init() {
hudViewController = ActivityRuuviLogoViewController()
activityPresenterViewProvider = ActivityPresenterViewProvider(stateHolder: stateHolder)
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(immediately: Bool) {
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()
let additionalWaitTime = immediately ? 0 :
executionTime < minAnimationTime ? (minAnimationTime - executionTime) : 0
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,29 @@
import UIKit
import SwiftUI

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

public class ActivityPresenterViewProvider: NSObject {
private var stateHolder: ActivityPresenterStateHolder

public init(stateHolder: ActivityPresenterStateHolder) {
self.stateHolder = stateHolder
}

public 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,14 @@
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(immediately: Bool)
}

public extension ActivityPresenter {
func dismiss(immediately: Bool = false) {
dismiss(immediately: immediately)
}
}
Loading