An observables framework for Swift

Overview

๐ŸŒ snail Carthage compatible Cocoapods codecov.io SwiftPM Compatible

SNAIL

A lightweight observables framework, also available in Kotlin

Installation

Carthage

You can install Carthage with Homebrew using the following command:

brew update
brew install carthage

To integrate Snail into your Xcode project using Carthage, specify it in your Cartfile where "x.x.x" is the current release:

github "UrbanCompass/Snail" "x.x.x"

Swift Package Manager

To install using Swift Package Manager have your Swift package set up, and add Snail as a dependency to your Package.swift.

dependencies: [
    .Package(url: "https://github.com/UrbanCompass/Snail.git", majorVersion: 0)
]

Manually

Add all the files from Snail/Snail to your project

Developing Locally

  1. Run the setup script to install required dependencies ./scripts/setup.sh

Creating Observables

let observable = Observable<thing>()

Disposer

What the Disposer IS

The disposer is used to maintain reference to many subscriptions in a single location. When a disposer is deinitialized, it removes all of its referenced subscriptions from memory. A disposer is usually located in a centralized place where most of the subscriptions happen (ie: UIViewController in an MVVM architecture). Since most of the subscriptions are to different observables, and those observables are tied to type, all the things that are going to be disposed need to comform to Disposable.

What the Disposer IS NOT

The disposer is not meant to prevent retain cycles. A common example is a UIViewController that has reference to a Disposer object. A subscription definition might look something like this:

extension MyViewController {
  button.tap.subscribe(onNext: { [weak self] in
    self?.navigationController.push(newVc)
  }).add(to: disposer)
}

Without specifying a [weak self] capture list in a scenario like this, a retain cycle is created between the subscriber and the view controller. In this example, without the capture list, the view controller will not be deallocated as expected, causing its disposer object to stay in memory as well. Since the Disposer removes its referenced subscribers when it is deinitialized, these subscribers will stay in memory as well.

See https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html for more details on memory management in Swift.

Closure Wrapper

The main usage for the Disposer is to get rid of subscription closures that we create on Observables, but the other usage that we found handy, is the ability to dispose of regular closures. As part of the library, we created a small Closure wrapper class that complies with Disposable. This way you can wrap simple closures to be disposed.

let closureCall = Closure {
    print("We โค๏ธ Snail")
}.add(to: Disposer)

Please note that this would not dispose of the closureCall reference to closure, it would only Dispose the content of the Closure.

Subscribing to Observables

observable.subscribe(
    onNext: { thing in ... }, // do something with thing
    onError: { error in ... }, // do something with error
    onDone: { ... } //do something when it's done
).add(to: disposer)

Closures are optional too...

observable.subscribe(
    onNext: { thing in ... } // do something with thing
).add(to: disposer)
observable.subscribe(
    onError: { error in ... } // do something with error
).add(to: disposer)

Creating Observables Variables

let variable = Variable<whatever>(some initial value)
let optionalString = Variable<String?>(nil)
optionalString.asObservable().subscribe(
    onNext: { string in ... } // do something with value changes
).add(to: disposer)

optionalString.value = "something"
let int = Variable<Int>(12)
int.asObservable().subscribe(
    onNext: { int in ... } // do something with value changes
).add(to: disposer)

int.value = 42

Combining Observable Variables

let isLoaderAnimating = Variable<Bool>(false)
isLoaderAnimating.bind(to: viewModel.isLoading) // forward changes from one Variable to another

viewModel.isLoading = true
print(isLoaderAnimating.value) // true
Observable.merge([userCreated, userUpdated]).subscribe(
  onNext: { user in ... } // do something with the latest value that got updated
}).add(to: disposer)

userCreated.value = User(name: "Russell") // triggers 
userUpdated.value = User(name: "Lee") // triggers 
Observable.combineLatest((isMapLoading, isListLoading)).subscribe(
  onNext: { isMapLoading, isListLoading in ... } // do something when both values are set, every time one gets updated
}).add(to: disposer)

isMapLoading.value = true
isListLoading.value = true // triggers

Miscellaneous Observables

let just = Just(1) // always returns the initial value (1 in this case)

enum TestError: Error {
  case test
}
let failure = Fail(TestError.test) // always fail with error

let n = 5
let replay = Replay(n) // replays the last N events when a new observer subscribes

Operators

