Simple and lightweight Functional Reactive Coding in Swift for the rest of us

Overview

Interstellar

Build Status CocoaPods Version CocoaPods Plattforms Carthage compatible

The simplest Observable<T> implementation for Functional Reactive Programming you will ever find.

This library does not use the term FRP (Functional Reactive Programming) in the way it was defined by Conal Elliot, but as a paradigm that is both functional and reactive. Read more about the difference at Why I cannot say FRP but I just did.

Features

  • Lightweight, simple, cross plattform FRP
  • Multithreading with GCD becomes a breeze
  • Most of your methods will conform to the needed syntax anyway.
  • Swift 3 and 4 compatibility
  • Multithreading with GCD becomes a breeze via WarpDrive
  • Supports Linux and swift build
  • BYOR™-technology (Bring Your Own Result<T>)

Requirements

  • iOS 7.0+ / Mac OS X 10.10+ / Ubuntu 14.10
  • Xcode 8

Usage

For a full guide on how this implementation works see the series of blog posts about Functional Reactive Programming in Swift or the talk at UIKonf 2015 How to use Functional Reactive Programming without Black Magic.

Creating and updating a signal

let text = Observable<String>()

text.subscribe { string in
  print("Hello \(string)")
}

text.update("World")

Mapping and transforming observables

let text = Observable<String>()

let greeting = text.map { subject in
  return "Hello \(subject)"
}

greeting.subscribe { text in
  print(text)
}

text.update("World")

Use functions as transforms

let text = Observable<String>()
let greet: (String)->String = { subject in
  return "Hello \(subject)"
}
text
  .map(greet)
  .subscribe { text in
    print(text)
  }
text.update("World")

Handle errors in sequences of functions

let text = Observable<String>()

func greetMaybe(subject: String) throws -> String {
  if subject.characters.count % 2 == 0 {
    return "Hello \(subject)"
  } else {
    throw NSError(domain: "Don't feel like greeting you.", code: 401, userInfo: nil)
  }
}

text
  .map(greetMaybe)
  .then { text in
    print(text)
  }
  .error { error in
    print("There was a greeting error")
  }
text.update("World")

This also works for asynchronous functions

let text = Observable<String>()
func greetMaybe(subject: String) -> Observable<Result<String>> {
  if subject.characters.count % 2 == 0 {
    return Observable(.success("Hello \(subject)"))
  } else {
    let error = NSError(domain: "Don't feel like greeting you.", code: 401, userInfo: nil)
    return Observable(.error(error))
  }
}

text
  .flatMap(greetMaybe)
  .then { text in
    print(text)
  }
  .error { _ in
    print("There was a greeting error")
  }
text.update(.success("World"))

Flatmap is also available on observables

let baseCost = Observable<Int>()

let total = baseCost
  .flatMap { base in
    // Marks up the price
    return Observable(base * 2)
  }
  .map { amount in
    // Adds sales tax
    return Double(amount) * 1.09
  }

total.subscribe { total in
  print("Your total is: \(total)")
}

baseCost.update(10) // prints "Your total is: 21.8"
baseCost.update(122) // prints "Your total is: 265.96"

Communication

  • If you found a bug, open an issue.
  • If you have a feature request, open an issue.
  • If you want to contribute, open an issue or submit a pull request.

Installation

Dynamic frameworks on iOS require a minimum deployment target of iOS 8 or later. To use Interstellar with a project targeting iOS 7, you must include all Swift files directly in your project.

CocoaPods

CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:

$ gem install cocoapods

To integrate Interstellar into your Xcode project using CocoaPods, specify it in your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!

pod 'Interstellar'

Then, run the following command:

$ pod install

swift build

Add Interstellar to your Package.swift:

import PackageDescription

let package = Package(
  name: "Your Awesome App",
  targets: [],
  dependencies: [
    .Package(url: "https://github.com/jensravens/interstellar.git", majorVersion: 2),
  ]
)

Carthage

Carthage is a decentralized dependency manager that automates the process of adding frameworks to your Cocoa application.

You can install Carthage with Homebrew using the following command:

$ brew update
$ brew install carthage

To integrate Interstellar into your Xcode project using Carthage, specify it in your Cartfile:

github "JensRavens/Interstellar"

FAQ

