A lightweight observables framework, also available in Kotlin
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"
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)
]
Add all the files from Snail/Snail
to your project
- Run the setup script to install required dependencies
./scripts/setup.sh
let observable = Observable<thing>()
A disposer is in charge of removing all the subscriptions. 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
.
If the Disposer
helps get rid of the closures and prevent retention cycles (see weak self section). For the sake of all the examples, let's have a disposer created:
let disposer = Disposer()
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
.
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)
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
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
let variable = Variable<String>("Something")
variable.map { $0.count }.asObservable().subscribe(
onNext: { (charactersCount: Int -> Void) in ... } // do something with Integer 'charactersCount' on value changes
).add(to: disposer)
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
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)
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:
-
You don't specify the queue. Your observer will be notified on the same thread as the observable published on.
-
You specified
main
queue AND the observable published on themain
queue. Your observer will be notified synchronously on themain
queue. -
You specified a queue. Your observer will be notified async on the specified queue.
Subscribing on DispatchQueue.main
observable.subscribe(queue: .main,
onNext: { thing in ... }
).add(to: disposer)
You can use [weak self]
if you want, but with the introduction of Disposer
, retention cycles are destroyed when calling disposer.disposeAll()
.
One idea would be to call disposer.disposeAll()
when you pop a view controller from the navigation stack.
protocol HasDisposer {
var disposer: Disposer
}
class NavigationController: UINavigationController {
public override func popViewController(animated: Bool) -> UIViewController? {
let viewController = super.popViewController(animated: animated)
(viewController as? HasDisposer).disposer.disposeAll()
return viewController
}
}
NotificationCenter.default.observeEvent(Notification.Name.UIKeyboardWillShow)
.subscribe(queue: .main, onNext: { notification in
self.keyboardWillShow(notification)
}).add(to: disposer)
let panGestureRecognizer = UIPanGestureRecognizer()
panGestureRecognizer.asObservable()
.subscribe(queue: .main, onNext: { sender in
// Your code here
}).add(to: disposer)
view.addGestureRecognizer(panGestureRecognizer)
navigationItem.leftBarButtonItem?.tap
.subscribe(onNext: {
self.dismiss(animated: true, completion: nil)
}).add(to: disposer)