Skip to content

Commit

Permalink
Merge pull request #10 from cats-oss/dynamic-member-lookup
Browse files Browse the repository at this point in the history
Support Dynamic member lookup
  • Loading branch information
marty-suzuki authored Sep 17, 2019
2 parents f837a9d + 650c502 commit 1c32e51
Show file tree
Hide file tree
Showing 23 changed files with 508 additions and 381 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: objective-c
os: osx
osx_image: xcode10.2
osx_image: xcode11
before_install:
- gem install xcpretty
- carthage update --no-use-binaries --platform ios
Expand Down
14 changes: 14 additions & 0 deletions Documentation/Unio0_5_0MigrationGuide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Unio 0.5.0 Migration Guide

Unio 0.5.0 introduces some breaking changes.

## Classes

- [RENAME] `Relay<Input>` -> `InputWrapper<Input>`
- [RENAME] `Relay<Output>` -> `OutputWrapper<Output>`
- [RENAME] `ReadOnly` -> `PrimitiveProperty`

## Methods

- [DELETE] `Dependency.readOnlyReference(from:for:)`
- Use `OutputWrapper.property(for:)` instead.
7 changes: 7 additions & 0 deletions Example/UnioSample.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 10 additions & 3 deletions Example/UnioSample/GitHubSearchAPIStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import RxSwift
import RxRelay

protocol GitHubSearchAPIStreamType: AnyObject {
var input: Relay<GitHubSearchAPIStream.Input> { get }
var output: Relay<GitHubSearchAPIStream.Output> { get }
var input: InputWrapper<GitHubSearchAPIStream.Input> { get }
var output: OutputWrapper<GitHubSearchAPIStream.Output> { get }
}

