Aftermath is a stateless message-driven micro-framework in Swift

Overview

⚠️ DEPRECATED, NO LONGER MAINTAINED

Aftermath

CI Status Version Carthage Compatible Swift License Platform

Description

Aftermath is a stateless message-driven micro-framework in Swift, which is based on the concept of the unidirectional data flow architecture.

At first sight Aftermath may seem to be just a type-safe implementation of the publish-subscribe messaging pattern, but actually it could be considered as a distinct mental model in application design, different from familiar MVC, MVVM or MVP approaches. Utilizing the ideas behind Event Sourcing and Flux patterns it helps to separate concerns, reduce code dependencies and make data flow more predictable.

The following diagram demonstrates the data flow in Aftermath architecture in details:


Table of Contents

Core components

Command

Command is a message with a set of instructions describing an intention to execute the corresponding behavior. Command could lead to data fetching, data mutation and any sort of sync or async operation that produces desirable output needed to update application/view state.

Every command can produce only one Output type.

Command Handler

Command Handler layer is responsible for business logic in the application. The submission of a command is received by a command handler, which usually performs short- or long-term operation, such as network request, database query, cache read/white process, etc. Command handler can be sync and publish the result immediately. On the other hand it's the best place in the application to write asynchronous code.

The restriction is to create only one command handler per command.

Event

Command Handler is responsible for publishing events that will be consumed by reactions. There are 3 types of events:

  • progress event indicates that the operation triggered by command has been started and is in the pending state at the moment.
  • data event holds the output produced by the command execution.
  • error notifies that an error has been occurred during the command execution.

Reaction

Reaction responds to event published by command handler. It is supposed to handle 3 possible event types by describing the desired behavior in the each scenario:

  • wait function reacts on progress type of the event
  • consume function reacts on data type of the event.
  • rescue function is a fallback for the case when error event has been received.

Normally reaction performs UI updates, but could also be used for other kinds of output processing.

The flow

Taking 4 core components described before, we can build a simplified version of the data flow:


Command execution

The first step is to declare a command. Your command type has to conform to the Aftermath.Command protocol and the Output type must be implicitly specified.

Let's say we want to fetch a list of books from some untrusted resource and correct typos in titles and author names 🤓 .

// This is our model we are going to work with.
struct Book {
  let id: Int
  let title: String
  let author: String
}

struct BookListCommand: Command {
  // Result of this command will be a list of books.
  typealias Output = [Book]
}

struct BookUpdateCommand: Command {
  // Result of this command will be an updated book.
  typealias Output = Book

  // Let's pass the entire model to the command to simplify this example.
  // Ideally we wouldn't do that because a command is supposed to be as simple
  // as possible, only with attributes that are needed for handler.
  let book: Book
}

Note that any type can play the role of Output, so if we want to add a date to our BookUpdateCommand it could look like the following:

typealias Output = (Book, Date)

In order to execute a command you have to conform to CommandProducer protocol:

class ViewController: UITableViewController, CommandProducer {

  // Fetch a list of books.
  func load() {
    execute(command: BookListCommand())
  }

  // Update a single book with corrected title and/or author name.
  func update(book: Book) {
    execute(command: BookUpdateCommand(book: book))
  }
}

Command handling

Command is an intention that needs to be translated into an action by a handler. The command handler is responsible for publishing events to notify about results of the operation it performs. The command handlers type has to conform to Aftermath.CommandHandler protocol, that needs to know about the command type it will work with:

struct BookListCommandHandler: CommandHandler {

  func handle(command: BookListCommand) throws -> Event
    {
    
   // Start network request to fetch data.

       fetchBooks { books, error 
   in
      
   if 
   let error 
   = error {
        
   // Publish error.

           
   self.
   publish(
   error: error)
        
   return
      }

      
   // Publish fetched data.

         
   self.
   publish(
   data: books)
    }

    
   // Load data from local database/cache.

       
   let localBooks 
   = 
   loadLocalBooks()

    
   // If the list is empty let the listeners know that operation is in the process.

       
   return Book.
   list.
   isEmpty 
   ? Event.
   progress 
   : Event.
   data(localBooks)
  }
}
  

