RxSwift+MVVMのお勉強?数字を暗記するゲーム

 

f:id:churabou:20180610234258g:plain

github.com


ちゃんとMVVMになっていると思うのだがどうだろうか

Inputs:  UIEvent(ボタンのタップ、ViewDidLoad)

outputs: Viewがタップできるか、解答中の数字文字列、問題の数字等

 

ViewModelのデータの変更をViewが反映できている。

ViewModelは問題のデータや結果をGameManager(Model)的な存在とやり取りをしている。

またViewModelはstateというView(ゲーム)の状態を管理する


[Inputs]

protocol MemoryGameViewModelInputs {
    func numberTapped(num: Int)
    func passButtonTapped()
    func clearButtonTapped()
    func viewDidLoad() 
}

①各ボタンをタップした時 ボタンがタップされる(view) 数字を追加するもしくは一文字削除する(viewModel) viewが更新される。

②viewDidLoadでViewModel内でもゲームを開始したい。

 


[outputs]

protocol MemoryGameViewModelOutputs {
    var targetString: Observable<String> { get set }
    var currentAnswerString: Observable<String> { get set }
    var result: Observable<GudgeResult> { get }
    var tapEnabled: Observable<Bool> { get }
    var gameFinished: Observable<Void> { get }
}

① targetString 問題が始まるときに更新され、ViewのLabelとbindする。

②currentAnswerString 入力中の文字列。ボタンのタップで更新され、Viewに反映される。

③result 答えあわせ時に更新される。結果がViewに反映される。

④tapEnabled 問題表示中と結果を表示中(stateが.showTarget, .gudgeResult)の時はタップを制限したい。

⑤gameFinished ゲームが終了した時に一度だけ流したいイベント。結果の画面に移動する。

viewModel.outputs
.nextTargetString
.bind(to: showTargetNumberForWhile)
.disposed(by: bag)

viewModel.outputs
.currentAnswerString
.bind(to: label.rx.text)
.disposed(by: bag)

viewModel.outputs
.result
.bind(to: showResultThenRequest)
.disposed(by: bag)

viewModel.outputs
.tapEnabled
.bind(to: baseView.rx.isActive)
.disposed(by: bag)

viewModel.outputs
.gameFinished
.bind(to: showGameResultVC)
.disposed(by: bag)

 

[state]

enum MemoryGameState {
    case showTarget //問題を表示する。 
    case trySolving //解答する
    case gudgeResult //答え合わせをする。
}

このゲームの流れとして ①showTarget 問題を一定時間表示 --> ボタンのタップを制限、1秒表示して ②へ

②trySolving 入力を受け付ける。数字やクリア、パスなどを受け付ける。 必要長さの回答を得られた場合に -> ③へ

③gudgeResult 結果を判定しViewが更新される。(正解または間違っている部分を表示する 。) Viewがn秒間結果を表示し終えると ①へと更新する。


 

画像エディターのぼかし処理について

ぼかし処理

    func boxBluredImage(radius: CGFloat) -> UIImage {
        
        let inputImage = CIImage(cgImage: self.cgImage!)
        let affineClampFilter = CIFilter(name: "CIAffineClamp")!
        affineClampFilter.setValue(inputImage, forKey: "inputImage")
        affineClampFilter.setValue(CGAffineTransform(scaleX: 1, y: 1), forKey: "inputTransform")
        let affineClampedImage = affineClampFilter.outputImage!

        
        let filter = CIFilter(name: "CIBoxBlur")!
        filter.setValue(affineClampedImage, forKey: "inputImage")
        filter.setValue(radius, forKey: "inputRadius")
        let bluredImage = filter.outputImage
        

        let cropFilter = CIFilter(name: "CICrop")!
        cropFilter.setValue(bluredImage, forKey: "inputImage")
        cropFilter.setValue(inputImage.extent, forKey: "inputRectangle")
        let croppedImage = cropFilter.outputImage!

        let context = CIContext()
        let cgImage = context.createCGImage(croppedImage, from: croppedImage.extent)!
        return UIImage(cgImage: cgImage)
    }