Why use Interstellar instead of [insert your favorite FRP framework here]?

Interstellar is meant to be lightweight. There are no UIKit bindings, no heavy constructs - just a simple Observable<T>. Therefore it's easy to understand and portable (there is no dependency except Foundation).

Also Interstellar is supporting BYOR (bring your own Result<T>). Due to its protocol based implementation you can use result types from other frameworks directly with Interstellar methods.


Credits

Interstellar is owned and maintained by Jens Ravens.

Changelog

  • 1.1 added compability with Swift 2. Also renamed bind to flatMap to be consistent with Optional and Array.
  • 1.2 Thread was moved to a new project called WarpDrive
  • 1.3 WarpDrive has been merged into Interstellar. Also Interstellar is now divided into subspecs via cocoapods to make it easy to just select the needed components. The basic signal library is now "Interstellar/Core".
  • 1.4 Support swift build and the new Swift package manager, including support for Linux. Also removed deprecated bind methods.
  • 2 Introducing Observable<T>, the successor of Signal. Use the observable property on signals to migrate your code from Signal<T>. Also adding Linux support for Warpdrive and introduce BYOR™-technology (Bring Your Own Result<T>).
  • 2.1 Update to Swift 3.2 to make it compatible with Swift 4.
  • 2.2 Update to Swift 4.1, fixing build warnings on Xcode 9.3 (Maintains backwards compatibility with Swift 3.3 projects).

License

Interstellar is released under the MIT license. See LICENSE for details.