Note that every command handler needs to be registered on Aftermath Engine.

Engine.shared.use(handler: BookListCommandHandler())

Reacting to events

The last step, but not the least, is to react to events published by the command handlers. Just conform to ReactionProducer protocol, implement reaction behavior and you're ready to go:

class ViewController: UITableViewController, CommandProducer, ReactionProducer {

  var books = [Book]()

  deinit {
    // Don't forget to dispose all reaction tokens.
    disposeAll()
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    // React to events.
    react(to: BookListCommand.self, with: Reaction(
      wait: { [weak self] in
        // Wait for results to come.
        self?.refreshControl?.beginRefreshing()
      },
      consume: { [weak self] books in
        // We're lucky, there are some books to display.
        self?.books = books
        self?.refreshControl?.endRefreshing()
        self?.tableView.reloadData()
      },
      rescue: { [weak self] error in
        // Well, seems like something went wrong.
        self?.refreshControl?.endRefreshing()
        print(error)
      }))
  }

  // ...
}

It's important to dispose all reaction tokens when your ReactionProducer instance is about to be deallocated or reaction needs to be unsubscribed from events.

// Disposes all reaction tokens for the current `ReactionProducer`.
disposeAll()

// Disposes a specified reaction token.
let token = react(to: BookListCommand.self, with: reaction)
dispose(token: token)

Extra

Action

Action is a variation of command that handles itself. It's a possibility to simplify the code when command itself or business logic are super tiny. There is no need to register an action, it will be automatically added to the list of active command handlers on the fly, when it's executed as a command.

import Sugar

struct WelcomeAction: Action {
  typealias Output = String

  let userId: String

  func handle(command: WelcomeAction) throws -> Event
    {
    
   fetchUser(
   id: userId) { user 
   in
      
   self.
   publish(
   data: 
   "Hello \(user.name)")
    }
    
   return Event.
   progress
  }
}


   // Execute action

   

   struct 
   WelcomeManager: 
   CommandProducer {

  
   func 
   salute() {
    
   execute(
   action: 
   WelcomeAction(
   userId: 
   11))
  }
}
  

Fact

Fact works like notification, with no async operations involved. It can be used when there is no need for a handler to generate an output. Fact is an output itself, so the only thing you want to do is notify all subscribers that something happened in the system, and they will react accordingly. In this sense it's closer to a type-safe alternative to Notification.

struct LoginFact: Fact {
  let username: String
}

class ProfileController: UIViewController, ReactionProducer {

  override func viewDidLoad() {
    super.viewDidLoad()

    // React
    next { (fact: LoginFact) in
      title = fact.username
    }
  }
}

struct AuthService: FactProducer {

  func login() {
    let fact = LoginFact(username: "John Doe")
    // Publish
    post(fact: fact)  
  }
}

Middleware

Middleware is a layer where commands and events can be intercepted before they reach their listeners.

It means you can modify/cancel/extend the executed command in Command Middleware before it's processed by the command handler:


Or you can do appropriate operation in Event Middleware before the published event is received by its reactions.


It's handy for logging, crash reporting, aborting particular commands or events, etc.

\(command)") throw error } } } Engine.shared.pipeCommands(through: [ErrorCommandMiddleware()]) // Event middleware struct LogEventMiddleware: EventMiddleware { // Don't forget to call `next` to invoke the next function in the chain. func intercept(event: AnyEvent, publish: Publish, next: Publish) throws { print("Event published -> \(event)") try next(event) } } Engine.shared.pipeEvents(through: [LogEventMiddleware()]) ">
// Command middleware
struct ErrorCommandMiddleware: CommandMiddleware {

  func intercept(command: AnyCommand, execute: Execute, next: Execute) throws {
    do {
      // Don't forget to call `next` to invoke the next function in the chain.
      try next(command)
    } catch {
      print("Command failed with error -> \(command)")
      throw error
    }
  }
}

Engine.shared.pipeCommands(through: [ErrorCommandMiddleware()])

// Event middleware
struct LogEventMiddleware: EventMiddleware {

