[iOS]KeyPathを利用してRxSwiftベースのViewModelを刷新する

# 2019/12/16追記

記事作成は2019年4月頃に行いましたがそこから半年以上が経ち、Unioも機能が増えたりしたので記事を更新しました。

また、Unioについての登壇なども行っておりますので、[こちらの資料](https://speakerdeck.com/martysuzuki/mvvmfalseshi-zhuang-wofu-ruframeworkwokai-fa-dao-ru-si-timudebaratukigaatutashi-zhuang-wotong-suru)も参考になりましたら幸いです。

# はじめに

RxSwiftを用いたMVVMでアプリケーションを開発する際、ViewModelが以下のような実装になりやすくはないでしょうか。

- ①公開用の入力メソッドと、内部でsubscribeするためのRelayの2つを定義している
- ②内部状態を公開するためのObservableと、値自体を公開するためのpropertyの2つを定義している
- ③内部状態なのか、入力のRelayなのかが一見わかりにくい

```swift:上記の問題があるViewModel
final class SearchViewModel {
    let repositories: Observable<[Repository]>
    let error: Observable<Error>

    var repositoriesValue: [Repository] {　return _repositories.value　}

    private let _repositories = BehaviorRelay<[Repository]>(value: [])
    private let _search = PublishRelay<String>()
    private let disposeBag = DisposeBag()

    init() {
        let apiAciton = SearchAPIAction()

        self.repositories = _repositories.asObservable()
        self.error = apiAction.error

        apiAction.response
            .bind(to: _repositories)
            .disposed(by: disposeBag)

        _search
            .subscribe(onNext: { apiAction.execute($0) })
            .disposed(by: disposeBag)
    }

    func search(query: String) {
        _search.accept(query)
    }
}
```

①、②については、Swift4から利用できるようになったKeyPathを用いて解決します。
[try! Swift Tokyo 2019](https://www.tryswift.co/events/2019/tokyo/jp/#)にて[Keypath入門](https://www.tryswift.co/events/2019/tokyo/jp/#keypath)のトークもあったので、KeyPathを用いて解決方法はちょうど良い利用事例になるのではないかなと思います。

<blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">I just published my slides for my <a href="https://twitter.com/tryswiftconf?ref_src=twsrc%5Etfw">@tryswiftconf</a> talk about <a href="https://twitter.com/hashtag/keypaths?src=hash&amp;ref_src=twsrc%5Etfw">#keypaths</a> 🗝🎉 in <a href="https://twitter.com/hashtag/swift?src=hash&amp;ref_src=twsrc%5Etfw">#swift</a>. <br>You can find them here.<a href="https://t.co/UJXxsETAMv">https://t.co/UJXxsETAMv</a><br><br>For any questions, feel free to contact me here. <a href="https://twitter.com/hashtag/tryswiftconf?src=hash&amp;ref_src=twsrc%5Etfw">#tryswiftconf</a></p>&mdash; Benedikt Terhechte (@terhechte) <a href="https://twitter.com/terhechte/status/1108614241198604288?ref_src=twsrc%5Etfw">March 21, 2019</a></blockquote>

③に関しては、定義する場所を明示することで解決しようと思います。

# KeyPathを用いて実装するViewModel

[<img src="https://user-images.githubusercontent.com/2082134/54809523-e1f9f080-4cc6-11e9-854b-a76ff3594de0.png" width="20px"/>Unio](https://github.com/cats-oss/Unio)というOSSを利用することで、[上記の問題](#はじめに)を解決することができます。Unioとは**Un**idirectional **I**nput **O**utputを示しており、InputからOutputまでの流れを単一方向にすることを目的としたframeworkです。まずは上記の`SearchViewModel`を、Unioを利用してInputとOutputの**流れ**を単一方向にした`SearchViewStream`として実装を置き換えます。

※Unioの内部実装については[後ほど](#unio内部でどのようにkeypathを用いているのか)解説します。

## Unioを利用した実装例

```swift:SearchViewStream.swift
protocol SearchViewStreamType: AnyObject {
    var input: InputWrapper<SearchViewStream.Input> { get }
    var output: OutputWrapper<SearchViewStream.Output> { get }
}

final class SearchViewStream: UnioStream<SearchViewStream>, SearchViewStreamType {

    struct Input: InputType {
        let search = PublishRelay<String>()
    }

    struct Output: OutputType {
        let repositories: BehaviorRelay<[Repository]>
        let error: Observable<Error>
    }

    struct State: StateType {
        let repositories = BehaviorRelay<[Repository]>(value: [])
    }

    struct Extra: ExtraType {
        let apiAction = SearchAPIAction()
    }

    static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output {

        let apiAction = dependency.extra.apiAction
        let state = dependency.state
        
        apiAction.response
            .bind(to: state.repositories)
            .disposed(by: disposeBag)

        dependency.inputObservables.search
            .subscribe(onNext: { apiAction.execute($0) })
            .disposed(by: disposeBag)

        return Output(repositories: state.repositories, error: apiAction.error)
    }

    init() {
        super.init(input: Input(), state: State(), extra: Extra())
    }
}
```

上記の`SearchViewStream`の実装が

- `Input`に外部からの入力を定義
- `Output`に外部への出力を定義
- `State`に内部状態を定義
- `Extra`に上記以外の依存を定義
- `Logic`がInput・State・ExtraをもとにOutputを生成

という区切り方で、明示的になっていることがわかるかと思います。

`SearchViewStream`を実際にViewControllerで利用すると以下のようになります。

```swift:SearchViewController.swift
final class SearchViewController: UIViewController {

    let viewStream: SearchViewStreamType = SearchViewStream()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewStream.input.search("search-text")

        viewStream.output.repositories
            .subscribe()
            .disposed(by: disposeBag)

        print(viewStream.output.repositories.value)
    }
}
```

まず、viewStreamで公開されているpropertyは、以下のように`input`と`output`のみになります。そして、それらはそれぞれ`InputWrapper`型と`OutputWrapper`型でラップされています。

<img width="696" alt="スクリーンショット 2019-12-16 19.32.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60325/42c212c3-54f1-0ead-60f8-6fc29aa320a4.png">

`input`は、KeyPathで指定しているオブジェクトが`PublishRelay`だった場合に、acceptを実行しています。（`PublishSubject`だった場合は、onEventを実行します）
また、[Swift5.1のKeyPath dynamicMemberLookup](https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath-dynamic-member-lookup.md)を利用することで、`input.search`のように直接closureとして呼び出すこともできます。

<img width="1045" alt="スクリーンショット 2019-12-16 19.37.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60325/66083193-1540-bbf2-4985-937ef184d4db.png">

そして`output`では、KeyPathで指定しているオブジェクトが`ObservableConvertibleType`だった場合に、`Observable<[Repository]>`を返しています。
加えて、KeyPathで指定しているオブジェクトが`BehaviorRelay`だった場合は、valueとして`[Repository]`を返しています。(`BehaviorSubject`だった場合は、throwableなメソッドでvalueを返します)
Outputでも、[Swift5.1のKeyPath dynamicMemberLookup](https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath-dynamic-member-lookup.md)を利用することで、`output.repositories`のように呼び出して、`Property<[Repository]>`・`Observable<[Repository]>`・`[Repository]`のいずれかの型で取り出すことができます。

<img width="1090" alt="スクリーンショット 2019-12-16 19.43.58.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60325/1dc70950-28fa-23b9-9baf-c99ca3315a4e.png">

## `Input`と`Output`のpropertyがinternalになっているが、単一方向は保証されているのか?

`SearchViewStream`で定義されている、`Input`と`Output`を再度見てみましょう。

```swift
extension SearchViewStream {

    struct Input: InputType {
        let search = PublishRelay<String>()
    }

    struct Output: OutputType {
        let repositories: BehaviorRelay<[Repository]>
        let error: Observable<Error>
    }

    ...
}
```

それぞれで保持しているpropertyはinternalになっています。一見、PublishRelayやBehaviorRelayに直接アクセスできそうで、単一方向が保証されていないように見えます。
続いて、`SearchViewStream`のsuper classである`UnioStream`の定義を見てみましょう。

```swift:Unio.UnioStream.swift
public typealias UnioStream<Logic: LogicType> = PrimitiveStream<Logic> & LogicType & UnioStreamType
```

UnioStreamはクラスではなく、`PrimitiveStream`と`LogicType`と`UnioStreamType`のtypealiasとなっています。
このような定義をすることでabstract classのように使えるので、任意のクラスを継承させつつ実装を強制することができます。
次に、`PrimitiveStream`の定義を見てみましょう。

```swift:Unio.PrimitiveStream.swift
open class PrimitiveStream<Logic: LogicType> {

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

    public init(input: Logic.Input, state: Logic.State, extra: Logic.Extra)
}
```

PrimitiveStreamでは`input: InputWrapper<Input>`と`output: OutputWrapper<Output>`が公開されています。InputやOutputが直接公開されているのではなく、それぞれラップされているということです。
そのため、`InputWrapper<T>`または`OutputWrapper<T>`とKeyPathを介してInputやOutputに定義されているpropertyの任意のメソッドにアクセスすることはできます。
しかし、InputやOutputだったりそれらが保持しているpropertyに**直接アクセスすることはできません**。
つまり、ラップしたオブジェクトからKeyPathを利用して、`Input`や`Output`で定義されているpropertyの任意のメソッドを1階層飛び越えて呼び出すために、それらのpropertyはinternalで定義されています。

## 内部状態なのか、入力のRelayなのかがわかりやすくなったのか？

`SearchViewStream`で、`Input`（入力）、`State`（内部状態）と`Extra`（その他依存）が定義されています。
定義自体が分かれているため、何を示しているものなのかはわかりやすくなりました。
それでは`Output`を生成する際に、それらがわかりやすい状態になっているのでしょうか？
`Output`を生成している`Logic`の`static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output`を見てみます。
(※`static func bind(from:disposeBag:) -> Output`はPrimitiveStreamが初期化される際に一度だけ呼び出されます。)

```swift
static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output {

    let apiAction = dependency.extra.apiAction
    let state = dependency.state
        
    apiAction.response
        .bind(to: state.repositories)
        .disposed(by: disposeBag)

    dependency.inputObservables.search
        .subscribe(onNext: { apiAction.execute($0) })
        .disposed(by: disposeBag)

    return Output(repositories: state.repositories, error: apiAction.error)
}
```

Outputを生成する際に必要なものは、`Dependency<Input, State, Extra>`として引数で渡されます。

`dependency.state`でアクセスができ、`dependency.inputObservable(for:)`または`dependency.inputObservables`からKeyPathを介して、InputのpropertyからObservableを取得します。
このようにOutput生成時でも、内部状態や入力が明示的になっています。

## 公開用のpropertyなどを2つ定義していた部分は不要になったのか？

[こちら](#inputとoutputのpropertyがinternalになっているが単一方向は保証されているのか)でも触れていますが、InputとOutputはラップしている型とKeyPathを介すことでアクセスできるものが変わるようになっています。
そのため、公開用と内部用であったり、Observable用とValue用で定義を2つする必要はなくなっていて、PublishRelayやBehaviorRelayを1つずつ定義するだけで済むようになっています。

# [<img src="https://user-images.githubusercontent.com/2082134/54809523-e1f9f080-4cc6-11e9-854b-a76ff3594de0.png" width="35px"/>Unio](https://github.com/cats-oss/Unio)内部でどのようにKeyPathを用いているのか

Unio内部の実装で、必要な部分だけを抜き出した類似コードを下記に記載しています。
そちらをもとに解説します。

まず、InputとOutputを表現するprotocolを定義します。
この2つは、様々なオブジェクトのGeneric Where Clauseで利用します。

```swift:InputとOutput
public protocol InputType {}
public protocol OutputType {}
```

PublishRelayとBehaviorRelayを表現するためのprotocolを定義します。
この2つは、KeyPathでpropertyにアクセスする際のGeneric Where Clauseで利用します。

```swift:PublishRelayとBehaviorRelay
public protocol PublishRelayType {
    associatedtype E
    func accept(_ element: E)
    func asObservable() -> Observable<E>
}

public protocol BehaviorRelayType: PublishRelayType {
    var value: E { get }
}

extension PublishRelay: PublishRelayType {}
extension BehaviorRelay: BehaviorRelayType {}
```

そして、InputとOutputをラップしている`InputWrapper`型と`OutputWrapper`型です。
それぞれのGeneric Argumentは、protocolによって型が制限されています。

```swift:InputWrapper
public final class InputWrapper<T: InputType> {

    internal let dependency: T

    public init(_ dependency: T) {
        self.dependency = dependency
    }
}
```

```swift:OutputWrapper
public final class swift:OutputWrapper<T: OutputType> {

    internal let dependency: T

    public init(_ dependency: T) {
        self.dependency = dependency
    }
}
```

`KeyPath<Root, Value>`のRootがInputでValueがPublishRelayである場合、`dependency: T`からKeyPathを介してacceptを実行できる実装をします。
dependencyの定義がinternalになっていても、InputWrapperのメソッドのKeyPathのRootをGeneric Argumentの`T`にすることで、階層が違っても内部では任意のpropertyの任意のメソッドなどにアクセスすることができるようになります。
ただ、それらにアクセスするためにはInputで定義しているproperty自体はinternalになっている必要があります。

```swift:TがInputTypeの場合 
extension InputWrapper {

    public func accept<U: PublishRelayType>(_ element: U.E, for keyPath: KeyPath<T, U>) {
        return dependency[keyPath: keyPath].accept(element)
    }
}
```

一方で、、`KeyPath<Root, Value>`のRootがOutputでValueがPublishRelayである場合、`dependency: T`からKeyPathを介してObservableを取得できる実装をします。
加えて、ValueがBehaviorRelayである場合、`dependency: T`からKeyPathを介してvalueを取得できる実装をします。

```swift:TがOutputTypeの場合 
extension OutputWrapper {

    public func observable<U: PublishRelayType>(for keyPath: KeyPath<T, U>) -> Observable<U.E> {
        return dependency[keyPath: keyPath].asObservable()
    }

    public func value<U: BehaviorRelayType>(for keyPath: KeyPath<T, U>) -> U.E {
        return dependency[keyPath: keyPath].value
    }
}
```

また、Outputを生成する際に必要となるDependencyはInputをラップし、KeyPathを利用してObservableを取得できる実装にします。
なぜInputWrapperを使わ回さないかというと、性質が逆になってしまうので単一方向を保てなくなってしまうので、別な型で表現しています。

```swift
public final class Dependency<T: InputType> {

    private let input: T
 
    internal init(_ input: T) {
        self.input = input
    }

    public func inputObservable<U: PublishRelayType>(for keyPath: KeyPath<T, U>) -> Observable<U.E> {
        return input[keyPath: keyPath].asObservable()
    }
}
```

そして、PrimitiveStream内では`Input`と`Output`をもとに、`InputWrapper<Input>`と`OutputWrapper<Output>`を生成しています。

```swift
open class UnioStream<Input: InputType, Output: OutputType> {

    public let input: InputWrapper<Input>
    public let output: OutputWrapper<Output>

    public init(input: Input, output: (Dependency<Input>) -> Output) {
        self.input = InputWrapper(input)

        let dependency = Dependency(input)
        self.output = OutputWrapper(output(dependency))
    }
}
```

UnioStreamが公開している`InputWrapper<Input>`と`OutputWrapper<Output>`をもとに、ViewControllerでは`viewStream.input`に入力だけを行い、`viewStream.output`から出力の受取だけを行えるようになっています。
これらが、KeyPathを利用してInput / Outputの流れをKeyPathを利用して保証している実装となります。

※Playground上で動作確認ができるサンプルコードを[Gist](https://gist.github.com/marty-suzuki/a462cf54d4b07f3c22732ffec58d26a8)で公開しています。

# その他

ここまでに、[<img src="https://user-images.githubusercontent.com/2082134/54809523-e1f9f080-4cc6-11e9-854b-a76ff3594de0.png" width="20px"/>Unio](https://github.com/cats-oss/Unio)が従来のViewModelとは何が違い、どういった動作をしているのかを説明してきました。
ここでは、開発でUnioを導入するための補足情報を紹介していきます。

## Xcode Templateを用いた自動生成

`./Tools/install-xcode-template.sh`を実行することで、UnioのXcode Templateがインストールされます。
インストールが完了すると、ファイルの追加時に以下のテンプレートが選べるようになります。

![](https://user-images.githubusercontent.com/2082134/54809497-cdb5f380-4cc6-11e9-97f1-a75bd4439891.png)

`Unio Components`を選択すると、ViewControllerとViewStreamが生成されます。
生成されたViewControllerはViewStreamがpropertyとして定義された状態になっています。
そしてViewStreamは、`Input`、`Output`、`State`、`Extra`と`Logic`が定義された状態になっています。加えて、ViewStreamTypeのprotocolも定義されるため、テストターゲットでMockも簡単に作成することが可能になっています。

![ezgif.com-optimize.gif](https://qiita-image-store.s3.amazonaws.com/0/60325/6790c7be-dadd-1695-273f-122fb760701e.gif)

## Stream内で別のStreamを利用する

ExtraにpropertyとしてStreamを定義することで、Stream内で別なStreamを利用することができます。
例として、SearchViewStreamで利用していた、SearchAPIActionをStream化します。

```swift:SearchAPIStream
protocol SearchAPIStreamType: AnyObject {
    var input: InputWrapper<SearchStream.Input> { get }
    var output: OutputWrapper<SearchStream.Output> { get }
}

final class SearchAPIStream: UnioStream<SearchStream>: SearchAPIStreamType {
    typealias State = NoState
    typealias Extra = NoExtra

    struct Input: InputType {
        let search = PublishRelay<String>()
    }

    struct Output: OutputType {
        let repositories: Observable<[Repository]>
        let error: Observable<Error>
    }

    static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output {

        let response = dependency.inputObservables.search
            .flatMapLatest { search -> Observable<Event<[Repository]>>
                // APIアクセスの実装
            }
            .share()

        return Output(repositories: response.flatMap { $0.element.map(Observable.just) ?? .empty() }
                      error: response.flatMap { $0.error.map(Observable.just) ?? .empty() })
    }

    init() {
        super.init(input: Input(), state: State(), extra: Extra())
    }
}
```

Inputにsearch、Outputにrepositoriesとerrorが定義されています。
SearchViewStreamでは、それらにアクセスして処理を実行するようなります。
SearchViewStreamでSearchAPIStreamを利用した場合、`// before:`の部分がもともとの実装との差分となります。

```swift:SearchViewStream
final class SearchViewStream: UnioStream<SearchViewStream>: SearchViewStreamType {

    struct Input: InputType {
        let search = PublishRelay<String>()
    }

    struct Output: OutputType {
        let repositories: BehaviorRelay<[Repository]>
        let error: Observable<Error>
    }

    struct State: StateType {
        let repositories = BehaviorRelay<[Repository]>(value: [])
    }

    struct Extra: ExtraType {
        let apiStream: SearchAPIStreamType
// before: let apiAction = SearchAPIAction()

    }

    static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output {

        let apiStream = dependency.extra.apiStream
// before:  let apiAction = dependency.extra.apiAction
        let state = dependency.state
        
        apiStream.output.observables.repositories
// before:  apiAction.response
            .bind(to: state.repositories)
            .disposed(by: disposeBag)

        dependency.inputObservables.search
            .bind(to: apiStream.input.search)
// before:  .subscribe(onNext: { apiAction.execute($0) })
            .disposed(by: disposeBag)

        return Output(repositories: state.repositories, error: apiStream.output.error)
// before:  return Output(repositories: state.repositories, error: apiAction.error)
        }
    }

    init(searchAPIStream: SearchAPIStreamType = SearchAPIStream()) {
        let extra = Extra(apiStream: searchAPIStream)
        super.init(input: Input(), state: State(), extra: extra)
// before: init() {
// before:    super.init(input: Input(), state: State(), extra: Extra())
    }
}
```

Stream内で別のStreamを利用する場合でも、input経由で入力、output経由でObservableの取得を行っていることがわかると思います。
また、SearchViewStreamのinitializerで`protocol SearchAPIStreamType`を受け取ることで、Streamをモック化してテストを容易に行うことができるようになります。

## UnioStreamのテスト方法

Streamをテストする場合、InputとOutputに注目してテストをすることになります。
SearchViewStreamを例に、テストを実装していきます。
まずSearchViewStreamは、SearchAPIStreamTypeに依存しているので、Mock化したSearchAPIStreamを定義します。

```swift:MockSearchAPIStream
final class MockSearchAPIStream: SearchAPIStreamType {
    let input: InputWrapper<SearchAPIStream.Input>
    let output: OutputWrapper<SearchAPIStream.Output>

    let _input = SearchAPIStream.Input()

    let _outputRepositories = BehaviorRelay<[Repository]?>(value: nil)
    let _outputError = BehaviorRelay<Error?>(value: nil)

    init() {
        self.input = InputWrapper(_input)
        
        let _repositories = _outputRepositories.flatMap { $0.map(Observable.just) ?? .empty() }
        let _error = _outputError.flatMap { $0.map(Observable.just) ?? .empty() }
        let _output = SearchAPIStream.Output(repositories: _repositories, error: _error)
        self.output = OutputWrapper(_output)
    }
}
```

`input: InputWrapper<SearchAPIStream.Input>`と`output: OutputWrapper<SearchAPIStream.Output>`はそれぞれ公開されているメソッドが限定されているため、依存しているものをpropertyで定義し外部からそれらに変更を加えられるようにします。
それでは、実際のテストケースの実装を見ていきます。
`func setUp()`では、テストターゲットとなるSearchViewStreamを、依存してるMockSearchAPIStream (SearchAPIStreamType)とともに初期化しています。

```swift
final class SearchViewtreamTests: XCTestCase {

    private var stream: SearchViewStream!
    private var mock: MockSearchAPIStream!

    override func setUp() {
        self.mock = MockSearchAPIStream()
        self.stream = SearchViewStream(searchAPIStream: mock)
    }
}
```

まず、入力のテストを見てみます。
SearchViewStreamの入力の`search`は、`Logic`の`static func bind(from:disposeBag:) -> Output`内でSearchAPIStreamの`search`に接続されています。
つまり、SearchAPIStreamの`search`から結果を確認することができます。
下記のように実装することで、Inputのテストが可能となります。

```swift
func testInput_search_is_called() {

    let expected = "test-search-text"
    let searchTextStack = BehaviorRelay<String?>(value: nil)

    let disposable = dependency.mock._input.search
        .bind(to: searchTextStack)

    stream.input.search(expected)

    XCTAssertEqual(expected, searchTextStack.value)

    disposable.dispose()
}
```

次に、出力のテストを見てみます。
SearchViewStreamのoutputの出力の`repositories`は、`Logic`の`static func bind(from:disposeBag:) -> Output`内でSearchAPIStreamの`repositories`から接続されています。
つまり、SearchAPIStreamの`repositories`から変更を通知することで確認することができます。
下記のように実装することで、Outputのテストが可能となります。

```swift
func testOutput_recieving_repositories() {
        
    let expected = [GitHub.Repository(...)]
    let repositoriesStack = BehaviorRelay<[Repository]?>(value: nil)

    let disposable = stream.output.repositories
        .bind(to: repositoriesStack)

    mock._outputRepositories.accept(expected)

    XCTAssertEqual(expected, repositoriesStack.value.first)

    disposable.dispose()
}
```

# 最後に

[はじめに](#はじめに)であげた①、②の冗長になる実装をKeyPathを利用することで改善し、①、②をまとめることができたことで`Input`、`Output`、`Extra`を明示することができので③も改善できました。
それらは[<img src="https://user-images.githubusercontent.com/2082134/54809523-e1f9f080-4cc6-11e9-854b-a76ff3594de0.png" width="20px"/>Unio](https://github.com/cats-oss/Unio)を利用することで簡単に実装することができるので、是非試してみてください！