Skip to content

Commit

Permalink
Add ActionsQueue
Browse files Browse the repository at this point in the history
  • Loading branch information
Šimon Šesták committed Nov 5, 2024
1 parent a682316 commit 6f672bd
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 0 deletions.
39 changes: 39 additions & 0 deletions Sources/FuturedArchitecture/Architecture/QueueAction.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// QueueAction.swift
//
//
// Created by Simon Sestak on 11/10/2024.
//

import Foundation

public protocol QueueAction: Identifiable {
/// Priority, which defines the order of actions addition to the queue
var priority: QueueActionPriority { get }
}

public enum QueueActionPriority {
/// Appended to the end of the queue
case normal
/// Added to the start of the queue, but after `highest`
/// If there is already a `high` priority action in the queue, new action will be added after it
case high
/// Added to the start of the queue
/// If there is already a `highest` priority action in the queue, new action will be added after it
case highest
/// Queue will replace all its elements when receives an action with `replaceAll` priority
case replaceAll

public var higher: Self? {
switch self {
case .normal:
return .high
case .high:
return .highest
case .highest:
return .replaceAll
case .replaceAll:
return nil
}
}
}
80 changes: 80 additions & 0 deletions Sources/FuturedHelpers/ActionsQueue/ActionsCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// ActionsCache.swift
//
//
// Created by Simon Sestak on 11/10/2024.
//

import FuturedArchitecture
import Combine

actor ActionsCache<A: QueueAction> {
let subject: CurrentValueSubject<A?, Never> = CurrentValueSubject(nil)

private var actions = [A]() {
didSet {
subject.send(actions.first)
}
}

var currentAction: A? {
actions.first
}

func add(action: A) {
// If there is already an action with the same id, just replace it with new one
if let index = actions.firstIndex(where: { $0.id == action.id }) {
actions[index] = action

// Else, append it regarding to its priority
} else {
switch action.priority {
case .normal:
actions.append(action)
case .high, .highest:
if let index = indexForAction(withPriority: action.priority) {
actions.insert(action, at: index)
} else {
actions.insert(action, at: 0)
}
case .replaceAll:
actions = [action]
}
}
}

func replaceFirstAction(where oldAction: (A) -> Bool, with newAction: A) {
if let index = actions.firstIndex(where: oldAction) {
actions[index] = newAction
} else {
add(action: newAction)
}
}

func finishCurrent() {
if !actions.isEmpty {
actions.removeFirst()
}
}

func finishAll(where condition: (A) -> Bool) {
actions.removeAll(where: condition)
}

func finishAll() {
actions.removeAll()
}

private func indexForAction(withPriority priority: QueueActionPriority) -> Int? {
guard let lastIndex = actions.lastIndex(where: { $0.priority == priority }) else {
if let higherPriority = priority.higher {
return indexForAction(withPriority: higherPriority)
} else {
return nil
}
}

return actions.index(after: lastIndex)
}
}

84 changes: 84 additions & 0 deletions Sources/FuturedHelpers/ActionsQueue/ActionsQueue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// ActionsQueue.swift
//
//
// Created by Simon Sestak on 11/10/2024.
//

import Combine
import FuturedArchitecture
import Foundation

/// Actions queue which emits new first action using publisher property after current first action is removed
public class ActionsQueue<A: QueueAction> {
/**
- Subscribe on change of the first element in queue
- Will emit new action after the current first action in queue was removed
- Ignores duplicates of the same element
*/
public let publisher: AnyPublisher<A?, Never>

private var actionsCache: ActionsCache<A>
private var cancellable: AnyCancellable?

/**
Init the queue to assign its changes to @Published property, which triggers ui updates automatically

- Parameters:
- published: @Published.Publisher property which is assigned to action queue publisher. Typically @Published property, that is responsible for presenting alerts, sheets, covers, toasts etc.
*/
public init(publisher: inout Published<A?>.Publisher) {
self.actionsCache = ActionsCache()
self.publisher = actionsCache.subject
.removeDuplicates { $0?.id == $1?.id }
.eraseToAnyPublisher()

// Send actions to @Published property
self.publisher
.receive(on: DispatchQueue.main)
.assign(to: &publisher)

// If @Published property is set to nil, remove first action from queue if present
self.cancellable =
publisher
.map { $0 == nil }
.removeDuplicates()
.sink(receiveValue: removeFirstIfNecessary)
}

/// Current action in queue (the first one in the array of actions)
public func currentAction() async -> A? {
await actionsCache.currentAction
}

/// Adds actions to queue
/// If there is already an action with the same id, just replace it with new one
public func add(action: A) async {
await actionsCache.add(action: action)
}

/// Removes first action in queue
public func finishCurrent() async {
await actionsCache.finishCurrent()
}

/// Removes all actions, which matches given condition
public func finishAll(where condition: (A) -> Bool) async {
await actionsCache.finishAll(where: condition)
}

public func finishAll() async {
await actionsCache.finishAll()
}

/// Replaces first found action in array, that matches given condition, with new one
public func replaceFirstAction(where oldAction: (A) -> Bool, with newAction: A) async {
await actionsCache.replaceFirstAction(where: oldAction, with: newAction)
}

private func removeFirstIfNecessary(isNil: Bool) {
if isNil {
Task { await finishCurrent() }
}
}
}

0 comments on commit 6f672bd

Please sign in to comment.