  // Don't forget to call `next` to invoke the next function in the chain.
  func intercept(event: AnyEvent, publish: Publish, next: Publish) throws {
    print("Event published -> \(event)")
    try next(event)
  }
}

Engine.shared.pipeEvents(through: [LogEventMiddleware()])

Note that it's necessary to call next to invoke the next function in the chain while building your custom middleware.

AnyCommand and AnyEvent are special protocols that every Command or Event conform to. They are used mostly in middleware to workaround restrictions of working with Swift generic protocols that have associatedtype.

Engine

Engine is the main entry point for Aftermath configuration:

  • Register command handlers:
Engine.shared.use(handler: BookListCommandHandler())
  • Add command and event middleware:
// Commands
Engine.shared.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])
// Events
Engine.shared.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])
  • Set global error handler to catch all unexpected errors and framework warnings:
\(error)") } else if let warning = error as? Warning { print("Engine warning -> \(warning)") } else { print("Unknown error -> \(error)") } } } Engine.shared.errorHandler = EngineErrorHandler() ">
struct EngineErrorHandler: ErrorHandler {

  func handleError(error: Error) {
    if let error = error as? Failure {
      print("Engine error -> \(error)")
    } else if let warning = error as? Warning {
      print("Engine warning -> \(warning)")
    } else {
      print("Unknown error -> \(error)")
    }
  }
}

Engine.shared.errorHandler = EngineErrorHandler()
  • Dispose all registered command handlers and event listeners (reactions):
Engine.shared.invalidate()

Life hacks

Stories

Naming is hard. It doesn't feel right to have names like BookListCommand, BookListCommandHandler and BookListWhatever, does it? If you agree, then you can work around this issue by introducing a new idea into the mix. You can group all related types into stories, which make the flow more concrete.

struct BookListStory {

  struct Command: Aftermath.Command {
    // ...
  }

  struct Handler: Aftermath.CommandHandler {
    // ...
  }
}

In this sense, it's close to user stories used in agile software development methodologies.

You can find more detailed example in AftermathNotes demo project.

Features

Some of the stories may seem very similar. Then in makes sense to make them more generic and reusable according to the DRY principle. For example, let's say we have the flow to fetch a single resource by id.

(id: id)) // Register reaction listener. react(to: DetailCommand .self, with: reaction) ">
import Aftermath
import Malibu

// Generic feature
protocol DetailFeature {
  associatedtype Model: Entity
  var resource: String { get }
}

// Command
struct DetailCommand<Feature: DetailFeature>: Aftermath.Command {
  typealias Output = Feature.Model
  let id: Int
}

// Command handler
struct DetailCommandHandler<Feature: DetailFeature>: Aftermath.CommandHandler {
  typealias Command = DetailCommand

  
    let feature: Feature

  
    func 
    handle(
    command: Command) 
    throws 
    -> Event
     {
    
    fetchDetail(
    "\(feature.resource)/\(command.id)") { json, error 
    in
      
    if 
    let error 
    = error {
        
    self.
    publish(
    error: error)
        
    return
      }

      
    do {
        
    self.
    publish(
    data: 
    try Feature.
    Model(json))
      } 
    catch {
        
    self.
    publish(
    error: error)
      }
    }

    
    return Event.
    progress
  }
}


    // Concrete feature

    
    struct 
    BookFeature: 
    ListFeature, 
    DeleteFeature, 
    CommandProducer {
  
    typealias 
    Model 
    = Todo
  
    var resource 
    = 
    "books"
}


    // Execute command to load a single resource.

    
    execute(
    command: DetailCommand
    <BookFeature
    >(
    id: id))


    // Register reaction listener.

    
    react(
    to: DetailCommand
    <BookFeature
    >.
    self, 
    with: reaction)
   

You can find more detailed example in AftermathNotesPlus demo project.

Summary

We believe that in iOS applications, in most of the cases, there is no real need for single global state (single source of truth) or multiple sub-states distributed between stores. Data is stored on disc in local persistence layer, such as database and cache, or it's fetched from network. Then this content, assembled piece by piece from different sources, is translated into the "view state", which is readable by the view to render it on the screen. This "view state" is kept in memory and valid at a given instant in time until we switch the context and the current view is deallocated.

