[iOS]いきなり！RxSwift (当方はSwift初心者でいきなりRxSwift！) よく使うRxSwift系ライブラリ RxKeyboard編

#はじめに
前回に引き続き、よく使うRxSwift系ライブラリとして、RxKeyboardをみていきたいと思います。

#環境
Xcode10.3
Swift5.0.1
RxSwift 5.0.0
RxCocoa 5.0.0
RxKeyboard 1.0.0
SwiftyImage 1.1
SwiftyColor 1.2.0
CGFloatLiteral 0.5.0
ManualLayout 1.3.0
SnapKit 5.0.0
Then 2.5.0
ReusableKit 3.0.0
UITextView+Placeholder 1.2.1

#つくるもの
iOSのMessageアプリのようなメッセージ形式のタイムライン&入力フォームのアプリケーションになります。

![625eea7a-ddbe-11e6-9984-529abae1bd1a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468725/b913e3c7-6670-f1c5-f344-574470e4ce29.gif)![14bd915c-8eb0-11e6-93ea-7618fc9c5d81.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468725/bc83e56d-3708-6abb-9605-bc020c8f7e9f.gif)



#準備

1.プロジェクト作成

-Xcodeを起動 
-Create a new Xcode project 
　＞Single View App
　＞Product Name:RxKeyboardSample
　＞完了。すぐにプロジェクトを閉じます。

2.ターミナルを起動して、ディレクトリに移動

```
$ cd RxKeyboardSample
```

3.Podfile作成/編集

```
$ pod init
$ vi Podfile
```

今回はたくさんのライブラリを使います

```:Podfile
target 'RxKeyboardSample' do
  use_frameworks!

  pod 'RxSwift'
  pod 'RxCocoa'  
  pod 'RxKeyboard'
  pod 'ReusableKit'
  pod 'SwiftyImage'
  pod 'SwiftyColor'
  pod 'CGFloatLiteral'
  pod 'ManualLayout'
  pod 'SnapKit'
  pod 'Then'
  pod 'UITextView+Placeholder'

end
````


4.ライブラリのインストール

```
$ pod install
```

5.プロジェクトを開く
.xcworkspaceから起動する


#Storyboardを削除
今回は、Storyboardもxibも使わずに、プログラムに直書きでレイアウトを表示します。

1.Main.storyboardの削除
/Main.storyboardをDelete > Move to Trash

2.Info.plist
Info.plistを開く ＞ Main storyboard file base nameの項目を削除(マイナスボタン)

3.AppDelegateの修正

```AppDelegate.swift

import UIKit

import CGFloatLiteral
import ManualLayout
import SnapKit
import SwiftyColor
import Then
import UITextView_Placeholder

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    //こうすることでSwift4.1以下でもdidFinishLaunchingWithOptionsが呼ばれるようにできる
    #if swift(>=4.2)
        typealias ApplicationLaunchOptionsKey = UIApplication.LaunchOptionsKey
    #else
        typealias ApplicationLaunchOptionsKey = UIApplicationLaunchOptionsKey
    #endif
    
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [ApplicationLaunchOptionsKey: Any]?
        ) -> Bool {

        let window = UIWindow(frame: UIScreen.main.bounds)
        window.backgroundColor = .white
        window.makeKeyAndVisible()
        
        let viewController = MessageListViewController()
        let navigationController = UINavigationController(rootViewController: viewController)
        window.rootViewController = navigationController
        
        self.window = window
        return true
    }
}
```

#MVVM用のグループを作成
Project Navigator - プロジェクトフォルダ上で右クリック - New Group
より、Models、Views、ViewControllersの3つのグループを作成し、

/ViewControllers
・ViewController.swiftをViewControllersフォルダに移動
・ファイル名を、MessageListViewController.swiftに変更
・クラス名を、MessageListViewControllerに変更

/Views
・MessageCell.swift
・MessageInputBar.swift
を作成

/Models
・User.swift
・Message.swift
を作成



<img width="217" alt="スクリーンショット 2019-08-07 15.22.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/468725/fdaced7f-74f4-f0c8-f9b7-dc9595525646.png">


#Model
User、MessageのModel編集します。

```User.swift
enum User {
    case other
    case me
}
```

```Message.sweft
struct Message {
    var user: User
    var text: String
}
```

#View

```MessageCell.swift
import Foundation