Comments
  • Interstellar 2.0

    Interstellar 2.0

    This issue is collecting all feature requests that require a breaking change to the public API of this library. Please discuss features in the corresponding issues (linked in the description):

    • Split up Signal<T> and Result<T>. This also adds support for non-failing signals and fixes the naming of some methods (flatMap becomes then, so flatMap is free to do a real flat-mapping). #15
    • Support Signal<Void>. This might require to get rid of sending the initial signal value on subscribe. #13
    • Add a way to unsubscribe from a signal using a subscribtion-object. #12
    enhancement 
    opened by JensRavens 18
  • Should flatMap be map?

    Should flatMap be map?

    flatMap should have a function signature of Signal a -> (a -> Signal b) -> Signal b, but it is currently Signal a -> (a -> b) -> Signal b which is a functor mapping, not flatMapping.

    enhancement question 
    opened by b123400 12
  • Migrate to Swift 3.2

    Migrate to Swift 3.2

    This pull requests migrates the project to be compatible with Swift 4. Though the codebase is now compatible to be marked with the Swift 4 build setting, the compiler flag is set to Swift 3.2 for now so that backwards compatibility may be maintained with Xcode 8.3

    As Xcode 9 has the nice new feature to mirror the xcodeproj directory structure to that of the on-disk structure, the directory structure has changed a bit (this was also necessary as Xcode 9 saw dead file references when opening the existing project).

    Currently, the module structure mimics the conceptual structure of the podspec:

    Source code for Interstellar lives in Sources/Interstellar and since Warpdrive is really an extension for Interstellar, the code lives at Sources/Interstellar/Warpdrive

    opened by loudmouth 11
  • No Unsubscribing from Signals

    No Unsubscribing from Signals

    There is no mechanism to unsubscribe from signals. Thus over the lifetime of the application callbacks to subscription blocks referencing deallocated objects will be sent. The amount of unnecessary callbacks will steadily increase while the application is used. This will finally lead to performance issues, especially in the case when Signals are used to update UI elements in view controllers.

    enhancement 
    opened by sotpt 11
  • Upgrade to Xcode 9.3 and Swift 4.1

    Upgrade to Xcode 9.3 and Swift 4.1

    ^=~~Drops support for Swift 3.2/3.3~~

    Additionally updates the Package.swift manifest to version 4.0 of swift-tools

    Hey @JensRavens

    What do you think about making this change now?

    If you're amenable, could we publish a new version? I imagine it may need to be a 3.0.0 as it will break backwards compatibility with Xcode 8.

    opened by loudmouth 10
  • Renames Result.Error to Result.Failure

    Renames Result.Error to Result.Failure

    This is a cosmetic change that disambiguates the Error case of the Result enum from the error value. Success and failure are better antonyms than success and error.

    (Unit tests & README also updated)

    opened by schwa 7
  • Moves public unsubscription to observer token.

    Moves public unsubscription to observer token.

    This is a first-pass, happy to amend based on any feedback. Changed the existing tests to support the new behaviour. If this looks good to you, I'll update the documentation, too.

    Fixes #40.

    opened by ashfurrow 6
  • Zero-Knowledge Disposal

    Zero-Knowledge Disposal

    Thanks for publishing this great library!

    I really loved it, but I quickly faced a problem (for me at least). I am trying to implement a "service-oriented architecture", which is basically just a bunch of services that are instantiated then bound together through observables to make the whole thing "tick". They should not know about each other, except for the Observable being passed in from the binding object. Doing this makes it impossible to clean up the observer, since unsubscribe requires access to the Observable.

    Pondering on this a bit, I thought of this, and would like to hear your thoughts about it. It could use a bit of type-erasure, and other tune ups, but was wondering if you're interested in this direction:

    final class Subscription<T> {
    
        private weak var observable: Observable<T>?
        private let token: ObserverToken
    
        init(observable: Observable<T>, token: ObserverToken) {
            self.observable = observable
            self.token = token
        }
    
        deinit {
            observable?.unsubscribe(token)
        }
    }
    
    extension ObserverToken {
    
        func subscription<T>(from observable: Observable<T>) -> Subscription<T> {
            return Subscription(observable: observable, token: self)
        }
    }
    
    opened by Mazyod 5
  • More complex unsubscription in v2

    More complex unsubscription in v2

    The current subscription/unsubscription mechanism in v2 relies on having both the Observable and the ObserverToken, which complicates things slightly. For use in a table view cell, for example, we need to unsubscribe during prepareForReuse(), and afterward set the observable and the token to be nil. This requires some awkwardness, either with:

    if let observerToken = observerToken, observable = observable {
        observable.unsubscribe(observerToken)
    }
    self.observerToken = nil
    self.observable = nil
    

    Or something with tuples:

    var subscription: (ObserverToken, Observable<Whatever>)?
    
    ...
    
    if let subscription = subscription {
        subscription.1.unsubscrive(subscription.0)
    }
    subscription = nil
    

    Neither is particularly awesome. I'd like to consider using a more complex ObserverToken that has a weak or unowned reference to the observable it's bound to, so we don't need to hang on to both observable and observer token. Any thoughts? Happy to send a PR if you like the idea.

    opened by ashfurrow 5
  • Yosemite support

    Yosemite support

    Hey, I was curious on what the intended/expected minimum OS X target is.

    The README says the requirements are iOS 7.0+ / Mac OS X 10.9+ / Ubuntu 14.10 but when I use Carthage fetch and install Interstellar it results in a framework with minimum deployment target of OS X 10.11

    I tried to change the Interstellar.xcodeproj to set a deployment target of 10.10, it builds fine... but I'm wondering whether its just an issue with the .xcodeproj and it should work fine or if the documentation is out of date. Any guidance would be helpful, thanks.

    bug 
    opened by danielrhammond 5
  • Adding Signal update(f: T? -> T)

    Adding Signal update(f: T? -> T)

    First of all - i am loving the simplicity of interstellar! ty for putting it online.

    In my projects i added an extension for Signals. I often use this in case of 1 Signal with a Tuple of values.

    Maybe you see some use case for this.

    extension Signal {
      func update(f: T? -> T){
        // internally:  self.update(f(self.value?.value))
        self.update(f(self.peek()))
      }
    }
    
    opened by elm4ward 5
  • Swift's 5 Result type

    Swift's 5 Result type

    Hi, I'm huge fan of the project. It is really well done and using it is a breeze.

    Today, Apple released Xcode 10.2 with Swift 5 and now we have Result type built into the language. How do you think it influences the project? Should it replace Interstellar's Result type or would you like to keep them both by conforming to ResultType protocol?

    opened by anaglik 5
  • Updating observable's value without triggering update

    Updating observable's value without triggering update

    I don't think it's possible to "silently" update an observable's value. Such a use-case kinda popped-up for me today...

    Do you think it makes sense to add it to Interstellar's API?

    opened by attheodo 1
  • Feature Request: API for cancelling underlying async task

    Feature Request: API for cancelling underlying async task

    I am wrapping URLSessionDataTask instances in Observable instances via code similar to that listed below (I have removed the use of generics and replaced them with concrete types to, hopefully, make the example more clear).

    // Where the conversion of the datatask to the signal occurs.
    func toObservable(url: URL, dataTaskToConvert: (URL, (Result<Data>) -> Void)) -> Observable<Result<Data>> {
    
        let signal = Observable<Result<Data>>()
    
        let value: URLSessionDataTask? = dataTaskToConvert(url) { result in
            signal.update(result)
        }
        return signal
    }
    
    // Example API for consumer to make network requests without ugly callbacks.
    func fetch(url: URL) ->  Observable<Result<Data>> {
        return toObservable(url: url, dataTaskToConvert: fetch)
    }
    
    // DataTask response handling is transformed to use an API with Interstellar's Result type like so:
    func fetch(url: URL, completion: @escaping (Result<Data>) -> Void) -> URLSessionDataTask {
        let task = sharedUrlSession.dataTask(with: url) { data, response, error in
            if let data = data {
                completion(Result.success(data))
                return
            }
    
            if let error = error {
                completion(Result.error(error))
                return
            }
    
            let sdkError = SDKError.invalidHTTPResponse(response: response)
            completion(Result.error(sdkError))
        }
    
        task.resume()
        return task
    }
    

    Unfortunately, I haven't figured out a way to expose the underlying URLSessionDataTask to the client to cancel the request in a clean fashion. I think ideally, there would be somesort of method on the observable to stop the signal. My workaround currently is to return tuple of (URLSessionDataTask, Observable<Result<Data>>) rather than just the Observable so that the client can take advantage of the Observable API provided by Interstellar while also having the ability to cancel the request. However, I find the tuple API ugly to use, as client code would look like the following:

    // must access the tuple element at index `1` (or named parameter)
    fetch(url: someURL).1.then { data in
     // do stuff with data
    }.error { 
        // handle error.
    }
    

    I am relatively new to the reactive style of programming, so I am currently unable to figure out what a good strategy for disposing the observable and it's underlying might be. I looked into the section on Disposables in the README for ReactiveKit, but I'm not sure how the same concept would integrate with Interstellar.

    Is there a way you might recommend cancelling the URLSessionDataTask? I would be very happy to work on a pull request (with a small bit of guidance to kick me off in the right direction) if you think this feature might be useful.

    opened by loudmouth 5
  • Debounce doesn't deliver last call to update

    Debounce doesn't deliver last call to update

    Debounce seems to deliver the first skipped call to update, instead of the last call to update.

    I think this happens because the skipped calls to update are scheduled to be evaluated again 1) without previous skipped calls being cancelled, and 2) are scheduled to be evaluated again very close to each other temporally. Observable.delay uses DispatchQueue.asyncAfter which isn't very precise, so whichever one fires first (usually the first one scheduled in my search-as-you-type use case) will set the new lastCalled value and ignore the others.

    // skip result if there was a newer result
    if currentTime.compare(lastCalled!) == .orderedDescending {
        let s = Observable<T>()
        s.delay(seconds - timeSinceLastCall!).subscribe(updateIfNeeded(observable))
        s.update(value)
    }
    
    opened by yood 0
  • Possible data race in function `subscribe`

    Possible data race in function `subscribe`

    In function subscribe

        public func subscribe(f: Result<T> -> Void) -> Signal<T> {
            if let value = value {
                f(value)
            }
            mutex.lock {
                callbacks.append(f)
            }
            return self
        }
    
    

    https://github.com/JensRavens/Interstellar/blob/master/Sources/Signal.swift#L145-L153

    it seems to me, there's a potential data race: access to value should be protected by the mutex.

    Proposed solution:

        public func subscribe(f: Result<T> -> Void) -> Signal<T> {
            var v: Result<T>?
            mutex.lock {
                v = self.value
                callbacks.append(f)
            }
            if let value = v {
                f(value)
            }
            return self
        }
    
    

    Still, this is not a reliable solution: it's not guaranteed, that function f will be invoked before any other mapping function which is invoked due to an update. These kind of issues can be solved only with introducing an "execution context" (for example a dispatch queue) which defines where the mapping function will be executed:

        public func subscribe(ec: ExecutionContext = <default>, f: Result<T> -> Void) -> Signal<T> {
            mutex.lock {
                if let value = self.value {
                    ec.execAsync { f(value) }
                }
                callbacks.append(f)
            }
            return self
        }
    
    
    opened by couchdeveloper 0
