画像エディターのテキスト入力時のラベルについて

引っ張ると回転しながらサイズも変えられるラベル。よくあるらしい。

初めて見たときは、これどうやって実装するんだ??と思ったけど、ラベルの中心が変わらないことに気づけば 適当に実装しても似た感じのができた。

デモ

ViewDidLoadに下記を貼ると

        let label = TextToolLabel()
        label.bind(to: view)
        label.text = "アイウエオ"

f:id:churabou:20180616231034g:plain

実装について

View自体はこんな感じに配置した。

f:id:churabou:20180616231820p:plain

メインの右下の丸は

中心を基準に回転と拡大縮小をする。フォントサイズが一定より小さいと、回転だけが行われる。

フォントサイズを更新 -> それにあうViewのサイズを決めるのか

Viewのサイズを変更 -> それに合うフォントのサイズを決定するのか

について悩んだ。

全体

class TextToolLabel: UIView {
    
    var text: String? {
        get {
            return label.text
        }
        
        set {
            label.text = newValue
            adjustSizeToFitFont()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .clear
        addSubview(label)
        addSubview(editView)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private lazy var label: UILabel  = {
        let l = UILabel()
        l.textColor = .black
        l.textAlignment = .center
        l.layer.borderColor = UIColor.gray.cgColor
        l.layer.borderWidth = 2
        l.isUserInteractionEnabled = true
        l.addGestureRecognizer(movePanGesture)
        return l
    }()
    
    private let circleSize: CGFloat = 30
    private lazy var editView: UIView = {
        let v = UIView()
        v.backgroundColor = .cyan
        v.layer.cornerRadius = circleSize / 2
        v.addGestureRecognizer(editPanGesture)
        //右下にぴったりくっつくように
        v.autoresizingMask = [.flexibleLeftMargin, .flexibleTopMargin]
        v.frame.origin = CGPoint(x: bounds.maxX-circleSize, y: bounds.maxY-circleSize)
        v.frame.size = .init(width: circleSize, height: circleSize)
        return v
    }()

    func bind(to: UIView) {
        to.addSubview(self)
        center = to.center
    }
    
    private lazy var movePanGesture: UIPanGestureRecognizer = {
        return UIPanGestureRecognizer(target: self, action: #selector(actionMove))
    }()
    private var previousLocation: CGPoint = .zero
    private var initialCenter: CGPoint = .zero
    //Labelに追加。ラベルの位置を移動する。
    @objc private func actionMove(_ sender: UIPanGestureRecognizer) {

        let touchLocation = sender.location(in: superview)
        
        if sender.state == .began {
            previousLocation = touchLocation
            initialCenter = center
        } else {
            
            let dx = touchLocation.x - previousLocation.x
            let dy = touchLocation.y - previousLocation.y
            center = CGPoint(x: initialCenter.x + dx, y: initialCenter.y + dy)
        }
    }
    
    
    private lazy var editPanGesture: UIPanGestureRecognizer = {
        return UIPanGestureRecognizer(target: self, action: #selector(actionEdit))
    }()
    
    private let minimumFontSize: CGFloat = 10
    private var initialDistance: CGFloat = 0
    private var initialiFontSize: CGFloat = 0
    
    //右下の丸いViewに追加する。引っ張って拡大縮小と回転する。
    @objc private func c(_ sender: UIPanGestureRecognizer) {
    
         let touchLocation = sender.location(in: superview)
        
        switch sender.state {
        case .began:
            initialDistance = CGPoint.distance(from: center, to: touchLocation)
            initialiFontSize = label.fontSize

        default:
            
            /*回転する処理*/
 
            //viewの中心とタップした座標の正弦を取得する
            let angle = atan2(touchLocation.y - center.y, touchLocation.x - center.x)
            //現在の適応されている角度
            let dif = transform.rotateAngle
            //差分だけ回転する
            transform = transform.rotated(by: angle-dif)
            
            /*文字を大きくする(Viewを拡大する)処理*/
            let distance = CGPoint.distance(from: center, to: touchLocation)
            let scale = Float(distance / initialDistance)

            //フォントサイズを大きくする -> それに合うようにViewのSizeも大きくする。
            let size = initialiFontSize*CGFloat(scale)
            label.updateFontsize(to: max(size, minimumFontSize))
            adjustSizeToFitFont()
        }
    }
    

    //テキストが増えるたびにフォントは変えずにViewのサイズを変更する。
    private func adjustSizeToFitFont() {
        
        //現在のフォントサイズで、テキストをぴったり表示するのに必要なサイズを計算する。
        let attributedText = NSAttributedString(string: label.text!, attributes: [.font: label.font!])
        
        let greatest = CGSize(width: CGFloat.greatestFiniteMagnitude,
                              height: CGFloat.greatestFiniteMagnitude)
        let rect = attributedText.boundingRect(with: greatest, options: .usesLineFragmentOrigin, context: nil)
        label.bounds.size = rect.size
        
        bounds.size = CGSize(width: label.bounds.size.width + circleSize,
                             height: label.bounds.size.height + circleSize)
        label.center = bounds.center
    }
}


fileprivate extension CGPoint {

    static func distance(from: CGPoint, to: CGPoint) -> CGFloat {
        let dx = from.x - to.x
        let dy = from.y - to.y
        return sqrt(dx * dx + dy * dy)
    }
}


fileprivate extension CGRect {
    
    var center: CGPoint {
        return CGPoint(x: midX, y: midY)
    }
}

fileprivate extension CGAffineTransform {
    //radian
    var rotateAngle: CGFloat {
        return atan2(b, a)
    }
}

fileprivate extension UILabel {

    var fontSize: CGFloat {
        return font.pointSize
    }
    
    func updateFontsize(to: CGFloat) {
        font = UIFont(name: font.fontName, size: to)
    }
}

小さな会社でのインターンの記録

もしかしたら悪い印象を与えるかもしれませんが、自分の思っていることを素直に書きました。一部上から目線ですいません。

1社目

株式会社L メンバー5人

  • 早稲田の院生の先輩と同時にスタートしたが、5ヶ月後に受け入れ終了された。
  • 当時はインターン落ちまくっていたので正直採用してくれたことに感謝をしていた。

ひどい点

  • 無給でシフトとかない。忙しくて全然インターン構ってくれない。実務一切やらなかった。最終的にあちらのの都合で解散になった。

マイナス面

  • なし

よかったところ

  • mac book pro と ディスプレイをかして頂いた。
  • 妙な緊張感、インターンだから行かなければならない、などいい意味で束縛されていて勉強に専念できるようになった。 (家ならすぐに諦めそうなエラーが出ても粘ることができたのが非常に大きい)
  • 結果独学でiOSアプリを作れるようになった。

不満はかなりある。交通費すらも結局貰わなかったし。 とりあえずインターンやろう的なベンチャー企業が学生の時間を無駄にする事案が多発している気がする。


2社目

代表 + インターン8名程度

キャリアバイトで応募して採用をくださった2つ目の企業。

ひどい点

  • なし

マイナス面

  • 7月から中途で3人正社員が採用されたが2ヶ月以内に全員辞めてしまった。あまりいい環境でななかったと自分も思う。
  • 社長のエンジニアとしてのスキルは低い。(動けばいい的な考え)
  • インターンの学生が七人程度いたが、それぞれのタスクが違っていたため、黙々と一人で作業する感じだった。
  • もっとフィードバック受けれる環境のがよかったと今は思う。

よかったところ

  • 色々な経験ができた。
  • 行くだけで時給が発生した。960円から1400円まで上がった。
  • 最後にはクラウドワークスに掲載した案件に挑戦して実質10時間程度の作業で10万円もらえた。(報酬)

正直もっといい環境があったと思う。しかし自分を雇ってくれたこと、ちゃんと報酬をくれたこと、色々できた点で非常に感謝している。


3社目

株式会社N 個人事業主のところ。

ひどい点

  • ひどい訳ではないが、代表の気分次第のところがある

マイナス面

  • フルリモート(一人黙々やる環境よりは近くにiOSエンジニアがいる環境の方がいい)
  • フィードバックが少なかった
  • インターン生が4人雇われたのだが3人辞めちゃった。(他の子が長く続かないのも、ある種の指標になる気がする)
  • ちょっと成長するには厳しい環境。
  • 給与の申請が難しい。 リモートで時給制だったのだが、自分が費やした時間の1/3程度しか申請できなかったと思う。
  • mac book airでバイバリの開発は厳しかった。(前の2社ではProを貸してくれた。)

よかったところ。

  • 技術書があった。
  • 有名なアプリの開発に複数携われた。
  • 近所のオフィスが使えた。(一人でいると普通にメンタルやられる。)
  • 自分の成果物もできた。

代表は優しい人で、自分の話も聞いてくださり非常に良かったと思う。ただ自分の求める環境とは少し違っていたなと思う。


3社共にインターン生として受け入れてくださったことに非常に感謝している。

ただ自分がもっと成長できる環境が他にあるならもう、多少貪欲に環境を求める必要があるのかもしれないと思った。

特に2社目は、少しでも学べることがあるかもしれないと思い、結局1年近く続けてしまった。

何より、いろんな意味で情報量が少なすぎたことが悔やまれる。

前キータに書いたやつ

RxSwiftを2週間ほど触ってみて。

RxSwiftは学習コストが高いと言われている。 しかし2018年となった今は情報量が非常に多く日本語の記事もたくさんある。 すなわち学習難易度は低いのではないかと思う。

以下自分が2週間くらい手探りでわちゃわちゃやった結果、こんな感じで学んでいけばいいのかなと思ったことを整理して見た。


ざっくりと入門記事を眺めて見る。

オブザーバーパターンから始めるRxSwift入門

いきなり全部は無理。でもなんとなく読んで頭に入ってくる部分だけ吸収していけば多少は前提知識が生まれる。

 


適当にObservable作ってみる。

let observable = Observable<Int>.create { (observer) in 
observer.on(.next(0))
observer.on(.next(0))
observer.on(.next(0))
observer.on(.complete)
}


observable.subscribe { event in 
    switch event {
       case .next(let element): print(element)
       case .error(let error): print(error)
       case .completed: print("complete")
    }
}

RxSwift 入門 その1 - タコさんブログ

UIイベントを試してみる 

let label = UILabel()
let textField = UITextField()
textField.rx.text.bind(to: label.rx.text)

テキストの入力がラベルに反映される、button.rx.tapぐらいを試せれば十分だった。

あとは公式がすごい充実してる。

RxSwift/GettingStarted.md at master · ReactiveX/RxSwift · GitHub

RxSwift/SimpleValidationViewController.swift at master · ReactiveX/RxSwift · GitHub

 

似たような記事をぐるぐるぐるぐる何回も読めばだいたいわかってくる。

  • subjectってなんだ
  • hot, coldとは
  • weakとunownedって(基本)
  • Single Driverとは

気になったところを調べつつ、徐々に知識を広げて行く。

インクリメンタルサーチをやって見る。

これはREADMEに書いてあるやつ。私はオフィスにたまたま本があったのでこちらを使った。

gihyo.jp

最初怖いけどオペレータの使い方が徐々にわかってきた。

  • distinctUntilChanged
  • take
  • startWwith
  • catchErrorJustReturn

RxSwiftの機能カタログ

RxMarbles: Interactive diagrams of Rx Observables

 

MVVMで実装して見る。

rxswift mvvm sample とか github search mvvmとかで調べればいっぱい出てくる。

私はさっきの養成読本の著者の方のを参考に作ってみて

github.com

  • mearge
  • zip
  • withLatestFrom

などの合成を使えるようになった。何よりより実務に近付いた気がする。


これで入門は終わってなんとなく自分で動かせるようになった。 少し間違ってるかもしれないがMVVMの数字記憶アプリを作ってみたりした。

最低限はやったのであとはインターン行ったりして、できる人が近くにいる環境にいたい。

でもまだまだできることはたくさんあって。 公式のExampeとか

  • QiitaでRx関連の記事網羅したり
  • 勉強会の資料とか
  • そもそもRxSwiftのソース読めるところ結構あるし
  • RxSwift勉強すればRedux flux ReactorKitなども勉強できる。  

やることなかったら

Amazon CAPTCHA

ここら辺もきになる・・

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秒間結果を表示し終えると ①へと更新する。


 

これ以上時間を無駄にしたくない。でも焦らずに身の丈にあった勉強を。

4月から2ヶ月間、週5日でiOSエンジニアとしてインターンをしていたら、どれくらい成長できたのだろうか。 そんなことを考えてしまった。 3ヶ月くらいのスパンで色々な企業でiOSエンジニアとしてインターンをした方が、だらだらと大学に行くより自分のためになる気がしてならない。 それでも大学を続ける決断をしてしまった。僕はこの決断を後悔しているが、決めたことを貫くことも必要だと思う。

夏までの50日は絶対に無駄にしたくない。これ以上無駄にしていい日なんてない。 本当はインターンに行きたかったけど、大学を続ける選択をしたおかげで、時間が微妙だ。

同級生に僕よりできる人がたくさんいる。頑張らなければいけないと焦るほど空回りする。 1日に何時間も勉強しようとするから気が滅入ってしまう。 1日2時間でいい。丁寧に、ゆっくり、コツコツと。

ただtocoさんにヒントを頂いた。 とりあえずRxSwiftをもっと勉強しよう。これのソースも読もうと思う。

github.com

その他

  • 1ヶ月でアプリを3本作成して出すのも今しかできないかも。
  • Alamofireをちゃんと解読する
  • iOSのタッチについてスライドにまとめる。

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

ぼかし処理

    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して合成するメソッドがあるのでそれを用いて画像を出力した。 わからない事だらけだが今の所このやり方でぼかし処理をしている。

6月の目標

日々目標を見つけず、だらだらと生活をしていおかげで大学も辞められずインターンもあやふやで・・ とにかくメンタルが弱い・・

4月 上旬: erekの面接
4月 中旬: c*ok**dの選考結果
4月 下旬: 大学デビュー
5月 中旬: サポーターズイベント

長期的なビジョンや短期的な目標がないので、目の前のイベントを経て色々と悩んでしまう。

6月もメンタルがやられそうなイベントがある

6月 上旬: メリのエンジニアの方と焼肉ランチ
6月 中旬: 逆求人イベント
6月 中旬: L
Eのインターン選考
6月 中旬: CA.swif

だから挫けないように今月はこの目標を実行してみようと思う。

本当に自分が適当に決めたものだ。

Storeでアプリをリリースする。

彼女が大学の授業中にホワイトボードを撮影するときに使っていた無音カメラのアプリが最近大量に広告を入れてきて使いにくいらしい。 だから本当にシンプルな広告のない無音カメラのアプリをリリースして見たい。2週間くらいで。あんまり簡単すぎたり他と似た感じだと審査通らないらし。あと盗撮対策とかあるし面倒臭くなって辞めちゃいそう・・

インターンで作成した画像ライブラリについての記事を書く。

技術力は変わらないけど、やってきたことをアピールできるので優先度高め。

  • トーンカーブの使い方。
  • なぞってぼかす実装
  • テキスト入力画面のUI

L**Eのインターンに参加したい。

  • AtCorderをpythonで解いて技術試験に備える。
  • ついでになんかアルゴリズムを1つ勉強する。