├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── RealEventsBus
│ ├── Bus+BufferedEvent.swift
│ ├── Bus+Event.swift
│ ├── Bus.swift
│ ├── Foundation Structures
│ ├── AtomicValue.swift
│ └── SynchronizedArray.swift
│ └── Structures
│ ├── BusStorage.swift
│ ├── Event.swift
│ └── EventObserver.swift
└── Tests
├── .DS_Store
└── RealEventsBusTests
└── RealEventsBusTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 | .DS_Store
92 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Daniele Margutti
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "RealEventsBus",
8 | products: [
9 | // Products define the executables and libraries a package produces, and make them visible to other packages.
10 | .library(
11 | name: "RealEventsBus",
12 | targets: ["RealEventsBus"]),
13 | ],
14 | dependencies: [
15 | // Dependencies declare other packages that this package depends on.
16 | // .package(url: /* package url */, from: "1.0.0"),
17 | ],
18 | targets: [
19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
20 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
21 | .target(
22 | name: "RealEventsBus",
23 | dependencies: []),
24 | .testTarget(
25 | name: "RealEventsBusTests",
26 | dependencies: ["RealEventsBus"]),
27 | ]
28 | )
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🚎 RealEventsBus
2 |
3 | RealEventsBus is a small swift experiment package to implement a basic **type-safe event bus** mechanism.
4 | Some other implementations in GitHub are the sources of inspiration for this package.
5 |
6 | You can use it as replacement for standard `NSNotification`'s one-to-many messaging.
7 | It uses GCD for messagings, it's thread-safe and, best of all, it's type safe.
8 |
9 | ## ⭐️ Feature Highlights
10 |
11 | - It's **type safe**
12 | - Implement **custom messages**; just set conformance to `Event` or `BufferedEvent` type
13 | - Messages/observers are posted and registered in thread safe
14 | - **Easy to use**; just one line to register and post events
15 | - Supports for **buffered events** (get the last value published by a bus)
16 |
17 | ## 🕵️ How It Works
18 |
19 | This example uses `enum` as datatype for event.
20 | Btw you can use any type you want as event, `struct` or `class` (see the other example below).
21 | First of all we need to define a custom event; if your event is a group of different messages this is the best thing you can do:
22 |
23 | ```swift
24 | public enum UserEvents: Event {
25 | case userDidLogged(username: String)
26 | case userLoggedOut
27 | case profileUpdated(fullName: String, age: Int)
28 | }
29 | ```
30 |
31 | Suppose you want to be notified about this kind of events in your `UIViewController`:
32 |
33 | ```swift
34 | class ViewController: UIViewController {
35 | override func viewDidLoad() {
36 | super.viewDidLoad()
37 |
38 | Bus.register(self) { event in
39 | switch event {
40 | case .profileUpdated(let fullName, let age):
41 | print("Profile updated with '\(fullName)', which is \(age) old")
42 | case .userDidLogged(let username):
43 | print("User '\(username)' logged in")
44 | case .userLoggedOut:
45 | print("User logged out")
46 | }
47 | }
48 | }
49 |
50 | deinit {
51 | // While it's not required (it does not generate any leak)
52 | // you may want to unregister an observer when it's not needed anymore.
53 | Bus.unregister(self)
54 | }
55 | }
56 | ```
57 |
58 | When you need to post new events to any registered obserer like the one above just use `post` function:
59 |
60 | ```swift
61 | Bus.post(.userDidLogged(username: "danielemm"))
62 | ```
63 |
64 | ### BufferedEvent
65 |
66 | If your event is conform to `BufferedEvent` instead of `Event` you can use the `lastValue()` function to get the latest posted value into the bus. It's like Rx.
67 | Moreover: when a new observer is registered it will receive the last value posted into the bus, if any.
68 |
69 | This is an example.
70 |
71 | First of all we define the message:
72 |
73 | ```swift
74 | public class CustomEvent: BufferedEvent {
75 |
76 | var messageValue: String
77 | var options: [String: Any?]?
78 |
79 | public init(value: String, options: [String: Any?]) {
80 | self.messageValue = value
81 | self.options = options
82 | }
83 |
84 | }
85 | ```
86 |
87 | ```swift
88 | // Post a new event
89 | Bus.post(.init(value: "Some message", options: ["a": 1, "b": "some"]))
90 |
91 | // At certain point in your code:
92 | let lastValue = Bus.lastValue() // print the type above!
93 | ```
94 |
95 | ### Custom Dispatch Queue
96 |
97 | You can also specify a queue where the message callback will be called.
98 | By default the `.main` queue is used.
99 |
100 | ```swift
101 | Bus.register(self, queue: .global()) { _ in // in background queue
102 | // do something
103 | }
104 | ```
105 |
106 | ## Install
107 |
108 | ### Swift Package Manager
109 |
110 | To install it using the Swift Package Manager specify it as dependency in the Package.swift file:
111 |
112 | ```swift
113 | dependencies: [
114 | .package(url: "https://github.com/malcommac/RealEventsBus.git", branch: "main"),
115 | ],
116 | ```
117 |
118 | ### CocoaPods
119 |
120 | Not yet supported.
121 |
122 | ## Author
123 |
124 | This little experiment was created by [Daniele Margutti](mailto:hello@danielemargutti.com).
125 | If you like it you can fork or open a PR or report an issue.
126 | If you want to support my work just [take a look at my Github Profile](https://github.com/malcommac).
127 |
128 | ## License
129 |
130 | It's the MIT license.
131 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Bus+BufferedEvent.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bus+StickyEvent.swift
3 | // RealEventsBus
4 | //
5 | // Created by Daniele Margutti on 28/11/21.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Public BufferedEvent extension for Bus
11 |
12 | public extension Bus where EventType: BufferedEvent {
13 |
14 | /// Registered a new observer for a `BufferedEvent`.
15 | ///
16 | /// - Parameters:
17 | /// - observer: observer object instance.
18 | /// - storage: storage where the bus is saved; `.default` if not passed.
19 | /// - queue: queue in which the callback will be triggered, `.main` if not passed.
20 | /// - callback: callback to trigger.
21 | static func register(_ observer: AnyObject, storage: BusStorage = .default,
22 | queue: DispatchQueue = .main,
23 | callback: @escaping EventCallback) {
24 | busForEventTypeIn(storage).register(observer, queue: queue, callback: callback)
25 | }
26 |
27 | /// Post a new event into the bus.
28 | ///
29 | /// - Parameters:
30 | /// - event: event to post.
31 | /// - storage: storage where the bus is saved, `.default` if not specified.
32 | static func post(_ event: EventType, storage: BusStorage = .default) {
33 | busForEventTypeIn(storage).post(event)
34 | }
35 |
36 | /// Return the last value buffered by the bus of this type.
37 | ///
38 | /// - Parameter storage: storage in which the bus is present, `.default` if not passed.
39 | /// - Returns: `EventType?`
40 | static func lastValue(storage: BusStorage = .default) -> EventType? {
41 | busForEventTypeIn(storage).lastValue
42 | }
43 |
44 | }
45 |
46 | // MARK: - Private BufferedEvent extension for Bus
47 |
48 | fileprivate extension Bus where EventType: BufferedEvent {
49 |
50 | func register(_ observer: AnyObject,
51 | queue: DispatchQueue,
52 | callback: @escaping EventCallback) {
53 | let observer = EventObserver(observer, queue, callback)
54 | observers.append(observer)
55 |
56 | if let lastValue = lastValue {
57 | observer.post(lastValue) // post the last value immediately.
58 | }
59 | }
60 |
61 | func post(_ event: EventType) {
62 | self.lastValue = event
63 | observers.forEach {
64 | $0.post(event)
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Bus+Event.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Bus+EventType.swift
3 | // RealEventsBus
4 | //
5 | // Created by Daniele Margutti on 28/11/21.
6 | //
7 |
8 | import Foundation
9 |
10 | public extension Bus where EventType: Event {
11 |
12 | // MARK: - Public Functions
13 |
14 | /// Post a new event into the bus.
15 | ///
16 | /// - Parameters:
17 | /// - event: event to post.
18 | /// - storage: storage destination, `.default` if none.
19 | static func post(_ event: EventType, storage: BusStorage = .default) {
20 | busForEventTypeIn(storage).post(event)
21 | }
22 |
23 | /// Register a new observer to receive events from this bus type.
24 | ///
25 | /// - Parameters:
26 | /// - observer: observer instance.
27 | /// - store: storage to use, `.default` if not specified.
28 | /// - queue: dispatch queue where the message must be delivered, `.main` if not specified.
29 | /// - callback: callback to execute when event is received.
30 | static func register(_ observer: AnyObject,
31 | storage: BusStorage = .default,
32 | queue: DispatchQueue = .main,
33 | _ callback: @escaping EventCallback) {
34 | busForEventTypeIn(storage).register(observer, queue: queue, callback: callback)
35 | }
36 |
37 | // MARK: - Private Functions
38 |
39 | /// Create a new listener for a new observer to register and add to the list of observers for this bus.
40 | ///
41 | /// - Parameters:
42 | /// - observer: observer to register.
43 | /// - queue: queue in which the callback should be executed.
44 | /// - callback: callback to execute.
45 | private func register(_ observer: AnyObject, queue: DispatchQueue, callback: @escaping EventCallback) {
46 | let observer = EventObserver(observer, queue, callback)
47 | observers.append(observer)
48 | }
49 |
50 | /// This notify all the observers about a new incoming event.
51 | ///
52 | /// - Parameter event: event arrived.
53 | private func post(_ event: EventType) {
54 | observers.forEach {
55 | $0.post(event)
56 | }
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Bus.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Messenger.swift
3 | // RealEventsBus
4 | //
5 | // Created by Daniele Margutti on 20/11/21.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Type erased protocol for bus related classes.
11 | public protocol AnyBus { }
12 |
13 | /// The bus class is used to carry out events to the registered observers.
14 | /// When a bus is created is registered inside a `BusStorage`; if you don't want
15 | /// to specify a bus storage you can use the default (`.default`).
16 | public class Bus: AnyBus {
17 |
18 | /// Defines the callback which is called for an event.
19 | public typealias EventCallback = (_ event: EventType) -> Void
20 |
21 | // MARK: - Public Properties
22 |
23 | /// Last value posted; it may be filled only for `BufferedEvent` even types.
24 | @AtomicValue
25 | internal var lastValue: EventType?
26 |
27 | // MARK: - Private Properties
28 |
29 | /// List of observers registered for this bus.
30 | internal var observers = SynchronizedArray>()
31 |
32 | // MARK: - Public Functions
33 |
34 | /// Return or create (if not exists) a bus for a given `EventType` inside
35 | /// the storage class you have specified.
36 | ///
37 | /// - Parameter storage: storage where the bus is get or created.
38 | /// - Returns: `Bus`
39 | static func busForEventTypeIn(_ storage: BusStorage) -> Bus {
40 | if let bus: Bus = storage.buseForEventType() {
41 | return bus // use the existing bus
42 | }
43 |
44 | // Create a new bus
45 | let bus = Bus()
46 | storage.buses.append(bus)
47 | return bus
48 | }
49 |
50 | }
51 |
52 |
53 | // MARK: - Public Extensions
54 |
55 | /// This defines a list of events which are not related to the `EventType` passed
56 | /// to create an object.
57 | extension Bus {
58 |
59 | /// Unregister an observer instance from the passed store.
60 | ///
61 | /// - Parameters:
62 | /// - observer: observer instance you want to unregister.
63 | /// - storage: storage instance.
64 | public static func unregister(_ observer: AnyObject?, storage: BusStorage = .default) {
65 | busForEventTypeIn(storage).unregister(observer: observer)
66 | }
67 |
68 | // MARK: - Private Functions
69 |
70 | /// Unregister given observer from self.
71 | ///
72 | /// - Parameter observer: observer to unregister, if `nil` nothing is done.
73 | fileprivate func unregister(observer: AnyObject?) {
74 | guard let observer = observer else {
75 | return
76 | }
77 |
78 | let rawPointer = UnsafeRawPointer(Unmanaged.passUnretained(observer).toOpaque())
79 | let newListeners = observers.filter {
80 | $0.observerRawPointer != rawPointer // remove observer which points to deallocated object.
81 | }
82 | self.observers = newListeners
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Foundation Structures/AtomicValue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Atomic.swift
3 | // RealEventsBus
4 | //
5 | // Created by Daniele Margutti on 23/11/21.
6 | //
7 |
8 | import Foundation
9 |
10 | /// This property wrapper allows you to define an atomic access for a given property.
11 | @propertyWrapper
12 | internal struct AtomicValue {
13 |
14 | // MARK: - Private Properties
15 |
16 | /// Value hold.
17 | private var value: Value
18 |
19 | /// Lock mechanism.
20 | private let lock = NSLock()
21 |
22 | // MARK: - Initialization
23 |
24 | /// Initialize a new atomic container for a given property.
25 | ///
26 | /// - Parameter value: property value.
27 | init(wrappedValue value: Value) {
28 | self.value = value
29 | }
30 |
31 | // MARK: - Property Wrapper Methods
32 |
33 | var wrappedValue: Value {
34 | get { return load() }
35 | set { store(newValue: newValue) }
36 | }
37 |
38 | func load() -> Value {
39 | lock.lock()
40 | defer { lock.unlock() }
41 | return value
42 | }
43 |
44 | mutating func store(newValue: Value) {
45 | lock.lock()
46 | defer { lock.unlock() }
47 | value = newValue
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Foundation Structures/SynchronizedArray.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SynchronizedArray.swift
3 | // RealEventsBus
4 | //
5 | // Thread safe array implementation for swift.
6 | // Created by Rafael Setragni on 07/06/21.
7 | // See package.
8 | //
9 |
10 | import Foundation
11 |
12 | /// A thread-safe array.
13 | internal class SynchronizedArray {
14 |
15 | // MARK: - Private Properties
16 |
17 | private let queue = DispatchQueue(label: "SynchronizedArrayAccess", attributes: .concurrent)
18 | private var array = [Element]()
19 |
20 | // MARK: - Initialization
21 |
22 | public init() { }
23 |
24 | public convenience init(_ array: [Element]) {
25 | self.init()
26 | self.array = array
27 | }
28 |
29 | }
30 |
31 | // MARK: - Properties
32 |
33 | internal extension SynchronizedArray {
34 |
35 | /// The first element of the collection.
36 | var first: Element? {
37 | var result: Element?
38 | queue.sync { result = self.array.first }
39 | return result
40 | }
41 |
42 | /// The last element of the collection.
43 | var last: Element? {
44 | var result: Element?
45 | queue.sync { result = self.array.last }
46 | return result
47 | }
48 |
49 | /// The number of elements in the array.
50 | var count: Int {
51 | var result = 0
52 | queue.sync { result = self.array.count }
53 | return result
54 | }
55 |
56 | /// A Boolean value indicating whether the collection is empty.
57 | var isEmpty: Bool {
58 | var result = false
59 | queue.sync { result = self.array.isEmpty }
60 | return result
61 | }
62 |
63 | /// A textual representation of the array and its elements.
64 | var description: String {
65 | var result = ""
66 | queue.sync { result = self.array.description }
67 | return result
68 | }
69 | }
70 |
71 | // MARK: - Immutable
72 |
73 | extension SynchronizedArray {
74 |
75 | /// Returns the first element of the sequence that satisfies the given predicate.
76 | ///
77 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
78 | /// - Returns: The first element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate.
79 | func first(where predicate: (Element) -> Bool) -> Element? {
80 | var result: Element?
81 | queue.sync { result = self.array.first(where: predicate) }
82 | return result
83 | }
84 |
85 | /// Returns the last element of the sequence that satisfies the given predicate.
86 | ///
87 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
88 | /// - Returns: The last element of the sequence that satisfies predicate, or nil if there is no element that satisfies predicate.
89 | func last(where predicate: (Element) -> Bool) -> Element? {
90 | var result: Element?
91 | queue.sync { result = self.array.last(where: predicate) }
92 | return result
93 | }
94 |
95 | /// Returns an array containing, in order, the elements of the sequence that satisfy the given predicate.
96 | ///
97 | /// - Parameter isIncluded: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element should be included in the returned array.
98 | /// - Returns: An array of the elements that includeElement allowed.
99 | func filter(_ isIncluded: @escaping (Element) -> Bool) -> SynchronizedArray {
100 | var result: SynchronizedArray?
101 | queue.sync { result = SynchronizedArray(self.array.filter(isIncluded)) }
102 | return result!
103 | }
104 |
105 | /// Returns the first index in which an element of the collection satisfies the given predicate.
106 | ///
107 | /// - Parameter predicate: A closure that takes an element as its argument and returns a Boolean value that indicates whether the passed element represents a match.
108 | /// - Returns: The index of the first element for which predicate returns true. If no elements in the collection satisfy the given predicate, returns nil.
109 | func index(where predicate: (Element) -> Bool) -> Int? {
110 | var result: Int?
111 | queue.sync { result = self.array.firstIndex(where: predicate) }
112 | return result
113 | }
114 |
115 | /// Returns the elements of the collection, sorted using the given predicate as the comparison between elements.
116 | ///
117 | /// - Parameter areInIncreasingOrder: A predicate that returns true if its first argument should be ordered before its second argument; otherwise, false.
118 | /// - Returns: A sorted array of the collection’s elements.
119 | func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> SynchronizedArray {
120 | var result: SynchronizedArray?
121 | queue.sync { result = SynchronizedArray(self.array.sorted(by: areInIncreasingOrder)) }
122 | return result!
123 | }
124 |
125 | /// Returns an array containing the results of mapping the given closure over the sequence’s elements.
126 | ///
127 | /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value.
128 | /// - Returns: An array of the non-nil results of calling transform with each element of the sequence.
129 | func map(_ transform: @escaping (Element) -> ElementOfResult) -> [ElementOfResult] {
130 | var result = [ElementOfResult]()
131 | queue.sync { result = self.array.map(transform) }
132 | return result
133 | }
134 |
135 | /// Returns an array containing the non-nil results of calling the given transformation with each element of this sequence.
136 | ///
137 | /// - Parameter transform: A closure that accepts an element of this sequence as its argument and returns an optional value.
138 | /// - Returns: An array of the non-nil results of calling transform with each element of the sequence.
139 | func compactMap(_ transform: (Element) -> ElementOfResult?) -> [ElementOfResult] {
140 | var result = [ElementOfResult]()
141 | queue.sync { result = self.array.compactMap(transform) }
142 | return result
143 | }
144 |
145 | /// Returns the result of combining the elements of the sequence using the given closure.
146 | ///
147 | /// - Parameters:
148 | /// - initialResult: The value to use as the initial accumulating value. initialResult is passed to nextPartialResult the first time the closure is executed.
149 | /// - nextPartialResult: A closure that combines an accumulating value and an element of the sequence into a new accumulating value, to be used in the next call of the nextPartialResult closure or returned to the caller.
150 | /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult.
151 | func reduce(_ initialResult: ElementOfResult, _ nextPartialResult: @escaping (ElementOfResult, Element) -> ElementOfResult) -> ElementOfResult {
152 | var result: ElementOfResult?
153 | queue.sync { result = self.array.reduce(initialResult, nextPartialResult) }
154 | return result ?? initialResult
155 | }
156 |
157 | /// Returns the result of combining the elements of the sequence using the given closure.
158 | ///
159 | /// - Parameters:
160 | /// - initialResult: The value to use as the initial accumulating value.
161 | /// - updateAccumulatingResult: A closure that updates the accumulating value with an element of the sequence.
162 | /// - Returns: The final accumulated value. If the sequence has no elements, the result is initialResult.
163 | func reduce(into initialResult: ElementOfResult, _ updateAccumulatingResult: @escaping (inout ElementOfResult, Element) -> ()) -> ElementOfResult {
164 | var result: ElementOfResult?
165 | queue.sync { result = self.array.reduce(into: initialResult, updateAccumulatingResult) }
166 | return result ?? initialResult
167 | }
168 |
169 | /// Calls the given closure on each element in the sequence in the same order as a for-in loop.
170 | ///
171 | /// - Parameter body: A closure that takes an element of the sequence as a parameter.
172 | func forEach(_ body: (Element) -> Void) {
173 | queue.sync { self.array.forEach(body) }
174 | }
175 |
176 | /// Returns a Boolean value indicating whether the sequence contains an element that satisfies the given predicate.
177 | ///
178 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element represents a match.
179 | /// - Returns: true if the sequence contains an element that satisfies predicate; otherwise, false.
180 | func contains(where predicate: (Element) -> Bool) -> Bool {
181 | var result = false
182 | queue.sync { result = self.array.contains(where: predicate) }
183 | return result
184 | }
185 |
186 | /// Returns a Boolean value indicating whether every element of a sequence satisfies a given predicate.
187 | ///
188 | /// - Parameter predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value that indicates whether the passed element satisfies a condition.
189 | /// - Returns: true if the sequence contains only elements that satisfy predicate; otherwise, false.
190 | func allSatisfy(_ predicate: (Element) -> Bool) -> Bool {
191 | var result = false
192 | queue.sync { result = self.array.allSatisfy(predicate) }
193 | return result
194 | }
195 | }
196 |
197 | // MARK: - Mutable
198 |
199 | extension SynchronizedArray {
200 |
201 | /// Adds a new element at the end of the array.
202 | ///
203 | /// - Parameter element: The element to append to the array.
204 | func append(_ element: Element) {
205 | queue.async(flags: .barrier) {
206 | self.array.append(element)
207 | }
208 | }
209 |
210 | /// Adds new elements at the end of the array.
211 | ///
212 | /// - Parameter element: The elements to append to the array.
213 | func append(_ elements: [Element]) {
214 | queue.async(flags: .barrier) {
215 | self.array += elements
216 | }
217 | }
218 |
219 | /// Inserts a new element at the specified position.
220 | ///
221 | /// - Parameters:
222 | /// - element: The new element to insert into the array.
223 | /// - index: The position at which to insert the new element.
224 | func insert(_ element: Element, at index: Int) {
225 | queue.async(flags: .barrier) {
226 | self.array.insert(element, at: index)
227 | }
228 | }
229 |
230 | /// Removes and returns the element at the specified position.
231 | ///
232 | /// - Parameters:
233 | /// - index: The position of the element to remove.
234 | /// - completion: The handler with the removed element.
235 | func remove(at index: Int, completion: ((Element) -> Void)? = nil) {
236 | queue.async(flags: .barrier) {
237 | let element = self.array.remove(at: index)
238 | DispatchQueue.main.async { completion?(element) }
239 | }
240 | }
241 |
242 | /// Removes and returns the elements that meet the criteria.
243 | ///
244 | /// - Parameters:
245 | /// - predicate: A closure that takes an element of the sequence as its argument and returns a Boolean value indicating whether the element is a match.
246 | /// - completion: The handler with the removed elements.
247 | func remove(where predicate: @escaping (Element) -> Bool, completion: (([Element]) -> Void)? = nil) {
248 | queue.async(flags: .barrier) {
249 | var elements = [Element]()
250 |
251 | while let index = self.array.firstIndex(where: predicate) {
252 | elements.append(self.array.remove(at: index))
253 | }
254 |
255 | DispatchQueue.main.async { completion?(elements) }
256 | }
257 | }
258 |
259 | /// Removes all elements from the array.
260 | ///
261 | /// - Parameter completion: The handler with the removed elements.
262 | func removeAll(completion: (([Element]) -> Void)? = nil) {
263 | queue.async(flags: .barrier) {
264 | let elements = self.array
265 | self.array.removeAll()
266 | DispatchQueue.main.async { completion?(elements) }
267 | }
268 | }
269 | }
270 |
271 | extension SynchronizedArray {
272 |
273 | /// Accesses the element at the specified position if it exists.
274 | ///
275 | /// - Parameter index: The position of the element to access.
276 | /// - Returns: optional element if it exists.
277 | subscript(index: Int) -> Element? {
278 | get {
279 | var result: Element?
280 |
281 | queue.sync {
282 | guard self.array.startIndex.. Bool {
307 | var result = false
308 | queue.sync { result = self.array.contains(element) }
309 | return result
310 | }
311 | }
312 |
313 | // MARK: - Infix operators
314 |
315 | extension SynchronizedArray {
316 |
317 | /// Adds a new element at the end of the array.
318 | ///
319 | /// - Parameters:
320 | /// - left: The collection to append to.
321 | /// - right: The element to append to the array.
322 | static func +=(left: inout SynchronizedArray, right: Element) {
323 | left.append(right)
324 | }
325 |
326 | /// Adds new elements at the end of the array.
327 | ///
328 | /// - Parameters:
329 | /// - left: The collection to append to.
330 | /// - right: The elements to append to the array.
331 | static func +=(left: inout SynchronizedArray, right: [Element]) {
332 | left.append(right)
333 | }
334 |
335 | }
336 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Structures/BusStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BusStorage.swift
3 | // RealEventsBus
4 | //
5 | // Created by Daniele Margutti on 28/11/21.
6 | //
7 |
8 | import Foundation
9 |
10 | /// The bus storage is used to keep the list of registered buses
11 | /// for event types along their observers. Typically you don't need
12 | /// to use custom store, but the library allows you to keep it so
13 | /// you can better manage custom needs.
14 | public class BusStorage {
15 |
16 | // MARK: - Public Properties
17 |
18 | /// The default bus storage.
19 | static public let `default` = BusStorage()
20 |
21 | // MARK: - Private Properties
22 |
23 | @AtomicValue
24 | /// The list of registered buses (as type-erased) in the storage.
25 | internal var buses = [AnyBus]()
26 |
27 | // MARK: - Initialization
28 |
29 | /// Initialize a new storage.
30 | public init() {
31 |
32 | }
33 |
34 | // MARK: - Private Functions
35 |
36 | /// Get the bus instance for a specified event type currently hold by the storage.
37 | ///
38 | /// - Returns: `Bus?`
39 | internal func buseForEventType() -> Bus? {
40 | for case let subscriber as Bus in buses {
41 | return subscriber
42 | }
43 |
44 | return nil
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Structures/Event.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Event.swift
3 | // RealEventsBus
4 | //
5 | // Created by Daniele Margutti on 28/11/21.
6 | //
7 |
8 | import Foundation
9 |
10 | /// Type-erased protocol which define an event.
11 | public protocol AnyEvent { }
12 |
13 | /// The base type of an event.
14 | public protocol Event: AnyEvent { }
15 |
16 | /// Buffered event allows you to query for
17 | /// the last value posted even outside the bus callback.
18 | public protocol BufferedEvent: AnyEvent { }
19 |
--------------------------------------------------------------------------------
/Sources/RealEventsBus/Structures/EventObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EventObserver.swift
3 | // RealEventsBus
4 | //
5 | // Created by Daniele Margutti on 28/11/21.
6 | //
7 |
8 | import Foundation
9 |
10 | internal class EventObserver {
11 |
12 | // MARK: - Private Properties
13 |
14 | /// Callback to call when observer is triggered.
15 | typealias EventCallback = Bus.EventCallback
16 |
17 | // MARK: - Private Properties
18 |
19 | /// The observer object itself. It's weakly referenced so we don't
20 | /// bother about memory issues.
21 | weak var observer: AnyObject?
22 |
23 | /// The pointer is used when you need to check the observer to remove.
24 | let observerRawPointer: UnsafeRawPointer
25 |
26 | /// Callback to call when the observer is triggered.
27 | let callback: EventCallback
28 |
29 | /// The name of the class of the event. This is used only for debug
30 | /// purpose in assertion failure case.
31 | let eventClassName: String
32 |
33 | /// Dispatch queue where the event should be called.
34 | let queue: DispatchQueue
35 |
36 | // MARK: - Initialization
37 |
38 | /// Initialize a new observer for a given object.
39 | ///
40 | /// - Parameters:
41 | /// - observer: object to observe.
42 | /// - queue: the dispatch queue in which the callback is triggered.
43 | /// - callback: callback to call when observer is triggered.
44 | init(_ observer: AnyObject, _ queue: DispatchQueue, _ callback: @escaping EventCallback) {
45 | self.observer = observer
46 | self.observerRawPointer = UnsafeRawPointer(Unmanaged.passUnretained(observer).toOpaque())
47 | self.eventClassName = String(describing: observer)
48 | self.callback = callback
49 | self.queue = queue
50 | }
51 |
52 | // MARK: - Private Function
53 |
54 | func post(_ event: EventType) {
55 | guard observer != nil else {
56 | // No leak is generated but it could be great if you call `unregister()` on dealloc.
57 | assertionFailure("Observer for '\(eventClassName)' was deallocated without calling unregister()")
58 | return
59 | }
60 |
61 | queue.async { [weak self] in
62 | self?.callback(event)
63 | }
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/Tests/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/malcommac/RealEventsBus/2093e288f2fe764e44b2234220e64b7a1fa2b748/Tests/.DS_Store
--------------------------------------------------------------------------------
/Tests/RealEventsBusTests/RealEventsBusTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import RealEventsBus
3 |
4 | final class RealEventsBusTests: XCTestCase {
5 | func testExample() throws {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 |
10 | }
11 | }
12 |
--------------------------------------------------------------------------------