Releases(2.1.0)
  • 2.1.0(Aug 18, 2017)

  • 2.0.0(Jan 27, 2017)

    Introducing Observable<T>, the successor of Signal. Use the observable property on signals to migrate your code from Signal<T>. Also adding Linux support for Warpdrive and introduce BYOR™-technology (Bring Your Own Result<T>).

    Source code(tar.gz)
    Source code(zip)
Owner
Jens Ravens
Swift developer, rails guy, meetup organizer. CTO at nerdgeschoss.
Jens Ravens
A powerful, minimal and composable architecture for building reactive iOS apps with SwiftUI or UIKit

SourceArchitecture A simple yet powerful framework for reactive programming with only a minimal optimized set of types. Sources are self-contained, hi

Daniel Hall 6 Nov 1, 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
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
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
Swift Reactive Programming.

ReactKit Swift Reactive Programming. How to install See Wiki page. Example For UI Demo, please see ReactKit/ReactKitCatalog. Key-Value Observing // cr

ReactKit 1.2k Nov 6, 2022
A reactive wrapper built around UIImagePickerController.

RxMediaPicker RxMediaPicker is a RxSwift wrapper built around UIImagePickerController consisting in a simple interface for common actions like picking

RxSwift Community 180 Apr 24, 2022
RxSwift reactive wrapper for view gestures