Keeping that in mind, it makes more sense to dispose the "view state" together with the view it belongs to, rather than retain no longer used replication in any sort of global state.

It should be enough to restore a state by re-playing previous events from the history.

Advantages of Aftermath

  • Separation of concerns
  • Code reusability
  • Unidirectional data flow
  • Type safety

**Disadvantages of Aftermath

  • No state (?)
  • Focusing on command output instead of actual data
  • Async command handler could confuse the flow

P.S. Even though Aftermath is a stateless framework at the moment, we have plans to introduce some sort of optional store(s) for better state management. It might be a new feature in v2, keep watching.

Tools

  • Aftermath comes with a set of development tools, such as additional helpers, useful command and event middleware for logging, error handling, etc.
// Commands
Engine.sharedInstance.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])
// Events
Engine.sharedInstance.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])

Installation

Aftermath is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Aftermath'

Aftermath is also available through Carthage. To install just write into your Cartfile:

github "hyperoslo/Aftermath"

Aftermath can also be installed manually. Just download and drop Sources folders in your project.

Examples

  • iOS Playground uses live view of interactive playground to show how to fetch data from network and display it in the UITableView.

  • AftermathNotes is a simple application that demonstrates how to setup networking stack and data cache layer using Aftermath. It uses the concept of stories to group related types and make the command -> event flow more readable.

  • AftermathNotesPlus is a more advanced example that extends AftermathNotes demo. It plays with generics and introduces the concept of features in order to reuse view controllers and RESTful network requests.

Extensions

This repository aims to be the core implementation of framework, but there are also a range of extensions that integrate Aftermath with other libraries and extend it with more features:

  • AftermathCompass is a message-driven routing system built on top of Aftermath and Compass.

  • AftermathSpots is made to improve development routines of building component-based UIs using Spots cross-platform view controller framework. It comes with custom reactions and injectable behaviors that move code reusability to the next level and make your application even more decoupled and flexible.

Alternatives

Still not sure about state management? It's not that easy to cover all scenarios and find a silver bullet for all occasions. But if you think it's time to break conventions and try new architecture in your next application, there are some links for further reading and research:

Author

Hyper Interaktiv AS, [email protected]

Influences

Aftermath is inspired by the idea of unidirectional data flow in Flux and utilizes some concepts like sequence of commands and events from Event Sourcing.

Contributing

We would love you to contribute to Aftermath, check the CONTRIBUTING file for more info.

License

Aftermath is available under the MIT license. See the LICENSE file for more info.

