[Xcode/Swift] UIKitで自作アコーディオンを作る

Xib & RxSwiftを用いて実装します。

実装

UI作り (Xib)

アコーディオン部分はわかりやすく色を変えてます、お好きにカスタマイズしてください。

全体をStackViewで入れて、二つのUIViewで制御する感じです。

ViewController

import RxCocoa
import RxSwift
import UIKit

final class HomeViewController: UIViewController {
    // MARK: - Properties
    @IBOutlet private weak var accrodionButton: UIButton!
    @IBOutlet private weak var accrodionView: UIView!
    @IBOutlet private weak var accordionViewHeightConstraint: NSLayoutConstraint!

    private let viewModel = HomeViewModel()
    private let disposeBag = DisposeBag()

    // MARK: - View Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
         bind()
    }
}

// MARK: - Bind
private extension HomeViewController {
    func bind() {
        accrodionButton.rx.tap.asSignal()
            .emit(to: viewModel.inputs.switchAccordion)
            .disposed(by: disposeBag)

        viewModel.outputs.isAccordionViewHidden
            .drive(onNext: { [weak self] isAccordionViewHidden in
                guard let self = self else { return }
                if !isAccordionViewHidden {
                    self.accrodionView.isHidden = false
                }
                UIView.animate(withDuration: 0.3, animations: {
                    self.accordionViewHeightConstraint.constant = isAccordionViewHidden ? 0 : 160
                    self.view.layoutIfNeeded()
                    self.accrodionView.alpha = isAccordionViewHidden ? 0.0 : 1.0
                }, completion: { _ in
                    if isAccordionViewHidden {
                        self.accrodionView.isHidden = true
                    }
                })
            })
            .disposed(by: disposeBag)
    }
}

ViewModel

import RxCocoa
import RxSwift

protocol HomeViewModelInputs: AnyObject {
    var switchAccordion: PublishRelay<Void> { get }
}

protocol HomeViewModelOutputs: AnyObject {
    var isAccordionViewHidden: Driver<Bool> { get }
}

protocol HomeViewModelType: AnyObject {
    var inputs: HomeViewModelInputs { get }
    var outputs: HomeViewModelOutputs { get }
}

final class HomeViewModel: HomeViewModelType, HomeViewModelInputs, HomeViewModelOutputs {
    // MARK: - Properties
    var inputs: HomeViewModelInputs { return self }
    var outputs: HomeViewModelOutputs { return self }

    // MARK: - Input Sources
    let switchAccordion = PublishRelay<Void>()
    // MARK: - Output Sources
    let isAccordionViewHidden: Driver<Bool>

    // MARK: - Properties
    private let _isAccordionViewHidden = BehaviorRelay<Bool>(value: true)
    private let disposeBag = DisposeBag()

    // MARK: - Initialize
    init() {
        self.isAccordionViewHidden = _isAccordionViewHidden.asDriver(onErrorDriveWith: .empty())
        switchAccordion.asObservable()
            .withLatestFrom(self.isAccordionViewHidden)
            .map { !$0 }
            .bind(to: _isAccordionViewHidden)
            .disposed(by: disposeBag)
    }
}