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

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

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

デモ

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)
    }
}