Snail provides some basic operators in order to transform and operate on observables.

  • map: This operator allows to map the value of an obsverable into another value. Similar to map on Collection types.

    let observable = Observable<Int>()
    let subject = observable.map { "Number: \($0)" }
    // -> subject emits `String` whenever `observable` emits.
  • filter: This operator allows filtering out certain values from the observable chain. Similar to filter on Collection types. You simply return true if the value should be emitted and false to filter it out.

    let observable = Observable<Int>()
    let subject = observable.filter { $0 % 2 == 0 }
    // -> subject will only emit even numbers.
  • flatMap: This operator allows mapping values into other observables, for example you may want to create an observable for a network request when a user tap observable emits.

    let fetchTrigger = Observable<Void>()
    let subject = fetchTrigger.flatMap { Variable(100).asObservable() }
    // -> subject is an `Observable<Int>` that is created when `fetchTrigger` emits.

Subscribing to Control Events

let control = UIControl()
control.controlEvent(.touchUpInside).subscribe(
  onNext: { ... }  // do something with thing
).add(to: disposer)

let button = UIButton()
button.tap.subscribe(
  onNext: { ... }  // do something with thing
).add(to: disposer)

Queues

You can specify which queue an observables will be notified on by using .subscribe(queue: <desired queue>). If you don't specify, then the observable will be notified on the same queue that the observable published on.

There are 3 scenarios:

  1. You don't specify the queue. Your observer will be notified on the same thread as the observable published on.

  2. You specified main queue AND the observable published on the main queue. Your observer will be notified synchronously on the main queue.

  3. You specified a queue. Your observer will be notified async on the specified queue.

Examples

Subscribing on DispatchQueue.main

observable.subscribe(queue: .main,
    onNext: { thing in ... }
).add(to: disposer)

In Practice

Subscribing to Notifications

NotificationCenter.default.observeEvent(Notification.Name.UIKeyboardWillShow)
  .subscribe(queue: .main, onNext: { notification in
    self.keyboardWillShow(notification)
  }).add(to: disposer)

Subscribing to Gestures

let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.asObservable()
  .subscribe(queue: .main, onNext: { sender in
    // Your code here
  }).add(to: disposer)
view.addGestureRecognizer(panGestureRecognizer)

Subscribing to UIBarButton Taps

navigationItem.leftBarButtonItem?.tap
  .subscribe(onNext: {
    self.dismiss(animated: true, completion: nil)
  }).add(to: disposer)