import UIKit

import SwiftyImage

final class MessageCell: UICollectionViewCell {
    
    // MARK: Types
    
    fileprivate enum BalloonAlignment {
        case left
        case right
    }
    
    // MARK: Constants
    
    struct Metric {
        static let maximumBalloonWidth = 240.f
        static let balloonViewInset = 10.f
    }
    
    struct Font {
        static let label = UIFont.systemFont(ofSize: 14.f)
    }
    
    // MARK: Properties
    
    fileprivate var balloonAlignment: BalloonAlignment = .left
    
    // MARK: UI
    
    //SwiftyImageによる拡張でオブジェクト作成
    fileprivate let otherBalloonViewImage = UIImage.resizable()
        .corner(radius: 5)
        .color(0xD9D9D9.color)
        .image
    
    //SwiftyImageによる拡張でオブジェクト作成
    fileprivate let myBalloonViewImage = UIImage.resizable()
        .corner(radius: 5)
        .color(0x1680FA.color)
        .image
    
    let balloonView = UIImageView()
    let label = UILabel().then {
        $0.font = Font.label
        $0.numberOfLines = 0
    }
    
    // MARK: Initializing
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.contentView.addSubview(self.balloonView)
        self.contentView.addSubview(self.label)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: Configuring
    
    func configure(message: Message) {
        self.label.text = message.text
        
        switch message.user {
        case .other:
            self.balloonAlignment = .left
            self.balloonView.image = self.otherBalloonViewImage
            self.label.textColor = .black
            
        case .me:
            self.balloonAlignment = .right
            self.balloonView.image = self.myBalloonViewImage
            self.label.textColor = .white
            
        }
        
        self.setNeedsLayout()   //再読み込み
    }
    
    // MARK: Size
    class func size(thatFitsWidth width: CGFloat, forMessage message: Message) -> CGSize {
        let labelWIdth = Metric.maximumBalloonWidth - Metric.balloonViewInset * 2
        let constraintSize = CGSize(width: labelWIdth, height: CGFloat.greatestFiniteMagnitude)
        let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading]
        let rect = message.text.boundingRect(with: constraintSize,
                                             options: options,
                                             attributes: [.font: Font.label],
                                             context: nil)
        let labelHeight = ceil(rect.height)
        return CGSize(width: width, height: labelHeight + Metric.balloonViewInset * 2)
    }
    
    // MARK: Layout
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        //ManualLayout によるプロパティ操作
        self.label.width = Metric.maximumBalloonWidth - Metric.balloonViewInset * 2
        self.label.sizeToFit()
        
        self.balloonView.width = self.label.width + Metric.balloonViewInset * 2
        self.balloonView.height = self.label.height + Metric.balloonViewInset * 2
        
        switch self.balloonAlignment {
        case .left:
            self.balloonView.left = 10
            
        case .right:
            self.balloonView.right = self.contentView.width - 10
        }
        
        self.label.top = self.balloonView.top + Metric.balloonViewInset
        self.label.left = self.balloonView.left + Metric.balloonViewInset
    }

}

```

```MessageInputBar.swift
import Foundation

import UIKit

import RxCocoa
import RxSwift
import RxKeyboard

final class MessageInputBar: UIView {
    
    // MARK: Properties
    
    private let disposeBag = DisposeBag()
    
    // MARK: UI
    
