5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[iOS][Swift]SnapKitをRxSwiftと連携する

Last updated at Posted at 2019-11-07

はじめに

iOSでコードベースでレイアウトを組む際に便利なSnapKitと、振る舞いを宣言的に記述できるRxSwiftを連携する方法について説明します。

前提

以下のライブラリについて基本的な知識を有している方向けの記事です

サンプルアプリ

https://github.com/idomazine/RxSnapKit
git clone後、 pod install を実行しXCodeでビルドします。

RxでConstraintの値を操作

以下のように記述することでSnapKit.ConstraintのプロパティをRxのBinderに適応させることができます。

import RxSwift
import RxCocoa
import SnapKit

extension Constraint: ReactiveCompatible { }
extension Reactive where Base: Constraint {
    var isActive: Binder<Bool> {
        Binder(base) { constraint, isActive in
            constraint.isActive = isActive
        }
    }
    
    var offset: Binder<Float> {
        Binder(base) { constraint, offset in
            constraint.update(offset: offset)
        }
    }
    
    var inset: Binder<Float> {
        Binder(base) { constraint, inset in
            constraint.update(inset: inset)
        }
    }
    
    var priority: Binder<ConstraintPriority> {
        Binder(base) { constraint, priority in
            constraint.update(priority: priority)
        }
    }
}

サンプルアプリの以下の例では、UISliderのValueを真ん中のビューのcenterXに対応するconstraintのoffsetにbindすることでスライダーに追従した動きを実現しています。

slider.gif

import UIKit
import RxSwift
import RxCocoa
import SnapKit

final class SliderViewController: UIViewController {
    private var flexibleView: UIView!
    private var centerXConstraint: Constraint!
    private var slider: UISlider!

    private let disposeBag = DisposeBag()

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        title = "Slider"
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func loadView() {
        super.loadView()
        view.backgroundColor = .systemPurple

        flexibleView = {
            let flexibleView = UIView()
            flexibleView.backgroundColor = .systemPink
            view.addSubview(flexibleView)
            flexibleView.snp.makeConstraints {
                $0.centerY.equalToSuperview()
                $0.height.width.equalTo(44)
                centerXConstraint = $0.centerX.equalToSuperview().constraint
            }
            return flexibleView
        }()

        slider = {
            let slider = UISlider()
            slider.minimumValue = -50
            slider.maximumValue = 50
            slider.value = 0
            slider.minimumTrackTintColor = .systemRed
            slider.maximumTrackTintColor = .systemBlue
            view.addSubview(slider)
            slider.snp.makeConstraints {
                $0.top.equalTo(flexibleView.snp.bottom).offset(10)
                $0.width.equalToSuperview().offset(-20)
                $0.centerX.equalToSuperview()
            }
            return slider
        }()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Sliderの値をConstraintのoffset値にバインドしている
        slider.rx.value
            .bind(to: centerXConstraint.rx.offset)
            .disposed(by: disposeBag)
    }
}

Rxで動的にレイアウトを定義

以下のように記述することでストリームで流れてくる値に合わせたレイアウトの定義を行い、自動でConstraintの切り替えを行うBinderを実装できます。

extension Reactive where Base: UIView {
    func makeConstraints<T>(makes: @escaping ((ConstraintMaker, T) -> Void)) -> Binder<T> {
        var constraints: [Constraint] = []
        return Binder(base) { view, value in
            constraints.forEach { $0.deactivate() }
            constraints = view.snp.prepareConstraints { maker in makes(maker, value) }
            constraints.forEach { $0.activate() }
        }
    }
}

サンプルアプリの以下の例では、真ん中の四角いビューがLeftボタンを押すと左に、Rightボタンを押すと右に移動します。
rx.makeConstraintsで定義されたレイアウトは、新たにストリームが流れるとリフレッシュされます。

lr.gif


import RxSwift
import RxCocoa
import SnapKit

final class LeftRightViewController: UIViewController {
    private var boxView: UIView!
    private var leftButton: UIButton!
    private var rightButton: UIButton!

    private let disposeBag = DisposeBag()
    
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        title = "LR"
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        super.loadView()
        view.backgroundColor = .systemPurple
        boxView = {
            let boxView = UIView()
            boxView.backgroundColor = .systemGreen
            view.addSubview(boxView)
            boxView.snp.makeConstraints {
                $0.width.height.equalTo(44)
                $0.centerX.equalToSuperview().priority(.high)
                $0.centerY.equalToSuperview()
            }
            return boxView
        }()

        leftButton = {
            let leftButton = UIButton()
            view.addSubview(leftButton)
            leftButton.setTitleColor(.white, for: .normal)
            leftButton.setTitle("Left", for: .normal)
            leftButton.snp.makeConstraints {
                $0.left.equalToSuperview().inset(20)
                $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(20)
            }
            return leftButton
        }()

        rightButton = {
            let rightButton = UIButton()
            view.addSubview(rightButton)
            rightButton.setTitleColor(.white, for: .normal)
            rightButton.setTitle("Right", for: .normal)
            rightButton.snp.makeConstraints {
                $0.right.equalToSuperview().inset(20)
                $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(20)
            }
            return rightButton
        }()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        let alignsLeft: Observable<Bool> = Observable.merge(leftButton.rx.tap.map { _ in true },
                                                            rightButton.rx.tap.map { _ in false})
        // Observableで流れてくる値に応じてレイアウトを切り替えている
        alignsLeft
            .bind(to: boxView.rx.makeConstraints { maker, alignsLeft in
                if alignsLeft {
                    maker.left.equalToSuperview().inset(10)
                } else {
                    maker.right.equalToSuperview().inset(10)
                }
            })
        .disposed(by: disposeBag)
    }
}

実践にあたって

サンプルアプリ内では単体のUIViewController内でRxのストリームが完結していますが、実際の開発で例えばMVVMなどを採用しているときなどはViewModelの流すストリームに合わせてレイアウトを切り替えることになるかと思います。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?