Comments
  • Aftermath and CQRS

    Aftermath and CQRS

    @zenangst @RamonGilabert @onmyway133

    Some ideas were taken from CQRS https://en.wikipedia.org/wiki/Command–query_separation principle, but in general there is at least one fundamental difference - we don't separate a query layer that only returns data to the caller and a command layer that only updates the state. Aftermath is for front-end apps where both read and update actions go through the same flow:

    Command -> Handler -> Event -> Reaction

    With that being said, should we care about terms that could potentially lead people into confusion? For example, Command could be renamed to Intent or even Action.

    opened by vadymmarkov 6
  • Refactor: events and reactions

    Refactor: events and reactions

    • Remove completion closure because Reaction is a listener and never completes.
    • Rename Event.Success to Event.Data
    • Rename reaction callbacks:
    process -> wait
    done -> consume
    fail -> rescue 
    
    opened by vadymmarkov 4
  • Improve/readme

    Improve/readme

    @zenangst @RamonGilabert @onmyway133 When this PR is merged, the README can be considered as completed 😄

    @zenangst I've tried to play with colours as you suggested, but as for me it didn't look so good at the diagram. So I would go with the current palette and will see if it needs to be improved later.

    opened by vadymmarkov 2
  • Feature: command output

    Feature: command output

    @zenangst @RamonGilabert @onmyway133

    1. Projection is removed, so we have less confusion with new terms 😄 Now you execute command with a specified output result type, and react to event produced by this command. The flow looks like this for example:
    struct UsersStory {
    
      struct Command: Aftermath.Command {
        typealias Output = [ViewModel]
      }
    
      struct Handler: Aftermath.CommandHandler {
    
        func handle(command: Command) throws -> Event<Command> {
          let request = UsersRequest()
    
          Malibu.networking("base").GET(request)
            .validate()
            .toJSONArray()
            .then({ array -> [User] in try array.map({ try User($0) }) })
            .then({ users -> [ViewModel] in
              return users.map({ user in
                ViewModel(
                  identifier: user.id,
                  title: user.name.capitalizedString,
                  subtitle: "Email: \(user.email)",
                  action: "users:\(user.id)")
              })
            })
            .done({ items in
              self.fulfill(items)
            })
            .fail({ error in
              self.reject(error)
            })
    
          return progress
        }
      }
    }
    
    1. Reaction's disposal tokens are handled automatically, the only thing that you need to do is to call disposeAll() from ReactionProducer.

    2. There is a basic implementation of an example project that uses Spots and a lot of other cool stuff.

    opened by vadymmarkov 2
  • Improve: demo and README

    Improve: demo and README

    @zenangst @RamonGilabert @onmyway133 Now we have 2 demo projects (simple and more advanced) + 1 interactive playground. More about them in README, which is almost completed as well.

    opened by vadymmarkov 1
  • Feature: actions and facts

    Feature: actions and facts

    @zenangst @RamonGilabert @onmyway133 Here we introduce 2 new helper types that could make life easier in some specific cases:

    Action Action is a command that handles itself. Sounds weird, but it's actually what it is if you look at protocol declaration:

    public protocol Action: Command, CommandHandler {
      associatedtype CommandType = Self
    }
    

    The idea is to have both command object and command handler in one place. I wouldn't do it very often, but sometimes it probably makes sense for better understanding the flow or when command itself or business logic are super tiny, so you feel like it's overhead to create 2 new types.

    import Sugar
    
    struct WelcomeAction: Action {
      typealias Output = String
    
      let name: String
    
      init(name: String) {
        self.name = name
      }
    
      func handle(command: WelcomeAction) throws -> Event<WelcomeAction> {
        delay(1.0) {
          self.publish(data: "Hello \(name)")
        }
        return Event.Progress
      }
    }
    
    // ...
    execute(WelcomeAction(name: "World"))
    

    The good thing about actions is that there is no need to register your command handler, it will be done automatically when you execute:

    execute(action: action)
    

    Fact On the other hand, fact works like notification with no async operations involved. It could be used when there is no need to have a handler and generate an output, fact is already an output and the only thing you want to do is notify all subscribers that something happened in the system and they will react accordingly. In this sense it's closer to a type-safe alternative to NSNotification.

    struct LoginFact: Fact {
      let username: String
    }
    
    class ProfileController: UIViewController, ReactionProducer {
    
      override func viewDidLoad() {
        super.viewDidLoad()
    
        reactionProducer.next { (fact: LoginFact) in
          title = fact.username
        }
      }
    }
    
    struct AuthService: FactProducer {
      func login() {
        let fact = LoginFact(username: "John Doe")
        post(fact: fact)  
      }
    }
    

    Facts are wrapped into events internally, so they go through EventMiddleware as well.

    opened by vadymmarkov 1
Releases(1.1.0)
Owner
HyperRedink
Connected creativity
HyperRedink
🎧 Protocol driven object observation

Listenable Swift object that provides an observable platform for multiple listeners. Requirements iOS 9.0+ Xcode 9.x+ Swift 4 Installation Listenable

Merrick Sapsford 9 Nov 30, 2022
MacOS Serial solution (Observable & Event-Driven) to make integration of Serial peripherals trivial

SerialSwift SerialSwift makes communicating with your Serial Peripherals on MacOS trivial. Better still, SerialSwift is designed to be fundamnetally O

Flowduino 2 Sep 9, 2022
An observables framework for Swift