    let toolbar = UIToolbar()
    let textView = UITextView().then {
        $0.placeholder = "Say Hi!"
        $0.isEditable = true
        $0.showsVerticalScrollIndicator = false
        $0.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.6).cgColor
        $0.layer.borderWidth = 1 / UIScreen.main.scale
        $0.layer.cornerRadius = 3
    }
    
    let sendButton = UIButton(type: .system).then {
        $0.titleLabel?.font = UIFont.boldSystemFont(ofSize: UIFont.systemFontSize)
        $0.setTitle("Send", for: .normal)
    }
    
    // MARK: Initializing
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addSubview(self.toolbar)
        self.addSubview(self.textView)
        self.addSubview(self.sendButton)
        
        //snp:SnapKit は AutoLayout の DSL
        self.toolbar.snp.makeConstraints { make in
            make.edges.equalTo(0)
        }
        
        self.textView.snp.makeConstraints { make in
            make.top.left.equalTo(7)
            make.right.equalTo(self.sendButton.snp.left).offset(-7)
            make.bottom.equalTo(-7)
        }
        
        self.sendButton.snp.makeConstraints { make in
            make.top.equalTo(7)
            make.bottom.equalTo(-7)
            make.right.equalTo(-7)
        }
        
        
        self.textView.rx.text
            .map { text in
                //1行記述はreturnを省略できる
                text?.isEmpty == false  //textViewのtextを監視し、textが空でなければ
            }
            .bind(to: self.sendButton.rx.isEnabled) //ボタンを押せるように
            .disposed(by: self.disposeBag)
        
        RxKeyboard.instance.visibleHeight
            .map{ $0 > 0}
            .distinctUntilChanged().drive(onNext: { [weak self] (visible) in
                guard let self = self else {
                    return
                }
                
                var bottomInset = 0.f
                if #available(iOS 11.0, *), !visible, let bottom = self.superview?.safeAreaInsets.bottom {
                    bottomInset =  bottom
                }
                
                self.toolbar.snp.remakeConstraints({ (make) in
                    make.left.right.top.equalTo(0)
                    make.bottom.equalTo(bottomInset)
                })
            })
            .disposed(by: disposeBag)
        
    }
    
    @available(iOS 11.0, *)
    override func safeAreaInsetsDidChange() {
        super.safeAreaInsetsDidChange()
        guard let bottomInset = self.superview?.safeAreaInsets.bottom else {
            return
        }
        
        self.toolbar.snp.remakeConstraints { make in
            make.top.left.right.equalTo(0)
            make.bottom.equalTo(bottomInset)
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: Size
    
    //自身のサイズを返す
    override var intrinsicContentSize: CGSize {
        return CGSize(width: self.width, height: 44)
    }
}

// MARK: - Reactive

//Rasctive
extension Reactive where Base: MessageInputBar {   //base変数の型がMessageInputBarになる
    
    var sendButtonTap: ControlEvent<String> {
        let source: Observable<String> = self.base.sendButton.rx.tap.withLatestFrom(self.base.textView.rx.text.asObservable())
            .flatMap { text -> Observable<String> in
                if let text = text, !text.isEmpty {
                    return .just(text)
                } else {
                    return .empty()
                }
            }
            .do(onNext: { [weak base = self.base] _ in
                base?.textView.text = nil
            })
        return ControlEvent(events: source)
    }
}

```

#ViewController

```MessageListViewController.swift
import UIKit

import ReusableKit
import RxKeyboard
import RxSwift

class MessageListViewController: UIViewController {

    // MARK: Constants
    
    struct Reusable {
        static let messageCell = ReusableCell<MessageCell>()    //ReusableCell：ReusableKitのクラス
    }
    
    // MARK: Properties
    
    private var didSetupViewConstaraints = false
    private var disposeBag = DisposeBag()
    
    fileprivate var messages: [Message] = [
        Message(user: .other, text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."),
        Message(user: .other, text: "Morbi et eros elementum, semper massa eu, pellentesque sapien."),
        Message(user: .me, text: "Aenean sollicitudin justo scelerisque tincidunt venenatis."),
        Message(user: .me, text: "Ut mollis magna nec interdum pellentesque."),
        Message(user: .me, text: "Aliquam semper nibh nec quam dapibus, a congue odio consequat."),
        Message(user: .other, text: "Nullam iaculis nisi in justo feugiat, at pharetra nulla dignissim."),
        Message(user: .me, text: "Fusce at nulla luctus, posuere mauris ut, viverra nunc."),
        Message(user: .other, text: "Nam feugiat urna non tortor ornare viverra."),
        Message(user: .other, text: "Donec vitae metus maximus, efficitur urna ac, blandit erat."),
        Message(user: .other, text: "Pellentesque luctus eros ac nisi ullamcorper pharetra nec vel felis."),
        Message(user: .me, text: "Duis vulputate magna quis urna porttitor, tempor malesuada metus volutpat."),
        Message(user: .me, text: "Duis aliquam urna quis metus tristique eleifend."),
        Message(user: .other, text: "Cras quis orci quis nisi vulputate mollis ut vitae magna."),
        Message(user: .other, text: "Fusce eu urna eu ipsum laoreet lobortis."),
        Message(user: .other, text: "Proin vitae tellus nec odio consequat varius ac non orci."),
        Message(user: .me, text: "Maecenas gravida arcu ut consectetur tincidunt."),
        Message(user: .me, text: "Quisque accumsan nisl ut ipsum rutrum, nec rutrum magna lobortis."),
        Message(user: .other, text: "Integer ac sem eu velit tincidunt hendrerit a in dui."),
        Message(user: .other, text: "Duis posuere arcu convallis tincidunt faucibus."),
    ]
    
    // MARK: UI
    
    let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()).then {
        $0.alwaysBounceVertical = true
        $0.keyboardDismissMode = .interactive
        $0.backgroundColor = .clear
        $0.register(Reusable.messageCell)
        ($0.collectionViewLayout as? UICollectionViewFlowLayout)?.do({
            $0.minimumLineSpacing = 6
            $0.sectionInset.top = 10
            $0.sectionInset.bottom = 10
        })
    }
    
    let messageInputBar = MessageInputBar()
    
    // MARK: Initializing
    
    init() {
        super.init(nibName: nil, bundle: nil)
        self.title = "RxKeyboard Example"
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.collectionView)
        self.view.addSubview(self.messageInputBar)
        
        self.collectionView.dataSource = self
        self.collectionView.delegate = self

        DispatchQueue.main.async {
            let indexPath = IndexPath(item: self.messageInputBar.accessibilityElementCount() - 1, section: 0)
            self.collectionView.scrollToItem(at: indexPath, at: [], animated: true)
        }
        
        RxKeyboard.instance.visibleHeight
            .drive(onNext: { [weak self] keyboardVisibleHeight in
                guard let self = self, self.didSetupViewConstaraints else { return }
                self.messageInputBar.snp.updateConstraints { make in
                    if #available(iOS 11.0, *) {
                        make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).offset(-keyboardVisibleHeight)
                    } else {
                        make.bottom.equalTo(self.bottomLayoutGuide.snp.top).offset(-keyboardVisibleHeight)
                    }
                }
                self.view.setNeedsLayout()
                UIView.animate(withDuration: 0) {
                    self.collectionView.contentInset.bottom = keyboardVisibleHeight + self.messageInputBar.height
                    self.collectionView.scrollIndicatorInsets.bottom = self.collectionView.contentInset.bottom
                    self.view.layoutIfNeeded()
                }
            })
            .disposed(by: self.disposeBag)
        
        RxKeyboard.instance.willShowVisibleHeight
            .drive(onNext: { keyboardVisibleHeight in
                self.collectionView.contentOffset.y += keyboardVisibleHeight
            })
            .disposed(by: self.disposeBag)
        
        self.messageInputBar.rx.sendButtonTap
            .subscribe(onNext: { [weak self] text in
                guard let self = self else { return }
                let message = Message(user: .me, text: text)
                self.messages.append(message)
                let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
                self.collectionView.insertItems(at: [indexPath])
                self.collectionView.scrollToItem(at: indexPath, at: [], animated: true)
            })
            .disposed(by: disposeBag)
    }
    
    override func updateViewConstraints() {
        super.updateViewConstraints()
        guard !self.didSetupViewConstaraints else { return }
        self.didSetupViewConstaraints = true
        
        self.collectionView.snp.makeConstraints { make in
            make.edges.equalTo(0)
        }
        self.messageInputBar.snp.makeConstraints { make in
            make.left.right.equalTo(0)
            if #available(iOS 11.0, *) {
                make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom)
            } else {
                make.bottom.equalTo(self.bottomLayoutGuide.snp.top)
            }
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        if self.collectionView.contentInset.bottom == 0 {
            self.collectionView.contentInset.bottom = self.messageInputBar.height
            self.collectionView.scrollIndicatorInsets.bottom = self.collectionView.contentInset.bottom
        }
    }
}

// MARK: - UICOllectionViewDataSource

extension MessageListViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.messages.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeue(Reusable.messageCell, for: indexPath)
        cell.configure(message: self.messages[indexPath.item])
        return cell
    }
}

// MARK: - UICollectionViewDelegateFlowLayout

extension MessageListViewController: UICollectionViewDelegateFlowLayout {
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let message = self.messages[indexPath.item]
        return MessageCell.size(thatFitsWidth: collectionView.width, forMessage: message)
    }
}


```

#参考
https://github.com/RxSwiftCommunity/RxKeyboard