final class GitHubSearchAPIStream: UnioStream<GitHubSearchAPIStream.Logic>, GitHubSearchAPIStreamType {
Expand Down Expand Up @@ -57,7 +57,14 @@ extension GitHubSearchAPIStream.Logic {
func bind(from dependency: Dependency<Input, State, Extra>) -> Output {

let session = dependency.extra.session
let searchResponseEvent = dependency.inputObservable(for: \.searchRepository)

let searchRepository: Observable<String>
#if swift(>=5.1)
searchRepository = dependency.inputObservables.searchRepository
#else
searchRepository = dependency.inputObservable(for: \.searchRepository)
#endif
let searchResponseEvent = searchRepository
.flatMapLatest { query -> Observable<Event<GitHub.ItemsResponse<GitHub.Repository>>> in
guard var components = URLComponents(string: "https://api.github.com/search/repositories") else {
return .empty()
Expand Down
31 changes: 25 additions & 6 deletions Example/UnioSample/GitHubSearchLogicStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import RxSwift
import RxRelay

protocol GitHubSearchLogicStreamType: AnyObject {
var input: Relay<GitHubSearchLogicStream.Input> { get }
var output: Relay<GitHubSearchLogicStream.Output> { get }
var input: InputWrapper<GitHubSearchLogicStream.Input> { get }
var output: OutputWrapper<GitHubSearchLogicStream.Output> { get }
}

final class GitHubSearchLogicStream: UnioStream<GitHubSearchLogicStream.Logic>, GitHubSearchLogicStreamType {
Expand Down Expand Up @@ -67,24 +67,43 @@ extension GitHubSearchLogicStream.Logic {
let extra = dependency.extra
let searchAPIStream = extra.searchAPIStream

searchAPIStream.output
.observable(for: \.searchResponse)
let searchResponse: Observable<GitHub.ItemsResponse<GitHub.Repository>>
#if swift(>=5.1)
searchResponse = searchAPIStream.output.searchResponse
#else
searchResponse = searchAPIStream.output.observable(for: \.searchResponse)
#endif
searchResponse
.map { $0.items }
.bind(to: state.repositories)
.disposed(by: disposeBag)

dependency.inputObservable(for: \.searchText)
let searchText: Observable<String?>
let searchRepository: AnyObserver<String>
#if swift(>=5.1)
searchText = dependency.inputObservables.searchText
searchRepository = searchAPIStream.input.searchRepository
#else
searchText = dependency.inputObservable(for: \.searchText)
searchRepository = searchAPIStream.input.accept(for: \.searchRepository)
#endif
searchText
.debounce(.milliseconds(300), scheduler: extra.scheduler)
.flatMap { query -> Observable<String> in
guard let query = query, !query.isEmpty else {
return .empty()
}
return .just(query)
}
.bind(to: searchAPIStream.input.accept(for: \.searchRepository))
.bind(to: searchRepository)
.disposed(by: disposeBag)

#if swift(>=5.1)
return Output(repositories: state.repositories,
error: searchAPIStream.output.searchError)
#else
return Output(repositories: state.repositories,
error: searchAPIStream.output.observable(for: \.searchError))
#endif
}
}
22 changes: 20 additions & 2 deletions Example/UnioSample/GitHubSearchViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,40 @@ final class GitHubSearchViewController: UIViewController {
do {
let input = viewStream.input

#if swift(>=5.1)
searchBar.rx.text
.bind(to: input.searchText)
.disposed(by: disposeBag)
#else
searchBar.rx.text
.bind(to: input.accept(for: \.searchText))
.disposed(by: disposeBag)
#endif
}

do {
let output = viewStream.output

output.observable(for: \.repositories)
let repositories: Observable<[GitHub.Repository]>
#if swift(>=5.1)
repositories = output.repositories
#else
repositories = output.observable(for: \.repositories)
#endif
repositories
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) { row, repository, cell in
cell.textLabel?.text = repository.fullName
cell.detailTextLabel?.text = repository.htmlUrl.absoluteString
}
.disposed(by: disposeBag)

output.observable(for: \.errorMessage)
let errorMessage: Observable<String>
#if swift(>=5.1)
errorMessage = output.errorMessage
#else
errorMessage = output.observable(for: \.errorMessage)
#endif
errorMessage
.bind(to: Binder(self) { me, message in
let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Close", style: .default, handler: nil))
Expand Down
13 changes: 11 additions & 2 deletions Example/UnioSample/GitHubSearchViewStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import RxSwift
import RxRelay

protocol GitHubSearchViewStreamType: AnyObject {
var input: Relay<GitHubSearchViewStream.Input> { get }
var output: Relay<GitHubSearchViewStream.Output> { get }
var input: InputWrapper<GitHubSearchViewStream.Input> { get }
var output: OutputWrapper<GitHubSearchViewStream.Output> { get }
}

final class GitHubSearchViewStream: UnioStream<GitHubSearchViewStream.Logic>, GitHubSearchViewStreamType {
Expand Down Expand Up @@ -62,11 +62,20 @@ extension GitHubSearchViewStream.Logic {

let logicStream = dependency.extra.logicStream

#if swift(>=5.1)
dependency.inputObservables.searchText
.bind(to: logicStream.input.searchText)
.disposed(by: disposeBag)

return Output(repositories: logicStream.output.repositories,
errorMessage: logicStream.output.error.map { $0.localizedDescription })
#else
dependency.inputObservable(for: \.searchText)
.bind(to: logicStream.input.accept(for: \.searchText))
.disposed(by: disposeBag)

return Output(repositories: logicStream.output.observable(for: \.repositories),
errorMessage: logicStream.output.observable(for: \.error).map { $0.localizedDescription })
#endif
}
}
8 changes: 4 additions & 4 deletions Example/UnioSampleTests/MockGitHubSearchAPIStream.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ import Unio

final class MockGitHubSearchAPIStream: GitHubSearchAPIStreamType {

let input: Relay<GitHubSearchAPIStream.Input>
let output: Relay<GitHubSearchAPIStream.Output>
let input: InputWrapper<GitHubSearchAPIStream.Input>
let output: OutputWrapper<GitHubSearchAPIStream.Output>

let _input = GitHubSearchAPIStream.Input()

let searchResponse = BehaviorRelay<GitHub.ItemsResponse<GitHub.Repository>?>(value: nil)
let searchError = BehaviorRelay<Error?>(value: nil)

init() {
self.input = Relay(_input)
self.input = InputWrapper(_input)
let _output = GitHubSearchAPIStream.Output(searchResponse: searchResponse.flatMap { $0.map(Observable.just) ?? .empty() },
searchError: searchError.flatMap { $0.map(Observable.just) ?? .empty() })
self.output = Relay(_output)
self.output = OutputWrapper(_output)
}
}
67 changes: 36 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,16 @@ struct Input: InputType {
```

Properties of Input are defined internal scope.
But these can only access `func accept(_:)` (or `func on(_:)`) via KeyPath if Input is wrapped with `Relay`.
But these can only access `func accept(_:)` (or `AnyObserver`) via KeyPath if Input is wrapped with `InputWrapper`.

```swift
let input: Relay<Input>
let input: InputWrapper<Input>

input.accept("query", for: \.searchText)
input.onEvent(.next(()), for: \.buttonTap)
input.searchText("query") // accesses `func accept(_:)`
input.buttonTap.onNext(()) // accesses `AnyObserver`
```

![](https://user-images.githubusercontent.com/2082134/54809413-8fb8cf80-4cc6-11e9-9dc2-07cd14f5c2d8.jpg)
![](https://user-images.githubusercontent.com/2082134/64858916-afbcc080-d663-11e9-8a70-92a9293f7c83.png)

### Output

Expand All @@ -110,30 +110,32 @@ struct Output: OutputType {
```

Properties of Output are defined internal scope.
But these can only access `func asObservable()` via KeyPath if Output is wrapped with `Relay`.
But these can only access `func asObservable()` via KeyPath if Output is wrapped with `OutputWrapper`.

```swift
let output: Relay<Output>
let output: OutputWrapper<Output>

output.observable(for: \.repositories)
output.repositories
.subscribe(onNext: { print($0) })

output.observable(for: \.isEnabled)
output.isEnabled
.subscribe(onNext: { print($0) })

output.observable(for: \.error)
output.error
.subscribe(onNext: { print($0) })
```

If a property is BehaviorRelay (or BehaviorSubject), be able to access value via KeyPath.

```swift
output.value(for: \.repositories)
let p: Property<[GitHub.Repository]> = output.repositories
p.value

try? output.value(for: \.isEnabled)
let t: ThrowableProperty<Bool> = output.isEnabled
try? t.throwableValue()
```

![](https://user-images.githubusercontent.com/2082134/54809443-a2cb9f80-4cc6-11e9-8d10-dfe2403f798b.jpg)
![](https://user-images.githubusercontent.com/2082134/64858314-f7dae380-d661-11e9-9a79-3ca5c53fd90a.png)

### State

Expand Down Expand Up @@ -179,8 +181,7 @@ Connect sequences and generate [Output](#output) in `func bind(from:)` to use be

- `dependency.state`
- `dependency.extra`
- `dependency.inputObservable(for:)` ... returns a Observable that is property of [Input](#input).
- `dependency.readOnlyReference(from:for:)` ... returns a read only BehaviorRelay (or BehaviorSubject) (that is wrapped by `ReadOnly<T>`) from property of [Output](#output).
- `dependency.inputObservables` ... returns a Observable that is property of [Input](#input).

Here is a exmaple of implementation.

Expand All @@ -190,12 +191,11 @@ extension GitHubSearchViewStream.Logic {
func bind(from dependency: Dependency<Input, State, Extra>) -> Output {
let apiStream = dependency.extra.apiStream

dependency.inputObservable(for: \.searchText)
.bind(to: apiStream.input.accept(for: \.searchText))
dependency.inputObservables.searchText
.bind(to: apiStream.searchText)
.disposed(by: disposeBag)

let repositories = apiStream.output
.observable(for: \.searchResponse)
let repositories = apiStream.output.searchResponse
.map { $0.items }

return Output(repositories: repositories)
Expand All @@ -206,14 +206,14 @@ extension GitHubSearchViewStream.Logic {
### UnioStream

UnioStream represents ViewModels of MVVM (it can also be used as Models).
It has `input: Relay<Input>` and `output: Relay<Output>`.
It automatically generates `input: Relay<Input>` and `output: Relay<Output>` from instances of [Input](#input), [State](#state), [Extra](#extra) and [Logic](#logic).
It has `input: InputWrapper<Input>` and `output: OutputWrapper<Output>`.
It automatically generates `input: InputWrapper<Input>` and `output: OutputWrapper<Output>` from instances of [Input](#input), [State](#state), [Extra](#extra) and [Logic](#logic).

```swift
class UnioStream<Logic: LogicType> {

let input: Relay<Logic.Input>
let output: Relay<Logic.Output>
let input: InputWrapper<Logic.Input>
let output: OutputWrapper<Logic.Output>

init(input: Logic.Input, state: Logic.State, extra: Logic.Extra, logic: Logic)
}
Expand All @@ -240,8 +240,8 @@ Define GitHubSearchViewStream for searching GitHub repositories.

```swift
protocol GitHubSearchViewStreamType: AnyObject {
var input: Relay<GitHubSearchViewStream.Input> { get }
var output: Relay<GitHubSearchViewStream.Output> { get }
var input: InputWrapper<GitHubSearchViewStream.Input> { get }
var output: OutputWrapper<GitHubSearchViewStream.Output> { get }
}

final class GitHubSearchViewStream: UnioStream<GitHubSearchViewStream.Logic>, GitHubSearchViewStreamType {
Expand Down Expand Up @@ -279,12 +279,11 @@ extension GitHubSearchViewStream.Logic {
func bind(from dependency: Dependency<Input, State, Extra>) -> Output {
let apiStream = dependency.extra.apiStream

dependency.inputObservable(for: \.searchText)
.bind(to: apiStream.input.accept(for: \.searchText))
dependency.inputObservables.searchText
.bind(to: apiStream.input.searchText)
.disposed(by: disposeBag)

let repositories = apiStream.output
.observable(for: \.searchResponse)
let repositories = apiStream.output.searchResponse
.map { $0.items }

return Output(repositories: repositories)
Expand All @@ -307,10 +306,10 @@ final class GitHubSearchViewController: UIViewController {
super.viewDidLoad()

searchBar.rx.text
.bind(to: viewStream.input.accept(for: \.searchText))
.bind(to: viewStream.input.searchText)
.disposed(by: disposeBag)

viewStream.output.observable(for: \.repositories)
viewStream.output.repositories
.bind(to: tableView.rx.items(cellIdentifier: "Cell")) {
(row, repository, cell) in
cell.textLabel?.text = repository.fullName
Expand All @@ -321,6 +320,12 @@ final class GitHubSearchViewController: UIViewController {
}
```

The documentation which does not use `KeyPath Dynamic Member Lookup` is [here](https://github.com/cats-oss/Unio/tree/0.4.1#about-unio).

#### Migration Guides

- [Unio 0.5.0 Migration Guide](./Documentation/Unio0_5_0MigrationGuide.md)

### Xcode Template

You can use Xcode Templates for Unio. Let's install with `./Tools/install-xcode-template.sh` command!
Expand Down
Loading

0 comments on commit 1c32e51

Please sign in to comment.