?? snail A lightweight observables framework, also available in Kotlin Installation Carthage You can install Carthage with Homebrew using the followin

Compass 179 Nov 21, 2022
A Swift framework for reactive programming.

CwlSignal An implementation of reactive programming. For details, see the article on Cocoa with Love, CwlSignal, a library for reactive programming. N

Matt Gallagher 304 Oct 25, 2022
Cocoa framework and Obj-C dynamism bindings for ReactiveSwift.

Reactive extensions to Cocoa frameworks, built on top of ReactiveSwift. ⚠️ Looking for the Objective-C API? ?? Migrating from RAC 4.x? ?? Release Road

null 20k Jan 8, 2023
Two-way data binding framework for iOS. Only one API to learn.

BindKit A simple to use two-way data binding framework for iOS. Only one API to learn. Supports Objective-C, Swift 5, Xcode 10.2, iOS 8 and above. Shi

Electric Bolt 13 May 25, 2022
RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.

About Architecture concerns RxReduce Installation The key principles How to use RxReduce Tools and dependencies Travis CI Frameworks Platform Licence

RxSwift Community 125 Jan 29, 2022
Open source implementation of Apple's Combine framework for processing values over time.

OpenCombine Open-source implementation of Apple's Combine framework for processing values over time. The main goal of this project is to provide a com

OpenCombine 2.4k Jan 2, 2023
This Repository holds learning data on Combine Framework

Combine Framework List of Topics Welcome, every section in this repo contains a collection of exercises demonstrating combine's utilization as well as

Julio Ismael Robles 2 Mar 17, 2022
Binding - Data binding framework (view model binding on MVVM) written using propertyWrapper and resultBuilder

Binding Data binding framework (view model binding on MVVM) written using @prope

Sugeng Wibowo 4 Mar 23, 2022
🌾 Harvest: Apple's Combine.framework + State Machine, inspired by Elm.

NOTE: This repository has been discontinued in favor of Actomaton. ?? Harvest Apple's Combine.framework (from iOS 13) + State Machine, inspired by Elm

Yasuhiro Inami 386 Dec 18, 2022
Dynamic and type-safe framework for building linear and non-linear flows.

FlowKit FlowKit is a dynamic flow framework capable of building a flow, based on conditions and ordered according to a logic of next steps. By using F

N26 55 Dec 20, 2022
Redux for Swift - a predictable state container for Swift apps

Merge / deprecation announcement: ReduxKit and Swift-Flow have joined forces! The result is ReSwift. The nitty gritty: We decided to deprecate ReduxKi

null 613 Jan 3, 2023
Unidirectional flow implemented using the latest Swift Generics and Swift Concurrency features.

swift-unidirectional-flow Unidirectional flow implemented using the latest Swift Generics and Swift Concurrency features. struct SearchState: Equatabl

Majid Jabrayilov 104 Dec 26, 2022
Reactive Programming in Swift

Rx is a generic abstraction of computation expressed through Observable<Element> interface, which lets you broadcast and subscribe to values and other

ReactiveX 23.1k Jan 5, 2023
RxSwift extentions for Swift optionals and "Occupiable" types

RxOptional RxSwift extentions for Swift optionals and "Occupiable" types. Usage All operators are available on Driver as well unless otherwise marked.

Thane Gill 8 Jun 28, 2020
Unidirectional Data Flow in Swift - Inspired by Redux

ReSwift Supported Swift Versions: Swift 4.2, 5.x For Swift 3.2 or 4.0 Support use Release 5.0.0 or earlier. For Swift 2.2 Support use Release 2.0.0 or

null 7.3k Dec 25, 2022
A Swift Reactive Programming Kit

ReactiveKit is a lightweight Swift framework for reactive and functional reactive programming that enables you to get into the reactive world today. T

Declarative Hub 1.2k Dec 29, 2022
RxSwift wrapper around the elegant HTTP networking in Swift Alamofire

RxAlamofire RxAlamofire is a RxSwift wrapper around the elegant HTTP networking in Swift Alamofire. Getting Started Wrapping RxSwift around Alamofire

RxSwift Community 1.6k Jan 3, 2023