上のはすごい適当ですがCIFilterを使ってぼかし処理を加えました。

なぞってマスク

とりあえず下記のクラスを作成してみました。

class DrawingMaskView: UIView {
    
    static func mask(to: UIView) {
        let maskView = DrawingMaskView()
        maskView.frame = to.frame
        maskView.imageView.frame = to.bounds
        to.superview?.addSubview(maskView)
        to.mask = maskView.imageView
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        print("init")
        addGestureRecognizer(panGesture)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private var imageView = UIImageView()
    private var maskImage = UIImage()
    private var lineWidth: CGFloat = 40
    
    private lazy var panGesture: UIPanGestureRecognizer = {
        return UIPanGestureRecognizer(target: self, action: #selector(actionPan))
    }()
    
    private var previousPosition: CGPoint = .zero
    
    @objc func actionPan(_ sender: UIPanGestureRecognizer) {
        
        let currentPosition = sender.location(in: sender.view)
        
        if sender.state == .began {
            previousPosition = currentPosition
        }
        
        if sender.state != .ended {
            drawLine(from: previousPosition, to: currentPosition)
        }
        previousPosition = currentPosition
    }
    
    func drawLine(from: CGPoint, to: CGPoint){
        
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)

        if let ctx = UIGraphicsGetCurrentContext() {
            maskImage.draw(at: .zero)
            ctx.setLineWidth(lineWidth)
            ctx.setLineCap(.round)
            ctx.setStrokeColor(UIColor.green.cgColor)
            
            ctx.move(to: from)
            ctx.addLine(to: to)
            ctx.strokePath()
            maskImage = UIGraphicsGetImageFromCurrentImageContext()!
        }
        UIGraphicsEndImageContext()
        imageView.image = maskToAlpha(maskImage)
    }
    
    func maskToAlpha(_ image: UIImage) -> UIImage {
        
        guard let cgImage = image.cgImage else {
            return UIImage()
        }
        
        let inputImage = CIImage(cgImage: cgImage)
        let filter = CIFilter(name: "CIMaskToAlpha")!
        filter.setValue(inputImage, forKeyPath: "inputImage")
        return UIImage(ciImage: filter.outputImage!)
    }
}

CoreGraphicsを使ってお絵かきの要領でなぞったところに絵を描いてきます。 ただViewのmaskは透明度が考慮されるのでCoreImageのフィルターを使ってマスク画像を透明に変換しました。

        
        let blue = UIView()
        blue.backgroundColor = .blue
        blue.frame = view.bounds
        view.addSubview(blue)

        let red = UIView()
        red.backgroundColor = .red
        red.frame = view.bounds
        view.addSubview(red)
        
