【iOS】ViewControllerを汚さずにUITableViewの下部からCellを追加する実装

# はじめに

メッセージアプリでUITableViewにCellが追加される際に、下図のようにUITableViewの上部からCellが埋まっていき、下に追加されていくものが多いかと思います。
<img src ="https://qiita-image-store.s3.amazonaws.com/0/60325/d0e8319d-69b0-5b39-8fc6-27a5c74d884e.gif" width="300px" />

しかし、本来実現したいインタラクションとしては、下図のようにUITableViewの下部からCellが埋まっていくものではないでしょうか？
<img src="https://qiita-image-store.s3.amazonaws.com/0/60325/b55c7ac4-cb9b-0b0b-59c7-25ea8a9ea817.gif" width="300px" />

このインタラクションを簡単に実現できるものが [ReverseExtension](https://github.com/marty-suzuki/ReverseExtension) (558Star 2017/03/21)になります。
それでは、下部からCellを追加していく実装を解説していきたいと思います。

# インストール方法

CocoaPodsまたはCarthageからインストールすることができます。

### CocoaPods

Podfileに下記を追加し、`pod update`を実行してください。

```ruby
pod "ReverseExtension"
```

### Carthage

Cartfileに下記を追加し、`carthage update`を実行してください。

```ruby
github "marty-suzuki/ReverseExtension"
```

`ReverseExtension.framework`を**Linked Frameworks and Libraries**と**copy-frameworks**に追加してください。

# 実装方法

[ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)は、`re`というプロパティの中でインタラクションなどの処理をラップしているので

```swift
tableView.re.dataSource = self
tableView.re.deleagate = self
```

のように、既存のUITableViewに対して`re`を追加するだけで実行することができるようになるという特徴があります。

実際にViewControllerで[ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)を使うと、下記のような実装になります。

```swift
import ReverseExtension

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    fileprivate var messages: [MessageModel] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "Cell")
        //reでdataSourceとdelegateを指定
        tableView.re.dataSource = self
        tableView.re.delegate = self
        tableView.estimatedRowHeight = 56
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    @IBAction func addButtonTapped(_ sender: UIBarButtonItem) {
        messages.append(MessageModel())
        tableView.beginUpdates()
        //reでinsert
        tableView.re.insertRows(at: [IndexPath(row: messages.count - 1, section: 0)], with: .automatic)
        tableView.endUpdates()
    }
    
    @IBAction func trashButtonTapped(_ sender: UIBarButtonItem) {
        messages.removeAll()
        tableView.reloadData()
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return messages.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        (cell as? TableViewCell)?.configure(with: messages[indexPath.row])
        return cell
    }
}

extension ViewController: UITableViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        //UITableViewDelegateのハンドリングも可能
    }
}
```

**また下部からCellを追加する処理を無くしたい場合は、`re`を無くすだけで標準のUITableViewの挙動に戻るようになります。**

## カスタムプロパティ

メッセージなどをリスト表示する際に、現在取得できているものよりも最新（または過去）のものを取得するという処理を実装するかと思います。
それらの処理の発火タイミングは、UITableViewのcontentOffsetが最下部（または最上部）に達したときになるかと思います。
ReverseExtensionではそれらのイベントを受け取れるように

- `tableView.re.scrollViewDidReachTop`
- `tableView.re.scrollViewDidReachBottom`

のクロージャーが利用できるようになっています。

```swift
class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

        tableView.re.delegate = self
        tableView.re.scrollViewDidReachTop = { scrollView in
            print("contentOffsetが最上部に達したイベントを取得")
        }
        tableView.re.scrollViewDidReachBottom = { scrollView in
            print("contentOffsetが最下部に達したイベントを取得")
        }
    }
}
```

# `re`とは

[ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)では、ViewControllerでの実装が肥大化しないようにするため、[ReverseExtension class](https://github.com/marty-suzuki/ReverseExtension/blob/master/ReverseExtension/ReverseExtension.swift#L27)の中でUITableViewDeleagateが実装されていて、そのメソッドを利用して反転処理などが行われています。
[RxSwift](https://github.com/ReactiveX/RxSwift)では[ReactiveCompatible](https://github.com/ReactiveX/RxSwift/blob/master/RxSwift/Reactive.swift)を採用しているクラスで、`rx`というプロパティを介して拡張したメソッドなどにアクセスできるようになっています。
[ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)の`re`はRxSwiftの`rx`に近い形ではあるのですが、ReactiveCompatibleの中では`rx`にアクセスする度に`Reactive(self)`が返されているので、新しいインスタンスが返されることになります。しかし、[ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)の`re`はdelegateやdataSourceを保持するために下記のように`AssociatedObject`としてインスタンスが保持されています。

```swift
extension UITableView {
    private struct AssociatedKey {
        static var re: UInt8 = 0
    }
    
    public var re: ReverseExtension {
        guard let re = objc_getAssociatedObject(self, &AssociatedKey.re) as? ReverseExtension else {
            let re = ReverseExtension(self)
            objc_setAssociatedObject(self, &AssociatedKey.re, re, .OBJC_ASSOCIATION_RETAIN)
            return re
        }
        return re
    }
}
```

## Delegateを複数のオブジェクトにメッセージ転送

[ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)の`re.delegate`にdelegate先を指定した場合、指定先のオブジェクトでもdelegateの処理を実行しつつ、[ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)の中でもdelegateの処理を実行しています。

### [DelegateTransporter](https://github.com/marty-suzuki/ReverseExtension/blob/master/ReverseExtension/DelegateTransporter.m)

複数のオブジェクトでdelegateの処理を実行するために、`DelegateTransporter`利用してメッセージ転送を行っています。
`forwardInvocation`がSwiftで利用できないため、Objective-Cで実装されています。

`DelegateTransporter`は`UITableViewDelegateTransporter`のような形で継承して使われるそうていなので、イニシャライザは`NS_REFINED_FOR_SWIFT`を使ってSwiftに適した形で書き換えができるようにしてあります。

```objc:DelegateTransporter.h
@interface DelegateTransporter : NSObject
- (nonnull instancetype)initWithDelegates:(NSArray<id> * __nonnull)delegates NS_REFINED_FOR_SWIFT;
@end
```

`DelegateTransporter`では複数のdelegate元を保持するのでうが、強参照で保持してしまうとオブジェクトがリリースされなくなってしまうので、`[NSHashTable weakObjectsHashTable]`で弱参照で保持するようになっています。

```objc:DelegateTransporter.m
#import "DelegateTransporter.h"

@interface DelegateTransporter ()
@property (nonnull, nonatomic, strong) NSHashTable<NSObject *> *delegates;
@end

@implementation DelegateTransporter

- (instancetype)initWithDelegates:(NSArray<id> *)delegates {
    self = [super init];
    if (self) {
        self.delegates = [NSHashTable weakObjectsHashTable];
        for (id delegate in delegates) {
            if (![delegate isKindOfClass:[NSObject class]]) { continue; }
            [self.delegates addObject: delegate];
        }
    }
    return self;
}

- (BOOL)respondsToSelector:(SEL)aSelector {
    for (NSObject *delegate in self.delegates) {
        if ([delegate isKindOfClass:[NSNull class]]) {
            continue;
        }
        if ([delegate respondsToSelector:aSelector]) {
            return YES;
        }
    }
    return NO;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    for (NSObject *delegate in self.delegates) {
        if ([delegate isKindOfClass:[NSNull class]]) {
            continue;
        }
        if ([delegate respondsToSelector:aSelector]) {
            return [delegate methodSignatureForSelector:aSelector];
        }
    }
    return nil;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    for (NSObject *delegate in self.delegates) {
        if ([delegate isKindOfClass:[NSNull class]]) {
            continue;
        }
        if ([delegate respondsToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:delegate];
        }
    }
}

@end
```

### [UITableViewDelegateTransporter](https://github.com/marty-suzuki/ReverseExtension/blob/master/ReverseExtension/UITableViewDelegateTransporter.swift)

`UITableViewDelegateTransporter`は`UITableViewDelegate`を採用したクラスとして定義します。
このようにするとこで、`tableView.delegate = UITableViewDelegateTransporter(deleagetes: [self, deleagete])`のように`UITableViewDelegateTransporter`を`UITableViewDelegate`として扱うことができるようになります。

```swift:UITableViewDelegateTransporter.swift
class UITableViewDelegateTransporter: DelegateTransporter, UITableViewDelegate {
    @nonobjc convenience init(delegates: [UITableViewDelegate]) {
        self.init(__delegates: delegates)
    }
}
```

## [ReverseExtension](https://github.com/marty-suzuki/ReverseExtension)で独自実装を内包する

上記で実装した`UITableViewDelegateTransporter`を利用して、反転処理などの独自実装を内包します。

```swift
extension UITableView.ReverseExtension: UITableViewDelegate {
    public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if cell.contentView.transform == CGAffineTransform.identity {
            UIView.setAnimationsEnabled(false)
            cell.contentView.transform = CGAffineTransform.identity.rotated(by: .pi)
            UIView.setAnimationsEnabled(true)
        }
    }
}
```

# 最後に

このようにして、UITableViewのインスタンスに`re`を追加するだけで、UITableViewの下部からCellを追加する処理を簡単に実装することができるようになります。