Merge branch 'develop' of github.com:ReactiveX/RxSwift into feature/documentation
This commit is contained in:
commit
cb69030ec0
|
|
@ -34,3 +34,8 @@ timeline.xctimeline
|
|||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build
|
||||
|
||||
|
||||
# Various
|
||||
|
||||
.DS_Store
|
||||
|
|
|
|||
96
CHANGELOG.md
96
CHANGELOG.md
|
|
@ -1,8 +1,104 @@
|
|||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
* Adds `ignoreElements` operator.
|
||||
* Adds `rx_attributedText` to `UILabel`.
|
||||
|
||||
---
|
||||
|
||||
## [2.0.0-beta.3](https://github.com/ReactiveX/RxSwift/releases/tag/2.0.0-beta.3)
|
||||
|
||||
### Updated
|
||||
|
||||
* Improves KVO mechanism.
|
||||
* Type of observed object is now first argument `view.rx_observe(CGRect.self, "frame")`
|
||||
* Support for observing ObjC bridged enums and `RawRepresentable` protocol
|
||||
* Support for easier extending of KVO using `KVORepresentable` protocol
|
||||
* Deprecates KVO extensions that don't accept type as first argument in favor of ones that do.
|
||||
* Adds `flatMapLatest` (also added to `Driver` unit).
|
||||
* Adds `flatMapFirst` operator (also added to `Driver` unit).
|
||||
* Adds `retryWhen` operator.
|
||||
* Adds `window` operator.
|
||||
* Adds `single` operator.
|
||||
* Adds `single` (blocking version) operator.
|
||||
* Adds `rx_primaryAction` on `UIButton` for `tvOS`.
|
||||
* Transforms error types in `RxSwift`/`RxCocoa` projects from `NSError`s to Swift enum types.
|
||||
* `RxError`
|
||||
* `RxCocoaError`
|
||||
* `RxCocoaURLError`
|
||||
* ...
|
||||
* `NSURLSession` extensions now return `Observable<(NSData!, NSHTTPURLResponse)>` instead of `Observable<(NSData!, NSURLResponse!)>`.
|
||||
* Optimizes consecutive map operators. For example `map(validate1).map(validate2).map(parse)` is now internally optimized to one `map` operator.
|
||||
* Adds overloads for `just`, `sequenceOf`, `toObservable` that accept scheduler.
|
||||
* Deprecates `asObservable` extension of `SequenceType` in favor of `toObservable`.
|
||||
* Adds `toObservable` extension to `Array`.
|
||||
* Improves table view animated data source example.
|
||||
* Polishing of `RxDataSourceStarterKit`
|
||||
* `differentiateForSectionedView` operator
|
||||
* `rx_itemsAnimatedWithDataSource` extension
|
||||
* Makes blocking operators run current thread's runloop while blocking and thus disabling deadlocks.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixes example with `Variable` in playgrounds so it less confusing regarding memory management.
|
||||
* Fixes `UIImageView` extensions to use `UIImage?` instead of `UIImage!`.
|
||||
* Fixes improper usage of `CustomStringConvertible` and replaces it with `CustomDebugStringConvertible`.
|
||||
|
||||
## [2.0.0-beta.2](https://github.com/ReactiveX/RxSwift/releases/tag/2.0.0-beta.2)
|
||||
|
||||
#### Updated
|
||||
|
||||
* Optimizations. System now performs significantly fewer allocations and is several times faster then 2.0.0-beta.1
|
||||
* Makes `AnonymousObservable` private in favor of `create` method.
|
||||
* Adds `toArray` operator (non blocking version).
|
||||
* Adds `withLatestFrom` operator, and also extends `Driver` with that operation.
|
||||
* Adds `elementAt` operator (non blocking version).
|
||||
* Adds `takeLast` operator.
|
||||
* Improves `RxExample` app. Adds retries example when network becomes available again.
|
||||
* Adds composite extensions to `Bag` (`on`, `disposeAllIn`).
|
||||
* Renames mistyped extension on `ObserverType` from `onComplete` to `onCompleted`.
|
||||
|
||||
#### Fixed
|
||||
|
||||
* Fixes minimal platform version in OSX version of library to 10.9
|
||||
|
||||
## [2.0.0-beta.1](https://github.com/ReactiveX/RxSwift/releases/tag/2.0.0-beta.1)
|
||||
|
||||
#### Updated
|
||||
|
||||
* Adds `Driver` unit. This unit uses Swift compiler to prove certain properties about observable sequences. Specifically
|
||||
* that fallback error handling is put in place
|
||||
* results are observed on main thread
|
||||
* work is performed only when there is a need (at least one subscriber)
|
||||
* computation results are shared between different observers (replay latest element)
|
||||
* Renames `ObserverOf` to `AnyObserver`.
|
||||
* Adds new interface `ObservableConvertibleType`.
|
||||
* Adds `BlockingObservable` to `RxBlocking` and makes it more consistent with `RxJava`.
|
||||
* Renames `func subscribe(next:error:completed:disposed:)` to `func subscribe(onNext:onError:onCompleted:onDisposed:)`
|
||||
* Adds concat convenience method `public func concat<O : ObservableConvertibleType where O.E == E>(second: O) -> RxSwift.Observable<Self.E>`
|
||||
* Adds `skipUntil` operator.
|
||||
* Adds `takeWhile` operator.
|
||||
* Renames `takeWhile` indexed version to `takeWhileWithIndex`
|
||||
* Adds `skipWhile` operator.
|
||||
* Adds `skipWhileWithIndex` operator.
|
||||
* Adds `using` operator.
|
||||
* Renames `func doOn(next:error:completed:)` to `func doOn(onNext:onError:onCompleted:)`.
|
||||
* Makes `RecursiveImmediateSchedulerOf` private.
|
||||
* Makes `RecursiveSchedulerOf` private.
|
||||
* Adds `ConcurrentMainScheduler`.
|
||||
* Adds overflow error so now in case of overflow, operators will return `RxErrorCode.Overflow`.
|
||||
* Adds `rx_modelAtIndexPath` to `UITableView` and `UICollectionView`.
|
||||
* Adds `var rx_didUpdateFocusInContextWithAnimationCoordinator: ControlEvent<(context:animationCoordinator:)>` to `UITableView` and `UICollectionView`
|
||||
* Makes `resultSelector` argument in `combineLatest` explicit `func combineLatest<O1, O2, R>(source1: O1, _ source2: O2, resultSelector: (O1.E, O2.E) throws -> R) -> RxSwift.Observable<R>`.
|
||||
* Makes `resultSelector` argument in `zip` explicit `func combineLatest<O1, O2, R>(source1: O1, _ source2: O2, resultSelector: (O1.E, O2.E) throws -> R) -> RxSwift.Observable<R>`.
|
||||
* Adds activity indicator example in `RxExample` app.
|
||||
* Adds two way binding example in `RxExample` app.
|
||||
* many other small features
|
||||
|
||||
#### Fixed
|
||||
|
||||
* Problem with xcodebuild 7.0.1 treating tvOS shared schemes as osx schemes.
|
||||
|
||||
## [2.0.0-alpha.4](https://github.com/ReactiveX/RxSwift/releases/tag/2.0.0-alpha.4)
|
||||
|
||||
#### Updated
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
There are multiple ways you can contribute to this project.
|
||||
|
||||
The easiest way to contribute is to report possible bugs, request features, [discuss ideas](mailto:krunoslav.zaher@gmail.com?subject=[RxSwift] I have an idea) and share excitement about this project.
|
||||
The easiest way to contribute is to report possible bugs, request features, [discuss ideas](https://github.com/ReactiveX/RxSwift/issues) and share excitement about this project.
|
||||
|
||||
You can also make pull requests.
|
||||
|
||||
|
|
@ -10,10 +10,8 @@ There are some best practices that will be followed during the development of th
|
|||
|
||||
So what does this mean in practice:
|
||||
|
||||
* If you notice a bug in **documentation** only, that could be considered non risky hotfix, so please make a PR to **master** branch
|
||||
* If you notice a bug in **source code** please make a PR to **develop** branch because otherwise it could get a lot worse :) If needed, hotfix will be created from commit with the fix and applied to master branch after the PR has been merged into develop and tested.
|
||||
* If you want to make a small contribution (dozen lines of code) that is not a bug fix please make a pull request to **develop** branch.
|
||||
* If you want to make a big contribution to the project, please [discuss it](mailto:krunoslav.zaher@gmail.com?subject=[RxSwift] I have an idea) first with me so we can make sure project is going in the right direction. All pull requests with **source code** contributions should be targeted to **develop** branch.
|
||||
* Please target your PR to **develop** branch
|
||||
* If you want to make a bigger contribution to the project, please [open an issue first](https://github.com/ReactiveX/RxSwift/issues/new) so we can plan that work, discuss the architecture and brainstorm around that idea first.
|
||||
|
||||
## Developer's Certificate of Origin 1.1
|
||||
|
||||
|
|
|
|||
|
|
@ -21,21 +21,29 @@ Operators are stateless by default.
|
|||
* [`never`](http://reactivex.io/documentation/operators/empty-never-throw.html)
|
||||
* [`returnElement` / `just`](http://reactivex.io/documentation/operators/just.html)
|
||||
* [`returnElements`](http://reactivex.io/documentation/operators/from.html)
|
||||
* [`range`](http://reactivex.io/documentation/operators/range.html)
|
||||
* [`repeatElement`](http://reactivex.io/documentation/operators/repeat.html)
|
||||
* [`timer`](http://reactivex.io/documentation/operators/timer.html)
|
||||
|
||||
#### Transforming Observables
|
||||
* [`flatMap`](http://reactivex.io/documentation/operators/flatmap.html)
|
||||
* [`map` / `select`](http://reactivex.io/documentation/operators/map.html)
|
||||
* [`scan`](http://reactivex.io/documentation/operators/scan.html)
|
||||
* [`buffer`](http://reactivex.io/documentation/operators/buffer.html)
|
||||
* [`flatMap`](http://reactivex.io/documentation/operators/flatmap.html)
|
||||
* [`flatMapFirst`](http://reactivex.io/documentation/operators/flatmap.html)
|
||||
* [`flatMapLatest`](http://reactivex.io/documentation/operators/flatmap.html)
|
||||
* [`map`](http://reactivex.io/documentation/operators/map.html)
|
||||
* [`scan`](http://reactivex.io/documentation/operators/scan.html)
|
||||
* [`window`](http://reactivex.io/documentation/operators/window.html)
|
||||
|
||||
#### Filtering Observables
|
||||
* [`debounce` / `throttle`](http://reactivex.io/documentation/operators/debounce.html)
|
||||
* [`distinctUntilChanged`](http://reactivex.io/documentation/operators/distinct.html)
|
||||
* [`filter` / `where`](http://reactivex.io/documentation/operators/filter.html)
|
||||
* [`elementAt`](http://reactivex.io/documentation/operators/elementat.html)
|
||||
* [`filter`](http://reactivex.io/documentation/operators/filter.html)
|
||||
* [`sample`](http://reactivex.io/documentation/operators/sample.html)
|
||||
* [`skip`](http://reactivex.io/documentation/operators/skip.html)
|
||||
* [`take`](http://reactivex.io/documentation/operators/take.html)
|
||||
* [`takeLast`](http://reactivex.io/documentation/operators/takelast.html)
|
||||
* [`single`](http://reactivex.io/documentation/operators/first.html)
|
||||
|
||||
#### Combining Observables
|
||||
|
||||
|
|
@ -49,6 +57,7 @@ Operators are stateless by default.
|
|||
|
||||
* [`catch`](http://reactivex.io/documentation/operators/catch.html)
|
||||
* [`retry`](http://reactivex.io/documentation/operators/retry.html)
|
||||
* [`retryWhen`](http://reactivex.io/documentation/operators/retry.html)
|
||||
|
||||
#### Observable Utility Operators
|
||||
|
||||
|
|
@ -57,10 +66,13 @@ Operators are stateless by default.
|
|||
* [`observeOn` / `observeSingleOn`](http://reactivex.io/documentation/operators/observeon.html)
|
||||
* [`subscribe`](http://reactivex.io/documentation/operators/subscribe.html)
|
||||
* [`subscribeOn`](http://reactivex.io/documentation/operators/subscribeon.html)
|
||||
* [`using`](http://reactivex.io/documentation/operators/using.html)
|
||||
* debug
|
||||
|
||||
#### Conditional and Boolean Operators
|
||||
* [`amb`](http://reactivex.io/documentation/operators/amb.html)
|
||||
* [`skipWhile`](http://reactivex.io/documentation/operators/skipwhile.html)
|
||||
* [`skipUntil`](http://reactivex.io/documentation/operators/skipuntil.html)
|
||||
* [`takeUntil`](http://reactivex.io/documentation/operators/takeuntil.html)
|
||||
* [`takeWhile`](http://reactivex.io/documentation/operators/takewhile.html)
|
||||
|
||||
|
|
@ -68,6 +80,7 @@ Operators are stateless by default.
|
|||
|
||||
* [`concat`](http://reactivex.io/documentation/operators/concat.html)
|
||||
* [`reduce` / `aggregate`](http://reactivex.io/documentation/operators/reduce.html)
|
||||
* [`toArray`](http://reactivex.io/documentation/operators/to.html)
|
||||
|
||||
#### Connectable Observable Operators
|
||||
|
||||
|
|
@ -433,6 +446,6 @@ extension NSTextField {
|
|||
public var rx_delegate: DelegateProxy {}
|
||||
|
||||
public var rx_text: ControlProperty<String> {}
|
||||
|
||||
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -56,14 +56,14 @@ let c = combineLatest(a, b) { $0 + $1 } // combines latest values of variabl
|
|||
// To pull values out of rx variable `c`, subscribe to values from `c`.
|
||||
// `subscribeNext` means subscribe to next (fresh) values of variable `c`.
|
||||
// That also includes the inital value "3 is positive".
|
||||
c.subscribeNext { println($0) } // prints: "3 is positive"
|
||||
c.subscribeNext { print($0) } // prints: "3 is positive"
|
||||
|
||||
// Now let's increase the value of `a`
|
||||
// a = 4 is in RxSwift
|
||||
a.next(4) // prints: 6 is positive
|
||||
// Sum of latest values is now `4 + 2`, `6` is >= 0, map operator
|
||||
// produces "6 is positive" and that result is "assigned" to `c`.
|
||||
// Since the value of `c` changed, `{ println($0) }` will get called,
|
||||
// Since the value of `c` changed, `{ print($0) }` will get called,
|
||||
// and "6 is positive" is printed.
|
||||
|
||||
// Now let's change the value of `b`
|
||||
|
|
@ -73,7 +73,7 @@ b.next(-8) // doesn't print anything
|
|||
// get executed.
|
||||
// That means that `c` still contains "6 is positive" and that's correct.
|
||||
// Since `c` hasn't been updated, that means next value hasn't been produced,
|
||||
// and `{ println($0) }` won't be called.
|
||||
// and `{ print($0) }` won't be called.
|
||||
|
||||
// ...
|
||||
```
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ This project tries to be consistent with [ReactiveX.io](http://reactivex.io/). T
|
|||
1. [Observables aka Sequences](#observables-aka-sequences)
|
||||
1. [Disposing](#disposing)
|
||||
1. [Implicit `Observable` guarantees](#implicit-observable-guarantees)
|
||||
1. [Creating your first `Observable` (aka sequence producers)](#creating-your-own-observable-aka-sequence-producers)
|
||||
1. [Creating your first `Observable` (aka observable sequence)](#creating-your-own-observable-aka-observable-sequence)
|
||||
1. [Creating an `Observable` that performs work](#creating-an-observable-that-performs-work)
|
||||
1. [Sharing subscription and `shareReplay` operator](#sharing-subscription-and-sharereplay-operator)
|
||||
1. [Operators](#operators)
|
||||
|
|
@ -92,7 +92,7 @@ protocol ObserverType {
|
|||
|
||||
**When sequence sends `Complete` or `Error` event all internal resources that compute sequence elements will be freed.**
|
||||
|
||||
**To cancel production of sequence elements and free resources immediatelly, call `dispose` on returned subscription.**
|
||||
**To cancel production of sequence elements and free resources immediately, call `dispose` on returned subscription.**
|
||||
|
||||
If a sequence terminates in finite time, not calling `dispose` or not using `addDisposableTo(disposeBag)` won't cause any permanent resource leaks, but those resources will be used until sequence completes in some way (finishes producing elements or error happens).
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ Event processing ended
|
|||
Event processing ended
|
||||
```
|
||||
|
||||
## Creating your own `Observable` (aka sequence producers)
|
||||
## Creating your own `Observable` (aka observable sequence)
|
||||
|
||||
There is one crucial thing to understand about observables.
|
||||
|
||||
|
|
@ -304,7 +304,7 @@ Let's create a function which creates a sequence that returns one element upon s
|
|||
func myJust<E>(element: E) -> Observable<E> {
|
||||
return create { observer in
|
||||
observer.on(.Next(element))
|
||||
obsever.on(.Completed)
|
||||
observer.on(.Completed)
|
||||
return NopDisposable.instance
|
||||
}
|
||||
}
|
||||
|
|
@ -745,7 +745,7 @@ If you are unsure how exactly some of the operators work, [playgrounds](../Rx.pl
|
|||
|
||||
The are two error mechanisms.
|
||||
|
||||
### Anynchronous error handling mechanism in observables
|
||||
### Asynchronous error handling mechanism in observables
|
||||
|
||||
Error handling is pretty straightforward. If one sequence terminates with error, then all of the dependent sequences will terminate with error. It's usual short circuit logic.
|
||||
|
||||
|
|
@ -753,68 +753,6 @@ You can recover from failure of observable by using `catch` operator. There are
|
|||
|
||||
There is also `retry` operator that enables retries in case of errored sequence.
|
||||
|
||||
### Synchronous error handling
|
||||
|
||||
Unfortunately Swift doesn't have a concept of exceptions or some kind of built in error monad so this project introduces `RxResult` enum.
|
||||
It is Swift port of Scala [`Try`](http://www.scala-lang.org/api/2.10.2/index.html#scala.util.Try) type. It is also similar to Haskell [`Either`](https://hackage.haskell.org/package/category-extras-0.52.0/docs/Control-Monad-Either.html) monad.
|
||||
|
||||
**This will be replaced in Swift 2.0 with try/throws**
|
||||
|
||||
```swift
|
||||
public enum RxResult<ResultType> {
|
||||
case Success(ResultType)
|
||||
case Error(ErrorType)
|
||||
}
|
||||
```
|
||||
|
||||
To enable writing more readable code, a few `Result` operators are introduced
|
||||
|
||||
```swift
|
||||
result1.flatMap { okValue in // success handling block
|
||||
// executed on success
|
||||
return ?
|
||||
}.recoverWith { error in // error handling block
|
||||
// executed on error
|
||||
return ?
|
||||
}
|
||||
```
|
||||
|
||||
### Error handling and function names
|
||||
|
||||
For every group of transforming functions there are versions with and without "OrDie" suffix.
|
||||
|
||||
**This will change in 2.0 version and map will have two overloads, with and without `throws`.**
|
||||
|
||||
e.g.
|
||||
|
||||
```swift
|
||||
public func mapOrDie<E, R>
|
||||
(selector: E -> RxResult<R>)
|
||||
-> (Observable<E> -> Observable<R>) {
|
||||
return { source in
|
||||
return selectOrDie(selector)(source)
|
||||
}
|
||||
}
|
||||
|
||||
public func map<E, R>
|
||||
(selector: E -> R)
|
||||
-> (Observable<E> -> Observable<R>) {
|
||||
return { source in
|
||||
return select(selector)(source)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returning an error from a selector will cause entire graph of dependent sequence transformers to "die" and fail with error. Dying implies that it will release all of its resources and never produce another sequence value. This is usually not an obvious effect.
|
||||
|
||||
If there is some `UITextField` bound to a observable sequence that fails with error or completes, screen won't be updated ever again.
|
||||
|
||||
To make those situations more obvious, RxCocoa debug build will throw an exception in case some sequence that is bound to UI control terminates with an error.
|
||||
|
||||
Using functions without "OrDie" suffix is usually a more safe option.
|
||||
|
||||
There is also the `catch` operator for easier error handling.
|
||||
|
||||
## Debugging Compile Errors
|
||||
|
||||
When writing elegant RxSwift/RxCocoa code, you are probably relying heavily on compiler to deduce types of `Observable`s. This is one of the reasons why Swift is awesome, but it can also be frustrating sometimes.
|
||||
|
|
@ -1008,32 +946,37 @@ There are two built in ways this library supports KVO.
|
|||
```swift
|
||||
// KVO
|
||||
extension NSObject {
|
||||
public func rx_observe<Element>(keyPath: String, retainSelf: Bool = true) -> Observable<Element?> {}
|
||||
public func rx_observe<E>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions, retainSelf: Bool = true) -> Observable<E?> {}
|
||||
}
|
||||
|
||||
#if !DISABLE_SWIZZLING
|
||||
// KVO
|
||||
extension NSObject {
|
||||
public func rx_observeWeakly<Element>(keyPath: String) -> Observable<Element?> {}
|
||||
public func rx_observeWeakly<E>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions) -> Observable<E?> {}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
**If Swift compiler doesn't have a way to deduce observed type (return Observable type), it will report error about function not existing.**
|
||||
Example how to observe frame of `UIView`.
|
||||
|
||||
Here are some ways you can give him hints about observed type:
|
||||
**WARNING: UIKit isn't KVO compliant, but this will work.**
|
||||
|
||||
```swift
|
||||
view.rx_observe("frame") as Observable<CGRect?>
|
||||
view
|
||||
.rx_observe(CGRect.self, "frame")
|
||||
.subscribeNext { frame in
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```swift
|
||||
view.rx_observe("frame")
|
||||
.map { (rect: CGRect?) in
|
||||
//
|
||||
}
|
||||
view
|
||||
.rx_observeWeakly(CGRect.self, "frame")
|
||||
.subscribeNext { frame in
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### `rx_observe`
|
||||
|
|
@ -1047,12 +990,12 @@ view.rx_observe("frame")
|
|||
E.g.
|
||||
|
||||
```swift
|
||||
self.rx_observe("view.frame", retainSelf = false) as Observable<CGRect?>
|
||||
self.rx_observe(CGRect.self, "view.frame", retainSelf: false)
|
||||
```
|
||||
|
||||
### `rx_observeWeakly`
|
||||
|
||||
`rx_observeWeakly` has somewhat worse performance because it has to handle object deallocation in case of weak references.
|
||||
`rx_observeWeakly` has somewhat slower then `rx_observe` because it has to handle object deallocation in case of weak references.
|
||||
|
||||
It can be used in all cases where `rx_observe` can be used and additionally
|
||||
|
||||
|
|
@ -1062,16 +1005,18 @@ It can be used in all cases where `rx_observe` can be used and additionally
|
|||
E.g.
|
||||
|
||||
```swift
|
||||
someSuspiciousViewController.rx_observeWeakly("behavingOk") as Observable<Bool?>
|
||||
someSuspiciousViewController.rx_observeWeakly(Bool.self, "behavingOk")
|
||||
```
|
||||
|
||||
### Observing structs
|
||||
|
||||
KVO is an Objective-C mechanism so it relies heavily on `NSValue`. RxCocoa has additional specializations for `CGRect`, `CGSize` and `CGPoint` that make it convenient to observe those types.
|
||||
KVO is an Objective-C mechanism so it relies heavily on `NSValue`.
|
||||
|
||||
When observing some other structures it is necessary to extract those structures from `NSValue` manually, or creating your own observable sequence specializations.
|
||||
**RxCocoa has built in support for KVO observing of `CGRect`, `CGSize` and `CGPoint` structs.**
|
||||
|
||||
[Here](../RxCocoa/Common/Observables/NSObject+Rx+CoreGraphics.swift) are examples how to correctly extract structures from `NSValue`.
|
||||
When observing some other structures it is necessary to extract those structures from `NSValue` manually.
|
||||
|
||||
[Here](../RxCocoa/Common/KVORepresentable+CoreGraphics.swift) are examples how to extend KVO observing mechanism and `rx_observe*` methods for other structs by implementing `KVORepresentable` protocol.
|
||||
|
||||
## UI layer tips
|
||||
|
||||
|
|
@ -1105,13 +1050,12 @@ Let's say you have something like this:
|
|||
let searchResults = searchText
|
||||
.throttle(0.3, $.mainScheduler)
|
||||
.distinctUntilChanged
|
||||
.map { query in
|
||||
.flatMapLatest { query in
|
||||
API.getSearchResults(query)
|
||||
.retry(3)
|
||||
.startWith([]) // clears results on new search term
|
||||
.catchErrorJustReturn([])
|
||||
}
|
||||
.switchLatest()
|
||||
.shareReplay(1) // <- notice the `shareReplay` operator
|
||||
```
|
||||
|
||||
|
|
@ -1119,6 +1063,8 @@ What you usually want is to share search results once calculated. That is what `
|
|||
|
||||
**It is usually a good rule of thumb in the UI layer to add `shareReplay` at the end of transformation chain because you really want to share calculated results. You don't want to fire separate HTTP connections when binding `searchResults` to multiple UI elements.**
|
||||
|
||||
**Also take a look at `Driver` unit. It is designed to transparently wrap those `shareReply` calls, make sure elements are observed on main UI thread and that no error can be bound to UI.**
|
||||
|
||||
## Making HTTP requests
|
||||
|
||||
Making http requests is one of the first things people try.
|
||||
|
|
|
|||
|
|
@ -94,6 +94,17 @@ Rx can use all types of schedulers, but it can also perform some additional opti
|
|||
|
||||
These are currently supported schedulers
|
||||
|
||||
## CurrentThreadScheduler (Serial scheduler)
|
||||
|
||||
Schedules units of work on the current thread.
|
||||
This is the default scheduler for operators that generate elements.
|
||||
|
||||
This scheduler is also sometimes called `trampoline scheduler`.
|
||||
|
||||
If `CurrentThreadScheduler.instance.schedule(state) { }` is called for first time on some thread, scheduled action will be executed immediately and hidden queue will be created where all recursively scheduled actions will be temporarily enqueued.
|
||||
|
||||
If some parent frame on call stack is already running `CurrentThreadScheduler.instance.schedule(state) { }`, scheduled action will be enqueued and executed when currently running action and all previously enqueued actions have finished executing.
|
||||
|
||||
## MainScheduler (Serial scheduler)
|
||||
|
||||
Abstracts work that needs to be performed on `MainThread`. In case `schedule` methods are called from main thread, it will perform action immediately without scheduling.
|
||||
|
|
@ -110,12 +121,12 @@ Main scheduler is an instance of `SerialDispatchQueueScheduler`.
|
|||
|
||||
## ConcurrentDispatchQueueScheduler (Concurrent scheduler)
|
||||
|
||||
Abstracts the work that needs to be peformed on a specific `dispatch_queue_t`. You can also pass a serial dispatch queue, it shouldn't cause any problems.
|
||||
Abstracts the work that needs to be performed on a specific `dispatch_queue_t`. You can also pass a serial dispatch queue, it shouldn't cause any problems.
|
||||
|
||||
This scheduler is suitable when some work needs to be performed in background.
|
||||
|
||||
## OperationQueueScheduler (Concurrent scheduler)
|
||||
|
||||
Abstracts the work that needs to be peformed on a specific `NSOperationQueue`.
|
||||
Abstracts the work that needs to be performed on a specific `NSOperationQueue`.
|
||||
|
||||
This scheduler is suitable for cases when there is some bigger chunk of work that needs to be performed in background and you want to fine tune concurrent processing using `maxConcurrentOperationCount`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
Units
|
||||
=====
|
||||
|
||||
This document will try to describe what are units, why are they a useful concept, how to use them and how to create them.
|
||||
|
||||
* [Why](#why)
|
||||
* [Design Rationale](#design-rationale)
|
||||
* ...
|
||||
|
||||
# Why
|
||||
|
||||
The purpose of units is to use the Swift compiler static type checking to prove your code is behaving like designed.
|
||||
|
||||
RxCocoa project already contains several units, but the most elaborate one is called `Driver`, so this unit will be used to explain the idea behind units.
|
||||
|
||||
`Driver` was named that way because it describes sequences that drive certain parts of the app. Those sequences will usually drive UI bindings, UI event pumps that keep your application responsive, but also drive application services, etc.
|
||||
|
||||
The purpose of `Driver` unit is to ensure the underlying observable sequence has the following properties.
|
||||
|
||||
* can't fail, all failures are being handled properly
|
||||
* elements are delivered on main thread
|
||||
* sequence computation resources are shared
|
||||
|
||||
TBD...
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
Warnings
|
||||
========
|
||||
|
||||
### <a name="unused-disposable"></a>Unused disposable (unused-disposable)
|
||||
|
||||
The same is valid for `subscribe*`, `bind*` and `drive*` family of functions that return `Disposable`.
|
||||
|
||||
Warning is probably presented in a context similar to this one:
|
||||
|
||||
```Swift
|
||||
let xs: Observable<E> ....
|
||||
|
||||
xs
|
||||
.filter { ... }
|
||||
.map { ... }
|
||||
.switchLatest()
|
||||
.subscribe(onNext: {
|
||||
...
|
||||
}, onError: {
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
`subscribe` function returns a subscription `Disposable` that can be used to cancel computation and free resources.
|
||||
|
||||
Preferred way of terminating these fluent calls is by using `.addDisposableTo(disposeBag)` or in some equivalent way.
|
||||
|
||||
```Swift
|
||||
let xs: Observable<E> ....
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
xs
|
||||
.filter { ... }
|
||||
.map { ... }
|
||||
.switchLatest()
|
||||
.subscribe(onNext: {
|
||||
...
|
||||
}, onError: {
|
||||
...
|
||||
})
|
||||
.addDisposableTo(disposeBag) // <--- note `addDisposableTo`
|
||||
```
|
||||
|
||||
When `disposeBag` gets deallocated, subscription will be automatically disposed.
|
||||
|
||||
In case `xs` terminates in a predictable way with `Completed` or `Error` message, not handling subscription `Disposable` won't leak any resources, but it's still preferred way because in that way element computation is terminated at predictable moment.
|
||||
|
||||
That will also make your code robust and future proof because resources will be properly disposed even if `xs` implementation changes.
|
||||
|
||||
Another way to make sure subscriptions and resources are tied with the lifetime of some object is by using `takeUntil` operator.
|
||||
|
||||
```Swift
|
||||
let xs: Observable<E> ....
|
||||
let someObject: NSObject ...
|
||||
|
||||
_ = xs
|
||||
.filter { ... }
|
||||
.map { ... }
|
||||
.switchLatest()
|
||||
.takeUntil(someObject.rx_dellocated) // <-- note the `takeUntil` operator
|
||||
.subscribe(onNext: {
|
||||
...
|
||||
}, onError: {
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
If ignoring the subscription `Disposable` is desired behavior, this is how to silence the compiler warning.
|
||||
|
||||
```Swift
|
||||
let xs: Observable<E> ....
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
_ = xs // <-- note the underscore
|
||||
.filter { ... }
|
||||
.map { ... }
|
||||
.switchLatest()
|
||||
.subscribe(onNext: {
|
||||
...
|
||||
}, onError: {
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
### <a name="unused-observable"></a>Unused observable sequence (unused-observable)
|
||||
|
||||
Warning is probably presented in a context similar to this one:
|
||||
|
||||
```Swift
|
||||
let xs: Observable<E> ....
|
||||
|
||||
xs
|
||||
.filter { ... }
|
||||
.map { ... }
|
||||
```
|
||||
|
||||
This code defines observable sequence that is filtered and mapped `xs` sequence but then ignores the result.
|
||||
|
||||
Since this code just defines an observable sequence and then ignores it, it doesn't actually do nothing and it's pretty much useless.
|
||||
|
||||
Your intention was probably to either store the observable sequence definition and use it later ...
|
||||
|
||||
```Swift
|
||||
let xs: Observable<E> ....
|
||||
|
||||
let ys = xs // <--- names definition as `ys`
|
||||
.filter { ... }
|
||||
.map { ... }
|
||||
```
|
||||
|
||||
... or start computation based on that definition
|
||||
|
||||
```Swift
|
||||
let xs: Observable<E> ....
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
xs
|
||||
.filter { ... }
|
||||
.map { ... }
|
||||
.subscribeNext { nextElement in // <-- note the `subscribe*` method
|
||||
... probably print or something
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
```
|
||||
|
|
@ -45,7 +45,7 @@ example("combineLatest 1") {
|
|||
let intOb1 = PublishSubject<String>()
|
||||
let intOb2 = PublishSubject<Int>()
|
||||
|
||||
combineLatest(intOb1, intOb2) {
|
||||
_ = combineLatest(intOb1, intOb2) {
|
||||
"\($0) \($1)"
|
||||
}
|
||||
.subscribe {
|
||||
|
|
@ -68,7 +68,7 @@ example("combineLatest 2") {
|
|||
let intOb1 = just(2)
|
||||
let intOb2 = sequenceOf(0, 1, 2, 3, 4)
|
||||
|
||||
combineLatest(intOb1, intOb2) {
|
||||
_ = combineLatest(intOb1, intOb2) {
|
||||
$0 * $1
|
||||
}
|
||||
.subscribe {
|
||||
|
|
@ -85,8 +85,8 @@ example("combineLatest 3") {
|
|||
let intOb2 = sequenceOf(0, 1, 2, 3)
|
||||
let intOb3 = sequenceOf(0, 1, 2, 3, 4)
|
||||
|
||||
combineLatest(intOb1, intOb2, intOb3) {
|
||||
($0 + $1) * $2
|
||||
_ = combineLatest(intOb1, intOb2, intOb3) {
|
||||
($0 + $1) * $2
|
||||
}
|
||||
.subscribe {
|
||||
print($0)
|
||||
|
|
@ -95,6 +95,39 @@ example("combineLatest 3") {
|
|||
|
||||
|
||||
|
||||
//: Combinelatest version that allows combining sequences with different types.
|
||||
|
||||
example("combineLatest 4") {
|
||||
let intOb = just(2)
|
||||
let stringOb = just("a")
|
||||
|
||||
_ = combineLatest(intOb, stringOb) {
|
||||
"\($0) " + $1
|
||||
}
|
||||
.subscribe {
|
||||
print($0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//: `combineLatest` extension method for Array of `ObservableType` conformable types
|
||||
//: The array must be formed by `Observables` of the same type.
|
||||
|
||||
example("combineLatest 5") {
|
||||
let intOb1 = just(2)
|
||||
let intOb2 = sequenceOf(0, 1, 2, 3)
|
||||
let intOb3 = sequenceOf(0, 1, 2, 3, 4)
|
||||
|
||||
_ = [intOb1, intOb2, intOb3].combineLatest { intArray -> Int in
|
||||
Int((intArray[0] + intArray[1]) * intArray[2])
|
||||
}
|
||||
.subscribe { (event: Event<Int>) -> Void in
|
||||
print(event)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*:
|
||||
### `zip`
|
||||
|
||||
|
|
@ -108,7 +141,7 @@ example("zip 1") {
|
|||
let intOb1 = PublishSubject<String>()
|
||||
let intOb2 = PublishSubject<Int>()
|
||||
|
||||
zip(intOb1, intOb2) {
|
||||
_ = zip(intOb1, intOb2) {
|
||||
"\($0) \($1)"
|
||||
}
|
||||
.subscribe {
|
||||
|
|
@ -132,7 +165,7 @@ example("zip 2") {
|
|||
|
||||
let intOb2 = sequenceOf(0, 1, 2, 3, 4)
|
||||
|
||||
zip(intOb1, intOb2) {
|
||||
_ = zip(intOb1, intOb2) {
|
||||
$0 * $1
|
||||
}
|
||||
.subscribe {
|
||||
|
|
@ -146,7 +179,7 @@ example("zip 3") {
|
|||
let intOb2 = sequenceOf(0, 1, 2, 3)
|
||||
let intOb3 = sequenceOf(0, 1, 2, 3, 4)
|
||||
|
||||
zip(intOb1, intOb2, intOb3) {
|
||||
_ = zip(intOb1, intOb2, intOb3) {
|
||||
($0 + $1) * $2
|
||||
}
|
||||
.subscribe {
|
||||
|
|
@ -170,7 +203,7 @@ example("merge 1") {
|
|||
let subject1 = PublishSubject<Int>()
|
||||
let subject2 = PublishSubject<Int>()
|
||||
|
||||
sequenceOf(subject1, subject2)
|
||||
_ = sequenceOf(subject1, subject2)
|
||||
.merge()
|
||||
.subscribeNext { int in
|
||||
print(int)
|
||||
|
|
@ -190,7 +223,7 @@ example("merge 2") {
|
|||
let subject1 = PublishSubject<Int>()
|
||||
let subject2 = PublishSubject<Int>()
|
||||
|
||||
sequenceOf(subject1, subject2)
|
||||
_ = sequenceOf(subject1, subject2)
|
||||
.merge(maxConcurrent: 2)
|
||||
.subscribe {
|
||||
print($0)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ example("takeUntil") {
|
|||
let originalSequence = PublishSubject<Int>()
|
||||
let whenThisSendsNextWorldStops = PublishSubject<Int>()
|
||||
|
||||
originalSequence
|
||||
_ = originalSequence
|
||||
.takeUntil(whenThisSendsNextWorldStops)
|
||||
.subscribe {
|
||||
print($0)
|
||||
|
|
@ -53,7 +53,7 @@ example("takeWhile") {
|
|||
|
||||
let sequence = PublishSubject<Int>()
|
||||
|
||||
sequence
|
||||
_ = sequence
|
||||
.takeWhile { int in
|
||||
int < 4
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
//: [<< Previous](@previous) - [Index](Index)
|
||||
|
||||
import RxSwift
|
||||
import XCPlayground
|
||||
|
||||
/*:
|
||||
## Connectable Observable Operators
|
||||
|
|
@ -15,13 +14,13 @@ func sampleWithoutConnectableOperators() {
|
|||
|
||||
let int1 = interval(1, MainScheduler.sharedInstance)
|
||||
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("first subscription \($0)")
|
||||
}
|
||||
|
||||
delay(5) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("second subscription \($0)")
|
||||
}
|
||||
|
|
@ -44,7 +43,7 @@ func sampleWithMulticast() {
|
|||
|
||||
let subject1 = PublishSubject<Int64>()
|
||||
|
||||
subject1
|
||||
_ = subject1
|
||||
.subscribe {
|
||||
print("Subject \($0)")
|
||||
}
|
||||
|
|
@ -52,7 +51,7 @@ func sampleWithMulticast() {
|
|||
let int1 = interval(1, MainScheduler.sharedInstance)
|
||||
.multicast(subject1)
|
||||
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("first subscription \($0)")
|
||||
}
|
||||
|
|
@ -62,14 +61,14 @@ func sampleWithMulticast() {
|
|||
}
|
||||
|
||||
delay(4) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("second subscription \($0)")
|
||||
}
|
||||
}
|
||||
|
||||
delay(6) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("third subscription \($0)")
|
||||
}
|
||||
|
|
@ -95,7 +94,7 @@ func sampleWithReplayBuffer0() {
|
|||
let int1 = interval(1, MainScheduler.sharedInstance)
|
||||
.replay(0)
|
||||
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("first subscription \($0)")
|
||||
}
|
||||
|
|
@ -105,14 +104,14 @@ func sampleWithReplayBuffer0() {
|
|||
}
|
||||
|
||||
delay(4) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("second subscription \($0)")
|
||||
}
|
||||
}
|
||||
|
||||
delay(6) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("third subscription \($0)")
|
||||
}
|
||||
|
|
@ -130,7 +129,7 @@ func sampleWithReplayBuffer2() {
|
|||
let int1 = interval(1, MainScheduler.sharedInstance)
|
||||
.replay(2)
|
||||
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("first subscription \($0)")
|
||||
}
|
||||
|
|
@ -140,14 +139,14 @@ func sampleWithReplayBuffer2() {
|
|||
}
|
||||
|
||||
delay(4) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("second subscription \($0)")
|
||||
}
|
||||
}
|
||||
|
||||
delay(6) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("third subscription \($0)")
|
||||
}
|
||||
|
|
@ -173,7 +172,7 @@ func sampleWithPublish() {
|
|||
let int1 = interval(1, MainScheduler.sharedInstance)
|
||||
.publish()
|
||||
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("first subscription \($0)")
|
||||
}
|
||||
|
|
@ -183,14 +182,14 @@ func sampleWithPublish() {
|
|||
}
|
||||
|
||||
delay(4) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("second subscription \($0)")
|
||||
}
|
||||
}
|
||||
|
||||
delay(6) {
|
||||
int1
|
||||
_ = int1
|
||||
.subscribe {
|
||||
print("third subscription \($0)")
|
||||
}
|
||||
|
|
@ -200,6 +199,6 @@ func sampleWithPublish() {
|
|||
|
||||
// sampleWithPublish()
|
||||
|
||||
XCPSetExecutionShouldContinueIndefinitely(true)
|
||||
playgroundShouldContinueIndefinitely()
|
||||
|
||||
//: [Index](Index)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ example("catchError 1") {
|
|||
let sequenceThatFails = PublishSubject<Int>()
|
||||
let recoverySequence = sequenceOf(100, 200, 300, 400)
|
||||
|
||||
sequenceThatFails
|
||||
_ = sequenceThatFails
|
||||
.catchError { error in
|
||||
return recoverySequence
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ example("catchError 1") {
|
|||
example("catchError 2") {
|
||||
let sequenceThatFails = PublishSubject<Int>()
|
||||
|
||||
sequenceThatFails
|
||||
_ = sequenceThatFails
|
||||
.catchErrorJustReturn(100)
|
||||
.subscribe {
|
||||
print($0)
|
||||
|
|
@ -83,7 +83,7 @@ example("retry") {
|
|||
return NopDisposable.instance
|
||||
}
|
||||
|
||||
funnyLookingSequence
|
||||
_ = funnyLookingSequence
|
||||
.retry()
|
||||
.subscribe {
|
||||
print($0)
|
||||
|
|
|
|||
|
|
@ -81,8 +81,8 @@ example("sequenceOf") {
|
|||
`from` creates a sequence from `SequenceType`
|
||||
*/
|
||||
|
||||
example("from") {
|
||||
let sequenceFromArray = [1, 2, 3, 4, 5].asObservable()
|
||||
example("toObservable") {
|
||||
let sequenceFromArray = [1, 2, 3, 4, 5].toObservable()
|
||||
|
||||
let subscription = sequenceFromArray
|
||||
.subscribe { event in
|
||||
|
|
@ -149,12 +149,12 @@ example("deferred") {
|
|||
}
|
||||
}
|
||||
|
||||
deferredSequence
|
||||
_ = deferredSequence
|
||||
.subscribe { event in
|
||||
print(event)
|
||||
}
|
||||
|
||||
deferredSequence
|
||||
_ = deferredSequence
|
||||
.subscribe { event in
|
||||
print(event)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ This function will perform a function on each element in the sequence until it i
|
|||
|
||||
*/
|
||||
example("reduce") {
|
||||
sequenceOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
_ = sequenceOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.reduce(0, +)
|
||||
.subscribe {
|
||||
print($0)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ A toolbox of useful Operators for working with Observables.
|
|||
example("subscribe") {
|
||||
let sequenceOfInts = PublishSubject<Int>()
|
||||
|
||||
sequenceOfInts
|
||||
_ = sequenceOfInts
|
||||
.subscribe {
|
||||
print($0)
|
||||
}
|
||||
|
|
@ -40,7 +40,7 @@ There are several variants of the `subscribe` operator.
|
|||
example("subscribeNext") {
|
||||
let sequenceOfInts = PublishSubject<Int>()
|
||||
|
||||
sequenceOfInts
|
||||
_ = sequenceOfInts
|
||||
.subscribeNext {
|
||||
print($0)
|
||||
}
|
||||
|
|
@ -58,7 +58,7 @@ example("subscribeNext") {
|
|||
example("subscribeCompleted") {
|
||||
let sequenceOfInts = PublishSubject<Int>()
|
||||
|
||||
sequenceOfInts
|
||||
_ = sequenceOfInts
|
||||
.subscribeCompleted {
|
||||
print("It's completed")
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ example("subscribeCompleted") {
|
|||
example("subscribeError") {
|
||||
let sequenceOfInts = PublishSubject<Int>()
|
||||
|
||||
sequenceOfInts
|
||||
_ = sequenceOfInts
|
||||
.subscribeError { error in
|
||||
print(error)
|
||||
}
|
||||
|
|
@ -98,7 +98,7 @@ register an action to take upon a variety of Observable lifecycle events
|
|||
example("doOn") {
|
||||
let sequenceOfInts = PublishSubject<Int>()
|
||||
|
||||
sequenceOfInts
|
||||
_ = sequenceOfInts
|
||||
.doOn {
|
||||
print("Intercepted event \($0)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ import RxSwift
|
|||
A Subject is a sort of bridge or proxy that is available in some implementations of ReactiveX that acts both as an observer and as an Observable. Because it is an observer, it can subscribe to one or more Observables, and because it is an Observable, it can pass through the items it observes by reemitting them, and it can also emit new items.
|
||||
*/
|
||||
|
||||
func writeSequenceToConsole<O: ObservableType>(name: String, sequence: O) {
|
||||
sequence
|
||||
func writeSequenceToConsole<O: ObservableType>(name: String, sequence: O) -> Disposable {
|
||||
return sequence
|
||||
.subscribe { e in
|
||||
print("Subscription: \(name), event: \(e)")
|
||||
}
|
||||
|
|
@ -27,11 +27,13 @@ func writeSequenceToConsole<O: ObservableType>(name: String, sequence: O) {
|
|||
|
||||
*/
|
||||
example("PublishSubject") {
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
let subject = PublishSubject<String>()
|
||||
writeSequenceToConsole("1", sequence: subject)
|
||||
writeSequenceToConsole("1", sequence: subject).addDisposableTo(disposeBag)
|
||||
subject.on(.Next("a"))
|
||||
subject.on(.Next("b"))
|
||||
writeSequenceToConsole("2", sequence: subject)
|
||||
writeSequenceToConsole("2", sequence: subject).addDisposableTo(disposeBag)
|
||||
subject.on(.Next("c"))
|
||||
subject.on(.Next("d"))
|
||||
}
|
||||
|
|
@ -46,12 +48,13 @@ example("PublishSubject") {
|
|||

|
||||
*/
|
||||
example("ReplaySubject") {
|
||||
let disposeBag = DisposeBag()
|
||||
let subject = ReplaySubject<String>.create(bufferSize: 1)
|
||||
|
||||
writeSequenceToConsole("1", sequence: subject)
|
||||
writeSequenceToConsole("1", sequence: subject).addDisposableTo(disposeBag)
|
||||
subject.on(.Next("a"))
|
||||
subject.on(.Next("b"))
|
||||
writeSequenceToConsole("2", sequence: subject)
|
||||
writeSequenceToConsole("2", sequence: subject).addDisposableTo(disposeBag)
|
||||
subject.on(.Next("c"))
|
||||
subject.on(.Next("d"))
|
||||
}
|
||||
|
|
@ -68,11 +71,13 @@ When an observer subscribes to a `BehaviorSubject`, it begins by emitting the it
|
|||

|
||||
*/
|
||||
example("BehaviorSubject") {
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
let subject = BehaviorSubject(value: "z")
|
||||
writeSequenceToConsole("1", sequence: subject)
|
||||
writeSequenceToConsole("1", sequence: subject).addDisposableTo(disposeBag)
|
||||
subject.on(.Next("a"))
|
||||
subject.on(.Next("b"))
|
||||
writeSequenceToConsole("2", sequence: subject)
|
||||
writeSequenceToConsole("2", sequence: subject).addDisposableTo(disposeBag)
|
||||
subject.on(.Next("c"))
|
||||
subject.on(.Next("d"))
|
||||
subject.on(.Completed)
|
||||
|
|
@ -86,11 +91,12 @@ example("BehaviorSubject") {
|
|||
|
||||
*/
|
||||
example("Variable") {
|
||||
let disposeBag = DisposeBag()
|
||||
let variable = Variable("z")
|
||||
writeSequenceToConsole("1", sequence: variable)
|
||||
writeSequenceToConsole("1", sequence: variable).addDisposableTo(disposeBag)
|
||||
variable.value = "a"
|
||||
variable.value = "b"
|
||||
writeSequenceToConsole("2", sequence: variable)
|
||||
writeSequenceToConsole("2", sequence: variable).addDisposableTo(disposeBag)
|
||||
variable.value = "c"
|
||||
variable.value = "d"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Transform the items emitted by an Observable by applying a function to each item
|
|||
example("map") {
|
||||
let originalSequence = sequenceOf(Character("A"), Character("B"), Character("C"))
|
||||
|
||||
originalSequence
|
||||
_ = originalSequence
|
||||
.map { char in
|
||||
char.hashValue
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ example("flatMap") {
|
|||
|
||||
let sequenceString = sequenceOf("A", "B", "C", "D", "E", "F", "--")
|
||||
|
||||
sequenceInt
|
||||
_ = sequenceInt
|
||||
.flatMap { int in
|
||||
sequenceString
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ Apply a function to each item emitted by an Observable, sequentially, and emit e
|
|||
example("scan") {
|
||||
let sequenceToSum = sequenceOf(0, 1, 2, 3, 4, 5)
|
||||
|
||||
sequenceToSum
|
||||
_ = sequenceToSum
|
||||
.scan(0) { acum, elem in
|
||||
acum + elem
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,4 +13,19 @@ public func delay(delay:Double, closure:()->()) {
|
|||
Int64(delay * Double(NSEC_PER_SEC))
|
||||
),
|
||||
dispatch_get_main_queue(), closure)
|
||||
}
|
||||
}
|
||||
|
||||
#if NOT_IN_PLAYGROUND
|
||||
|
||||
public func playgroundShouldContinueIndefinitely() {
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
import XCPlayground
|
||||
|
||||
public func playgroundShouldContinueIndefinitely() {
|
||||
XCPSetExecutionShouldContinueIndefinitely(true)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='osx' requires-full-environment='true' display-mode='rendered'>
|
||||
<playground version='6.0' target-platform='osx' display-mode='rendered'>
|
||||
<pages>
|
||||
<page name='Index'/>
|
||||
<page name='Introduction'/>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -28,26 +28,7 @@
|
|||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D20034311BB9AFA70035511A"
|
||||
BuildableName = "RxSwift-tvOSTests.xctest"
|
||||
BlueprintName = "RxSwift-tvOSTests"
|
||||
ReferencedContainer = "container:Rx.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D2EA280B1BB9B5A200880ED3"
|
||||
BuildableName = "RxSwift.framework"
|
||||
BlueprintName = "RxSwift-tvOS"
|
||||
ReferencedContainer = "container:Rx.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "RxBlocking"
|
||||
s.version = "2.0.0-alpha.4"
|
||||
s.version = "2.0.0-beta.3"
|
||||
s.summary = "RxSwift Blocking operatos"
|
||||
s.description = <<-DESC
|
||||
Set of blocking operators for unit testing
|
||||
Set of blocking operators for RxSwift.
|
||||
DESC
|
||||
s.homepage = "https://github.com/ReactiveX/RxSwift"
|
||||
s.license = 'MIT'
|
||||
|
|
@ -19,5 +19,5 @@ Pod::Spec.new do |s|
|
|||
|
||||
s.source_files = 'RxBlocking/**/*.swift'
|
||||
|
||||
s.dependency 'RxSwift', '~> 2.0.0-alpha'
|
||||
s.dependency 'RxSwift', '~> 2.0.0-beta'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,221 @@
|
|||
//
|
||||
// BlockingObservable+Operators.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 10/19/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension BlockingObservable {
|
||||
/**
|
||||
Blocks current thread until sequence terminates.
|
||||
|
||||
If sequence terminates with error, terminating error will be thrown.
|
||||
|
||||
- returns: All elements of sequence.
|
||||
*/
|
||||
public func toArray() throws -> [E] {
|
||||
var elements: [E] = Array<E>()
|
||||
|
||||
var error: ErrorType?
|
||||
|
||||
let lock = RunLoopLock()
|
||||
|
||||
let d = SingleAssignmentDisposable()
|
||||
|
||||
lock.dispatch {
|
||||
d.disposable = self.source.subscribe { e in
|
||||
switch e {
|
||||
case .Next(let element):
|
||||
elements.append(element)
|
||||
case .Error(let e):
|
||||
error = e
|
||||
lock.stop()
|
||||
case .Completed:
|
||||
lock.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock.run()
|
||||
|
||||
d.dispose()
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
}
|
||||
|
||||
extension BlockingObservable {
|
||||
/**
|
||||
Blocks current thread until sequence produces first element.
|
||||
|
||||
If sequence terminates with error before producing first element, terminating error will be thrown.
|
||||
|
||||
- returns: First element of sequence. If sequence is empty `nil` is returned.
|
||||
*/
|
||||
public func first() throws -> E? {
|
||||
var element: E?
|
||||
|
||||
var error: ErrorType?
|
||||
|
||||
let d = SingleAssignmentDisposable()
|
||||
|
||||
let lock = RunLoopLock()
|
||||
|
||||
lock.dispatch {
|
||||
d.disposable = self.source.subscribe { e in
|
||||
switch e {
|
||||
case .Next(let e):
|
||||
if element == nil {
|
||||
element = e
|
||||
}
|
||||
break
|
||||
case .Error(let e):
|
||||
error = e
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
lock.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lock.run()
|
||||
|
||||
d.dispose()
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
extension BlockingObservable {
|
||||
/**
|
||||
Blocks current thread until sequence terminates.
|
||||
|
||||
If sequence terminates with error, terminating error will be thrown.
|
||||
|
||||
- returns: Last element in the sequence. If sequence is empty `nil` is returned.
|
||||
*/
|
||||
public func last() throws -> E? {
|
||||
var element: E?
|
||||
|
||||
var error: ErrorType?
|
||||
|
||||
let d = SingleAssignmentDisposable()
|
||||
|
||||
let lock = RunLoopLock()
|
||||
|
||||
lock.dispatch {
|
||||
d.disposable = self.source.subscribe { e in
|
||||
switch e {
|
||||
case .Next(let e):
|
||||
element = e
|
||||
return
|
||||
case .Error(let e):
|
||||
error = e
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
lock.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lock.run()
|
||||
|
||||
d.dispose()
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
extension BlockingObservable {
|
||||
/**
|
||||
Blocks current thread until sequence terminates.
|
||||
|
||||
If sequence terminates with error before producing first element, terminating error will be thrown.
|
||||
|
||||
- returns: Returns the only element of an sequence, and reports an error if there is not exactly one element in the observable sequence.
|
||||
*/
|
||||
public func single() throws -> E? {
|
||||
return try single { _ in true }
|
||||
}
|
||||
|
||||
/**
|
||||
Blocks current thread until sequence terminates.
|
||||
|
||||
If sequence terminates with error before producing first element, terminating error will be thrown.
|
||||
|
||||
- parameter predicate: A function to test each source element for a condition.
|
||||
- returns: Returns the only element of an sequence that satisfies the condition in the predicate, and reports an error if there is not exactly one element in the sequence.
|
||||
*/
|
||||
public func single(predicate: (E) throws -> Bool) throws -> E? {
|
||||
var element: E?
|
||||
|
||||
var error: ErrorType?
|
||||
|
||||
let d = SingleAssignmentDisposable()
|
||||
|
||||
let lock = RunLoopLock()
|
||||
|
||||
lock.dispatch {
|
||||
d.disposable = self.source.subscribe { e in
|
||||
if d.disposed {
|
||||
return
|
||||
}
|
||||
switch e {
|
||||
case .Next(let e):
|
||||
do {
|
||||
if try !predicate(e) {
|
||||
return
|
||||
}
|
||||
if element == nil {
|
||||
element = e
|
||||
} else {
|
||||
throw RxError.MoreThanOneElement
|
||||
}
|
||||
} catch (let err) {
|
||||
error = err
|
||||
d.dispose()
|
||||
lock.stop()
|
||||
}
|
||||
return
|
||||
case .Error(let e):
|
||||
error = e
|
||||
case .Completed:
|
||||
if element == nil {
|
||||
error = RxError.NoElements
|
||||
}
|
||||
}
|
||||
|
||||
lock.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lock.run()
|
||||
d.dispose()
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// BlockingObservable.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 10/19/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
/**
|
||||
`BlockingObservable` is a variety of `Observable` that provides blocking operators.
|
||||
|
||||
It can be useful for testing and demo purposes, but is generally inappropriate for production applications.
|
||||
|
||||
If you think you need to use a `BlockingObservable` this is usually a sign that you should rethink your
|
||||
design.
|
||||
*/
|
||||
public struct BlockingObservable<E> {
|
||||
let source: Observable<E>
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>Krunoslav-Zaher.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>2.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
//
|
||||
// Observable+Blocking.swift
|
||||
// RxBlocking
|
||||
//
|
||||
// Created by Krunoslav Zaher on 7/12/15.
|
||||
// Copyright (c) 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension ObservableType {
|
||||
/**
|
||||
Blocks current thread until sequence terminates.
|
||||
|
||||
If sequence terminates with error, terminating error will be thrown.
|
||||
|
||||
- returns: All elements of sequence.
|
||||
*/
|
||||
public func toArray() throws -> [E] {
|
||||
let condition = NSCondition()
|
||||
|
||||
var elements = [E]()
|
||||
|
||||
var error: ErrorType?
|
||||
|
||||
var ended = false
|
||||
|
||||
_ = self.subscribe { e in
|
||||
switch e {
|
||||
case .Next(let element):
|
||||
elements.append(element)
|
||||
case .Error(let e):
|
||||
error = e
|
||||
condition.lock()
|
||||
ended = true
|
||||
condition.signal()
|
||||
condition.unlock()
|
||||
case .Completed:
|
||||
condition.lock()
|
||||
ended = true
|
||||
condition.signal()
|
||||
condition.unlock()
|
||||
}
|
||||
}
|
||||
condition.lock()
|
||||
while !ended {
|
||||
condition.wait()
|
||||
}
|
||||
condition.unlock()
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
}
|
||||
|
||||
extension ObservableType {
|
||||
/**
|
||||
Blocks current thread until sequence produces first element.
|
||||
|
||||
If sequence terminates with error before producing first element, terminating error will be thrown.
|
||||
|
||||
- returns: First element of sequence. If sequence is empty `nil` is returned.
|
||||
*/
|
||||
public func first() throws -> E? {
|
||||
let condition = NSCondition()
|
||||
|
||||
var element: E?
|
||||
|
||||
var error: ErrorType?
|
||||
|
||||
var ended = false
|
||||
|
||||
let d = SingleAssignmentDisposable()
|
||||
|
||||
d.disposable = self.subscribe { e in
|
||||
switch e {
|
||||
case .Next(let e):
|
||||
if element == nil {
|
||||
element = e
|
||||
}
|
||||
break
|
||||
case .Error(let e):
|
||||
error = e
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
condition.lock()
|
||||
ended = true
|
||||
condition.signal()
|
||||
condition.unlock()
|
||||
}
|
||||
|
||||
condition.lock()
|
||||
while !ended {
|
||||
condition.wait()
|
||||
}
|
||||
d.dispose()
|
||||
condition.unlock()
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
extension ObservableType {
|
||||
/**
|
||||
Blocks current thread until sequence terminates.
|
||||
|
||||
If sequence terminates with error, terminating error will be thrown.
|
||||
|
||||
- returns: Last element in the sequence. If sequence is empty `nil` is returned.
|
||||
*/
|
||||
public func last() throws -> E? {
|
||||
let condition = NSCondition()
|
||||
|
||||
var element: E?
|
||||
|
||||
var error: ErrorType?
|
||||
|
||||
var ended = false
|
||||
|
||||
let d = SingleAssignmentDisposable()
|
||||
|
||||
d.disposable = self.subscribe { e in
|
||||
switch e {
|
||||
case .Next(let e):
|
||||
element = e
|
||||
return
|
||||
case .Error(let e):
|
||||
error = e
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
condition.lock()
|
||||
ended = true
|
||||
condition.signal()
|
||||
condition.unlock()
|
||||
}
|
||||
|
||||
condition.lock()
|
||||
while !ended {
|
||||
condition.wait()
|
||||
}
|
||||
d.dispose()
|
||||
condition.unlock()
|
||||
|
||||
if let error = error {
|
||||
throw error
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// Observable+Blocking.swift
|
||||
// RxBlocking
|
||||
//
|
||||
// Created by Krunoslav Zaher on 7/12/15.
|
||||
// Copyright (c) 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension ObservableConvertibleType {
|
||||
/**
|
||||
Converts an Observable into a `BlockingObservable` (an Observable with blocking operators).
|
||||
|
||||
- returns: `BlockingObservable` version of `self`
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func toBlocking() -> BlockingObservable<E> {
|
||||
return BlockingObservable(source: self.asObservable())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// RunLoopLock.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/5/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
class RunLoopLock : NSObject {
|
||||
let currentRunLoop: CFRunLoopRef
|
||||
|
||||
override init() {
|
||||
currentRunLoop = CFRunLoopGetCurrent()
|
||||
}
|
||||
|
||||
func dispatch(action: () -> ()) {
|
||||
CFRunLoopPerformBlock(currentRunLoop, kCFRunLoopDefaultMode) {
|
||||
if CurrentThreadScheduler.isScheduleRequired {
|
||||
CurrentThreadScheduler.instance.schedule(()) { _ in
|
||||
action()
|
||||
return NopDisposable.instance
|
||||
}
|
||||
}
|
||||
else {
|
||||
action()
|
||||
}
|
||||
}
|
||||
CFRunLoopWakeUp(currentRunLoop)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
CFRunLoopPerformBlock(currentRunLoop, kCFRunLoopDefaultMode) {
|
||||
CFRunLoopStop(self.currentRunLoop)
|
||||
}
|
||||
CFRunLoopWakeUp(currentRunLoop)
|
||||
}
|
||||
|
||||
func run() {
|
||||
CFRunLoopRun()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
Pod::Spec.new do |s|
|
||||
s.name = "RxCocoa"
|
||||
s.version = "2.0.0-alpha.4"
|
||||
s.version = "2.0.0-beta.3"
|
||||
s.summary = "RxSwift Cocoa extensions"
|
||||
s.description = <<-DESC
|
||||
* UI extensions
|
||||
|
|
@ -25,5 +25,5 @@ Pod::Spec.new do |s|
|
|||
s.watchos.source_files = 'RxCocoa/iOS/**/*.swift'
|
||||
s.tvos.source_files = 'RxCocoa/iOS/**/*.swift'
|
||||
|
||||
s.dependency 'RxSwift', '~> 2.0.0-alpha'
|
||||
s.dependency 'RxSwift', '~> 2.0.0-beta'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ public struct ControlEvent<PropertyType> : ControlEventType {
|
|||
let source: Observable<PropertyType>
|
||||
|
||||
init(source: Observable<PropertyType>) {
|
||||
self.source = source
|
||||
self.source = source.subscribeOn(ConcurrentMainScheduler.sharedInstance)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -55,6 +55,7 @@ public struct ControlEvent<PropertyType> : ControlEventType {
|
|||
/**
|
||||
- returns: `Observable` interface.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asObservable() -> Observable<E> {
|
||||
return self.source
|
||||
}
|
||||
|
|
@ -62,6 +63,7 @@ public struct ControlEvent<PropertyType> : ControlEventType {
|
|||
/**
|
||||
- returns: `ControlEvent` interface.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asControlEvent() -> ControlEvent<E> {
|
||||
return self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ public struct ControlProperty<PropertyType> : ControlPropertyType {
|
|||
public typealias E = PropertyType
|
||||
|
||||
let source: Observable<PropertyType>
|
||||
let observer: ObserverOf<PropertyType>
|
||||
let observer: AnyObserver<PropertyType>
|
||||
|
||||
init(source: Observable<PropertyType>, observer: ObserverOf<PropertyType>) {
|
||||
self.source = source
|
||||
init(source: Observable<PropertyType>, observer: AnyObserver<PropertyType>) {
|
||||
self.source = source.subscribeOn(ConcurrentMainScheduler.sharedInstance)
|
||||
self.observer = observer
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +58,7 @@ public struct ControlProperty<PropertyType> : ControlPropertyType {
|
|||
/**
|
||||
- returns: `Observable` interface.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asObservable() -> Observable<E> {
|
||||
return self.source
|
||||
}
|
||||
|
|
@ -65,6 +66,7 @@ public struct ControlProperty<PropertyType> : ControlPropertyType {
|
|||
/**
|
||||
- returns: `ControlProperty` interface.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asControlProperty() -> ControlProperty<E> {
|
||||
return self
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// ControlEvent+Driver.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 9/19/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension ControlEvent {
|
||||
/**
|
||||
Converts `ControlEvent` to `Driver` unit.
|
||||
|
||||
`ControlEvent` already can't fail, so no special case needs to be handled.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asDriver() -> Driver<E> {
|
||||
return self.asDriver { (error) -> Driver<E> in
|
||||
#if DEBUG
|
||||
rxFatalError("Somehow driver received error from a source that shouldn't fail.")
|
||||
#else
|
||||
return Drive.empty()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// ControlProperty+Driver.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 9/19/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension ControlProperty {
|
||||
/**
|
||||
Converts `ControlProperty` to `Driver` unit.
|
||||
|
||||
`ControlProperty` already can't fail, so no special case needs to be handled.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asDriver() -> Driver<E> {
|
||||
return self.asDriver { (error) -> Driver<E> in
|
||||
#if DEBUG
|
||||
rxFatalError("Somehow driver received error from a source that shouldn't fail.")
|
||||
#else
|
||||
return Drive.empty()
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
// This file is autogenerated.
|
||||
// Take a look at `Preprocessor` target in RxSwift project
|
||||
//
|
||||
// Driver+Operators+arity.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 10/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
// 2
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<O1: DriverConvertibleType, O2: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, resultSelector: (O1.E, O2.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<O1: DriverConvertibleType, O2: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, resultSelector: (O1.E, O2.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 3
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, resultSelector: (O1.E, O2.E, O3.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, resultSelector: (O1.E, O2.E, O3.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 4
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, resultSelector: (O1.E, O2.E, O3.E, O4.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, resultSelector: (O1.E, O2.E, O3.E, O4.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 5
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 6
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, O6: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, _ source6: O6, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E, O6.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(), source6.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, O6: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, _ source6: O6, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E, O6.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(), source6.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 7
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, O6: DriverConvertibleType, O7: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, _ source6: O6, _ source7: O7, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E, O6.E, O7.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(), source6.asDriver().asObservable(), source7.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, O6: DriverConvertibleType, O7: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, _ source6: O6, _ source7: O7, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E, O6.E, O7.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(), source6.asDriver().asObservable(), source7.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 8
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, O6: DriverConvertibleType, O7: DriverConvertibleType, O8: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, _ source6: O6, _ source7: O7, _ source8: O8, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E, O6.E, O7.E, O8.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(), source6.asDriver().asObservable(), source7.asDriver().asObservable(), source8.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<O1: DriverConvertibleType, O2: DriverConvertibleType, O3: DriverConvertibleType, O4: DriverConvertibleType, O5: DriverConvertibleType, O6: DriverConvertibleType, O7: DriverConvertibleType, O8: DriverConvertibleType, R>
|
||||
(source1: O1, _ source2: O2, _ source3: O3, _ source4: O4, _ source5: O5, _ source6: O6, _ source7: O7, _ source8: O8, resultSelector: (O1.E, O2.E, O3.E, O4.E, O5.E, O6.E, O7.E, O8.E) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
source1.asDriver().asObservable(), source2.asDriver().asObservable(), source3.asDriver().asObservable(), source4.asDriver().asObservable(), source5.asDriver().asObservable(), source6.asDriver().asObservable(), source7.asDriver().asObservable(), source8.asDriver().asObservable(),
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// Driver+Operators+arity.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 10/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
<% for i in 2 ... 8 { %>
|
||||
|
||||
// <%= i %>
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<<%= (Array(1...i).map { "O\($0): DriverConvertibleType" }).joinWithSeparator(", ") %>, R>
|
||||
(<%= (Array(1...i).map { "source\($0): O\($0)" }).joinWithSeparator(", _ ") %>, resultSelector: (<%= (Array(1...i).map { "O\($0).E" }).joinWithSeparator(", ") %>) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = zip(
|
||||
<%= (Array(1...i).map { "source\($0).asDriver().asObservable()" }).joinWithSeparator(", ") %>,
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<<%= (Array(1...i).map { "O\($0): DriverConvertibleType" }).joinWithSeparator(", ") %>, R>
|
||||
(<%= (Array(1...i).map { "source\($0): O\($0)" }).joinWithSeparator(", _ ") %>, resultSelector: (<%= (Array(1...i).map { "O\($0).E" }).joinWithSeparator(", ") %>) throws -> R)
|
||||
-> Driver<R> {
|
||||
let source = combineLatest(
|
||||
<%= (Array(1...i).map { "source\($0).asDriver().asObservable()" }).joinWithSeparator(", ") %>,
|
||||
resultSelector: resultSelector
|
||||
)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
<% } %>
|
||||
|
|
@ -0,0 +1,381 @@
|
|||
//
|
||||
// Driver+Operators.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 9/19/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Projects each element of an observable sequence into a new form.
|
||||
|
||||
- parameter selector: A transform function to apply to each source element.
|
||||
- returns: An observable sequence whose elements are the result of invoking the transform function on each element of source.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func map<R>(selector: E -> R) -> Driver<R> {
|
||||
let source = self
|
||||
.asObservable()
|
||||
.map(selector)
|
||||
return Driver<R>(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Filters the elements of an observable sequence based on a predicate.
|
||||
|
||||
- parameter predicate: A function to test each source element for a condition.
|
||||
- returns: An observable sequence that contains elements from the input sequence that satisfy the condition.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func filter(predicate: (E) -> Bool) -> Driver<E> {
|
||||
let source = self
|
||||
.asObservable()
|
||||
.filter(predicate)
|
||||
return Driver(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension DriverConvertibleType where E : DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Transforms an observable sequence of observable sequences into an observable sequence
|
||||
producing values only from the most recent observable sequence.
|
||||
|
||||
Each time a new inner observable sequence is received, unsubscribe from the
|
||||
previous inner observable sequence.
|
||||
|
||||
- returns: The observable sequence that at any point in time produces the elements of the most recent inner observable sequence that has been received.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func switchLatest() -> Driver<E.E> {
|
||||
let source: Observable<E.E> = self
|
||||
.asObservable()
|
||||
.map { $0.asDriver() }
|
||||
.switchLatest()
|
||||
return Driver<E.E>(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension DriverConvertibleType {
|
||||
/**
|
||||
Projects each element of an observable sequence into a new sequence of observable sequences and then
|
||||
transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.
|
||||
|
||||
It is a combination of `map` + `switchLatest` operator
|
||||
|
||||
- parameter selector: A transform function to apply to each element.
|
||||
- returns: An observable sequence whose elements are the result of invoking the transform function on each element of source producing an
|
||||
Observable of Observable sequences and that at any point in time produces the elements of the most recent inner observable sequence that has been received.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func flatMapLatest<R>(selector: (E) -> Driver<R>)
|
||||
-> Driver<R> {
|
||||
let source: Observable<R> = self
|
||||
.asObservable()
|
||||
.flatMapLatest(selector)
|
||||
return Driver<R>(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Invokes an action for each event in the observable sequence, and propagates all observer messages through the result sequence.
|
||||
|
||||
- parameter eventHandler: Action to invoke for each event in the observable sequence.
|
||||
- returns: The source sequence with the side-effecting behavior applied.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func doOn(eventHandler: (Event<E>) -> Void)
|
||||
-> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.doOn(eventHandler)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Invokes an action for each event in the observable sequence, and propagates all observer messages through the result sequence.
|
||||
|
||||
- parameter onNext: Action to invoke for each element in the observable sequence.
|
||||
- parameter onError: Action to invoke upon errored termination of the observable sequence.
|
||||
- parameter onCompleted: Action to invoke upon graceful termination of the observable sequence.
|
||||
- returns: The source sequence with the side-effecting behavior applied.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func doOn(onNext onNext: (E -> Void)? = nil, onError: (ErrorType -> Void)? = nil, onCompleted: (() -> Void)? = nil)
|
||||
-> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.doOn(onNext: onNext, onError: onError, onCompleted: onCompleted)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Prints received events for all observers on standard output.
|
||||
|
||||
- parameter identifier: Identifier that is printed together with event description to standard output.
|
||||
- returns: An observable sequence whose events are printed to standard output.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func debug(identifier: String = "\(__FILE__):\(__LINE__)") -> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.debug(identifier)
|
||||
return Driver(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension DriverConvertibleType where E: Equatable {
|
||||
|
||||
/**
|
||||
Returns an observable sequence that contains only distinct contiguous elements according to equality operator.
|
||||
|
||||
- returns: An observable sequence only containing the distinct contiguous elements, based on equality operator, from the source sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func distinctUntilChanged()
|
||||
-> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.distinctUntilChanged({ $0 }, comparer: { ($0 == $1) })
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Returns an observable sequence that contains only distinct contiguous elements according to the `keySelector`.
|
||||
|
||||
- parameter keySelector: A function to compute the comparison key for each element.
|
||||
- returns: An observable sequence only containing the distinct contiguous elements, based on a computed key value, from the source sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func distinctUntilChanged<K: Equatable>(keySelector: (E) -> K) -> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.distinctUntilChanged(keySelector, comparer: { $0 == $1 })
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an observable sequence that contains only distinct contiguous elements according to the `comparer`.
|
||||
|
||||
- parameter comparer: Equality comparer for computed key values.
|
||||
- returns: An observable sequence only containing the distinct contiguous elements, based on `comparer`, from the source sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func distinctUntilChanged(comparer: (lhs: E, rhs: E) -> Bool) -> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.distinctUntilChanged({ $0 }, comparer: comparer)
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an observable sequence that contains only distinct contiguous elements according to the keySelector and the comparer.
|
||||
|
||||
- parameter keySelector: A function to compute the comparison key for each element.
|
||||
- parameter comparer: Equality comparer for computed key values.
|
||||
- returns: An observable sequence only containing the distinct contiguous elements, based on a computed key value and the comparer, from the source sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func distinctUntilChanged<K>(keySelector: (E) -> K, comparer: (lhs: K, rhs: K) -> Bool) -> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.distinctUntilChanged(keySelector, comparer: comparer)
|
||||
return Driver(source)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Projects each element of an observable sequence to an observable sequence and merges the resulting observable sequences into one observable sequence.
|
||||
|
||||
- parameter selector: A transform function to apply to each element.
|
||||
- returns: An observable sequence whose elements are the result of invoking the one-to-many transform function on each element of the input sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func flatMap<R>(selector: (E) -> Driver<R>) -> Driver<R> {
|
||||
let source = self.asObservable()
|
||||
.flatMap(selector)
|
||||
|
||||
return Driver<R>(source)
|
||||
}
|
||||
}
|
||||
|
||||
// merge
|
||||
extension DriverConvertibleType where E : DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Merges elements from all observable sequences in the given enumerable sequence into a single observable sequence.
|
||||
|
||||
- parameter maxConcurrent: Maximum number of inner observable sequences being subscribed to concurrently.
|
||||
- returns: The observable sequence that merges the elements of the observable sequences.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func merge() -> Driver<E.E> {
|
||||
let source = self.asObservable()
|
||||
.map { $0.asDriver() }
|
||||
.merge()
|
||||
return Driver<E.E>(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges elements from all inner observable sequences into a single observable sequence, limiting the number of concurrent subscriptions to inner sequences.
|
||||
|
||||
- returns: The observable sequence that merges the elements of the inner sequences.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func merge(maxConcurrent maxConcurrent: Int)
|
||||
-> Driver<E.E> {
|
||||
let source = self.asObservable()
|
||||
.map { $0.asDriver() }
|
||||
.merge(maxConcurrent: maxConcurrent)
|
||||
return Driver<E.E>(source)
|
||||
}
|
||||
}
|
||||
|
||||
// throttle
|
||||
extension DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Ignores elements from an observable sequence which are followed by another element within a specified relative time duration, using the specified scheduler to run throttling timers.
|
||||
|
||||
`throttle` and `debounce` are synonyms.
|
||||
|
||||
- parameter dueTime: Throttling duration for each element.
|
||||
- parameter scheduler: Scheduler to run the throttle timers and send events on.
|
||||
- returns: The throttled sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func throttle<S: SchedulerType>(dueTime: S.TimeInterval, _ scheduler: S)
|
||||
-> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.throttle(dueTime, scheduler)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Ignores elements from an observable sequence which are followed by another element within a specified relative time duration, using the specified scheduler to run throttling timers.
|
||||
|
||||
`throttle` and `debounce` are synonyms.
|
||||
|
||||
- parameter dueTime: Throttling duration for each element.
|
||||
- parameter scheduler: Scheduler to run the throttle timers and send events on.
|
||||
- returns: The throttled sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func debounce<S: SchedulerType>(dueTime: S.TimeInterval, _ scheduler: S)
|
||||
-> Driver<E> {
|
||||
let source = self.asObservable()
|
||||
.debounce(dueTime, scheduler)
|
||||
|
||||
return Driver(source)
|
||||
}
|
||||
}
|
||||
|
||||
// scan
|
||||
extension DriverConvertibleType {
|
||||
/**
|
||||
Applies an accumulator function over an observable sequence and returns each intermediate result. The specified seed value is used as the initial accumulator value.
|
||||
|
||||
For aggregation behavior with no intermediate results, see `reduce`.
|
||||
|
||||
- parameter seed: The initial accumulator value.
|
||||
- parameter accumulator: An accumulator function to be invoked on each element.
|
||||
- returns: An observable sequence containing the accumulated values.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func scan<A>(seed: A, accumulator: (A, E) -> A)
|
||||
-> Driver<A> {
|
||||
let source = self.asObservable()
|
||||
.scan(seed, accumulator: accumulator)
|
||||
return Driver<A>(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension SequenceType where Generator.Element : DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Concatenates all observable sequences in the given sequence, as long as the previous observable sequence terminated successfully.
|
||||
|
||||
- returns: An observable sequence that contains the elements of each given sequence, in sequential order.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func concat()
|
||||
-> Driver<Generator.Element.E> {
|
||||
let source: Observable<Generator.Element.E> = self.lazy.map { $0.asDriver() }.concat()
|
||||
return Driver<Generator.Element.E>(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension CollectionType where Generator.Element : DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever all of the observable sequences have produced an element at a corresponding index.
|
||||
|
||||
- parameter resultSelector: Function to invoke for each series of elements at corresponding indexes in the sources.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func zip<R>(resultSelector: [Generator.Element.E] throws -> R) -> Driver<R> {
|
||||
let source: Observable<R> = self.map { $0.asDriver() }.zip(resultSelector)
|
||||
return Driver<R>(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension CollectionType where Generator.Element : DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Merges the specified observable sequences into one observable sequence by using the selector function whenever any of the observable sequences produces an element.
|
||||
|
||||
- parameter resultSelector: Function to invoke whenever any of the sources produces an element.
|
||||
- returns: An observable sequence containing the result of combining elements of the sources using the specified result selector function.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func combineLatest<R>(resultSelector: [Generator.Element.E] throws -> R) -> Driver<R> {
|
||||
let source : Observable<R> = self.map { $0.asDriver() }.combineLatest(resultSelector)
|
||||
return Driver<R>(source)
|
||||
}
|
||||
}
|
||||
|
||||
extension DriverConvertibleType {
|
||||
|
||||
/**
|
||||
Merges two observable sequences into one observable sequence by combining each element from self with the latest element from the second source, if any.
|
||||
|
||||
- parameter second: Second observable source.
|
||||
- parameter resultSelector: Function to invoke for each element from the self combined with the latest element from the second source, if any.
|
||||
- returns: An observable sequence containing the result of combining each element of the self with the latest element from the second source, if any, using the specified result selector function.
|
||||
*/
|
||||
public func withLatestFrom<SecondO: DriverConvertibleType, ResultType>(second: SecondO, resultSelector: (E, SecondO.E) -> ResultType) -> Driver<ResultType> {
|
||||
let source = self.asObservable()
|
||||
.withLatestFrom(second.asDriver(), resultSelector: resultSelector)
|
||||
|
||||
return Driver<ResultType>(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Merges two observable sequences into one observable sequence by using latest element from the second sequence every time when `self` emitts an element.
|
||||
|
||||
- parameter second: Second observable source.
|
||||
- returns: An observable sequence containing the result of combining each element of the self with the latest element from the second source, if any, using the specified result selector function.
|
||||
*/
|
||||
public func withLatestFrom<SecondO: DriverConvertibleType>(second: SecondO) -> Driver<SecondO.E> {
|
||||
let source = self.asObservable()
|
||||
.withLatestFrom(second.asDriver())
|
||||
|
||||
return Driver<SecondO.E>(source)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// Driver+Extensions.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 9/19/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension DriverConvertibleType {
|
||||
/**
|
||||
Creates new subscription and sends elements to observer.
|
||||
|
||||
In this form it's equivalent to `subscribe` method, but it communicates intent better.
|
||||
|
||||
- parameter observer: Observer that receives events.
|
||||
- returns: Disposable object that can be used to unsubscribe the observer from the subject.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func drive<O: ObserverType where O.E == E>(observer: O) -> Disposable {
|
||||
return self.asObservable().subscribe(observer)
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribes to observable sequence using custom binder function.
|
||||
|
||||
- parameter with: Function used to bind elements from `self`.
|
||||
- returns: Object representing subscription.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func drive<R>(transformation: Observable<E> -> R) -> R {
|
||||
return transformation(self.asObservable())
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribes to observable sequence using custom binder function and final parameter passed to binder function
|
||||
after `self` is passed.
|
||||
|
||||
public func drive<R1, R2>(with: Self -> R1 -> R2, curriedArgument: R1) -> R2 {
|
||||
return with(self)(curriedArgument)
|
||||
}
|
||||
|
||||
- parameter with: Function used to bind elements from `self`.
|
||||
- parameter curriedArgument: Final argument passed to `binder` to finish binding process.
|
||||
- returns: Object representing subscription.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func drive<R1, R2>(with: Observable<E> -> R1 -> R2, curriedArgument: R1) -> R2 {
|
||||
return with(self.asObservable())(curriedArgument)
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribes an element handler, a completion handler and disposed handler to an observable sequence.
|
||||
|
||||
Error callback is not exposed because `Driver` can't error out.
|
||||
|
||||
- parameter onNext: Action to invoke for each element in the observable sequence.
|
||||
- parameter onCompleted: Action to invoke upon graceful termination of the observable sequence.
|
||||
gracefully completed, errored, or if the generation is cancelled by disposing subscription)
|
||||
- parameter onDisposed: Action to invoke upon any type of termination of sequence (if the sequence has
|
||||
gracefully completed, errored, or if the generation is cancelled by disposing subscription)
|
||||
- returns: Subscription object used to unsubscribe from the observable sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func drive(onNext onNext: ((E) -> Void)? = nil, onCompleted: (() -> Void)? = nil, onDisposed: (() -> Void)? = nil) -> Disposable {
|
||||
return self.asObservable().subscribe(onNext: onNext, onCompleted: onCompleted, onDisposed: onDisposed)
|
||||
}
|
||||
|
||||
/**
|
||||
Subscribes an element handler to an observable sequence.
|
||||
|
||||
- parameter onNext: Action to invoke for each element in the observable sequence.
|
||||
- returns: Subscription object used to unsubscribe from the observable sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func driveNext(onNext: E -> Void) -> Disposable {
|
||||
return self.asObservable().subscribeNext(onNext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// Driver.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 8/27/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
/**
|
||||
A type that can be converted to `Driver`.
|
||||
*/
|
||||
public protocol DriverConvertibleType : ObservableConvertibleType {
|
||||
|
||||
/**
|
||||
Converts self to `Driver`.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
func asDriver() -> Driver<E>
|
||||
}
|
||||
|
||||
extension DriverConvertibleType {
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asObservable() -> Observable<E> {
|
||||
return asDriver().asObservable()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Unit that represents observable sequence with following properties:
|
||||
|
||||
- it never fails
|
||||
- it delivers events on `MainScheduler.sharedInstance`
|
||||
- `shareReplay(1)` behavior
|
||||
- all observers share sequence computation resources
|
||||
- it's stateful, upon subscription (calling subscribe) last element is immediatelly replayed if it was produced
|
||||
- computation of elements is reference counted with respect to the number of observers
|
||||
- if there are no subscribers, it will release sequence computation resources
|
||||
|
||||
`Driver<Element>` can be considered a builder pattern for observable sequences that drive the application.
|
||||
|
||||
To find out more about units and how to use them, please go to `Documentation/Units.md`.
|
||||
*/
|
||||
public struct Driver<Element> : DriverConvertibleType {
|
||||
public typealias E = Element
|
||||
|
||||
let _source: Observable<E>
|
||||
|
||||
init(_ source: Observable<E>) {
|
||||
self._source = source.shareReplay(1)
|
||||
}
|
||||
|
||||
init(raw: Observable<E>) {
|
||||
self._source = raw
|
||||
}
|
||||
|
||||
#if EXPANDABLE_DRIVER
|
||||
public static func createUnsafe<O: ObservableType>(source: O) -> Driver<O.E> {
|
||||
return Driver<O.E>(raw: source.asObservable())
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
- returns: Built observable sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asObservable() -> Observable<E> {
|
||||
return _source
|
||||
}
|
||||
|
||||
/**
|
||||
- returns: `self`
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asDriver() -> Driver<E> {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public struct Drive {
|
||||
|
||||
#if !RX_NO_MODULE
|
||||
|
||||
/**
|
||||
Returns an empty observable sequence, using the specified scheduler to send out the single `Completed` message.
|
||||
|
||||
- returns: An observable sequence with no elements.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public static func empty<E>() -> Driver<E> {
|
||||
return Driver(raw: RxSwift.empty().subscribeOn(ConcurrentMainScheduler.sharedInstance))
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a non-terminating observable sequence, which can be used to denote an infinite duration.
|
||||
|
||||
- returns: An observable sequence whose observers will never get called.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public static func never<E>() -> Driver<E> {
|
||||
return Driver(raw: RxSwift.never().subscribeOn(ConcurrentMainScheduler.sharedInstance))
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an observable sequence that contains a single element.
|
||||
|
||||
- parameter element: Single element in the resulting observable sequence.
|
||||
- returns: An observable sequence containing the single specified element.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public static func just<E>(element: E) -> Driver<E> {
|
||||
return Driver(raw: RxSwift.just(element).subscribeOn(ConcurrentMainScheduler.sharedInstance))
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
/**
|
||||
Returns an empty observable sequence, using the specified scheduler to send out the single `Completed` message.
|
||||
|
||||
- returns: An observable sequence with no elements.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public static func empty<E>() -> Driver<E> {
|
||||
return Driver(raw: _empty().subscribeOn(ConcurrentMainScheduler.sharedInstance))
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a non-terminating observable sequence, which can be used to denote an infinite duration.
|
||||
|
||||
- returns: An observable sequence whose observers will never get called.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public static func never<E>() -> Driver<E> {
|
||||
return Driver(raw: _never().subscribeOn(ConcurrentMainScheduler.sharedInstance))
|
||||
}
|
||||
|
||||
/**
|
||||
Returns an observable sequence that contains a single element.
|
||||
|
||||
- parameter element: Single element in the resulting observable sequence.
|
||||
- returns: An observable sequence containing the single specified element.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public static func just<E>(element: E) -> Driver<E> {
|
||||
return Driver(raw: _just(element).subscribeOn(ConcurrentMainScheduler.sharedInstance))
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public static func sequenceOf<E>(elements: E ...) -> Driver<E> {
|
||||
let source = elements.toObservable().subscribeOn(ConcurrentMainScheduler.sharedInstance)
|
||||
return Driver(raw: source)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// name clashes :(
|
||||
|
||||
#if RX_NO_MODULE
|
||||
|
||||
func _empty<E>() -> Observable<E> {
|
||||
return empty()
|
||||
}
|
||||
|
||||
func _never<E>() -> Observable<E> {
|
||||
return never()
|
||||
}
|
||||
|
||||
func _just<E>(element: E) -> Observable<E> {
|
||||
return just(element)
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// ObservableConvertibleType+Driver.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 9/19/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension ObservableConvertibleType {
|
||||
/**
|
||||
Converts anything convertible to `Observable` to `Driver` unit.
|
||||
|
||||
- parameter onErrorJustReturn: Element to return in case of error and after that complete the sequence.
|
||||
- returns: Driving observable sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asDriver(onErrorJustReturn onErrorJustReturn: E) -> Driver<E> {
|
||||
let source = self
|
||||
.asObservable()
|
||||
.observeOn(MainScheduler.sharedInstance)
|
||||
.catchErrorJustReturn(onErrorJustReturn)
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Converts anything convertible to `Observable` to `Driver` unit.
|
||||
|
||||
- parameter onErrorDriveWith: Driver that continues to drive the sequence in case of error.
|
||||
- returns: Driving observable sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asDriver(onErrorDriveWith onErrorDriveWith: Driver<E>) -> Driver<E> {
|
||||
let source = self
|
||||
.asObservable()
|
||||
.observeOn(MainScheduler.sharedInstance)
|
||||
.catchError { _ in
|
||||
onErrorDriveWith.asObservable()
|
||||
}
|
||||
return Driver(source)
|
||||
}
|
||||
|
||||
/**
|
||||
Converts anything convertible to `Observable` to `Driver` unit.
|
||||
|
||||
- parameter onErrorRecover: Calculates driver that continues to drive the sequence in case of error.
|
||||
- returns: Driving observable sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func asDriver(onErrorRecover onErrorRecover: (error: ErrorType) -> Driver<E>) -> Driver<E> {
|
||||
let source = self
|
||||
.asObservable()
|
||||
.observeOn(MainScheduler.sharedInstance)
|
||||
.catchError { error in
|
||||
onErrorRecover(error: error).asObservable()
|
||||
}
|
||||
return Driver(source)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ public class DelegateProxy : _RXDelegateProxy {
|
|||
|
||||
private var subjectsForSelector = [Selector: PublishSubject<[AnyObject]>]()
|
||||
|
||||
unowned let parentObject: AnyObject
|
||||
weak var parentObject: AnyObject?
|
||||
|
||||
/**
|
||||
Initializes new instance.
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ Returns existing proxy for object or installs new instance of delegate proxy.
|
|||
return proxyForObject(self) as RxSearchBarDelegateProxy
|
||||
}
|
||||
|
||||
public var rx_searchText: ControlProperty<String> {
|
||||
public var rx_text: ControlProperty<String> {
|
||||
let source: Observable<String> = self.rx_delegate.observe("searchBar:textDidChange:")
|
||||
...
|
||||
}
|
||||
|
|
@ -224,25 +224,28 @@ extension ObservableType {
|
|||
let proxy: P = proxyForObject(object)
|
||||
let disposable = installDelegate(proxy, delegate: dataSource, retainDelegate: retainDataSource, onProxyForObject: object)
|
||||
|
||||
// we should never let the subscriber to complete because it should retain data source
|
||||
let source = sequenceOf(self.asObservable(), never()) as Observable<Observable<E>>
|
||||
let subscription = source.concat().subscribe { (event: Event<E>) in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
assert(proxy === P.currentDelegateFor(object), "Proxy changed from the time it was first set.\nOriginal: \(proxy)\nExisting: \(P.currentDelegateFor(object))")
|
||||
|
||||
binding(proxy, event)
|
||||
|
||||
switch event {
|
||||
case .Error(let error):
|
||||
bindingErrorToInterface(error)
|
||||
disposable.dispose()
|
||||
case .Completed:
|
||||
disposable.dispose()
|
||||
default:
|
||||
break
|
||||
let subscription = self.asObservable()
|
||||
// source can't ever end, otherwise it will release the subscriber
|
||||
.concat(never())
|
||||
.subscribe { [weak object] (event: Event<E>) in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
if let object = object {
|
||||
assert(proxy === P.currentDelegateFor(object), "Proxy changed from the time it was first set.\nOriginal: \(proxy)\nExisting: \(P.currentDelegateFor(object))")
|
||||
}
|
||||
|
||||
binding(proxy, event)
|
||||
|
||||
switch event {
|
||||
case .Error(let error):
|
||||
bindingErrorToInterface(error)
|
||||
disposable.dispose()
|
||||
case .Completed:
|
||||
disposable.dispose()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return StableCompositeDisposable.create(subscription, disposable)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// KVORepresentable+CoreGraphics.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
import CoreGraphics
|
||||
|
||||
#if arch(x86_64) || arch(arm64)
|
||||
let CGRectType = "{CGRect={CGPoint=dd}{CGSize=dd}}"
|
||||
let CGSizeType = "{CGSize=dd}"
|
||||
let CGPointType = "{CGPoint=dd}"
|
||||
#elseif arch(i386) || arch(arm)
|
||||
let CGRectType = "{CGRect={CGPoint=ff}{CGSize=ff}}"
|
||||
let CGSizeType = "{CGSize=ff}"
|
||||
let CGPointType = "{CGPoint=ff}"
|
||||
#endif
|
||||
|
||||
extension CGRect : KVORepresentable {
|
||||
public typealias KVOType = NSValue
|
||||
|
||||
/**
|
||||
Constructs self from `NSValue`.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
if strcmp(KVOValue.objCType, CGRectType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGRect(x: 0, y: 0, width: 0, height: 0)
|
||||
KVOValue.getValue(&typedValue)
|
||||
self = typedValue
|
||||
}
|
||||
}
|
||||
|
||||
extension CGPoint : KVORepresentable {
|
||||
public typealias KVOType = NSValue
|
||||
|
||||
/**
|
||||
Constructs self from `NSValue`.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
if strcmp(KVOValue.objCType, CGPointType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGPoint(x: 0, y: 0)
|
||||
KVOValue.getValue(&typedValue)
|
||||
self = typedValue
|
||||
}
|
||||
}
|
||||
|
||||
extension CGSize : KVORepresentable {
|
||||
public typealias KVOType = NSValue
|
||||
|
||||
/**
|
||||
Constructs self from `NSValue`.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
if strcmp(KVOValue.objCType, CGSizeType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGSize(width: 0, height: 0)
|
||||
KVOValue.getValue(&typedValue)
|
||||
self = typedValue
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// KVORepresentable+Swift.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Int : KVORepresentable {
|
||||
public typealias KVOType = NSNumber
|
||||
|
||||
/**
|
||||
Constructs `Self` using KVO value.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
self.init(KVOValue.integerValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Int32 : KVORepresentable {
|
||||
public typealias KVOType = NSNumber
|
||||
|
||||
/**
|
||||
Constructs `Self` using KVO value.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
self.init(KVOValue.intValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Int64 : KVORepresentable {
|
||||
public typealias KVOType = NSNumber
|
||||
|
||||
/**
|
||||
Constructs `Self` using KVO value.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
self.init(KVOValue.longLongValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt : KVORepresentable {
|
||||
public typealias KVOType = NSNumber
|
||||
|
||||
/**
|
||||
Constructs `Self` using KVO value.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
self.init(KVOValue.unsignedLongValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt32 : KVORepresentable {
|
||||
public typealias KVOType = NSNumber
|
||||
|
||||
/**
|
||||
Constructs `Self` using KVO value.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
self.init(KVOValue.unsignedIntValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt64 : KVORepresentable {
|
||||
public typealias KVOType = NSNumber
|
||||
|
||||
/**
|
||||
Constructs `Self` using KVO value.
|
||||
*/
|
||||
public init?(KVOValue: KVOType) {
|
||||
self.init(KVOValue.unsignedLongLongValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension RawRepresentable where RawValue: KVORepresentable {
|
||||
/**
|
||||
Constructs `Self` using optional KVO value.
|
||||
*/
|
||||
init?(KVOValue: RawValue.KVOType?) {
|
||||
guard let KVOValue = KVOValue else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let rawValue = RawValue(KVOValue: KVOValue) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
//
|
||||
// KVORepresentable.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
Type that is KVO representable (KVO mechanism can be used to observe it).
|
||||
*/
|
||||
public protocol KVORepresentable {
|
||||
/**
|
||||
Associated KVO type.
|
||||
*/
|
||||
typealias KVOType
|
||||
|
||||
/**
|
||||
Constructs `Self` using KVO value.
|
||||
*/
|
||||
init?(KVOValue: KVOType)
|
||||
}
|
||||
|
||||
extension KVORepresentable {
|
||||
init?(KVOValue: KVOType?) {
|
||||
guard let KVOValue = KVOValue else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(KVOValue: KVOValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Observable+Extensions.swift
|
||||
// Observable+Bind.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 8/29/15.
|
||||
|
|
@ -21,6 +21,7 @@ extension ObservableType {
|
|||
- parameter observer: Observer that receives events.
|
||||
- returns: Disposable object that can be used to unsubscribe the observer from the subject.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func bindTo<O: ObserverType where O.E == E>(observer: O) -> Disposable {
|
||||
return self.subscribe(observer)
|
||||
}
|
||||
|
|
@ -31,6 +32,7 @@ extension ObservableType {
|
|||
- parameter binder: Function used to bind elements from `self`.
|
||||
- returns: Object representing subscription.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func bindTo<R>(binder: Self -> R) -> R {
|
||||
return binder(self)
|
||||
}
|
||||
|
|
@ -47,8 +49,20 @@ extension ObservableType {
|
|||
- parameter curriedArgument: Final argument passed to `binder` to finish binding process.
|
||||
- returns: Object representing subscription.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func bindTo<R1, R2>(binder: Self -> R1 -> R2, curriedArgument: R1) -> R2 {
|
||||
return binder(self)(curriedArgument)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Subscribes an element handler to an observable sequence.
|
||||
|
||||
- parameter onNext: Action to invoke for each element in the observable sequence.
|
||||
- returns: Subscription object used to unsubscribe from the observable sequence.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.ud")
|
||||
public func bindNext(onNext: E -> Void) -> Disposable {
|
||||
return subscribeNext(onNext)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ class ControlTarget: RxTarget {
|
|||
|
||||
let selector: Selector = "eventHandler:"
|
||||
|
||||
unowned let control: Control
|
||||
weak var control: Control?
|
||||
#if os(iOS) || os(tvOS)
|
||||
let controlEvents: UIControlEvents
|
||||
#endif
|
||||
|
|
@ -72,7 +72,7 @@ class ControlTarget: RxTarget {
|
|||
#endif
|
||||
|
||||
func eventHandler(sender: Control!) {
|
||||
if let callback = self.callback {
|
||||
if let callback = self.callback, control = self.control {
|
||||
callback(control)
|
||||
}
|
||||
}
|
||||
|
|
@ -80,10 +80,10 @@ class ControlTarget: RxTarget {
|
|||
override func dispose() {
|
||||
super.dispose()
|
||||
#if os(iOS) || os(tvOS)
|
||||
self.control.removeTarget(self, action: self.selector, forControlEvents: self.controlEvents)
|
||||
self.control?.removeTarget(self, action: self.selector, forControlEvents: self.controlEvents)
|
||||
#elseif os(OSX)
|
||||
self.control.target = nil
|
||||
self.control.action = nil
|
||||
self.control?.target = nil
|
||||
self.control?.action = nil
|
||||
#endif
|
||||
self.callback = nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,8 +11,11 @@ import Foundation
|
|||
import RxSwift
|
||||
#endif
|
||||
|
||||
class KVOObservable<Element> : _Producer<Element?>
|
||||
, KVOObservableProtocol {
|
||||
class KVOObservable<Element>
|
||||
: ObservableType
|
||||
, KVOObservableProtocol {
|
||||
typealias E = Element?
|
||||
|
||||
unowned var target: AnyObject
|
||||
var strongTarget: AnyObject?
|
||||
|
||||
|
|
@ -30,7 +33,7 @@ class KVOObservable<Element> : _Producer<Element?>
|
|||
}
|
||||
}
|
||||
|
||||
override func run<O : ObserverType where O.E == Element?>(observer: O, cancel: Disposable, setSink: (Disposable) -> Void) -> Disposable {
|
||||
func subscribe<O : ObserverType where O.E == Element?>(observer: O) -> Disposable {
|
||||
let observer = KVOObserver(parent: self) { (value) in
|
||||
if value as? NSNull != nil {
|
||||
observer.on(.Next(nil))
|
||||
|
|
@ -99,7 +102,7 @@ func observeWeaklyKeyPathFor(
|
|||
|
||||
let property = class_getProperty(object_getClass(target), propertyName);
|
||||
if property == nil {
|
||||
return failWith(rxError(.KeyPathInvalid, "Object \(target) doesn't have property named `\(propertyName)`"))
|
||||
return failWith(RxCocoaError.InvalidPropertyName(object: target, propertyName: propertyName))
|
||||
}
|
||||
let propertyAttributes = property_getAttributes(property);
|
||||
|
||||
|
|
@ -109,7 +112,7 @@ func observeWeaklyKeyPathFor(
|
|||
|
||||
// KVO recursion for value changes
|
||||
return propertyObservable
|
||||
.map { (nextTarget: AnyObject?) -> Observable<AnyObject?> in
|
||||
.flatMapLatest { (nextTarget: AnyObject?) -> Observable<AnyObject?> in
|
||||
if nextTarget == nil {
|
||||
return just(nil)
|
||||
}
|
||||
|
|
@ -118,7 +121,7 @@ func observeWeaklyKeyPathFor(
|
|||
let strongTarget: AnyObject? = weakTarget
|
||||
|
||||
if nextObject == nil {
|
||||
return failWith(rxError(.KeyPathInvalid, "Observed \(nextTarget) as property `\(propertyName)` on `\(strongTarget)` which is not `NSObject`."))
|
||||
return failWith(RxCocoaError.InvalidObjectOnKeyPath(object: nextTarget!, sourceObject: strongTarget ?? NSNull(), propertyName: propertyName))
|
||||
}
|
||||
|
||||
// if target is alive, then send change
|
||||
|
|
@ -139,7 +142,6 @@ func observeWeaklyKeyPathFor(
|
|||
return nextElementsObservable
|
||||
}
|
||||
}
|
||||
.switchLatest()
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ extension NSNotificationCenter {
|
|||
- parameter object: Optional object used to filter notifications.
|
||||
- returns: Observable sequence of posted notifications.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_notification(name: String, object: AnyObject? = nil) -> Observable<NSNotification> {
|
||||
return create { observer in
|
||||
let nsObserver = self.addObserverForName(name, object: object, queue: nil) { notification in
|
||||
|
|
|
|||
|
|
@ -12,38 +12,19 @@ import RxSwift
|
|||
#endif
|
||||
import CoreGraphics
|
||||
|
||||
#if arch(x86_64) || arch(arm64)
|
||||
let CGRectType = "{CGRect={CGPoint=dd}{CGSize=dd}}"
|
||||
let CGSizeType = "{CGSize=dd}"
|
||||
let CGPointType = "{CGPoint=dd}"
|
||||
#elseif arch(i386) || arch(arm)
|
||||
let CGRectType = "{CGRect={CGPoint=ff}{CGSize=ff}}"
|
||||
let CGSizeType = "{CGSize=ff}"
|
||||
let CGPointType = "{CGPoint=ff}"
|
||||
#endif
|
||||
// MARK: Deprecated, CGPoint, CGRect, CGSize are now KVORepresentable
|
||||
|
||||
// rx_observe + CoreGraphics
|
||||
extension NSObject {
|
||||
/**
|
||||
Specialization of generic `rx_observe` method.
|
||||
|
||||
|
||||
For more information take a look at `rx_observe` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument.")
|
||||
public func rx_observe(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<CGRect?> {
|
||||
return rx_observe(keyPath, options: options, retainSelf: retainSelf)
|
||||
.map { (value: NSValue?) in
|
||||
if let value = value {
|
||||
if strcmp(value.objCType, CGRectType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGRect(x: 0, y: 0, width: 0, height: 0)
|
||||
value.getValue(&typedValue)
|
||||
return typedValue
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return rx_observe(NSValue.self, keyPath, options: options, retainSelf: retainSelf)
|
||||
.map(CGRect.init)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -51,21 +32,11 @@ extension NSObject {
|
|||
|
||||
For more information take a look at `rx_observe` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument.")
|
||||
public func rx_observe(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<CGSize?> {
|
||||
return rx_observe(keyPath, options: options, retainSelf: retainSelf)
|
||||
.map { (value: NSValue?) in
|
||||
if let value = value {
|
||||
if strcmp(value.objCType, CGSizeType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGSize(width: 0, height: 0)
|
||||
value.getValue(&typedValue)
|
||||
return typedValue
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return rx_observe(NSValue.self, keyPath, options: options, retainSelf: retainSelf)
|
||||
.map(CGSize.init)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,21 +44,11 @@ extension NSObject {
|
|||
|
||||
For more information take a look at `rx_observe` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument.")
|
||||
public func rx_observe(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<CGPoint?> {
|
||||
return rx_observe(keyPath, options: options, retainSelf: retainSelf)
|
||||
.map { (value: NSValue?) in
|
||||
if let value = value {
|
||||
if strcmp(value.objCType, CGPointType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGPoint(x: 0, y: 0)
|
||||
value.getValue(&typedValue)
|
||||
return typedValue
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return rx_observe(NSValue.self, keyPath, options: options, retainSelf: retainSelf)
|
||||
.map(CGPoint.init)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,21 +62,11 @@ extension NSObject {
|
|||
|
||||
For more information take a look at `rx_observeWeakly` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument.")
|
||||
public func rx_observeWeakly(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<CGRect?> {
|
||||
return rx_observeWeakly(keyPath, options: options)
|
||||
.map { (value: NSValue?) in
|
||||
if let value = value {
|
||||
if strcmp(value.objCType, CGRectType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGRect(x: 0, y: 0, width: 0, height: 0)
|
||||
value.getValue(&typedValue)
|
||||
return typedValue
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return rx_observeWeakly(NSValue.self, keyPath, options: options)
|
||||
.map(CGRect.init)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,21 +74,11 @@ extension NSObject {
|
|||
|
||||
For more information take a look at `rx_observeWeakly` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument.")
|
||||
public func rx_observeWeakly(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<CGSize?> {
|
||||
return rx_observeWeakly(keyPath, options: options)
|
||||
.map { (value: NSValue?) in
|
||||
if let value = value {
|
||||
if strcmp(value.objCType, CGSizeType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGSize(width: 0, height: 0)
|
||||
value.getValue(&typedValue)
|
||||
return typedValue
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return rx_observeWeakly(NSValue.self, keyPath, options: options)
|
||||
.map(CGSize.init)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -145,21 +86,11 @@ extension NSObject {
|
|||
|
||||
For more information take a look at `rx_observeWeakly` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument.")
|
||||
public func rx_observeWeakly(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<CGPoint?> {
|
||||
return rx_observeWeakly(keyPath, options: options)
|
||||
.map { (value: NSValue?) in
|
||||
if let value = value {
|
||||
if strcmp(value.objCType, CGPointType) != 0 {
|
||||
return nil
|
||||
}
|
||||
var typedValue = CGPoint(x: 0, y: 0)
|
||||
value.getValue(&typedValue)
|
||||
return typedValue
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return rx_observeWeakly(NSValue.self, keyPath, options: options)
|
||||
.map(CGPoint.init)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// NSObject+Rx+KVORepresentable.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension NSObject {
|
||||
|
||||
/**
|
||||
Specialization of generic `rx_observe` method.
|
||||
|
||||
This is a special overload because to observe values of some type (for example `Int`), first values of KVO type
|
||||
need to be observed (`NSNumber`), and then converted to result type.
|
||||
|
||||
For more information take a look at `rx_observe` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_observe<E: KVORepresentable>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<E?> {
|
||||
return rx_observe(E.KVOType.self, keyPath, options: options, retainSelf: retainSelf)
|
||||
.map(E.init)
|
||||
}
|
||||
}
|
||||
|
||||
#if !DISABLE_SWIZZLING
|
||||
// KVO
|
||||
extension NSObject {
|
||||
/**
|
||||
Specialization of generic `rx_observeWeakly` method.
|
||||
|
||||
For more information take a look at `rx_observeWeakly` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_observeWeakly<E: KVORepresentable>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<E?> {
|
||||
return rx_observeWeakly(E.KVOType.self, keyPath, options: options)
|
||||
.map(E.init)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
//
|
||||
// NSObject+Rx+RawRepresentable.swift
|
||||
// Rx
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/9/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
extension NSObject {
|
||||
/**
|
||||
Specialization of generic `rx_observe` method.
|
||||
|
||||
This specialization first observes `KVORepresentable` value and then converts it to `RawRepresentable` value.
|
||||
|
||||
It is useful for observing bridged ObjC enum values.
|
||||
|
||||
For more information take a look at `rx_observe` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_observe<E: RawRepresentable where E.RawValue: KVORepresentable>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<E?> {
|
||||
return rx_observe(E.RawValue.KVOType.self, keyPath, options: options, retainSelf: retainSelf)
|
||||
.map(E.init)
|
||||
}
|
||||
}
|
||||
|
||||
#if !DISABLE_SWIZZLING
|
||||
|
||||
// rx_observeWeakly + RawRepresentable
|
||||
extension NSObject {
|
||||
|
||||
/**
|
||||
Specialization of generic `rx_observeWeakly` method.
|
||||
|
||||
This specialization first observes `KVORepresentable` value and then converts it to `RawRepresentable` value.
|
||||
|
||||
It is useful for observing bridged ObjC enum values.
|
||||
|
||||
For more information take a look at `rx_observeWeakly` method.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_observeWeakly<E: RawRepresentable where E.RawValue: KVORepresentable>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<E?> {
|
||||
return rx_observeWeakly(E.RawValue.KVOType.self, keyPath, options: options)
|
||||
.map(E.init)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -18,29 +18,52 @@ var deallocatingSubjectContext: UInt8 = 0
|
|||
var deallocatedSubjectTriggerContext: UInt8 = 0
|
||||
var deallocatedSubjectContext: UInt8 = 0
|
||||
|
||||
// KVO is a tricky mechanism.
|
||||
//
|
||||
// When observing child in a ownership hierarchy, usually retaining observing target is wanted behavior.
|
||||
// When observing parent in a ownership hierarchy, usually retaining target isn't wanter behavior.
|
||||
//
|
||||
// KVO with weak references is especially tricky. For it to work, some kind of swizzling is required.
|
||||
// That can be done by
|
||||
// * replacing object class dynamically (like KVO does)
|
||||
// * by swizzling `dealloc` method on all instances for a class.
|
||||
// * some third method ...
|
||||
//
|
||||
// Both approaches can fail in certain scenarios:
|
||||
// * problems arise when swizzlers return original object class (like KVO does when nobody is observing)
|
||||
// * Problems can arise because replacing dealloc method isn't atomic operation (get implementation,
|
||||
// set implementation).
|
||||
//
|
||||
// Second approach is chosen. It can fail in case there are multiple libraries dynamically trying
|
||||
// to replace dealloc method. In case that isn't the case, it should be ok.
|
||||
//
|
||||
/**
|
||||
KVO is a tricky mechanism.
|
||||
|
||||
// KVO
|
||||
When observing child in a ownership hierarchy, usually retaining observing target is wanted behavior.
|
||||
When observing parent in a ownership hierarchy, usually retaining target isn't wanter behavior.
|
||||
|
||||
KVO with weak references is especially tricky. For it to work, some kind of swizzling is required.
|
||||
That can be done by
|
||||
* replacing object class dynamically (like KVO does)
|
||||
* by swizzling `dealloc` method on all instances for a class.
|
||||
* some third method ...
|
||||
|
||||
Both approaches can fail in certain scenarios:
|
||||
* problems arise when swizzlers return original object class (like KVO does when nobody is observing)
|
||||
* Problems can arise because replacing dealloc method isn't atomic operation (get implementation,
|
||||
set implementation).
|
||||
|
||||
Second approach is chosen. It can fail in case there are multiple libraries dynamically trying
|
||||
to replace dealloc method. In case that isn't the case, it should be ok.
|
||||
*/
|
||||
extension NSObject {
|
||||
|
||||
|
||||
/**
|
||||
Observes values on `keyPath` starting from `self` with `options` and retains `self` if `retainSelf` is set.
|
||||
|
||||
`rx_observe` is just a simple and performant wrapper around KVO mechanism.
|
||||
|
||||
* it can be used to observe paths starting from `self` or from ancestors in ownership graph (`retainSelf = false`)
|
||||
* it can be used to observe paths starting from descendants in ownership graph (`retainSelf = true`)
|
||||
* the paths have to consist only of `strong` properties, otherwise you are risking crashing the system by not unregistering KVO observer before dealloc.
|
||||
|
||||
If support for weak properties is needed or observing arbitrary or unknown relationships in the
|
||||
ownership tree, `rx_observeWeakly` is the preferred option.
|
||||
|
||||
- parameter keyPath: Key path of property names to observe.
|
||||
- parameter options: KVO mechanism notification options.
|
||||
- parameter retainSelf: Retains self during observation if set `true`.
|
||||
- returns: Observable sequence of objects on `keyPath`.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_observe<E>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<E?> {
|
||||
return KVOObservable(object: self, keyPath: keyPath, options: options, retainTarget: retainSelf).asObservable()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Observes values on `keyPath` starting from `self` with `options` and retains `self` if `retainSelf` is set.
|
||||
|
||||
|
|
@ -58,8 +81,10 @@ extension NSObject {
|
|||
- parameter retainSelf: Retains self during observation if set `true`.
|
||||
- returns: Observable sequence of objects on `keyPath`.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument `rx_observe<Element>(type: Element.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<Element?>`")
|
||||
public func rx_observe<Element>(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial], retainSelf: Bool = true) -> Observable<Element?> {
|
||||
return KVOObservable(object: self, keyPath: keyPath, options: options, retainTarget: retainSelf)
|
||||
return KVOObservable(object: self, keyPath: keyPath, options: options, retainTarget: retainSelf).asObservable()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -67,7 +92,28 @@ extension NSObject {
|
|||
#if !DISABLE_SWIZZLING
|
||||
// KVO
|
||||
extension NSObject {
|
||||
|
||||
/**
|
||||
Observes values on `keyPath` starting from `self` with `options` and doesn't retain `self`.
|
||||
|
||||
It can be used in all cases where `rx_observe` can be used and additionally
|
||||
|
||||
* because it won't retain observed target, it can be used to observe arbitrary object graph whose ownership relation is unknown
|
||||
* it can be used to observe `weak` properties
|
||||
|
||||
**Since it needs to intercept object deallocation process it needs to perform swizzling of `dealloc` method on observed object.**
|
||||
|
||||
- parameter keyPath: Key path of property names to observe.
|
||||
- parameter options: KVO mechanism notification options.
|
||||
- returns: Observable sequence of objects on `keyPath`.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_observeWeakly<E>(type: E.Type, _ keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<E?> {
|
||||
return observeWeaklyKeyPathFor(self, keyPath: keyPath, options: options)
|
||||
.map { n in
|
||||
return n as? E
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Observes values on `keyPath` starting from `self` with `options` and doesn't retain `self`.
|
||||
|
||||
|
|
@ -82,6 +128,8 @@ extension NSObject {
|
|||
- parameter options: KVO mechanism notification options.
|
||||
- returns: Observable sequence of objects on `keyPath`.
|
||||
*/
|
||||
@available(*, deprecated=2.0.0, message="Please use version that takes type as first argument `rx_observeWeakly<Element>(type: Element.Type, keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<Element?>`")
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_observeWeakly<Element>(keyPath: String, options: NSKeyValueObservingOptions = [.New, .Initial]) -> Observable<Element?> {
|
||||
return observeWeaklyKeyPathFor(self, keyPath: keyPath, options: options)
|
||||
.map { n in
|
||||
|
|
|
|||
|
|
@ -11,6 +11,48 @@ import Foundation
|
|||
import RxSwift
|
||||
#endif
|
||||
|
||||
/**
|
||||
RxCocoa URL errors.
|
||||
*/
|
||||
public enum RxCocoaURLError
|
||||
: ErrorType
|
||||
, CustomDebugStringConvertible {
|
||||
/**
|
||||
Unknown error occurred.
|
||||
*/
|
||||
case Unknown
|
||||
/**
|
||||
Response is not NSHTTPURLResponse
|
||||
*/
|
||||
case NonHTTPResponse(response: NSURLResponse)
|
||||
/**
|
||||
Response is not successful. (not in `200 ..< 300` range)
|
||||
*/
|
||||
case HTTPRequestFailed(response: NSHTTPURLResponse, data: NSData?)
|
||||
/**
|
||||
Deserialization error.
|
||||
*/
|
||||
case DeserializationError(error: ErrorType)
|
||||
}
|
||||
|
||||
public extension RxCocoaURLError {
|
||||
/**
|
||||
A textual representation of `self`, suitable for debugging.
|
||||
*/
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .Unknown:
|
||||
return "Unknown error has occurred."
|
||||
case let .NonHTTPResponse(response):
|
||||
return "Response is not NSHTTPURLResponse `\(response)`."
|
||||
case let .HTTPRequestFailed(response, _):
|
||||
return "HTTP request failed with `\(response.statusCode)`."
|
||||
case let .DeserializationError(error):
|
||||
return "Error during deserialization of the response: \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func escapeTerminalString(value: String) -> String {
|
||||
return value.stringByReplacingOccurrencesOfString("\"", withString: "\\\"", options:[], range: nil)
|
||||
}
|
||||
|
|
@ -74,7 +116,8 @@ extension NSURLSession {
|
|||
- parameter request: URL request.
|
||||
- returns: Observable sequence of URL responses.
|
||||
*/
|
||||
public func rx_response(request: NSURLRequest) -> Observable<(NSData!, NSURLResponse!)> {
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_response(request: NSURLRequest) -> Observable<(NSData!, NSHTTPURLResponse)> {
|
||||
return create { observer in
|
||||
|
||||
// smart compiler should be able to optimize this out
|
||||
|
|
@ -92,13 +135,18 @@ extension NSURLSession {
|
|||
print(convertResponseToString(data, response, error, interval))
|
||||
}
|
||||
|
||||
if data == nil || response == nil {
|
||||
observer.on(.Error(error ?? RxError.UnknownError))
|
||||
guard let response = response, data = data else {
|
||||
observer.on(.Error(error ?? RxCocoaURLError.Unknown))
|
||||
return
|
||||
}
|
||||
else {
|
||||
observer.on(.Next(data as NSData!, response as NSURLResponse!))
|
||||
observer.on(.Completed)
|
||||
|
||||
guard let httpResponse = response as? NSHTTPURLResponse else {
|
||||
observer.on(.Error(RxCocoaURLError.NonHTTPResponse(response: response)))
|
||||
return
|
||||
}
|
||||
|
||||
observer.on(.Next(data as NSData!, httpResponse))
|
||||
observer.on(.Completed)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -126,20 +174,14 @@ extension NSURLSession {
|
|||
- parameter request: URL request.
|
||||
- returns: Observable sequence of response data.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_data(request: NSURLRequest) -> Observable<NSData> {
|
||||
return rx_response(request).map { (data, response) -> NSData in
|
||||
guard let response = response as? NSHTTPURLResponse else {
|
||||
throw RxError.UnknownError
|
||||
}
|
||||
|
||||
if 200 ..< 300 ~= response.statusCode {
|
||||
return data ?? NSData()
|
||||
}
|
||||
else {
|
||||
throw rxError(.NetworkError, message: "Server returned failure", userInfo: [
|
||||
RxCocoaErrorHTTPResponseKey: response,
|
||||
RxCocoaErrorHTTPResponseDataKey: data ?? NSData()
|
||||
])
|
||||
throw RxCocoaURLError.HTTPRequestFailed(response: response, data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,9 +203,14 @@ extension NSURLSession {
|
|||
- parameter request: URL request.
|
||||
- returns: Observable sequence of response JSON.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_JSON(request: NSURLRequest) -> Observable<AnyObject!> {
|
||||
return rx_data(request).map { (data) -> AnyObject! in
|
||||
return try NSJSONSerialization.JSONObjectWithData(data ?? NSData(), options: [])
|
||||
do {
|
||||
return try NSJSONSerialization.JSONObjectWithData(data ?? NSData(), options: [])
|
||||
} catch let error {
|
||||
throw RxCocoaURLError.DeserializationError(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,6 +231,7 @@ extension NSURLSession {
|
|||
- parameter URL: URL of `NSURLRequest` request.
|
||||
- returns: Observable sequence of response JSON.
|
||||
*/
|
||||
@warn_unused_result(message="http://git.io/rxs.uo")
|
||||
public func rx_JSON(URL: NSURL) -> Observable<AnyObject!> {
|
||||
return rx_JSON(NSURLRequest(URL: URL))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,45 +14,52 @@ import RxSwift
|
|||
import UIKit
|
||||
#endif
|
||||
|
||||
public enum RxCocoaError : Int {
|
||||
case Unknown = 0
|
||||
case NetworkError = 1
|
||||
case InvalidOperation = 2
|
||||
case KeyPathInvalid = 3
|
||||
/**
|
||||
RxCocoa errors.
|
||||
*/
|
||||
public enum RxCocoaError
|
||||
: ErrorType
|
||||
, CustomDebugStringConvertible {
|
||||
/**
|
||||
Unknown error has occurred.
|
||||
*/
|
||||
case Unknown
|
||||
/**
|
||||
Invalid operation was attempted.
|
||||
*/
|
||||
case InvalidOperation(object: AnyObject)
|
||||
/**
|
||||
Items are not yet bound to user interface but have been requested.
|
||||
*/
|
||||
case ItemsNotYetBound(object: AnyObject)
|
||||
/**
|
||||
Invalid KVO Path.
|
||||
*/
|
||||
case InvalidPropertyName(object: AnyObject, propertyName: String)
|
||||
/**
|
||||
Invalid object on key path.
|
||||
*/
|
||||
case InvalidObjectOnKeyPath(object: AnyObject, sourceObject: AnyObject, propertyName: String)
|
||||
}
|
||||
|
||||
/**
|
||||
Error domain for internal RxCocoa errors.
|
||||
*/
|
||||
public let RxCocoaErrorDomain = "RxCocoaError"
|
||||
|
||||
/**
|
||||
`userInfo` key for `NSURLResponse` object when `RxCocoaError.NetworkError` happens.
|
||||
*/
|
||||
public let RxCocoaErrorHTTPResponseKey = "RxCocoaErrorHTTPResponseKey"
|
||||
|
||||
/**
|
||||
`userInfo` key for `NSData` object when `RxCocoaError.NetworkError` happens.
|
||||
*/
|
||||
public let RxCocoaErrorHTTPResponseDataKey = "RxCocoaErrorHTTPResponseDataKey"
|
||||
|
||||
func rxError(errorCode: RxCocoaError, _ message: String) -> NSError {
|
||||
return NSError(domain: RxCocoaErrorDomain, code: errorCode.rawValue, userInfo: [NSLocalizedDescriptionKey: message])
|
||||
}
|
||||
|
||||
#if !RELEASE
|
||||
public func _rxError(errorCode: RxCocoaError, message: String, userInfo: NSDictionary) -> NSError {
|
||||
return rxError(errorCode, message: message, userInfo: userInfo)
|
||||
}
|
||||
#endif
|
||||
|
||||
func rxError(errorCode: RxCocoaError, message: String, userInfo: NSDictionary) -> NSError {
|
||||
var resultInfo: [NSObject: AnyObject] = [:]
|
||||
resultInfo[NSLocalizedDescriptionKey] = message
|
||||
for k in userInfo.allKeys {
|
||||
resultInfo[k as! NSObject] = userInfo[k as! NSCopying]
|
||||
public extension RxCocoaError {
|
||||
/**
|
||||
A textual representation of `self`, suitable for debugging.
|
||||
*/
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .Unknown:
|
||||
return "Unknown error occurred"
|
||||
case let .InvalidOperation(object):
|
||||
return "Invalid operation was attempted on `\(object)`"
|
||||
case let .ItemsNotYetBound(object):
|
||||
return "Data source is set, but items are not yet bound to user interface for `\(object)`"
|
||||
case let .InvalidPropertyName(object, propertyName):
|
||||
return "Object `\(object)` dosn't have a property named `\(propertyName)`"
|
||||
case let .InvalidObjectOnKeyPath(object, sourceObject, propertyName):
|
||||
return "Unobservable object `\(object)` was observed as `\(propertyName)` of `\(sourceObject)`"
|
||||
}
|
||||
}
|
||||
return NSError(domain: RxCocoaErrorDomain, code: Int(errorCode.rawValue), userInfo: resultInfo)
|
||||
}
|
||||
|
||||
func bindingErrorToInterface(error: ErrorType) {
|
||||
|
|
@ -65,11 +72,11 @@ func bindingErrorToInterface(error: ErrorType) {
|
|||
}
|
||||
|
||||
func rxAbstractMethodWithMessage<T>(message: String) -> T {
|
||||
return rxFatalErrorAndDontReturn(message)
|
||||
rxFatalError(message)
|
||||
}
|
||||
|
||||
func rxAbstractMethod<T>() -> T {
|
||||
return rxFatalErrorAndDontReturn("Abstract method")
|
||||
rxFatalError("Abstract method")
|
||||
}
|
||||
|
||||
// workaround for Swift compiler bug, cheers compiler team :)
|
||||
|
|
@ -85,7 +92,6 @@ func castOrFatalError<T>(value: AnyObject!, message: String) -> T {
|
|||
let maybeResult: T? = value as? T
|
||||
guard let result = maybeResult else {
|
||||
rxFatalError(message)
|
||||
return maybeResult!
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
@ -95,7 +101,6 @@ func castOrFatalError<T>(value: AnyObject!) -> T {
|
|||
let maybeResult: T? = value as? T
|
||||
guard let result = maybeResult else {
|
||||
rxFatalError("Failure converting from \(value) to \(T.self)")
|
||||
return maybeResult!
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
@ -109,14 +114,9 @@ let delegateNotSet = "Delegate not set"
|
|||
// }
|
||||
|
||||
|
||||
func rxFatalErrorAndDontReturn<T>(lastMessage: String) -> T {
|
||||
rxFatalError(lastMessage)
|
||||
return (nil as T!)!
|
||||
}
|
||||
|
||||
#if !RX_NO_MODULE
|
||||
|
||||
func rxFatalError(lastMessage: String) {
|
||||
@noreturn func rxFatalError(lastMessage: String) {
|
||||
// The temptation to comment this line is great, but please don't, it's for your own good. The choice is yours.
|
||||
fatalError(lastMessage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,26 @@ class RxTarget : NSObject
|
|||
override init() {
|
||||
super.init()
|
||||
self.retainSelf = self
|
||||
|
||||
#if TRACE_RESOURCES
|
||||
OSAtomicIncrement32(&resourceCount)
|
||||
#endif
|
||||
|
||||
#if DEBUG
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
#endif
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
#if DEBUG
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
#endif
|
||||
self.retainSelf = nil
|
||||
}
|
||||
|
||||
#if TRACE_RESOURCES
|
||||
deinit {
|
||||
OSAtomicDecrement32(&resourceCount)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>Krunoslav-Zaher.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>2.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
|
|
|||
|
|
@ -12,37 +12,75 @@ import Cocoa
|
|||
import RxSwift
|
||||
#endif
|
||||
|
||||
var rx_value_key: UInt8 = 0
|
||||
var rx_control_events_key: UInt8 = 0
|
||||
|
||||
extension NSControl {
|
||||
|
||||
/**
|
||||
Reactive wrapper for control event.
|
||||
*/
|
||||
public var rx_controlEvents: ControlEvent<Void> {
|
||||
let source: Observable<Void> = AnonymousObservable { observer in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
let observer = ControlTarget(control: self) { control in
|
||||
observer.on(.Next())
|
||||
}
|
||||
|
||||
return observer
|
||||
}.takeUntil(rx_deallocated)
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
let source = rx_lazyInstanceObservable(&rx_control_events_key) { () -> Observable<Void> in
|
||||
create { [weak self] observer in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
guard let control = self else {
|
||||
observer.on(.Completed)
|
||||
return NopDisposable.instance
|
||||
}
|
||||
|
||||
let observer = ControlTarget(control: control) { control in
|
||||
observer.on(.Next())
|
||||
}
|
||||
|
||||
return observer
|
||||
}.takeUntil(self.rx_deallocated)
|
||||
}
|
||||
|
||||
return ControlEvent(source: source)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Helper to make sure that `Observable` returned from `createCachedObservable` is only created once.
|
||||
This is important because on OSX there is only one `target` and `action` properties on `NSControl`.
|
||||
*/
|
||||
func rx_lazyInstanceObservable<T: AnyObject>(key: UnsafePointer<Void>, createCachedObservable: () -> T) -> T {
|
||||
if let value = objc_getAssociatedObject(self, key) {
|
||||
return value as! T
|
||||
}
|
||||
|
||||
let observable = createCachedObservable()
|
||||
|
||||
objc_setAssociatedObject(self, key, observable, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
|
||||
return observable
|
||||
}
|
||||
|
||||
func rx_value<T>(getter getter: () -> T, setter: T -> Void) -> ControlProperty<T> {
|
||||
let source: Observable<T> = AnonymousObservable { observer in
|
||||
observer.on(.Next(getter()))
|
||||
|
||||
let observer = ControlTarget(control: self) { control in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
let source = rx_lazyInstanceObservable(&rx_value_key) { () -> Observable<T> in
|
||||
return create { [weak self] observer in
|
||||
guard let control = self else {
|
||||
observer.on(.Completed)
|
||||
return NopDisposable.instance
|
||||
}
|
||||
|
||||
observer.on(.Next(getter()))
|
||||
}
|
||||
|
||||
return observer
|
||||
}.takeUntil(rx_deallocated)
|
||||
|
||||
return ControlProperty(source: source, observer: ObserverOf { event in
|
||||
|
||||
let observer = ControlTarget(control: control) { control in
|
||||
observer.on(.Next(getter()))
|
||||
}
|
||||
|
||||
return observer
|
||||
}.takeUntil(self.rx_deallocated)
|
||||
}
|
||||
|
||||
|
||||
return ControlProperty(source: source, observer: AnyObserver { event in
|
||||
switch event {
|
||||
case .Next(let value):
|
||||
setter(value)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ extension NSImageView {
|
|||
/**
|
||||
Bindable sink for `image` property.
|
||||
*/
|
||||
public var rx_image: ObserverOf<NSImage!> {
|
||||
public var rx_image: AnyObserver<NSImage!> {
|
||||
return self.rx_imageAnimated(nil)
|
||||
}
|
||||
|
||||
|
|
@ -26,8 +26,8 @@ extension NSImageView {
|
|||
|
||||
- parameter transitionType: Optional transition type while setting the image (kCATransitionFade, kCATransitionMoveIn, ...)
|
||||
*/
|
||||
public func rx_imageAnimated(transitionType: String?) -> ObserverOf<NSImage!> {
|
||||
return ObserverOf { [weak self] event in
|
||||
public func rx_imageAnimated(transitionType: String?) -> AnyObserver<NSImage!> {
|
||||
return AnyObserver { [weak self] event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
switch event {
|
||||
|
|
|
|||
|
|
@ -15,13 +15,12 @@ import RxSwift
|
|||
class RxTextFieldDelegate : DelegateProxy
|
||||
, NSTextFieldDelegate
|
||||
, DelegateProxyType {
|
||||
let textField: NSTextField
|
||||
let textSubject = ReplaySubject<String>.create(bufferSize: 1)
|
||||
let textSubject = PublishSubject<String>()
|
||||
|
||||
required init(parentObject: AnyObject) {
|
||||
self.textField = parentObject as! NSTextField
|
||||
let textField = parentObject as! NSTextField
|
||||
super.init(parentObject: parentObject)
|
||||
self.textSubject.on(.Next(self.textField.stringValue))
|
||||
self.textSubject.on(.Next(textField.stringValue))
|
||||
}
|
||||
|
||||
override func controlTextDidChange(notification: NSNotification) {
|
||||
|
|
@ -59,9 +58,11 @@ extension NSTextField {
|
|||
public var rx_text: ControlProperty<String> {
|
||||
let delegate = proxyForObject(self) as RxTextFieldDelegate
|
||||
|
||||
let source = delegate.textSubject
|
||||
let source = deferred { [weak self] in
|
||||
delegate.textSubject.startWith(self?.stringValue ?? "")
|
||||
}.takeUntil(rx_deallocated)
|
||||
|
||||
return ControlProperty(source: source, observer: ObserverOf { [weak self] event in
|
||||
return ControlProperty(source: source, observer: AnyObserver { [weak self] event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
switch event {
|
||||
|
|
|
|||
|
|
@ -20,19 +20,20 @@ class RxScrollViewDelegateProxy : DelegateProxy
|
|||
, DelegateProxyType {
|
||||
private var _contentOffsetSubject: ReplaySubject<CGPoint>?
|
||||
|
||||
unowned let scrollView: UIScrollView
|
||||
weak var scrollView: UIScrollView?
|
||||
|
||||
var contentOffsetSubject: Observable<CGPoint> {
|
||||
if _contentOffsetSubject == nil {
|
||||
_contentOffsetSubject = ReplaySubject.create(bufferSize: 1)
|
||||
_contentOffsetSubject!.on(.Next(self.scrollView.contentOffset))
|
||||
let replaySubject = ReplaySubject<CGPoint>.create(bufferSize: 1)
|
||||
_contentOffsetSubject = replaySubject
|
||||
replaySubject.on(.Next(self.scrollView?.contentOffset ?? CGPointZero))
|
||||
}
|
||||
|
||||
return _contentOffsetSubject!
|
||||
}
|
||||
|
||||
required init(parentObject: AnyObject) {
|
||||
self.scrollView = parentObject as! UIScrollView
|
||||
self.scrollView = (parentObject as! UIScrollView)
|
||||
super.init(parentObject: parentObject)
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ class RxScrollViewDelegateProxy : DelegateProxy
|
|||
|
||||
func scrollViewDidScroll(scrollView: UIScrollView) {
|
||||
if let contentOffset = _contentOffsetSubject {
|
||||
contentOffset.on(.Next(self.scrollView.contentOffset))
|
||||
contentOffset.on(.Next(scrollView.contentOffset))
|
||||
}
|
||||
self._forwardToDelegate?.scrollViewDidScroll?(scrollView)
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@ class RxScrollViewDelegateProxy : DelegateProxy
|
|||
// delegate proxy
|
||||
|
||||
override class func createProxyForObject(object: AnyObject) -> AnyObject {
|
||||
let scrollView = object as! UIScrollView
|
||||
let scrollView = (object as! UIScrollView)
|
||||
|
||||
return castOrFatalError(scrollView.rx_createDelegateProxy())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ extension UIBarButtonItem {
|
|||
/**
|
||||
Bindable sink for `enabled` property.
|
||||
*/
|
||||
public var rx_enabled: ObserverOf<Bool> {
|
||||
return ObserverOf { [weak self] event in
|
||||
public var rx_enabled: AnyObserver<Bool> {
|
||||
return AnyObserver { [weak self] event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
switch event {
|
||||
|
|
@ -38,8 +38,14 @@ extension UIBarButtonItem {
|
|||
Reactive wrapper for target action pattern on `self`.
|
||||
*/
|
||||
public var rx_tap: ControlEvent<Void> {
|
||||
let source: Observable<Void> = AnonymousObservable { observer in
|
||||
let target = BarButtonItemTarget(barButtonItem: self) {
|
||||
let source: Observable<Void> = create { [weak self] observer in
|
||||
|
||||
guard let control = self else {
|
||||
observer.on(.Completed)
|
||||
return NopDisposable.instance
|
||||
}
|
||||
|
||||
let target = BarButtonItemTarget(barButtonItem: control) {
|
||||
observer.on(.Next())
|
||||
}
|
||||
return target
|
||||
|
|
@ -52,7 +58,7 @@ extension UIBarButtonItem {
|
|||
|
||||
|
||||
@objc
|
||||
class BarButtonItemTarget: NSObject, Disposable {
|
||||
class BarButtonItemTarget: RxTarget {
|
||||
typealias Callback = () -> Void
|
||||
|
||||
weak var barButtonItem: UIBarButtonItem?
|
||||
|
|
@ -66,12 +72,11 @@ class BarButtonItemTarget: NSObject, Disposable {
|
|||
barButtonItem.action = Selector("action:")
|
||||
}
|
||||
|
||||
deinit {
|
||||
dispose()
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
override func dispose() {
|
||||
super.dispose()
|
||||
#if DEBUG
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
#endif
|
||||
|
||||
barButtonItem?.target = nil
|
||||
barButtonItem?.action = nil
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
// Copyright (c) 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
#if os(iOS)
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
|
|
@ -26,3 +26,23 @@ extension UIButton {
|
|||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if os(tvOS)
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
import UIKit
|
||||
|
||||
extension UIButton {
|
||||
|
||||
/**
|
||||
Reactive wrapper for `PrimaryActionTriggered` control event.
|
||||
*/
|
||||
public var rx_primaryAction: ControlEvent<Void> {
|
||||
return rx_controlEvents(.PrimaryActionTriggered)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -143,14 +143,49 @@ extension UICollectionView {
|
|||
|
||||
*/
|
||||
public func rx_modelSelected<T>() -> ControlEvent<T> {
|
||||
let source: Observable<T> = rx_itemSelected .map { indexPath in
|
||||
let dataSource: RxCollectionViewReactiveArrayDataSource<T> = castOrFatalError(self.rx_dataSource.forwardToDelegate(), message: "This method only works in case one of the `rx_itemsWith*` methods was used.")
|
||||
|
||||
return dataSource.modelAtIndex(indexPath.item)!
|
||||
let source: Observable<T> = rx_itemSelected.flatMap { [weak self] indexPath -> Observable<T> in
|
||||
guard let view = self else {
|
||||
return empty()
|
||||
}
|
||||
|
||||
return just(try view.rx_modelAtIndexPath(indexPath))
|
||||
}
|
||||
|
||||
return ControlEvent(source: source)
|
||||
}
|
||||
|
||||
/**
|
||||
Syncronous helper method for retrieving a model at indexPath through a reactive data source
|
||||
*/
|
||||
public func rx_modelAtIndexPath<T>(indexPath: NSIndexPath) throws -> T {
|
||||
let dataSource: RxCollectionViewReactiveArrayDataSource<T> = castOrFatalError(self.rx_dataSource.forwardToDelegate(), message: "This method only works in case one of the `rx_itemsWith*` methods was used.")
|
||||
|
||||
guard let element = dataSource.modelAtIndex(indexPath.item) else {
|
||||
throw RxCocoaError.ItemsNotYetBound(object: self)
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(tvOS)
|
||||
|
||||
extension UICollectionView {
|
||||
|
||||
/**
|
||||
Reactive wrapper for `delegate` message `collectionView:didUpdateFocusInContext:withAnimationCoordinator:`.
|
||||
*/
|
||||
public var rx_didUpdateFocusInContextWithAnimationCoordinator: ControlEvent<(context: UIFocusUpdateContext, animationCoordinator: UIFocusAnimationCoordinator)> {
|
||||
|
||||
let source = rx_delegate.observe("collectionView:didUpdateFocusInContext:withAnimationCoordinator:")
|
||||
.map { a -> (context: UIFocusUpdateContext, animationCoordinator: UIFocusAnimationCoordinator) in
|
||||
let context = a[1] as! UIFocusUpdateContext
|
||||
let animationCoordinator = a[2] as! UIFocusAnimationCoordinator
|
||||
return (context: context, animationCoordinator: animationCoordinator)
|
||||
}
|
||||
|
||||
return ControlEvent(source: source)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ extension UIControl {
|
|||
/**
|
||||
Bindable sink for `enabled` property.
|
||||
*/
|
||||
public var rx_enabled: ObserverOf<Bool> {
|
||||
return ObserverOf { [weak self] event in
|
||||
public var rx_enabled: AnyObserver<Bool> {
|
||||
return AnyObserver { [weak self] event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
switch event {
|
||||
|
|
@ -41,10 +41,15 @@ extension UIControl {
|
|||
- parameter controlEvents: Filter for observed event types.
|
||||
*/
|
||||
public func rx_controlEvents(controlEvents: UIControlEvents) -> ControlEvent<Void> {
|
||||
let source: Observable<Void> = AnonymousObservable { observer in
|
||||
let source: Observable<Void> = create { [weak self] observer in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
let controlTarget = ControlTarget(control: self, controlEvents: controlEvents) {
|
||||
|
||||
guard let control = self else {
|
||||
observer.on(.Completed)
|
||||
return NopDisposable.instance
|
||||
}
|
||||
|
||||
let controlTarget = ControlTarget(control: control, controlEvents: controlEvents) {
|
||||
control in
|
||||
observer.on(.Next())
|
||||
}
|
||||
|
|
@ -58,11 +63,15 @@ extension UIControl {
|
|||
}
|
||||
|
||||
func rx_value<T>(getter getter: () -> T, setter: T -> Void) -> ControlProperty<T> {
|
||||
let source: Observable<T> = AnonymousObservable { observer in
|
||||
|
||||
let source: Observable<T> = create { [weak self] observer in
|
||||
guard let control = self else {
|
||||
observer.on(.Completed)
|
||||
return NopDisposable.instance
|
||||
}
|
||||
|
||||
observer.on(.Next(getter()))
|
||||
|
||||
let controlTarget = ControlTarget(control: self, controlEvents: [.EditingChanged, .ValueChanged]) { control in
|
||||
|
||||
let controlTarget = ControlTarget(control: control, controlEvents: [.EditingChanged, .ValueChanged]) { control in
|
||||
observer.on(.Next(getter()))
|
||||
}
|
||||
|
||||
|
|
@ -71,9 +80,9 @@ extension UIControl {
|
|||
}
|
||||
}.takeUntil(rx_deallocated)
|
||||
|
||||
return ControlProperty<T>(source: source, observer: ObserverOf { event in
|
||||
return ControlProperty<T>(source: source, observer: AnyObserver { event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
|
||||
switch event {
|
||||
case .Next(let value):
|
||||
setter(value)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ class GestureTarget: RxTarget {
|
|||
|
||||
let selector = Selector("eventHandler:")
|
||||
|
||||
unowned let gestureRecognizer: UIGestureRecognizer
|
||||
weak var gestureRecognizer: UIGestureRecognizer?
|
||||
var callback: Callback?
|
||||
|
||||
init(_ gestureRecognizer: UIGestureRecognizer, callback: Callback) {
|
||||
|
|
@ -38,15 +38,15 @@ class GestureTarget: RxTarget {
|
|||
}
|
||||
|
||||
func eventHandler(sender: UIGestureRecognizer!) {
|
||||
if let callback = self.callback {
|
||||
callback(self.gestureRecognizer)
|
||||
if let callback = self.callback, gestureRecognizer = self.gestureRecognizer {
|
||||
callback(gestureRecognizer)
|
||||
}
|
||||
}
|
||||
|
||||
override func dispose() {
|
||||
super.dispose()
|
||||
|
||||
self.gestureRecognizer.removeTarget(self, action: self.selector)
|
||||
self.gestureRecognizer?.removeTarget(self, action: self.selector)
|
||||
self.callback = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -57,12 +57,17 @@ extension UIGestureRecognizer {
|
|||
Reactive wrapper for gesture recognizer events.
|
||||
*/
|
||||
public var rx_event: ControlEvent<UIGestureRecognizer> {
|
||||
let source: Observable<UIGestureRecognizer> = AnonymousObservable { [weak self] observer in
|
||||
let source: Observable<UIGestureRecognizer> = create { [weak self] observer in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
guard let control = self else {
|
||||
observer.on(.Completed)
|
||||
return NopDisposable.instance
|
||||
}
|
||||
|
||||
let observer = GestureTarget(self!) {
|
||||
let observer = GestureTarget(control) {
|
||||
control in
|
||||
observer.on(.Next(self!))
|
||||
observer.on(.Next(control))
|
||||
}
|
||||
|
||||
return observer
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ extension UIImageView {
|
|||
/**
|
||||
Bindable sink for `image` property.
|
||||
*/
|
||||
public var rx_image: ObserverOf<UIImage!> {
|
||||
public var rx_image: AnyObserver<UIImage?> {
|
||||
return self.rx_imageAnimated(nil)
|
||||
}
|
||||
|
||||
|
|
@ -28,8 +28,8 @@ extension UIImageView {
|
|||
|
||||
- parameter transitionType: Optional transition type while setting the image (kCATransitionFade, kCATransitionMoveIn, ...)
|
||||
*/
|
||||
public func rx_imageAnimated(transitionType: String?) -> ObserverOf<UIImage!> {
|
||||
return ObserverOf { [weak self] event in
|
||||
public func rx_imageAnimated(transitionType: String?) -> AnyObserver<UIImage?> {
|
||||
return AnyObserver { [weak self] event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
switch event {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ extension UILabel {
|
|||
/**
|
||||
Bindable sink for `text` property.
|
||||
*/
|
||||
public var rx_text: ObserverOf<String> {
|
||||
return ObserverOf { [weak self] event in
|
||||
public var rx_text: AnyObserver<String> {
|
||||
return AnyObserver { [weak self] event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
switch event {
|
||||
|
|
@ -34,6 +34,25 @@ extension UILabel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Bindable sink for `attributedText` property.
|
||||
*/
|
||||
public var rx_attributedText: AnyObserver<NSAttributedString?> {
|
||||
return AnyObserver { [weak self] event in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
switch event {
|
||||
case .Next(let value):
|
||||
self?.attributedText = value
|
||||
case .Error(let error):
|
||||
bindingErrorToInterface(error)
|
||||
break
|
||||
case .Completed:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ extension UIScrollView {
|
|||
public var rx_contentOffset: ControlProperty<CGPoint> {
|
||||
let proxy = proxyForObject(self) as RxScrollViewDelegateProxy
|
||||
|
||||
return ControlProperty(source: proxy.contentOffsetSubject, observer: ObserverOf { [weak self] event in
|
||||
return ControlProperty(source: proxy.contentOffsetSubject, observer: AnyObserver { [weak self] event in
|
||||
switch event {
|
||||
case .Next(let value):
|
||||
self?.contentOffset = value
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ extension UISearchBar {
|
|||
.startWith(text)
|
||||
}
|
||||
|
||||
return ControlProperty(source: source, observer: ObserverOf { [weak self] event in
|
||||
return ControlProperty(source: source, observer: AnyObserver { [weak self] event in
|
||||
switch event {
|
||||
case .Next(let value):
|
||||
self?.text = value
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ extension UITableView {
|
|||
return cell
|
||||
}
|
||||
|
||||
return self.rx_itemsWithDataSource(dataSource)(source: source)
|
||||
return self.rx_itemsWithDataSource(dataSource)(source: source)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -187,15 +187,50 @@ extension UITableView {
|
|||
|
||||
*/
|
||||
public func rx_modelSelected<T>() -> ControlEvent<T> {
|
||||
let source: Observable<T> = rx_itemSelected.map { ip in
|
||||
let dataSource: RxTableViewReactiveArrayDataSource<T> = castOrFatalError(self.rx_dataSource.forwardToDelegate(), message: "This method only works in case one of the `rx_subscribeItemsTo` methods was used.")
|
||||
|
||||
return dataSource.modelAtIndex(ip.item)!
|
||||
let source: Observable<T> = rx_itemSelected.flatMap { [weak self] indexPath -> Observable<T> in
|
||||
guard let view = self else {
|
||||
return empty()
|
||||
}
|
||||
|
||||
return just(try view.rx_modelAtIndexPath(indexPath))
|
||||
}
|
||||
|
||||
return ControlEvent(source: source)
|
||||
}
|
||||
|
||||
/**
|
||||
Synchronous helper method for retrieving a model at indexPath through a reactive data source
|
||||
*/
|
||||
public func rx_modelAtIndexPath<T>(indexPath: NSIndexPath) throws -> T {
|
||||
let dataSource: RxTableViewReactiveArrayDataSource<T> = castOrFatalError(self.rx_dataSource.forwardToDelegate(), message: "This method only works in case one of the `rx_items*` methods was used.")
|
||||
|
||||
guard let element = dataSource.modelAtIndex(indexPath.item) else {
|
||||
throw RxCocoaError.ItemsNotYetBound(object: self)
|
||||
}
|
||||
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if os(tvOS)
|
||||
|
||||
extension UITableView {
|
||||
|
||||
/**
|
||||
Reactive wrapper for `delegate` message `tableView:didUpdateFocusInContext:withAnimationCoordinator:`.
|
||||
*/
|
||||
public var rx_didUpdateFocusInContextWithAnimationCoordinator: ControlEvent<(context: UIFocusUpdateContext, animationCoordinator: UIFocusAnimationCoordinator)> {
|
||||
|
||||
let source = rx_delegate.observe("tableView:didUpdateFocusInContext:withAnimationCoordinator:")
|
||||
.map { a -> (context: UIFocusUpdateContext, animationCoordinator: UIFocusAnimationCoordinator) in
|
||||
let context = a[1] as! UIFocusUpdateContext
|
||||
let animationCoordinator = a[2] as! UIFocusAnimationCoordinator
|
||||
return (context: context, animationCoordinator: animationCoordinator)
|
||||
}
|
||||
|
||||
return ControlEvent(source: source)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ extension UITextView {
|
|||
.startWith(text)
|
||||
}
|
||||
|
||||
return ControlProperty(source: source, observer: ObserverOf { [weak self] event in
|
||||
return ControlProperty(source: source, observer: AnyObserver { [weak self] event in
|
||||
switch event {
|
||||
case .Next(let value):
|
||||
self?.text = value
|
||||
|
|
|
|||
|
|
@ -13,20 +13,22 @@ import RxSwift
|
|||
import RxCocoa
|
||||
#endif
|
||||
|
||||
struct ItemPath : CustomStringConvertible {
|
||||
struct ItemPath : CustomDebugStringConvertible {
|
||||
let sectionIndex: Int
|
||||
let itemIndex: Int
|
||||
|
||||
var description : String {
|
||||
var debugDescription : String {
|
||||
get {
|
||||
return "(\(sectionIndex), \(itemIndex))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Changeset<S: SectionModelType> : CustomStringConvertible {
|
||||
public struct Changeset<S: SectionModelType> : CustomDebugStringConvertible {
|
||||
typealias I = S.Item
|
||||
|
||||
var reloadData: Bool = false
|
||||
|
||||
var finalSections: [S] = []
|
||||
|
||||
var insertedSections: [Int] = []
|
||||
|
|
@ -43,10 +45,12 @@ public struct Changeset<S: SectionModelType> : CustomStringConvertible {
|
|||
var initialValue = Changeset<S>()
|
||||
initialValue.insertedSections = Array(0 ..< sections.count)
|
||||
initialValue.finalSections = sections
|
||||
initialValue.reloadData = true
|
||||
|
||||
return initialValue
|
||||
}
|
||||
|
||||
public var description : String {
|
||||
public var debugDescription : String {
|
||||
get {
|
||||
let serializedSections = "[\n" + finalSections.map { "\($0)" }.joinWithSeparator(",\n") + "\n]\n"
|
||||
return " >> Final sections"
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public class RxCollectionViewSectionedDataSource<S: SectionModelType> : _RxColle
|
|||
public typealias CellFactory = (UICollectionView, NSIndexPath, I) -> UICollectionViewCell
|
||||
public typealias SupplementaryViewFactory = (UICollectionView, String, NSIndexPath) -> UICollectionReusableView
|
||||
|
||||
public typealias IncrementalUpdateObserver = ObserverOf<Changeset<S>>
|
||||
public typealias IncrementalUpdateObserver = AnyObserver<Changeset<S>>
|
||||
|
||||
public typealias IncrementalUpdateDisposeKey = Bag<IncrementalUpdateObserver>.KeyType
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,12 @@ class RxTableViewSectionedAnimatedDataSource<S: SectionModelType> : RxTableViewS
|
|||
for c in element {
|
||||
//print("Animating ==============================\n\(c)\n===============================\n")
|
||||
setSections(c.finalSections)
|
||||
tableView.performBatchUpdates(c)
|
||||
if c.reloadData {
|
||||
tableView.reloadData()
|
||||
}
|
||||
else {
|
||||
tableView.performBatchUpdates(c)
|
||||
}
|
||||
}
|
||||
case .Error(let error):
|
||||
bindingErrorToInterface(error)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ public class RxTableViewSectionedDataSource<S: SectionModelType> : _RxTableViewS
|
|||
public typealias Section = S
|
||||
public typealias CellFactory = (UITableView, NSIndexPath, I) -> UITableViewCell
|
||||
|
||||
public typealias IncrementalUpdateObserver = ObserverOf<Changeset<S>>
|
||||
public typealias IncrementalUpdateObserver = AnyObserver<Changeset<S>>
|
||||
|
||||
public typealias IncrementalUpdateDisposeKey = Bag<IncrementalUpdateObserver>.KeyType
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,31 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
enum EditEvent : CustomStringConvertible {
|
||||
public enum DifferentiatorError
|
||||
: ErrorType
|
||||
, CustomDebugStringConvertible {
|
||||
case DuplicateItem(item: Any)
|
||||
}
|
||||
|
||||
extension DifferentiatorError {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case let .DuplicateItem(item):
|
||||
return "Duplicate item \(item)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum EditEvent : CustomDebugStringConvertible {
|
||||
case Inserted // can't be found in old sections
|
||||
case Deleted // Was in old, not in new, in it's place is something "not new" :(, otherwise it's Updated
|
||||
case Moved // same item, but was on different index, and needs explicit move
|
||||
case MovedAutomatically // don't need to specify any changes for those rows
|
||||
case Untouched
|
||||
|
||||
var description: String {
|
||||
}
|
||||
|
||||
extension EditEvent {
|
||||
var debugDescription: String {
|
||||
get {
|
||||
switch self {
|
||||
case .Inserted:
|
||||
|
|
@ -33,39 +50,48 @@ enum EditEvent : CustomStringConvertible {
|
|||
}
|
||||
}
|
||||
|
||||
struct SectionAdditionalInfo : CustomStringConvertible {
|
||||
struct SectionAdditionalInfo : CustomDebugStringConvertible {
|
||||
var event: EditEvent
|
||||
var indexAfterDelete: Int?
|
||||
|
||||
var description: String {
|
||||
}
|
||||
|
||||
extension SectionAdditionalInfo {
|
||||
var debugDescription: String {
|
||||
get {
|
||||
return "\(event), \(indexAfterDelete)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ItemAdditionalInfo : CustomStringConvertible {
|
||||
struct ItemAdditionalInfo : CustomDebugStringConvertible {
|
||||
var event: EditEvent
|
||||
var indexAfterDelete: Int?
|
||||
|
||||
var description: String {
|
||||
}
|
||||
|
||||
extension ItemAdditionalInfo {
|
||||
var debugDescription: String {
|
||||
get {
|
||||
return "\(event) \(indexAfterDelete)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func indexSections<S: SectionModelType where S: Hashable, S.Item: Hashable>(sections: [S]) -> [S : Int] {
|
||||
func indexSections<S: SectionModelType where S: Hashable, S.Item: Hashable>(sections: [S]) throws -> [S : Int] {
|
||||
var indexedSections: [S : Int] = [:]
|
||||
for (i, section) in sections.enumerate() {
|
||||
precondition(indexedSections[section] == nil, "Section \(section) has already been indexed at \(indexedSections[section]!)")
|
||||
guard indexedSections[section] == nil else {
|
||||
#if DEBUG
|
||||
precondition(indexedSections[section] == nil, "Section \(section) has already been indexed at \(indexedSections[section]!)")
|
||||
#endif
|
||||
throw DifferentiatorError.DuplicateItem(item: section)
|
||||
}
|
||||
indexedSections[section] = i
|
||||
}
|
||||
|
||||
return indexedSections
|
||||
}
|
||||
|
||||
func indexSectionItems<S: SectionModelType where S: Hashable, S.Item: Hashable>(sections: [S]) -> [S.Item : (Int, Int)] {
|
||||
func indexSectionItems<S: SectionModelType where S: Hashable, S.Item: Hashable>(sections: [S]) throws -> [S.Item : (Int, Int)] {
|
||||
var totalItems = 0
|
||||
for i in 0 ..< sections.count {
|
||||
totalItems += sections[i].items.count
|
||||
|
|
@ -76,7 +102,12 @@ func indexSectionItems<S: SectionModelType where S: Hashable, S.Item: Hashable>(
|
|||
|
||||
for i in 0 ..< sections.count {
|
||||
for (j, item) in sections[i].items.enumerate() {
|
||||
precondition(indexedItems[item] == nil, "Item \(item) has already been indexed at \(indexedItems[item]!)" )
|
||||
guard indexedItems[item] == nil else {
|
||||
#if DEBUG
|
||||
precondition(indexedItems[item] == nil, "Item \(item) has already been indexed at \(indexedItems[item]!)" )
|
||||
#endif
|
||||
throw DifferentiatorError.DuplicateItem(item: item)
|
||||
}
|
||||
indexedItems[item] = (i, j)
|
||||
}
|
||||
}
|
||||
|
|
@ -185,7 +216,6 @@ to = [
|
|||
]
|
||||
*/
|
||||
|
||||
|
||||
// Generates differential changes suitable for sectioned view consumption.
|
||||
// It will not only detect changes between two states, but it will also try to compress those changes into
|
||||
// almost minimal set of changes.
|
||||
|
|
@ -209,11 +239,11 @@ to = [
|
|||
//
|
||||
// There maybe exists a better division, but time will tell.
|
||||
//
|
||||
func differentiate<S: SectionModelType where S: Hashable, S.Item: Hashable>(
|
||||
func differencesForSectionedView<S: SectionModelType where S: Hashable, S.Item: Hashable>(
|
||||
initialSections: [S],
|
||||
finalSections: [S]
|
||||
)
|
||||
-> [Changeset<S>] {
|
||||
throws -> [Changeset<S>] {
|
||||
|
||||
typealias I = S.Item
|
||||
|
||||
|
|
@ -236,11 +266,11 @@ func differentiate<S: SectionModelType where S: Hashable, S.Item: Hashable>(
|
|||
return [ItemAdditionalInfo](count: s.items.count, repeatedValue: defaultItemInfo)
|
||||
}
|
||||
|
||||
initialSectionIndexes = indexSections(initialSections)
|
||||
finalSectionIndexes = indexSections(finalSections)
|
||||
initialSectionIndexes = try indexSections(initialSections)
|
||||
finalSectionIndexes = try indexSections(finalSections)
|
||||
|
||||
var initialItemIndexes: [I: (Int, Int)] = indexSectionItems(initialSections)
|
||||
var finalItemIndexes: [I: (Int, Int)] = indexSectionItems(finalSections)
|
||||
var initialItemIndexes: [I: (Int, Int)] = try indexSectionItems(initialSections)
|
||||
var finalItemIndexes: [I: (Int, Int)] = try indexSectionItems(finalSections)
|
||||
|
||||
// mark deleted sections {
|
||||
// 1rst stage
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// ObservableConvertibleType+Differentiator.swift
|
||||
// RxExample
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
#endif
|
||||
|
||||
extension ObservableConvertibleType where E: SequenceType, E.Generator.Element : protocol<SectionModelType, Hashable>, E.Generator.Element.Item: Hashable {
|
||||
typealias Section = E.Generator.Element
|
||||
|
||||
func differentiateForSectionedView()
|
||||
-> Observable<[Changeset<Section>]> {
|
||||
|
||||
return self.asObservable().multicast({
|
||||
return PublishSubject()
|
||||
}) { (sharedSource: Observable<E>) in
|
||||
let newValues = sharedSource.skip(1)
|
||||
|
||||
let initialValueSequence: Observable<[Changeset<Section>]>= sharedSource
|
||||
.take(1)
|
||||
.map { [Changeset.initialValue(Array($0))] }
|
||||
|
||||
let differences = zip(sharedSource, newValues) { oldSections, newSections -> [Changeset<Section>] in
|
||||
do {
|
||||
return try differencesForSectionedView(Array(oldSections), finalSections: Array(newSections))
|
||||
}
|
||||
// in case of error, print it to terminal only
|
||||
catch let e {
|
||||
print(e)
|
||||
return [Changeset.initialValue(Array(newSections))]
|
||||
}
|
||||
}
|
||||
|
||||
return sequenceOf(initialValueSequence, differences).merge()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// UISectionedView+RxAnimatedDataSource.swift
|
||||
// RxExample
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/14/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
#endif
|
||||
|
||||
extension UITableView {
|
||||
public func rx_itemsAnimatedWithDataSource<
|
||||
DataSource: protocol<RxTableViewDataSourceType, UITableViewDataSource>,
|
||||
S: SequenceType,
|
||||
O: ObservableConvertibleType,
|
||||
Section: protocol<SectionModelType, Hashable>
|
||||
where
|
||||
DataSource.Element == [Changeset<Section>],
|
||||
O.E == S,
|
||||
S.Generator.Element == Section,
|
||||
Section.Item: Hashable
|
||||
>
|
||||
(dataSource: DataSource)
|
||||
(source: O)
|
||||
-> Disposable {
|
||||
let differences = source.differentiateForSectionedView()
|
||||
return self.rx_itemsWithDataSource(dataSource)(source: differences)
|
||||
}
|
||||
}
|
||||
|
||||
extension UICollectionView {
|
||||
public func rx_itemsAnimatedWithDataSource<
|
||||
DataSource: protocol<RxCollectionViewDataSourceType, UICollectionViewDataSource>,
|
||||
S: SequenceType,
|
||||
O: ObservableConvertibleType,
|
||||
Section: protocol<SectionModelType, Hashable>
|
||||
where
|
||||
DataSource.Element == [Changeset<Section>],
|
||||
O.E == S,
|
||||
S.Generator.Element == Section,
|
||||
Section.Item: Hashable
|
||||
>
|
||||
(dataSource: DataSource)
|
||||
(source: O)
|
||||
-> Disposable {
|
||||
let differences = source.differentiateForSectionedView()
|
||||
return self.rx_itemsWithDataSource(dataSource)(source: differences)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,264 @@
|
|||
//
|
||||
// GitHubSearchRepositoriesAPI.swift
|
||||
// RxExample
|
||||
//
|
||||
// Created by Krunoslav Zaher on 10/18/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
/**
|
||||
Parsed GitHub respository.
|
||||
*/
|
||||
struct Repository: CustomDebugStringConvertible {
|
||||
var name: String
|
||||
var url: String
|
||||
|
||||
init(name: String, url: String) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
extension Repository {
|
||||
var debugDescription: String {
|
||||
return "\(name) | \(url)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
ServiceState state.
|
||||
*/
|
||||
enum ServiceState {
|
||||
case Online
|
||||
case Offline
|
||||
}
|
||||
|
||||
/**
|
||||
Raw response from GitHub API
|
||||
*/
|
||||
enum SearchRepositoryResponse {
|
||||
/**
|
||||
New repositories just fetched
|
||||
*/
|
||||
case Repositories(repositories: [Repository], nextURL: NSURL?)
|
||||
|
||||
/**
|
||||
In case there was some problem fetching data from service, this will be returned.
|
||||
It really doesn't matter if that is a failure in network layer, parsing error or something else.
|
||||
In case data can't be read and parsed properly, something is wrong with server response.
|
||||
*/
|
||||
case ServiceOffline
|
||||
|
||||
/**
|
||||
This example uses unauthenticated GitHub API. That API does have throttling policy and you won't
|
||||
be able to make more then 10 requests per minute.
|
||||
|
||||
That is actually an awesome scenario to demonstrate complex retries using alert views and combination of timers.
|
||||
|
||||
Just search like mad, and everything will be handled right.
|
||||
*/
|
||||
case LimitExceeded
|
||||
}
|
||||
|
||||
/**
|
||||
This is the final result of loading. Crème de la crème.
|
||||
*/
|
||||
struct RepositoriesState {
|
||||
/**
|
||||
List of parsed repositories ready to be shown in the UI.
|
||||
*/
|
||||
let repositories: [Repository]
|
||||
|
||||
/**
|
||||
Current network state.
|
||||
*/
|
||||
let serviceState: ServiceState?
|
||||
|
||||
/**
|
||||
Limit exceeded
|
||||
*/
|
||||
let limitExceeded: Bool
|
||||
|
||||
static let empty = RepositoriesState(repositories: [], serviceState: nil, limitExceeded: false)
|
||||
}
|
||||
|
||||
|
||||
class GitHubSearchRepositoriesAPI {
|
||||
|
||||
static let sharedAPI = GitHubSearchRepositoriesAPI(wireframe: DefaultWireframe())
|
||||
|
||||
let activityIndicator = ActivityIndicator()
|
||||
|
||||
// Why would network service have wireframe service? It's here to abstract promting user
|
||||
// Do we really want to make this example project factory/fascade/service competition? :)
|
||||
private let _wireframe: Wireframe
|
||||
|
||||
private init(wireframe: Wireframe) {
|
||||
_wireframe = wireframe
|
||||
}
|
||||
|
||||
private static let parseLinksPattern = "\\s*,?\\s*<([^\\>]*)>\\s*;\\s*rel=\"([^\"]*)\""
|
||||
private static let linksRegex = try! NSRegularExpression(pattern: parseLinksPattern, options: [.AllowCommentsAndWhitespace])
|
||||
|
||||
private static func parseLinks(links: String) throws -> [String: String] {
|
||||
|
||||
let length = (links as NSString).length
|
||||
let matches = GitHubSearchRepositoriesAPI.linksRegex.matchesInString(links, options: NSMatchingOptions(), range: NSRange(location: 0, length: length))
|
||||
|
||||
var result: [String: String] = [:]
|
||||
|
||||
for m in matches {
|
||||
let matches = (1 ..< m.numberOfRanges).map { rangeIndex -> String in
|
||||
let range = m.rangeAtIndex(rangeIndex)
|
||||
let startIndex = links.startIndex.advancedBy(range.location)
|
||||
let endIndex = startIndex.advancedBy(range.length)
|
||||
let stringRange = Range(start: startIndex, end: endIndex)
|
||||
return links.substringWithRange(stringRange)
|
||||
}
|
||||
|
||||
if matches.count != 2 {
|
||||
throw exampleError("Error parsing links")
|
||||
}
|
||||
|
||||
result[matches[1]] = matches[0]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private static func parseNextURL(httpResponse: NSHTTPURLResponse) throws -> NSURL? {
|
||||
guard let serializedLinks = httpResponse.allHeaderFields["Link"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let links = try GitHubSearchRepositoriesAPI.parseLinks(serializedLinks)
|
||||
|
||||
guard let nextPageURL = links["next"] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let nextUrl = NSURL(string: nextPageURL) else {
|
||||
throw exampleError("Error parsing next url `\(nextPageURL)`")
|
||||
}
|
||||
|
||||
return nextUrl
|
||||
}
|
||||
|
||||
/**
|
||||
Public fascade for search.
|
||||
*/
|
||||
func search(query: String, loadNextPageTrigger: Observable<Void>) -> Observable<RepositoriesState> {
|
||||
let escapedQuery = URLEscape(query)
|
||||
let url = NSURL(string: "https://api.github.com/search/repositories?q=\(escapedQuery)")!
|
||||
return recursivelySearch([], loadNextURL: url, loadNextPageTrigger: loadNextPageTrigger)
|
||||
// Here we go again
|
||||
.startWith(RepositoriesState.empty)
|
||||
}
|
||||
|
||||
private func recursivelySearch(loadedSoFar: [Repository], loadNextURL: NSURL, loadNextPageTrigger: Observable<Void>) -> Observable<RepositoriesState> {
|
||||
return loadSearchURL(loadNextURL).flatMap { searchResponse -> Observable<RepositoriesState> in
|
||||
switch searchResponse {
|
||||
/**
|
||||
If service is offline, that's ok, that means that this isn't the last thing we've heard from that API.
|
||||
It will retry until either battery drains, you become angry and close the app or evil machine comes back
|
||||
from the future, steals your device and Googles Sarah Connor's address.
|
||||
*/
|
||||
case .ServiceOffline:
|
||||
return just(RepositoriesState(repositories: loadedSoFar, serviceState: .Offline, limitExceeded: false))
|
||||
case .LimitExceeded:
|
||||
return just(RepositoriesState(repositories: loadedSoFar, serviceState: .Online, limitExceeded: true))
|
||||
case .Repositories:
|
||||
break
|
||||
}
|
||||
|
||||
// Repositories without next url? The party is done.
|
||||
guard case .Repositories(let newPageRepositories, let maybeNextURL) = searchResponse else {
|
||||
fatalError("Some fourth case?")
|
||||
}
|
||||
|
||||
var loadedRepositories = loadedSoFar
|
||||
loadedRepositories.appendContentsOf(newPageRepositories)
|
||||
|
||||
let appenedRepositories = RepositoriesState(repositories: loadedRepositories, serviceState: .Online, limitExceeded: false)
|
||||
|
||||
// if next page can't be loaded, just return what was loaded, and stop
|
||||
guard let nextURL = maybeNextURL else {
|
||||
return just(appenedRepositories)
|
||||
}
|
||||
|
||||
return [
|
||||
// return loaded immediately
|
||||
just(appenedRepositories),
|
||||
// wait until next page can be loaded
|
||||
never().takeUntil(loadNextPageTrigger),
|
||||
// load next page
|
||||
self.recursivelySearch(loadedRepositories, loadNextURL: nextURL, loadNextPageTrigger: loadNextPageTrigger)
|
||||
].concat()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Displays UI that prompts the user when to retry.
|
||||
*/
|
||||
private func buildRetryPrompt() -> Observable<Void> {
|
||||
return _wireframe.promptFor(
|
||||
"Exceeded limit of 10 non authenticated requests per minute for GitHub API. Please wait a minute. :(\nhttps://developer.github.com/v3/#rate-limiting",
|
||||
cancelAction: RetryResult.Cancel,
|
||||
actions: [RetryResult.Retry]
|
||||
)
|
||||
.filter { (x: RetryResult) in x == .Retry }
|
||||
.map { _ in () }
|
||||
}
|
||||
|
||||
private func loadSearchURL(searchURL: NSURL) -> Observable<SearchRepositoryResponse> {
|
||||
return NSURLSession.sharedSession()
|
||||
.rx_response(NSURLRequest(URL: searchURL))
|
||||
.retry(3)
|
||||
.trackActivity(self.activityIndicator)
|
||||
.observeOn(Dependencies.sharedDependencies.backgroundWorkScheduler)
|
||||
.map { data, httpResponse -> SearchRepositoryResponse in
|
||||
if httpResponse.statusCode == 403 {
|
||||
return .LimitExceeded
|
||||
}
|
||||
|
||||
let jsonRoot = try GitHubSearchRepositoriesAPI.parseJSON(httpResponse, data: data)
|
||||
|
||||
guard let json = jsonRoot as? [String: AnyObject] else {
|
||||
throw exampleError("Casting to dictionary failed")
|
||||
}
|
||||
|
||||
let repositories = try GitHubSearchRepositoriesAPI.parseRepositories(json)
|
||||
|
||||
let nextURL = try GitHubSearchRepositoriesAPI.parseNextURL(httpResponse)
|
||||
|
||||
return .Repositories(repositories: repositories, nextURL: nextURL)
|
||||
}
|
||||
.retryOnBecomesReachable(.ServiceOffline, reachabilityService: ReachabilityService.sharedReachabilityService)
|
||||
}
|
||||
|
||||
private static func parseJSON(httpResponse: NSHTTPURLResponse, data: NSData) throws -> AnyObject {
|
||||
if !(200 ..< 300 ~= httpResponse.statusCode) {
|
||||
throw exampleError("Call failed")
|
||||
}
|
||||
|
||||
return try NSJSONSerialization.JSONObjectWithData(data ?? NSData(), options: [])
|
||||
}
|
||||
|
||||
private static func parseRepositories(json: [String: AnyObject]) throws -> [Repository] {
|
||||
guard let items = json["items"] as? [[String: AnyObject]] else {
|
||||
throw exampleError("Can't find items")
|
||||
}
|
||||
return try items.map { item in
|
||||
guard let name = item["name"] as? String,
|
||||
url = item["url"] as? String else {
|
||||
throw exampleError("Can't parse repository")
|
||||
}
|
||||
return Repository(name: name, url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,160 +12,29 @@ import RxSwift
|
|||
import RxCocoa
|
||||
#endif
|
||||
|
||||
struct Repository: CustomStringConvertible {
|
||||
var name: String
|
||||
var url: String
|
||||
|
||||
init(name: String, url: String) {
|
||||
self.name = name
|
||||
self.url = url
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return "\(name) | \(url)"
|
||||
}
|
||||
struct Colors {
|
||||
static let OfflineColor = UIColor(red: 1.0, green: 0.6, blue: 0.6, alpha: 1.0)
|
||||
static let OnlineColor = nil as UIColor?
|
||||
}
|
||||
|
||||
enum SearchRepositoryResponse {
|
||||
case Repositories([Repository])
|
||||
case LimitExceeded
|
||||
}
|
||||
extension UINavigationController {
|
||||
var rx_serviceState: AnyObserver<ServiceState?> {
|
||||
return AnyObserver { event in
|
||||
switch event {
|
||||
case .Next(let maybeServiceState):
|
||||
// if nil is being bound, then don't change color, it's not perfect, but :)
|
||||
if let serviceState = maybeServiceState {
|
||||
let isOffline = serviceState ?? .Online == .Offline
|
||||
|
||||
class GitHubSearchRepositoriesAPI {
|
||||
|
||||
static let sharedAPI = GitHubSearchRepositoriesAPI()
|
||||
|
||||
private init() {}
|
||||
|
||||
private static let parseLinksPattern = "\\s*,?\\s*<([^\\>]*)>\\s*;\\s*rel=\"([^\"]*)\""
|
||||
private static let linksRegex = try! NSRegularExpression(pattern: parseLinksPattern, options: [.AllowCommentsAndWhitespace])
|
||||
|
||||
private static func parseLinks(links: String) throws -> [String: String] {
|
||||
|
||||
let length = (links as NSString).length
|
||||
let matches = GitHubSearchRepositoriesAPI.linksRegex.matchesInString(links, options: NSMatchingOptions(), range: NSRange(location: 0, length: length))
|
||||
|
||||
var result: [String: String] = [:]
|
||||
|
||||
for m in matches {
|
||||
let matches = (1 ..< m.numberOfRanges).map { rangeIndex -> String in
|
||||
let range = m.rangeAtIndex(rangeIndex)
|
||||
let startIndex = links.startIndex.advancedBy(range.location)
|
||||
let endIndex = startIndex.advancedBy(range.length)
|
||||
let stringRange = Range(start: startIndex, end: endIndex)
|
||||
return links.substringWithRange(stringRange)
|
||||
}
|
||||
|
||||
if matches.count != 2 {
|
||||
throw exampleError("Error parsing links")
|
||||
}
|
||||
|
||||
result[matches[1]] = matches[0]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private static func parseNextURL(httpResponse: NSHTTPURLResponse) throws -> NSURL? {
|
||||
guard let serializedLinks = httpResponse.allHeaderFields["Link"] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let links = try GitHubSearchRepositoriesAPI.parseLinks(serializedLinks)
|
||||
|
||||
guard let nextPageURL = links["next"] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let nextUrl = NSURL(string: nextPageURL) else {
|
||||
throw exampleError("Error parsing next url `\(nextPageURL)`")
|
||||
}
|
||||
|
||||
return nextUrl
|
||||
}
|
||||
|
||||
/**
|
||||
Public fascade for search.
|
||||
*/
|
||||
func search(query: String, loadNextPageTrigger: Observable<Void>) -> Observable<SearchRepositoryResponse> {
|
||||
let escapedQuery = URLEscape(query)
|
||||
let url = NSURL(string: "https://api.github.com/search/repositories?q=\(escapedQuery)")!
|
||||
return recursivelySearch([], loadNextURL: url, loadNextPageTrigger: loadNextPageTrigger)
|
||||
.startWith(.Repositories([]))
|
||||
}
|
||||
|
||||
private func recursivelySearch(loadedSoFar: [Repository], loadNextURL: NSURL, loadNextPageTrigger: Observable<Void>) -> Observable<SearchRepositoryResponse> {
|
||||
return loadSearchURL(loadNextURL).flatMap { (newPageRepositoriesResponse, nextURL) -> Observable<SearchRepositoryResponse> in
|
||||
// in case access denied, just stop
|
||||
guard case .Repositories(let newPageRepositories) = newPageRepositoriesResponse else {
|
||||
return just(newPageRepositoriesResponse)
|
||||
}
|
||||
|
||||
var loadedRepositories = loadedSoFar
|
||||
loadedRepositories.appendContentsOf(newPageRepositories)
|
||||
|
||||
// if next page can't be loaded, just return what was loaded, and stop
|
||||
guard let nextURL = nextURL else {
|
||||
return just(.Repositories(loadedRepositories))
|
||||
}
|
||||
|
||||
return [
|
||||
// return loaded immediately
|
||||
just(.Repositories(loadedRepositories)),
|
||||
// wait until next page can be loaded
|
||||
never().takeUntil(loadNextPageTrigger),
|
||||
// load next page
|
||||
self.recursivelySearch(loadedRepositories, loadNextURL: nextURL, loadNextPageTrigger: loadNextPageTrigger)
|
||||
].concat()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSearchURL(searchURL: NSURL) -> Observable<(response: SearchRepositoryResponse, nextURL: NSURL?)> {
|
||||
return NSURLSession.sharedSession()
|
||||
.rx_response(NSURLRequest(URL: searchURL))
|
||||
.observeOn(Dependencies.sharedDependencies.backgroundWorkScheduler)
|
||||
.map { data, response in
|
||||
guard let httpResponse = response as? NSHTTPURLResponse else {
|
||||
throw exampleError("not getting http response")
|
||||
self.navigationBar.backgroundColor = isOffline
|
||||
? Colors.OfflineColor
|
||||
: Colors.OnlineColor
|
||||
}
|
||||
|
||||
if httpResponse.statusCode == 403 {
|
||||
return (response: .LimitExceeded, nextURL: nil)
|
||||
}
|
||||
|
||||
let jsonRoot = try GitHubSearchRepositoriesAPI.parseJSON(httpResponse, data: data)
|
||||
|
||||
guard let json = jsonRoot as? [String: AnyObject] else {
|
||||
throw exampleError("Casting to dictionary failed")
|
||||
}
|
||||
|
||||
let repositories = try GitHubSearchRepositoriesAPI.parseRepositories(json)
|
||||
|
||||
let nextURL = try GitHubSearchRepositoriesAPI.parseNextURL(httpResponse)
|
||||
|
||||
return (response: .Repositories(repositories), nextURL: nextURL)
|
||||
case .Error(let error):
|
||||
bindingErrorToInterface(error)
|
||||
case .Completed:
|
||||
break
|
||||
}
|
||||
.observeOn(Dependencies.sharedDependencies.mainScheduler)
|
||||
}
|
||||
|
||||
private static func parseJSON(httpResponse: NSHTTPURLResponse, data: NSData) throws -> AnyObject {
|
||||
if !(200 ..< 300 ~= httpResponse.statusCode) {
|
||||
throw exampleError("Call failed")
|
||||
}
|
||||
|
||||
return try NSJSONSerialization.JSONObjectWithData(data ?? NSData(), options: [])
|
||||
}
|
||||
|
||||
private static func parseRepositories(json: [String: AnyObject]) throws -> [Repository] {
|
||||
guard let items = json["items"] as? [[String: AnyObject]] else {
|
||||
throw exampleError("Can't find items")
|
||||
}
|
||||
return try items.map { item in
|
||||
guard let name = item["name"] as? String,
|
||||
url = item["url"] as? String else {
|
||||
throw exampleError("Can't parse repository")
|
||||
}
|
||||
return Repository(name: name, url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -177,12 +46,10 @@ class GitHubSearchRepositoriesViewController: ViewController, UITableViewDelegat
|
|||
return contentOffset.y + tableView.frame.size.height + startLoadingOffset > tableView.contentSize.height
|
||||
}
|
||||
|
||||
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
@IBOutlet weak var searchBar: UISearchBar!
|
||||
|
||||
var disposeBag = DisposeBag()
|
||||
let repositories = Variable([Repository]())
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Repository>>()
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
|
@ -193,11 +60,6 @@ class GitHubSearchRepositoriesViewController: ViewController, UITableViewDelegat
|
|||
let tableView = self.tableView
|
||||
let searchBar = self.searchBar
|
||||
|
||||
let allRepositories = repositories
|
||||
.map { repositories in
|
||||
return [SectionModel(model: "Repositories", items: repositories)]
|
||||
}
|
||||
|
||||
dataSource.cellFactory = { (tv, ip, repository: Repository) in
|
||||
let cell = tv.dequeueReusableCellWithIdentifier("Cell")!
|
||||
cell.textLabel?.text = repository.name
|
||||
|
|
@ -210,10 +72,6 @@ class GitHubSearchRepositoriesViewController: ViewController, UITableViewDelegat
|
|||
return section.items.count > 0 ? "Repositories (\(section.items.count))" : "No repositories found"
|
||||
}
|
||||
|
||||
// reactive data source
|
||||
allRepositories
|
||||
.bindTo(tableView.rx_itemsWithDataSource(dataSource))
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
let loadNextPageTrigger = tableView.rx_contentOffset
|
||||
.flatMap { offset in
|
||||
|
|
@ -222,27 +80,32 @@ class GitHubSearchRepositoriesViewController: ViewController, UITableViewDelegat
|
|||
: empty()
|
||||
}
|
||||
|
||||
searchBar.rx_text
|
||||
let searchResult = searchBar.rx_text.asDriver()
|
||||
.throttle(0.3, $.mainScheduler)
|
||||
.distinctUntilChanged()
|
||||
.map { query -> Observable<SearchRepositoryResponse> in
|
||||
.flatMapLatest { query -> Driver<RepositoriesState> in
|
||||
if query.isEmpty {
|
||||
return just(.Repositories([]))
|
||||
return Drive.just(RepositoriesState.empty)
|
||||
} else {
|
||||
return GitHubSearchRepositoriesAPI.sharedAPI.search(query, loadNextPageTrigger: loadNextPageTrigger)
|
||||
.retry(3)
|
||||
.catchErrorJustReturn(.Repositories([]))
|
||||
.asDriver(onErrorJustReturn: RepositoriesState.empty)
|
||||
}
|
||||
}
|
||||
.switchLatest()
|
||||
.subscribeNext { [unowned self] result in
|
||||
switch result {
|
||||
case .Repositories(let repositories):
|
||||
self.repositories.value = repositories
|
||||
case .LimitExceeded:
|
||||
self.repositories.value = []
|
||||
showAlert("Exceeded limit of 10 non authenticated requests per minute for GitHub API. Please wait a minute. :(\nhttps://developer.github.com/v3/#rate-limiting")
|
||||
}
|
||||
|
||||
searchResult
|
||||
.map { $0.serviceState }
|
||||
.drive(navigationController!.rx_serviceState)
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
searchResult
|
||||
.map { [SectionModel(model: "Repositories", items: $0.repositories)] }
|
||||
.drive(tableView.rx_itemsWithDataSource(dataSource))
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
searchResult
|
||||
.flatMap { $0.limitExceeded ? Drive.just() : Drive.empty() }
|
||||
.driveNext { n in
|
||||
showAlert("Exceeded limit of 10 non authenticated requests per minute for GitHub API. Please wait a minute. :(\nhttps://developer.github.com/v3/#rate-limiting")
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
|
|
@ -258,6 +121,16 @@ class GitHubSearchRepositoriesViewController: ViewController, UITableViewDelegat
|
|||
// so normal delegate customization can also be used
|
||||
tableView.rx_setDelegate(self)
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
// activity indicator in status bar
|
||||
// {
|
||||
GitHubSearchRepositoriesAPI.sharedAPI.activityIndicator
|
||||
.distinctUntilChanged()
|
||||
.driveNext { active in
|
||||
UIApplication.sharedApplication().networkActivityIndicatorVisible = active
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
// }
|
||||
}
|
||||
|
||||
// MARK: Table view delegate
|
||||
|
|
@ -265,4 +138,9 @@ class GitHubSearchRepositoriesViewController: ViewController, UITableViewDelegat
|
|||
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return 30
|
||||
}
|
||||
|
||||
deinit {
|
||||
// I know, I know, this isn't a good place of truth, but it's no
|
||||
self.navigationController?.navigationBar.backgroundColor = nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ class CalculatorViewController: ViewController {
|
|||
|
||||
let CLEAR_STATE = CalState(previousNumber: nil, action: .Clear, currentNumber: "0", inScreen: "0", replace: true)
|
||||
|
||||
let diposeBag = DisposeBag()
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
override func viewDidLoad() {
|
||||
let commands:[Observable<Action>] = [
|
||||
|
|
@ -98,7 +98,7 @@ class CalculatorViewController: ViewController {
|
|||
]
|
||||
|
||||
commands
|
||||
.asObservable()
|
||||
.toObservable()
|
||||
.merge()
|
||||
.scan(CLEAR_STATE) { [unowned self] a, x in
|
||||
return self.tranformState(a, x)
|
||||
|
|
@ -122,7 +122,7 @@ class CalculatorViewController: ViewController {
|
|||
self?.lastSignLabel.text = ""
|
||||
}
|
||||
}
|
||||
.addDisposableTo(diposeBag)
|
||||
.addDisposableTo(disposeBag)
|
||||
}
|
||||
|
||||
func tranformState(a: CalState, _ x: Action) -> CalState {
|
||||
|
|
|
|||
|
|
@ -58,13 +58,8 @@ class GitHubAPI {
|
|||
|
||||
})
|
||||
return self.URLSession.rx_response(request)
|
||||
.map { (maybeData, maybeResponse) in
|
||||
if let response = maybeResponse as? NSHTTPURLResponse {
|
||||
return response.statusCode == 404
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
.map { (maybeData, response) in
|
||||
return response.statusCode == 404
|
||||
}
|
||||
.observeOn(self.dataScheduler)
|
||||
.catchErrorJustReturn(false)
|
||||
|
|
@ -73,8 +68,8 @@ class GitHubAPI {
|
|||
func signup(username: String, password: String) -> Observable<SignupState> {
|
||||
// this is also just a mock
|
||||
let signupResult = SignupState.SignedUp(signedUp: arc4random() % 5 == 0 ? false : true)
|
||||
return [just(signupResult), never()]
|
||||
.concat()
|
||||
return just(signupResult)
|
||||
.concat(never())
|
||||
.throttle(2, MainScheduler.sharedInstance)
|
||||
.startWith(SignupState.SigningUp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,30 @@ import RxSwift
|
|||
import RxCocoa
|
||||
#endif
|
||||
|
||||
let okColor = UIColor(red: 138.0 / 255.0, green: 221.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0)
|
||||
let errorColor = UIColor.redColor()
|
||||
|
||||
typealias ValidationResult = (valid: Bool?, message: String?)
|
||||
typealias ValidationObservable = Observable<ValidationResult>
|
||||
|
||||
// Two way binding operator between control property and variable, that's all it takes {
|
||||
|
||||
infix operator <-> {
|
||||
}
|
||||
|
||||
func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable {
|
||||
let bindToUIDisposable = variable
|
||||
.bindTo(property)
|
||||
let bindToVariable = property
|
||||
.subscribe(onNext: { n in
|
||||
variable.value = n
|
||||
}, onCompleted: {
|
||||
bindToUIDisposable.dispose()
|
||||
})
|
||||
|
||||
return StableCompositeDisposable.create(bindToUIDisposable, bindToVariable)
|
||||
}
|
||||
|
||||
// }
|
||||
|
||||
|
||||
class ValidationService {
|
||||
let API: GitHubAPI
|
||||
|
||||
|
|
@ -30,7 +48,7 @@ class ValidationService {
|
|||
|
||||
let minPasswordCount = 5
|
||||
|
||||
func validateUsername(username: String) -> Observable<ValidationResult> {
|
||||
func validateUsername(username: String) -> ValidationObservable {
|
||||
if username.characters.count == 0 {
|
||||
return just((false, nil))
|
||||
}
|
||||
|
|
@ -93,6 +111,15 @@ class GitHubSignupViewController : ViewController {
|
|||
|
||||
@IBOutlet weak var signupOutlet: UIButton!
|
||||
@IBOutlet weak var signingUpOulet: UIActivityIndicatorView!
|
||||
|
||||
let username = Variable("")
|
||||
let password = Variable("")
|
||||
let repeatedPassword = Variable("")
|
||||
|
||||
struct ValidationColors {
|
||||
static let okColor = UIColor(red: 138.0 / 255.0, green: 221.0 / 255.0, blue: 109.0 / 255.0, alpha: 1.0)
|
||||
static let errorColor = UIColor.redColor()
|
||||
}
|
||||
|
||||
var disposeBag = DisposeBag()
|
||||
|
||||
|
|
@ -108,9 +135,8 @@ class GitHubSignupViewController : ViewController {
|
|||
let validationColor: UIColor
|
||||
|
||||
if let valid = v.valid {
|
||||
validationColor = valid ? okColor : errorColor
|
||||
}
|
||||
else {
|
||||
validationColor = valid ? ValidationColors.okColor : ValidationColors.errorColor
|
||||
} else {
|
||||
validationColor = UIColor.grayColor()
|
||||
}
|
||||
|
||||
|
|
@ -128,44 +154,42 @@ class GitHubSignupViewController : ViewController {
|
|||
super.viewDidLoad()
|
||||
|
||||
let tapBackground = UITapGestureRecognizer(target: self, action: Selector("dismissKeyboard:"))
|
||||
tapBackground.numberOfTouchesRequired = 1
|
||||
view.addGestureRecognizer(tapBackground)
|
||||
|
||||
self.disposeBag = DisposeBag()
|
||||
|
||||
let API = self.API
|
||||
|
||||
let validationService = ValidationService(API: API)
|
||||
|
||||
let username = usernameOutlet.rx_text
|
||||
let password = passwordOutlet.rx_text
|
||||
let repeatPassword = repeatedPasswordOutlet.rx_text
|
||||
let signupSampler = self.signupOutlet.rx_tap
|
||||
|
||||
// bind UI values to variables {
|
||||
usernameOutlet.rx_text <-> username
|
||||
passwordOutlet.rx_text <-> password
|
||||
repeatedPasswordOutlet.rx_text <-> repeatedPassword
|
||||
// }
|
||||
|
||||
let signupSampler = signupOutlet.rx_tap
|
||||
|
||||
let usernameValidation = username
|
||||
.map { username in
|
||||
.flatMapLatest { username in
|
||||
return validationService.validateUsername(username)
|
||||
}
|
||||
.switchLatest()
|
||||
.shareReplay(1)
|
||||
|
||||
|
||||
let passwordValidation = password
|
||||
.map { password in
|
||||
return validationService.validatePassword(password)
|
||||
}
|
||||
.shareReplay(1)
|
||||
|
||||
let repeatPasswordValidation = combineLatest(password, repeatPassword) { (password, repeatedPassword) in
|
||||
|
||||
let repeatPasswordValidation = combineLatest(password, repeatedPassword) { (password, repeatedPassword) in
|
||||
validationService.validateRepeatedPassword(password, repeatedPassword: repeatedPassword)
|
||||
}
|
||||
.shareReplay(1)
|
||||
|
||||
let signingProcess = combineLatest(username, password) { ($0, $1) }
|
||||
.sampleLatest(signupSampler)
|
||||
.map { (username, password) in
|
||||
.flatMapLatest { (username, password) in
|
||||
return API.signup(username, password: password)
|
||||
}
|
||||
.switchLatest()
|
||||
.startWith(SignupState.InitialState)
|
||||
.shareReplay(1)
|
||||
|
||||
|
|
@ -174,8 +198,11 @@ class GitHubSignupViewController : ViewController {
|
|||
passwordValidation,
|
||||
repeatPasswordValidation,
|
||||
signingProcess
|
||||
) { un, p, pr, signingState in
|
||||
return (un.valid ?? false) && (p.valid ?? false) && (pr.valid ?? false) && signingState != SignupState.SigningUp
|
||||
) { username, password, repeatPassword, signingState in
|
||||
return (username.valid ?? false) &&
|
||||
(password.valid ?? false) &&
|
||||
(repeatPassword.valid ?? false) &&
|
||||
signingState != SignupState.SigningUp
|
||||
}
|
||||
|
||||
bindValidationResultToUI(
|
||||
|
|
@ -220,15 +247,17 @@ class GitHubSignupViewController : ViewController {
|
|||
}
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
}
|
||||
|
||||
// This is one of the reasons why it's a good idea for disposal to be detached from allocations.
|
||||
// If resources weren't disposed before view controller is being deallocated, signup alert view
|
||||
// could be presented on top of wrong screen or crash your app if it was being presented while
|
||||
// navigation stack is popping.
|
||||
// could be presented on top of the wrong screen or could crash your app if it was being presented
|
||||
// while navigation stack is popping.
|
||||
|
||||
// This will work well with UINavigationController, but has an assumption that view controller will
|
||||
// never be readded as a child view controller.
|
||||
// It it was readded UI wouldn't be bound anymore.
|
||||
// never be added as a child view controller. If we didn't recreate the dispose bag here,
|
||||
// then our resources would never be properly released.
|
||||
override func willMoveToParentViewController(parent: UIViewController?) {
|
||||
if let parent = parent {
|
||||
assert(parent.isKindOfClass(UINavigationController), "Please read comments")
|
||||
|
|
|
|||
|
|
@ -116,19 +116,9 @@ class PartialUpdatesViewController : ViewController {
|
|||
|
||||
skinTableViewDataSource(tvAnimatedDataSource)
|
||||
skinTableViewDataSource(reloadDataSource)
|
||||
let newSections = self.sections .skip(1)
|
||||
|
||||
let initialState = [Changeset.initialValue(self.sections.value)]
|
||||
|
||||
// reactive data sources
|
||||
|
||||
let updates = zip(self.sections, newSections) { (old, new) in
|
||||
return differentiate(old, finalSections: new)
|
||||
}
|
||||
.startWith(initialState)
|
||||
|
||||
updates
|
||||
.bindTo(partialUpdatesTableViewOutlet.rx_itemsWithDataSource(tvAnimatedDataSource))
|
||||
self.sections
|
||||
.bindTo(partialUpdatesTableViewOutlet.rx_itemsAnimatedWithDataSource(tvAnimatedDataSource))
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
self.sections
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
struct User: Equatable, CustomStringConvertible {
|
||||
struct User: Equatable, CustomDebugStringConvertible {
|
||||
|
||||
var firstName: String
|
||||
var lastName: String
|
||||
|
|
@ -12,8 +12,10 @@ struct User: Equatable, CustomStringConvertible {
|
|||
self.lastName = lastName
|
||||
self.imageURL = imageURL
|
||||
}
|
||||
|
||||
var description: String {
|
||||
}
|
||||
|
||||
extension User {
|
||||
var debugDescription: String {
|
||||
get {
|
||||
return firstName + " " + lastName
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import RxCocoa
|
|||
class SearchResultViewModel {
|
||||
let searchResult: WikipediaSearchResult
|
||||
|
||||
var title: Observable<String>
|
||||
var imageURLs: Observable<[NSURL]>
|
||||
var title: Driver<String>
|
||||
var imageURLs: Driver<[NSURL]>
|
||||
|
||||
let API = DefaultWikipediaAPI.sharedAPI
|
||||
let $: Dependencies = Dependencies.sharedDependencies
|
||||
|
|
@ -24,13 +24,13 @@ class SearchResultViewModel {
|
|||
init(searchResult: WikipediaSearchResult) {
|
||||
self.searchResult = searchResult
|
||||
|
||||
self.title = never()
|
||||
self.imageURLs = never()
|
||||
self.title = Drive.never()
|
||||
self.imageURLs = Drive.never()
|
||||
|
||||
let URLs = configureImageURLs()
|
||||
|
||||
self.imageURLs = URLs.catchErrorJustReturn([])
|
||||
self.title = configureTitle(URLs).catchErrorJustReturn("Error during fetching")
|
||||
self.imageURLs = URLs.asDriver(onErrorJustReturn: [])
|
||||
self.title = configureTitle(URLs).asDriver(onErrorJustReturn: "Error during fetching")
|
||||
}
|
||||
|
||||
// private methods
|
||||
|
|
@ -51,6 +51,7 @@ class SearchResultViewModel {
|
|||
return "\(searchResult.title) loading ..."
|
||||
}
|
||||
}
|
||||
.retryOnBecomesReachable("⚠️ Service offline ⚠️", reachabilityService: ReachabilityService.sharedReachabilityService)
|
||||
}
|
||||
|
||||
func configureImageURLs() -> Observable<[NSURL]> {
|
||||
|
|
@ -64,7 +65,5 @@ class SearchResultViewModel {
|
|||
return []
|
||||
}
|
||||
}
|
||||
.observeOn($.mainScheduler)
|
||||
.shareReplay(1)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ import RxCocoa
|
|||
class SearchViewModel {
|
||||
|
||||
// outputs
|
||||
let rows: Observable<[SearchResultViewModel]>
|
||||
let rows: Driver<[SearchResultViewModel]>
|
||||
|
||||
let subscriptions = DisposeBag()
|
||||
|
||||
// public methods
|
||||
|
||||
init(searchText: Observable<String>,
|
||||
selectedResult: Observable<SearchResultViewModel>) {
|
||||
init(searchText: Driver<String>,
|
||||
selectedResult: Driver<SearchResultViewModel>) {
|
||||
|
||||
let $: Dependencies = Dependencies.sharedDependencies
|
||||
let wireframe = Dependencies.sharedDependencies.wireframe
|
||||
|
|
@ -31,13 +31,13 @@ class SearchViewModel {
|
|||
self.rows = searchText
|
||||
.throttle(0.3, $.mainScheduler)
|
||||
.distinctUntilChanged()
|
||||
.map { query in
|
||||
.flatMapLatest { query in
|
||||
API.getSearchResults(query)
|
||||
.retry(3)
|
||||
.retryOnBecomesReachable([], reachabilityService: ReachabilityService.sharedReachabilityService)
|
||||
.startWith([]) // clears results on new search term
|
||||
.catchErrorJustReturn([])
|
||||
.asDriver(onErrorJustReturn: [])
|
||||
}
|
||||
.switchLatest()
|
||||
.map { results in
|
||||
results.map {
|
||||
SearchResultViewModel(
|
||||
|
|
@ -47,7 +47,7 @@ class SearchViewModel {
|
|||
}
|
||||
|
||||
selectedResult
|
||||
.subscribeNext { searchResult in
|
||||
.driveNext { searchResult in
|
||||
wireframe.openURL(searchResult.searchResult.URL)
|
||||
}
|
||||
.addDisposableTo(subscriptions)
|
||||
|
|
|
|||
|
|
@ -17,15 +17,16 @@ public class CollectionViewImageCell: UICollectionViewCell {
|
|||
@IBOutlet var imageOutlet: UIImageView!
|
||||
|
||||
var disposeBag: DisposeBag!
|
||||
|
||||
var image: Observable<UIImage!>! {
|
||||
didSet {
|
||||
|
||||
var downloadableImage: Observable<DownloadableImage>?{
|
||||
didSet{
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
self.image
|
||||
.subscribe(imageOutlet.rx_imageAnimated(kCATransitionFade))
|
||||
|
||||
self.downloadableImage?
|
||||
.asDriver(onErrorJustReturn: DownloadableImage.OfflinePlaceholder)
|
||||
.drive(imageOutlet.rxex_downloadableImageAnimated(kCATransitionFade))
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
|
||||
self.disposeBag = disposeBag
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,21 +33,16 @@ public class WikipediaSearchCell: UITableViewCell {
|
|||
didSet {
|
||||
let disposeBag = DisposeBag()
|
||||
|
||||
(viewModel?.title ?? just(""))
|
||||
.subscribe(self.titleOutlet.rx_text)
|
||||
(viewModel?.title ?? Drive.just(""))
|
||||
.drive(self.titleOutlet.rx_text)
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
self.URLOutlet.text = viewModel.searchResult.URL.absoluteString ?? ""
|
||||
|
||||
viewModel.imageURLs
|
||||
.bindTo(self.imagesOutlet.rx_itemsWithCellIdentifier("ImageCell")) { [unowned self] (_, URL, cell: CollectionViewImageCell) in
|
||||
let loadingPlaceholder: UIImage? = nil
|
||||
|
||||
cell.image = self.imageService.imageFromURL(URL)
|
||||
.map { $0 as UIImage? }
|
||||
.catchErrorJustReturn(nil)
|
||||
.startWith(loadingPlaceholder)
|
||||
}
|
||||
.drive(self.imagesOutlet.rx_itemsWithCellIdentifier("ImageCell")) { [unowned self] (_, URL, cell: CollectionViewImageCell) in
|
||||
cell.downloadableImage = self.imageService.imageFromURL(URL)
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
|
||||
self.disposeBag = disposeBag
|
||||
|
|
@ -62,4 +57,5 @@ public class WikipediaSearchCell: UITableViewCell {
|
|||
|
||||
deinit {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,17 +33,15 @@ class WikipediaSearchViewController: ViewController {
|
|||
|
||||
resultsTableView.rowHeight = 194
|
||||
|
||||
let selectedResult: Observable<SearchResultViewModel> = resultsTableView.rx_modelSelected().asObservable()
|
||||
|
||||
let viewModel = SearchViewModel(
|
||||
searchText: searchBar.rx_text.asObservable(),
|
||||
selectedResult: selectedResult
|
||||
searchText: searchBar.rx_text.asDriver(),
|
||||
selectedResult: resultsTableView.rx_modelSelected().asDriver()
|
||||
)
|
||||
|
||||
// map table view rows
|
||||
// {
|
||||
viewModel.rows
|
||||
.bindTo(resultsTableView.rx_itemsWithCellIdentifier("WikipediaSearchCell")) { (_, viewModel, cell: WikipediaSearchCell) in
|
||||
.drive(resultsTableView.rx_itemsWithCellIdentifier("WikipediaSearchCell")) { (_, viewModel, cell: WikipediaSearchCell) in
|
||||
cell.viewModel = viewModel
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
|
|
@ -52,7 +50,8 @@ class WikipediaSearchViewController: ViewController {
|
|||
// dismiss keyboard on scroll
|
||||
// {
|
||||
resultsTableView.rx_contentOffset
|
||||
.subscribeNext { _ in
|
||||
.asDriver()
|
||||
.driveNext { _ in
|
||||
if searchBar.isFirstResponder() {
|
||||
_ = searchBar.resignFirstResponder()
|
||||
}
|
||||
|
|
@ -61,5 +60,18 @@ class WikipediaSearchViewController: ViewController {
|
|||
|
||||
self.viewModel = viewModel
|
||||
// }
|
||||
|
||||
// activity indicator spinner
|
||||
// {
|
||||
combineLatest(
|
||||
DefaultWikipediaAPI.sharedAPI.loadingWikipediaData,
|
||||
DefaultImageService.sharedImageService.loadingImage
|
||||
) { $0 || $1 }
|
||||
.distinctUntilChanged()
|
||||
.driveNext { active in
|
||||
UIApplication.sharedApplication().networkActivityIndicatorVisible = active
|
||||
}
|
||||
.addDisposableTo(disposeBag)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,17 +32,24 @@ class DefaultWikipediaAPI: WikipediaAPI {
|
|||
static let sharedAPI = DefaultWikipediaAPI() // Singleton
|
||||
|
||||
let $: Dependencies = Dependencies.sharedDependencies
|
||||
|
||||
|
||||
let loadingWikipediaData = ActivityIndicator()
|
||||
|
||||
private init() {}
|
||||
|
||||
|
||||
private func rx_JSON(URL: NSURL) -> Observable<AnyObject!> {
|
||||
return $.URLSession
|
||||
.rx_JSON(URL)
|
||||
.trackActivity(loadingWikipediaData)
|
||||
}
|
||||
|
||||
// Example wikipedia response http://en.wikipedia.org/w/api.php?action=opensearch&search=Rx
|
||||
func getSearchResults(query: String) -> Observable<[WikipediaSearchResult]> {
|
||||
let escapedQuery = URLEscape(query)
|
||||
let urlContent = "http://en.wikipedia.org/w/api.php?action=opensearch&search=\(escapedQuery)"
|
||||
let url = NSURL(string: urlContent)!
|
||||
|
||||
return $.URLSession
|
||||
.rx_JSON(url)
|
||||
return rx_JSON(url)
|
||||
.observeOn($.backgroundWorkScheduler)
|
||||
.map { json in
|
||||
guard let json = json as? [AnyObject] else {
|
||||
|
|
@ -61,7 +68,7 @@ class DefaultWikipediaAPI: WikipediaAPI {
|
|||
return failWith(apiError("Can't create url"))
|
||||
}
|
||||
|
||||
return $.URLSession.rx_JSON(url)
|
||||
return rx_JSON(url)
|
||||
.map { jsonResult in
|
||||
guard let json = jsonResult as? NSDictionary else {
|
||||
throw exampleError("Parsing error")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import Foundation
|
|||
import RxSwift
|
||||
#endif
|
||||
|
||||
struct WikipediaSearchResult: CustomStringConvertible {
|
||||
struct WikipediaSearchResult: CustomDebugStringConvertible {
|
||||
let title: String
|
||||
let description: String
|
||||
let URL: NSURL
|
||||
|
|
@ -52,3 +52,9 @@ struct WikipediaSearchResult: CustomStringConvertible {
|
|||
return searchResults
|
||||
}
|
||||
}
|
||||
|
||||
extension WikipediaSearchResult {
|
||||
var debugDescription: String {
|
||||
return "[\(title)](\(URL))"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// ActivityIndicator.swift
|
||||
// RxExample
|
||||
//
|
||||
// Created by Krunoslav Zaher on 10/18/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
#endif
|
||||
|
||||
struct ActivityToken<E> : ObservableConvertibleType, Disposable {
|
||||
private let _source: Observable<E>
|
||||
private let _dispose: AnonymousDisposable
|
||||
|
||||
init(source: Observable<E>, disposeAction: () -> ()) {
|
||||
_source = source
|
||||
_dispose = AnonymousDisposable(disposeAction)
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
_dispose.dispose()
|
||||
}
|
||||
|
||||
func asObservable() -> Observable<E> {
|
||||
return _source
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Enables monitoring of sequence computation.
|
||||
|
||||
If there is at least one sequence computation in progress, `true` will be sent.
|
||||
When all activities complete `false` will be sent.
|
||||
*/
|
||||
class ActivityIndicator : DriverConvertibleType {
|
||||
typealias E = Bool
|
||||
|
||||
private let _lock = NSRecursiveLock()
|
||||
private let _variable = Variable(0)
|
||||
private let _loading: Driver<Bool>
|
||||
|
||||
init() {
|
||||
_loading = _variable
|
||||
.map { $0 > 0 }
|
||||
.asDriver { (error: ErrorType) -> Driver<Bool> in
|
||||
_ = fatalError("Loader can't fail")
|
||||
return Drive.empty()
|
||||
}
|
||||
}
|
||||
|
||||
func trackActivity<O: ObservableConvertibleType>(source: O) -> Observable<O.E> {
|
||||
return using({ () -> ActivityToken<O.E> in
|
||||
self.increment()
|
||||
return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
|
||||
}) { t in
|
||||
return t.asObservable()
|
||||
}
|
||||
}
|
||||
|
||||
private func increment() {
|
||||
_lock.lock()
|
||||
_variable.value = _variable.value + 1
|
||||
_lock.unlock()
|
||||
}
|
||||
|
||||
private func decrement() {
|
||||
_lock.lock()
|
||||
_variable.value = _variable.value - 1
|
||||
_lock.unlock()
|
||||
}
|
||||
|
||||
func asDriver() -> Driver<E> {
|
||||
return _loading
|
||||
}
|
||||
}
|
||||
|
||||
extension ObservableConvertibleType {
|
||||
func trackActivity(activityIndicator: ActivityIndicator) -> Observable<E> {
|
||||
return activityIndicator.trackActivity(self)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// DownloadableImage.swift
|
||||
// RxExample
|
||||
//
|
||||
// Created by Vodovozov Gleb on 31.10.2015.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(OSX)
|
||||
import Cocoa
|
||||
#endif
|
||||
|
||||
enum DownloadableImage{
|
||||
case Content(image:Image)
|
||||
case OfflinePlaceholder
|
||||
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ import RxCocoa
|
|||
#endif
|
||||
|
||||
protocol ImageService {
|
||||
func imageFromURL(URL: NSURL) -> Observable<Image>
|
||||
func imageFromURL(URL: NSURL) -> Observable<DownloadableImage>
|
||||
}
|
||||
|
||||
class DefaultImageService: ImageService {
|
||||
|
|
@ -29,19 +29,21 @@ class DefaultImageService: ImageService {
|
|||
let $: Dependencies = Dependencies.sharedDependencies
|
||||
|
||||
// 1st level cache
|
||||
let imageCache = NSCache()
|
||||
|
||||
private let _imageCache = NSCache()
|
||||
|
||||
// 2nd level cache
|
||||
let imageDataCache = NSCache()
|
||||
private let _imageDataCache = NSCache()
|
||||
|
||||
let loadingImage = ActivityIndicator()
|
||||
|
||||
private init() {
|
||||
// cost is approx memory usage
|
||||
self.imageDataCache.totalCostLimit = 10 * MB
|
||||
_imageDataCache.totalCostLimit = 10 * MB
|
||||
|
||||
self.imageCache.countLimit = 20
|
||||
_imageCache.countLimit = 20
|
||||
}
|
||||
|
||||
func decodeImage(imageData: NSData) -> Observable<Image> {
|
||||
private func decodeImage(imageData: NSData) -> Observable<Image> {
|
||||
return just(imageData)
|
||||
.observeOn($.backgroundWorkScheduler)
|
||||
.map { data in
|
||||
|
|
@ -49,41 +51,56 @@ class DefaultImageService: ImageService {
|
|||
// some error
|
||||
throw apiError("Decoding image error")
|
||||
}
|
||||
return image
|
||||
return image.forceLazyImageDecompression()
|
||||
}
|
||||
.observeOn($.mainScheduler)
|
||||
}
|
||||
|
||||
func imageFromURL(URL: NSURL) -> Observable<Image> {
|
||||
private func _imageFromURL(URL: NSURL) -> Observable<Image> {
|
||||
return deferred {
|
||||
let maybeImage = self.imageCache.objectForKey(URL) as? Image
|
||||
|
||||
let decodedImage: Observable<Image>
|
||||
|
||||
// best case scenario, it's already decoded an in memory
|
||||
if let image = maybeImage {
|
||||
decodedImage = just(image)
|
||||
}
|
||||
else {
|
||||
let cachedData = self.imageDataCache.objectForKey(URL) as? NSData
|
||||
let maybeImage = self._imageCache.objectForKey(URL) as? Image
|
||||
|
||||
let decodedImage: Observable<Image>
|
||||
|
||||
// does image data cache contain anything
|
||||
if let cachedData = cachedData {
|
||||
decodedImage = self.decodeImage(cachedData)
|
||||
// best case scenario, it's already decoded an in memory
|
||||
if let image = maybeImage {
|
||||
decodedImage = just(image)
|
||||
}
|
||||
else {
|
||||
// fetch from network
|
||||
decodedImage = self.$.URLSession.rx_data(NSURLRequest(URL: URL))
|
||||
.doOn(next: { data in
|
||||
self.imageDataCache.setObject(data, forKey: URL)
|
||||
})
|
||||
.flatMap(self.decodeImage)
|
||||
let cachedData = self._imageDataCache.objectForKey(URL) as? NSData
|
||||
|
||||
// does image data cache contain anything
|
||||
if let cachedData = cachedData {
|
||||
decodedImage = self.decodeImage(cachedData)
|
||||
}
|
||||
else {
|
||||
// fetch from network
|
||||
decodedImage = self.$.URLSession.rx_data(NSURLRequest(URL: URL))
|
||||
.doOn(onNext: { data in
|
||||
self._imageDataCache.setObject(data, forKey: URL)
|
||||
})
|
||||
.flatMap(self.decodeImage)
|
||||
.trackActivity(self.loadingImage)
|
||||
}
|
||||
}
|
||||
|
||||
return decodedImage.doOn(onNext: { image in
|
||||
self._imageCache.setObject(image, forKey: URL)
|
||||
})
|
||||
}
|
||||
|
||||
return decodedImage.doOn(next: { image in
|
||||
self.imageCache.setObject(image, forKey: URL)
|
||||
})
|
||||
}.observeOn($.mainScheduler)
|
||||
}
|
||||
|
||||
/**
|
||||
Service that tries to download image from URL.
|
||||
|
||||
In case there were some problems with network connectivity and image wasn't downloaded, automatic retry will be fired when networks becomes
|
||||
available.
|
||||
|
||||
After image is sucessfully downloaded, sequence is completed.
|
||||
*/
|
||||
func imageFromURL(URL: NSURL) -> Observable<DownloadableImage> {
|
||||
return _imageFromURL(URL)
|
||||
.map { DownloadableImage.Content(image: $0) }
|
||||
.retryOnBecomesReachable(DownloadableImage.OfflinePlaceholder, reachabilityService: ReachabilityService.sharedReachabilityService)
|
||||
.startWith(.Content(image: Image()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,388 @@
|
|||
/*
|
||||
Copyright (c) 2014, Ashley Mills
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
import SystemConfiguration
|
||||
import Foundation
|
||||
|
||||
enum ReachabilityError: ErrorType {
|
||||
case FailedToCreateWithAddress(sockaddr_in)
|
||||
case FailedToCreateWithHostname(String)
|
||||
case UnableToSetCallback
|
||||
case UnableToSetDispatchQueue
|
||||
}
|
||||
|
||||
public let ReachabilityChangedNotification = "ReachabilityChangedNotification"
|
||||
|
||||
func callback(reachability:SCNetworkReachability, flags: SCNetworkReachabilityFlags, info: UnsafeMutablePointer<Void>) {
|
||||
let reachability = Unmanaged<Reachability>.fromOpaque(COpaquePointer(info)).takeUnretainedValue()
|
||||
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
reachability.reachabilityChanged(flags)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class Reachability: NSObject {
|
||||
|
||||
public typealias NetworkReachable = (Reachability) -> ()
|
||||
public typealias NetworkUnreachable = (Reachability) -> ()
|
||||
|
||||
public enum NetworkStatus: CustomStringConvertible {
|
||||
|
||||
case NotReachable, ReachableViaWiFi, ReachableViaWWAN
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .ReachableViaWWAN:
|
||||
return "Cellular"
|
||||
case .ReachableViaWiFi:
|
||||
return "WiFi"
|
||||
case .NotReachable:
|
||||
return "No Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - *** Public properties ***
|
||||
|
||||
public var whenReachable: NetworkReachable?
|
||||
public var whenUnreachable: NetworkUnreachable?
|
||||
public var reachableOnWWAN: Bool
|
||||
public var notificationCenter = NSNotificationCenter.defaultCenter()
|
||||
|
||||
public var currentReachabilityStatus: NetworkStatus {
|
||||
if isReachable() {
|
||||
if isReachableViaWiFi() {
|
||||
return .ReachableViaWiFi
|
||||
}
|
||||
if isRunningOnDevice {
|
||||
return .ReachableViaWWAN
|
||||
}
|
||||
}
|
||||
|
||||
return .NotReachable
|
||||
}
|
||||
|
||||
public var currentReachabilityString: String {
|
||||
return "\(currentReachabilityStatus)"
|
||||
}
|
||||
|
||||
// MARK: - *** Initialisation methods ***
|
||||
|
||||
required public init(reachabilityRef: SCNetworkReachability) {
|
||||
reachableOnWWAN = true
|
||||
self.reachabilityRef = reachabilityRef
|
||||
}
|
||||
|
||||
public convenience init(hostname: String) throws {
|
||||
|
||||
let nodename = (hostname as NSString).UTF8String
|
||||
guard let ref = SCNetworkReachabilityCreateWithName(nil, nodename) else { throw ReachabilityError.FailedToCreateWithHostname(hostname) }
|
||||
|
||||
self.init(reachabilityRef: ref)
|
||||
}
|
||||
|
||||
public class func reachabilityForInternetConnection() throws -> Reachability {
|
||||
|
||||
var zeroAddress = sockaddr_in()
|
||||
zeroAddress.sin_len = UInt8(sizeofValue(zeroAddress))
|
||||
zeroAddress.sin_family = sa_family_t(AF_INET)
|
||||
|
||||
guard let ref = withUnsafePointer(&zeroAddress, {
|
||||
SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0))
|
||||
}) else { throw ReachabilityError.FailedToCreateWithAddress(zeroAddress) }
|
||||
|
||||
return Reachability(reachabilityRef: ref)
|
||||
}
|
||||
|
||||
public class func reachabilityForLocalWiFi() throws -> Reachability {
|
||||
|
||||
var localWifiAddress: sockaddr_in = sockaddr_in(sin_len: __uint8_t(0), sin_family: sa_family_t(0), sin_port: in_port_t(0), sin_addr: in_addr(s_addr: 0), sin_zero: (0, 0, 0, 0, 0, 0, 0, 0))
|
||||
localWifiAddress.sin_len = UInt8(sizeofValue(localWifiAddress))
|
||||
localWifiAddress.sin_family = sa_family_t(AF_INET)
|
||||
|
||||
// IN_LINKLOCALNETNUM is defined in <netinet/in.h> as 169.254.0.0
|
||||
let address: UInt32 = 0xA9FE0000
|
||||
localWifiAddress.sin_addr.s_addr = in_addr_t(address.bigEndian)
|
||||
|
||||
guard let ref = withUnsafePointer(&localWifiAddress, {
|
||||
SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0))
|
||||
}) else { throw ReachabilityError.FailedToCreateWithAddress(localWifiAddress) }
|
||||
|
||||
return Reachability(reachabilityRef: ref)
|
||||
}
|
||||
|
||||
// MARK: - *** Notifier methods ***
|
||||
public func startNotifier() throws {
|
||||
|
||||
if notifierRunning { return }
|
||||
|
||||
var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
|
||||
context.info = UnsafeMutablePointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
|
||||
if !SCNetworkReachabilitySetCallback(reachabilityRef!, callback, &context) {
|
||||
stopNotifier()
|
||||
throw ReachabilityError.UnableToSetCallback
|
||||
}
|
||||
|
||||
if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef!, reachabilitySerialQueue) {
|
||||
stopNotifier()
|
||||
throw ReachabilityError.UnableToSetDispatchQueue
|
||||
}
|
||||
|
||||
notifierRunning = true
|
||||
}
|
||||
|
||||
|
||||
public func stopNotifier() {
|
||||
if let reachabilityRef = reachabilityRef {
|
||||
SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil)
|
||||
SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil)
|
||||
}
|
||||
notifierRunning = false
|
||||
}
|
||||
|
||||
// MARK: - *** Connection test methods ***
|
||||
public func isReachable() -> Bool {
|
||||
return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
|
||||
return self.isReachableWithFlags(flags)
|
||||
})
|
||||
}
|
||||
|
||||
public func isReachableViaWWAN() -> Bool {
|
||||
|
||||
if isRunningOnDevice {
|
||||
return isReachableWithTest() { flags -> Bool in
|
||||
// Check we're REACHABLE
|
||||
if self.isReachable(flags) {
|
||||
|
||||
// Now, check we're on WWAN
|
||||
if self.isOnWWAN(flags) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func isReachableViaWiFi() -> Bool {
|
||||
|
||||
return isReachableWithTest() { flags -> Bool in
|
||||
|
||||
// Check we're reachable
|
||||
if self.isReachable(flags) {
|
||||
|
||||
if self.isRunningOnDevice {
|
||||
// Check we're NOT on WWAN
|
||||
if self.isOnWWAN(flags) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - *** Private methods ***
|
||||
private var isRunningOnDevice: Bool = {
|
||||
#if (arch(i386) || arch(x86_64)) && os(iOS)
|
||||
return false
|
||||
#else
|
||||
return true
|
||||
#endif
|
||||
}()
|
||||
|
||||
private var notifierRunning = false
|
||||
private var reachabilityRef: SCNetworkReachability?
|
||||
private let reachabilitySerialQueue = dispatch_queue_create("uk.co.ashleymills.reachability", DISPATCH_QUEUE_SERIAL)
|
||||
|
||||
private func reachabilityChanged(flags: SCNetworkReachabilityFlags) {
|
||||
if isReachableWithFlags(flags) {
|
||||
if let block = whenReachable {
|
||||
block(self)
|
||||
}
|
||||
} else {
|
||||
if let block = whenUnreachable {
|
||||
block(self)
|
||||
}
|
||||
}
|
||||
|
||||
notificationCenter.postNotificationName(ReachabilityChangedNotification, object:self)
|
||||
}
|
||||
|
||||
private func isReachableWithFlags(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
|
||||
let reachable = isReachable(flags)
|
||||
|
||||
if !reachable {
|
||||
return false
|
||||
}
|
||||
|
||||
if isConnectionRequiredOrTransient(flags) {
|
||||
return false
|
||||
}
|
||||
|
||||
if isRunningOnDevice {
|
||||
if isOnWWAN(flags) && !reachableOnWWAN {
|
||||
// We don't want to connect when on 3G.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func isReachableWithTest(test: (SCNetworkReachabilityFlags) -> (Bool)) -> Bool {
|
||||
|
||||
if let reachabilityRef = reachabilityRef {
|
||||
|
||||
var flags = SCNetworkReachabilityFlags(rawValue: 0)
|
||||
let gotFlags = withUnsafeMutablePointer(&flags) {
|
||||
SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0))
|
||||
}
|
||||
|
||||
if gotFlags {
|
||||
return test(flags)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// WWAN may be available, but not active until a connection has been established.
|
||||
// WiFi may require a connection for VPN on Demand.
|
||||
private func isConnectionRequired() -> Bool {
|
||||
return connectionRequired()
|
||||
}
|
||||
|
||||
private func connectionRequired() -> Bool {
|
||||
return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
|
||||
return self.isConnectionRequired(flags)
|
||||
})
|
||||
}
|
||||
|
||||
// Dynamic, on demand connection?
|
||||
private func isConnectionOnDemand() -> Bool {
|
||||
return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
|
||||
return self.isConnectionRequired(flags) && self.isConnectionOnTrafficOrDemand(flags)
|
||||
})
|
||||
}
|
||||
|
||||
// Is user intervention required?
|
||||
private func isInterventionRequired() -> Bool {
|
||||
return isReachableWithTest({ (flags: SCNetworkReachabilityFlags) -> (Bool) in
|
||||
return self.isConnectionRequired(flags) && self.isInterventionRequired(flags)
|
||||
})
|
||||
}
|
||||
|
||||
private func isOnWWAN(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
#if os(iOS)
|
||||
return flags.contains(.IsWWAN)
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
private func isReachable(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.Reachable)
|
||||
}
|
||||
private func isConnectionRequired(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.ConnectionRequired)
|
||||
}
|
||||
private func isInterventionRequired(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.InterventionRequired)
|
||||
}
|
||||
private func isConnectionOnTraffic(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.ConnectionOnTraffic)
|
||||
}
|
||||
private func isConnectionOnDemand(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.ConnectionOnDemand)
|
||||
}
|
||||
func isConnectionOnTrafficOrDemand(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return !flags.intersect([.ConnectionOnTraffic, .ConnectionOnDemand]).isEmpty
|
||||
}
|
||||
private func isTransientConnection(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.TransientConnection)
|
||||
}
|
||||
private func isLocalAddress(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.IsLocalAddress)
|
||||
}
|
||||
private func isDirect(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
return flags.contains(.IsDirect)
|
||||
}
|
||||
private func isConnectionRequiredOrTransient(flags: SCNetworkReachabilityFlags) -> Bool {
|
||||
let testcase:SCNetworkReachabilityFlags = [.ConnectionRequired, .TransientConnection]
|
||||
return flags.intersect(testcase) == testcase
|
||||
}
|
||||
|
||||
private var reachabilityFlags: SCNetworkReachabilityFlags {
|
||||
if let reachabilityRef = reachabilityRef {
|
||||
|
||||
var flags = SCNetworkReachabilityFlags(rawValue: 0)
|
||||
let gotFlags = withUnsafeMutablePointer(&flags) {
|
||||
SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0))
|
||||
}
|
||||
|
||||
if gotFlags {
|
||||
return flags
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
override public var description: String {
|
||||
|
||||
var W: String
|
||||
if isRunningOnDevice {
|
||||
W = isOnWWAN(reachabilityFlags) ? "W" : "-"
|
||||
} else {
|
||||
W = "X"
|
||||
}
|
||||
let R = isReachable(reachabilityFlags) ? "R" : "-"
|
||||
let c = isConnectionRequired(reachabilityFlags) ? "c" : "-"
|
||||
let t = isTransientConnection(reachabilityFlags) ? "t" : "-"
|
||||
let i = isInterventionRequired(reachabilityFlags) ? "i" : "-"
|
||||
let C = isConnectionOnTraffic(reachabilityFlags) ? "C" : "-"
|
||||
let D = isConnectionOnDemand(reachabilityFlags) ? "D" : "-"
|
||||
let l = isLocalAddress(reachabilityFlags) ? "l" : "-"
|
||||
let d = isDirect(reachabilityFlags) ? "d" : "-"
|
||||
|
||||
return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)"
|
||||
}
|
||||
|
||||
deinit {
|
||||
stopNotifier()
|
||||
|
||||
reachabilityRef = nil
|
||||
whenReachable = nil
|
||||
whenUnreachable = nil
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
//
|
||||
// ReachabilityService.swift
|
||||
// RxExample
|
||||
//
|
||||
// Created by Vodovozov Gleb on 22.10.2015.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
#if !RX_NO_MODULE
|
||||
import RxSwift
|
||||
#endif
|
||||
|
||||
public enum ReachabilityStatus {
|
||||
case Reachable, Unreachable
|
||||
}
|
||||
|
||||
class ReachabilityService {
|
||||
|
||||
private let reachabilityRef = try! Reachability.reachabilityForInternetConnection()
|
||||
|
||||
private let _reachabilityChangedSubject = PublishSubject<ReachabilityStatus>()
|
||||
private var reachabilityChanged: Observable<ReachabilityStatus> {
|
||||
get {
|
||||
return _reachabilityChangedSubject.asObservable()
|
||||
}
|
||||
}
|
||||
|
||||
// singleton
|
||||
static let sharedReachabilityService = ReachabilityService()
|
||||
|
||||
init(){
|
||||
reachabilityRef.whenReachable = { reachability in
|
||||
self._reachabilityChangedSubject.on(.Next(.Reachable))
|
||||
}
|
||||
|
||||
reachabilityRef.whenUnreachable = { reachability in
|
||||
self._reachabilityChangedSubject.on(.Next(.Unreachable))
|
||||
}
|
||||
|
||||
try! reachabilityRef.startNotifier()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension ObservableConvertibleType {
|
||||
func retryOnBecomesReachable(valueOnFailure:E, reachabilityService: ReachabilityService) -> Observable<E> {
|
||||
return self.asObservable()
|
||||
.catchError { (e) -> Observable<E> in
|
||||
reachabilityService.reachabilityChanged
|
||||
.filter { $0 == .Reachable }
|
||||
.flatMap { _ in failWith(e) }
|
||||
.startWith(valueOnFailure)
|
||||
}
|
||||
.retry()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// UIImage+Extensions.swift
|
||||
// RxExample
|
||||
//
|
||||
// Created by Krunoslav Zaher on 11/1/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
extension Image {
|
||||
func forceLazyImageDecompression() -> Image {
|
||||
#if os(iOS)
|
||||
UIGraphicsBeginImageContext(CGSizeMake(1, 1))
|
||||
self.drawAtPoint(CGPointZero)
|
||||
UIGraphicsEndImageContext()
|
||||
#endif
|
||||
return self
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue