├── .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 | --------------------------------------------------------------------------------