RxGesture Usage To run the example project, clone the repo, in the Example folder open RxGesture.xcworkspace. You might need to run pod install from t

RxSwift Community 1.3k Dec 30, 2022
Reactive Keyboard in iOS

RxKeyboard RxKeyboard provides a reactive way of observing keyboard frame changes. Forget about keyboard notifications. It also perfectly works with U

RxSwift Community 1.4k Dec 29, 2022
Reactive WebSockets

RxWebSocket Reactive extensions for websockets. A lightweight abstraction layer over Starscream to make it reactive. Installation RxWebSocket is avail

Flávio Caetano 57 Jul 22, 2022
Simple, lightweight swift bindings

Bindy Just a simple bindings. Installation Add pod 'Bindy' to your podfile, and run pod install SPM is supported too. Usage For now, Bindy has a coupl

Maxim Kotliar 25 Dec 12, 2022
Lightweight observations and bindings in Swift

What is Hanson? Hanson is a simple, lightweight library to observe and bind values in Swift. It's been developed to support the MVVM architecture in o

Blendle 526 Oct 18, 2022
📬 A lightweight implementation of an observable sequence that you can subscribe to.

Features Lightweight Observable is a simple implementation of an observable sequence that you can subscribe to. The framework is designed to be minima

Felix M. 133 Aug 17, 2022
A lightweight, event-driven architectural framework

Trellis Trellis features a declarative DSL that simplifies service bootstrapping: let cluster = try await Bootstrap { Group { Store(model:

Valentin Radu 25 Aug 16, 2022
A lightweight Elm-like Store for SwiftUI

ObservableStore A simple Elm-like Store for SwiftUI, based on ObservableObject. ObservableStore helps you craft more reliable apps by centralizing all

Subconscious 28 Nov 8, 2022
UIKitPreviews - A simple way to see your UIKit ViewController changes live and on-demand

SwiftUI preview provider for UIKit A simple way to see your UIKit ViewController

Emad Beyrami 8 Jan 8, 2023
Super Simple Pager with RxSwift extension

SSPager Super Simple Pager Example To run the example project, clone the repo, and run pod install from the SSPagerExample directory first. Installati

9oya 4 Jul 10, 2022
A super simple library for state management with unidirectional data flow.

OneWay ?? OneWay is still experimental. As such, expect things to break and change in the coming months. OneWay is a super simple library for state ma

SeungYeop Yeom 41 Dec 20, 2022
A Simple exemple of a launching screen made of 100% using SwuiftUI.

A Simple exemple of a launching screen made of 100% using SwuiftUI. You can modify and use it in your app Compatible from iOS 14 (older not tested ) to iOS 16 (beta tested only )

Maxime 2 Nov 4, 2022
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