        DrawingMaskView.mask(to: red)

こんな感じでViewを二つ並べてなぞるとなぞったとこだけ赤いViewが出現すます。

f:id:churabou:20180601130448g:plain

この二つのViewをオリジナルの画像とぼかし画像をもつImageViewにするとなぞった部分のぼかし画像が見えるようになりUI上でのぼかし処理が完成しました。画像を変えるだけでモザイク処理にもできました。

f:id:churabou:20180601130452g:plain

f:id:churabou:20180601130454g:plain

あとはCoreImageにもCoreGraphicsにも画像をmaskして合成するメソッドがあるのでそれを用いて画像を出力した。 わからない事だらけだが今の所このやり方でぼかし処理をしている。

コードレイアウトで肥大化したViewのファイルを圧縮する

現在とあるアプリの受託開発をしております。 Objc -> Swiftの移行と共にプロジェクトからIBを無くしたいとのことです。

そこでstoryboardにあるものをコード化しているのですが、作成するViewのファイルがどうしても肥大化してしまいます。

ある程度はしょうがないと思うのですが、少しくらいは改善に務めてみようと思いました。


500行のファイルが200行になりました。 何を当たり前のことをいってるんだと思われるかもしれませんが、 Viewファイルの見通しがよくなったので個人的に満足したという記事です。もちろんプロジェクト自体のコードは増えますが・・・


① addSubViewsの追加。

addSubViews(view1, view2, view3, view4, view5, view6, view7)

②UIクラスのイニシャライザを追加

static func 'default'(imageName: "") -> UIImageView {
    let v = UIImageView()
    v.translatesAutoresizingMaskIntoConstraints = false
    v.image = UIImage(named: imageName)
    return v
}

before

private lazy var imageView: UIImageView = {
    let v = UIImageView()
    v.translatesAutoresizingMaskIntoConstraints = false
    v.image = UIImage(named: "")
    return v
}()

after

private var imageView = UIImageView.default(imageName: "file")

translatesAutoresizingMaskIntoConstraintsなど毎回書く処理も多いので。今まではこの程度と思ってたけど使ってみると普通に便利です。

③ オートレイアウト

オートレイアウトのコードが非常に多いと思いました。 現在SnapKitを使っています。

before

textField.snp.remakeConstraints { (make) in
    make.width.equalTo(250)
    make.height.equalTo(45)
    make.top.equalTo(145)
    make.centerX.equalToSuperview()
}

各Viewに制約を付ける際改行込みでみで最大7行が追加されてしいます。 これをもう少し減らしたいと思いショートカットを作成しました。

after

textField.snpex
    .constrainX(left: label.snp.right + 10), right: imageView.snp.left)
    .constrainY(top: label.snp.top, height: 45)

コード量も減りましたが、X軸Y軸と順番にコードを書くのを強制することで制約のつけ忘れや重複がなくなるといった利点もありました。

というか1時間くらいで適当に作ってみました。上記は問題ないですが.equalToSuperViewにも対応したいです。 swift-snippet/Library/ExSnapKit at master · churabou/swift-snippet · GitHub

後日メソッドチェーン でオートレウアウトをかけるExtensionを作成しました。

github.com


それでも大学を続けます


高校生の時から大学は遊ぶところ、やりたいことが見つからない人が行くところだと思っていた。大半の人はそうだと思う。 大学1年の秋は毎日授業があるせいでインターンの時間を確保できなかった。休学して自分でサービス作りたい!フルコミで開発して本気でスキル高める!とかずっとで思っていた。

もともとやる気がなく、ずっと休学・中退を考えていた私であるが、自分の弱さを認めて最終学歴を大卒にすることにした。

技術職を目指すのだから、大学を辞めて本気で2年間やった方が気持ちいいとも思う。

授業は聞かずにとりあえず出席カードを提出する、期末前だけレポートを書く、テストだけ受けて単位を取る。 そんな大学生活にどんな意味があるのだろうか。

週1で授業いってる頃の学費って日割りすると1日3万円とか。日給3万円稼ぐ大変さ。母親が時給1000円のバイトを30時間で稼ぐお金を1日で無駄にする。

それでも気づけば3年生になっていた。 しかも普通にやれば多少楽しながら卒業できる程度の単位がある状態だ。リスクを背負って本気でやりたいことがないし、もしあったとしても大学生活と両立不可能なことではなかったりする。 つまり、悔しいけど、泣きながら、大学を続けルシカナイ。

個人的に行きたくない理由

  • もともと行くつもりがなかった。
  • 高校を辞めずに後悔してる。自分でちゃんと決断をしたい。
  • やる気がないのに高い授業料払ってイヤイヤ続ける無駄な感じ
  • 大学卒業すること以外の選択をたくさん知っている。
  • 楽しそうな大学生をみて精神的に凹む
  • 大学は遊ぶところだと思っていたけど、全然遊ばないで奮闘している。
  • 背水の陣的なアレがしたい。
  • 大学続けるのも惰性

いやいや続ける理由

  • 週に2日くらいなら実際我慢できる(ある程度単位取れれたため週2日程度で大丈夫)
  • 決断力がなかった自分への罰
  • 辞めなければならない理由がない
  • 親が喜ぶ。
  • このままいけばいい感じに就職できそう。

まとめ

  • 私立文系は楽
  • 大学卒業することは自分への罰