Comments
  • Add swift-tools-version, update gitignore, update Package.swift

    Add swift-tools-version, update gitignore, update Package.swift

    Hey! Hope you guys are doing well :D!

    I was moving a project over to use SPM and noticed a few errors when I added Snail.

    1. Looks like Package.swift must begin with swift-tools-version:. I set the version to 4.2 and 5.2 but I'm happy to change that to whatever you guys want :).

    2. Looks like exclude isn't a parameter in the Package initializer (I'm not sure when that changed)?

      Error with `exclude`

      snail-exclude-error

    3. Added .swiftpm/ to the gitignore as per this

    opened by nkanetka 3
  • Addition Bind Function

    Addition Bind Function

    Description

    We had this discussion some time ago on Slack, regarding adding this helper / convenient function that removes unnecessarily / cumbersome binding code.

    We would replace:

    userService.isRefreshing.asObservable().subscribe(onNext: { [weak self] isRefreshing in
         self?.loadingMap.value = isRefreshing
    })
    

    By this single line:

    loadingMap.bind(to: userService.isRefreshing)
    

    Note: some of the other suggestions that were "not preferred" by the team:

    // Custom operator
    loadingMap <~ userService.isRefreshing
    
    // Static function
    Observable.bind(loadingMap, to: userService.isRefreshing)
    
    // Free function
    bind(loadingMap, to: userService.isRefreshing)
    
    opened by bastienFalcou 3
  • Installation Section

    Installation Section

    Hey, your library is really interesting.

    The only problem I found was the README.md, which lacks an Installation Section I created this iOS Open source Readme Template so you can take a look on how to easily create an Installation Section If you want, I can help you to organize the lib.

    What are your thoughts? ๐Ÿ˜„

    opened by lfarah 3
  • Update podspce file, change deployment target to ios 8

    Update podspce file, change deployment target to ios 8

    I noticed top closed PR that shows "Don't commit the podspec", but sorry to say our app still supports iOS system earlier than 11, and I see that Snail supports iOS 8+ actally, so could we change deployment target to iOS 8?

    opened by XueshiQiao 2
  • Add map, filter and flatMap operators to Observable

    Add map, filter and flatMap operators to Observable

    This PR aims to add some needed functionality to Snail. As well as cleans up some of the API.

    Currently Observable type doesn't support, mapping, filtering or creating other observables.

    This PR adds:

    • map function to Observable
    • filter๏ปฟfunction to Observable
    • flatMap function to Observable
    • Adds combineLatest for 3 and 4 observables.
    • Adds a timeout option to block function.
    • Tests for the above

    Note:

    • This is a breaking API change to combineLatest since I removed the tuple requirement from the function call.
    • This is a breaking API change to Variable.map since I made transform argument parameter not required.
    opened by luispadron 2
  • Variable Mapping

    Variable Mapping

    I am suggesting this addition to Snail. This allows to map a Unique<T> into a Unique<U> that will receive the same notifications, with the value transformed from its initial type T to the expected new type U.

    This is useful for me here when I use:

    public enum ValidationField {
        case binding(Variable<String?>) // expects a Variable of type <String?>
        // ...
    }
    

    But in my case, I need to bind viewModel.showingAgent which is of type Unique<FullContact?>. I don't want to create a new enumeration member taking that type specifically. I prefer to reuse that existing enumeration member and map viewModel.showingAgent into a Unique<String?>.

    I think this could be useful in multiple places as well along the way ๐Ÿ‘

    opened by bastienFalcou 2
  • Add twoWayBind to Variable<T>

    Add twoWayBind to Variable

    Summary

    • Create TwoWayBind protocol
    • Connect two Variable<T>s so that any update to one, updates the other.
    VarA<Int> -----1----------2-------3-------4---------------5-------------------------------->
                    \         |        \       \              |
                     |       /          |       |            /
    VarB<Int> -------1------2-----------3-------4-----------5---------------------------------->
    
    varA.twoWayBind(with: varB)
    
    varA.asObservable().subscribe(onNext: { value in
        print(value) // 1, 2, 3, 4, 5
    }
    varB.asObservable().subscribe(onNext: { value in
        print(value) // 1, 2, 3, 4, 5
    }
    
    opened by jacksoncheek 1
  • Add zip operator

    Add zip operator

    Summary

    • Adding Observable.zip(...) operator, which is very similar to Observable.combineLatest(...)
    • http://reactivex.io/documentation/operators/zip.html
    • The Zip method returns an Observable that applies a function of your choosing to the combination of items emitted, in sequence, by two (or more) other Observables, with the results of this function becoming the items emitted by the returned Observable. It applies this function in strict sequence, so the first item emitted by the new Observable will be the result of the function applied to the first item emitted by Observable #1 and the first item emitted by Observable #2; the second item emitted by the new zip-Observable will be the result of the function applied to the second item emitted by Observable #1 and the second item emitted by Observable #2; and so forth. It will only emit as many items as the number of items emitted by the source Observable that emits the fewest items.
    • Only adding for two observables at this time for simplicity. If we want more (similar to .combineLatest), let's wait for that use case to reduce complexity in the implementations.
    opened by jacksoncheek 1
  • Add thread safety to Observable & Replay

    Add thread safety to Observable & Replay

    Summary

    Both Observable and Replay have issues regarding data races when accessing events and subscribers arrays.

    In this PR we are adding a dedicated queue for accessing these arrays.

    We use sync when reading the values from the array and use async with a barrier (which guarantees to wait for the last access to finish before a new access).

    I've also added two new tests which both previously fail when running with thread sanitizer but pass now with these changes.

    opened by luispadron 1
  • Removed Inheritance for Unique

    Removed Inheritance for Unique

    • Inheritance was causing errors on the requirements for the superclass
    • This PR removes that inheritance since we're dealing with two different types
    • Fixes #136
    opened by lucaslain 1
Releases(0.13.0)
Owner
Compass
Compass Real Estate
Compass
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
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
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
An array class implemented in Swift that can be observed using ReactiveCocoa's Signals

ReactiveArray An array class implemented in Swift that can be observed using ReactiveCocoa's Signals. Installation Carthage Add the following to your

Wolox 53 Jan 29, 2022
Simple and lightweight Functional Reactive Coding in Swift for the rest of us

The simplest Observable<T> implementation for Functional Reactive Programming you will ever find. This library does not use the term FRP (Functional R

Jens Ravens 1.1k Jan 3, 2023