はじめに
前回は、RxSwiftの概要だけでお腹いっぱいになってしまいました。今回は実食に移りたいと思います。
環境
Xcode10.3
Swift5.0.1
RxSwift 4.3.1
RxCocoa 4.3.1
作るもの
前回に引き続き、
こちらの書籍から、カウンターアプリを作っていきます。
機能としては、
・カウントの値を見ることができる
・カウントアップができる
・カウントダウンができる
・リセットができる
になります。
準備
1.プロジェクト作成
-Xcodeを起動
-Create a new Xcode project
>Single View App
>Product Name:CounterApp
>完了。すぐにプロジェクトを閉じます。
2.ターミナルを起動して、ディレクトリに移動
$ cd CounterApp
3.Podfile作成/編集
$ pod init
$ vi Podfile
# platform :ios, '9.0'
target 'CounterApp' do
use_frameworks!
pod 'RxSwift', '~> 4.3.1' #この行を追加
pod 'RxCocoa', '~> 4.3.1' #この行を追加
end
4.ライブラリのインストール
$ pod install
5.プロジェクトを開く
必ずCounterApp.xcworkspaceから起動する(.xcodeprojから起動した場合、導入したライブラリーが使えません)
Storyboardを削除!?
Storyboardを取り除き、ViewController + xibスタイルで開発をしていくとのことです。
著者様によりますと、Storyboardのデメリットとして、
- アプリが大きくなるほど画面遷移が複雑になり見辛くなる
- 都度、ViewControllerの生成が面倒
- チーム開発になると*.storyboardがconflictしまくる
だそうです。当方、チーム開発経験がないため、わからず。
とりあえず、著者様のおっしゃる通りに進めてみましょう。
1.Main.storyboardの削除
/CounterApp/Main.storyboardをDelete > Move to Trash
2.Info.plist
Info.plistを開く > Main storyboard file base nameの項目を削除(マイナスボタン)
3.AppDelegateの修正
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
/* 追加 ここから */
self.window = UIWindow(frame: UIScreen.main.bounds)
let navigationController = UINavigationController(rootViewController: ViewController())
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
/* 追加 ここまで */
return true
}
4.ViewController.xibの作成
- New File > View > Save As: ViewController.xib > Create
- ViewController.xibを開く
- Placeholders > File's Ownerを選択
- ClassにViewControllerを指定
- OutletのviewとXibのViewを接続
- Build & Run > 成功でOK
レイアウト修正
ボタンを3つ > IBActionとしてViewController.swiftに接続
ラベルを1つ > IBOutletとしてViewController.swiftに接続
以下のように配置します。

↓接続後はこんな形になります。
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var countLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func countUp(_ sender: Any) {
}
@IBAction func countDown(_ sender: Any) {
}
@IBAction func countReset(_ sender: Any) {
}
}
パターン1:callbackを利用
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var countLabel: UILabel!
private var viewModel: CounterViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = CounterViewModel()
}
@IBAction func countUp(_ sender: Any) {
viewModel.incrementCount(callback: { [weak self] count in
self?.updateCountLabel(count)
})
}
@IBAction func countDown(_ sender: Any) {
viewModel.decrementCount(callback: {[weak self] count in
self?.updateCountLabel(count)
})
}
@IBAction func countReset(_ sender: Any) {
viewModel.resetCount(callback: {[weak self] count in
self?.updateCountLabel(count)
})
}
private func updateCountLabel(_ count: Int) {
countLabel.text = String(count)
}
}
class CounterViewModel {
private(set) var count = 0
func incrementCount(callback: (Int) -> ()) {
count += 1
callback(count)
}
func decrementCount(callback: (Int) -> ()) {
count -= 1
callback(count)
}
func resetCount(callback: (Int) -> ()) {
count = 0
callback(count)
}
}
パターン2:delegateを利用
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var countLabel: UILabel!
private let presenter = CounterPresenter()
override func viewDidLoad() {
super.viewDidLoad()
presenter.attachView(self)
}
@IBAction func countUp(_ sender: Any) {
presenter.increamentCount()
}
@IBAction func countDown(_ sender: Any) {
presenter.decrementCount()
}
@IBAction func countReset(_ sender: Any) {
presenter.resetCount()
}
}
extension ViewController: CounterDelegate {
func updateCount(count: Int) {
countLabel.text = String(count)
}
}
protocol CounterDelegate {
func updateCount(count: Int)
}
class CounterPresenter {
private var count = 0 {
didSet {
delegate?.updateCount(count: count)
}
}
private var delegate: CounterDelegate?
func attachView(_ delegate: CounterDelegate) {
self.delegate = delegate
}
func detachView() {
self.delegate = nil
}
func increamentCount() {
count += 1
}
func decrementCount() {
count -= 1
}
func resetCount() {
count = 0
}
}
パターン3:RxSwiftを利用
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var countLabel: UILabel!
@IBOutlet weak var countUpButton: UIButton!
@IBOutlet weak var countDownButton: UIButton!
@IBOutlet weak var countResetButton: UIButton!
private let diposeBag = DisposeBag()
private var viewModel: CounterRxViewModel!
override func viewDidLoad() {
super.viewDidLoad()
setupViewModel()
}
private func setupViewModel() {
viewModel = CounterRxViewModel()
let input = CounterViewModelInput(
countUpButton: countUpButton.rx.tap.asObservable(),
countDownButton: countDownButton.rx.tap.asObservable(),
countResetButton: countResetButton.rx.tap.asObservable()
)
viewModel.setup(input: input)
//iOSはメインスレッドでUI操作のため、Observable(BehaviorRelay)をDriverに変換して、監視するらしい
viewModel.outputs?.counterText
.drive(countLabel.rx.text)
.disposed(by: diposeBag)
}
}
struct CounterViewModelInput {
let countUpButton: Observable<Void>
let countDownButton: Observable<Void>
let countResetButton: Observable<Void>
}
protocol CounterViewModelOutput {
var counterText: Driver<String?> {get}
}
protocol CounterViewModelType {
var outputs: CounterViewModelOutput? {get}
func setup(input: CounterViewModelInput)
}
//ViewModel
class CounterRxViewModel: CounterViewModelType {
var outputs: CounterViewModelOutput?
private let countRelay = BehaviorRelay<Int>(value: 0)
private let initialCount = 0
private let disposeBag = DisposeBag()
init() {
self.outputs = self
resetCount()
}
func setup(input: CounterViewModelInput) {
input.countUpButton
.subscribe(onNext: { [weak self] in
self?.incrementCount()
})
.disposed(by: disposeBag)
input.countDownButton
.subscribe(onNext: { [weak self] in
self?.decrementCount()
})
.disposed(by: disposeBag)
input.countResetButton
.subscribe(onNext: { [weak self] in
self?.resetCount()
})
.disposed(by: disposeBag)
}
private func incrementCount() {
let count = countRelay.value + 1
countRelay.accept(count)
}
private func decrementCount() {
let count = countRelay.value - 1
countRelay.accept(count)
}
private func resetCount() {
countRelay.accept(initialCount)
}
}
extension CounterRxViewModel: CounterViewModelOutput {
var counterText: Driver<String?> {
return countRelay
.map {"\($0)" }
.asDriver(onErrorJustReturn: nil)
}
}