文字の周りをくるくる回るUILabel

ローディング中の表示にこんなのを用意したくなりました。
ちょっと悩んだので、誰かの参考になればと。
![2019-04-09 10.40.55.gif](https://qiita-image-store.s3.amazonaws.com/0/42826/b771c2d3-0be3-6fe9-d94e-6a53865d1b1f.gif)

# コード

解説は下記に。

```swift
class LoadingLabel: UILabel {
    
    let strokeRate: CGFloat = 0.9
    let lineWidth: CGFloat = 1
    let cornerRadius: CGFloat = 4
    
    let borderLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        initialize()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initialize()
    }
    
    func initialize() {
        borderLayer.fillColor = nil
        borderLayer.lineWidth = lineWidth
        borderLayer.strokeColor = UIColor.blue.cgColor
        borderLayer.strokeStart = 0
        borderLayer.strokeEnd = 0.5
        borderLayer.zPosition = 1 // 解説.3
        self.layer.addSublayer(borderLayer)
        
        let animationStrokeStart = CABasicAnimation(keyPath: "strokeStart")
        animationStrokeStart.fromValue = 0
        animationStrokeStart.toValue = 0.5
        animationStrokeStart.duration = 2
        animationStrokeStart.repeatDuration = .infinity
        borderLayer.add(animationStrokeStart, forKey: nil)
        
        let animationStrokeEnd = CABasicAnimation(keyPath: "strokeEnd")
        animationStrokeEnd.fromValue = 0.5 * strokeRate
        animationStrokeEnd.toValue = 0.5 * strokeRate + 0.5
        animationStrokeEnd.duration = 2
        animationStrokeEnd.repeatDuration = .infinity
        borderLayer.add(animationStrokeEnd, forKey: nil)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        borderLayer.frame = bounds
        
        // 解説.1 解説.2
        let rect = CGRect(x: lineWidth/2, y: lineWidth/2, width: bounds.width - lineWidth, height: bounds.height - lineWidth)
        let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius)
        path.append(UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius))
        borderLayer.path = path.cgPath
    }
    
    // 解説.4
    override var intrinsicContentSize: CGSize {
        var size = super.intrinsicContentSize
        size.width += 20
        size.height += 6
        return size
    }
}
```

# 解説

## 1. 二重のパス

この中で何が工夫されてるかというと、 `UIBezierPath` で同じパスを二重に書いてるところです。

### 問題点

`strokeStart` と `strokeEnd` で線の開始位置を終了位置を指定できます。
この値を変化させることでアニメーションをしようとしています。

順当に書くと、とあるところを起点に、時計回りに 0.0 ~ 1.0 になります。 
`strokeStart < strokeEnd` となるように 0.0 ~ 1.0 の範囲で指定することを考えると、起点 `0` をまたぐ線は引くことができません。
<img src="https://qiita-image-store.s3.amazonaws.com/0/42826/5d3320e6-b9c3-f2ed-90c5-e8dcf7e1faf1.png" width=300><img src="https://qiita-image-store.s3.amazonaws.com/0/42826/063b0004-2a4b-a879-d290-6801028adc04.png" width=300>

### 解決策

二重にパスを引くことで、一周を 0.0 ~ 0.5 とします。そうすることで、0.4 ~ 0.7 といったような、起点 `0` をまたぐ線を描けます。
<img src="https://qiita-image-store.s3.amazonaws.com/0/42826/de1db55b-9b11-81a5-d610-33a84053480c.png" width=300><img src="https://qiita-image-store.s3.amazonaws.com/0/42826/15ce70ec-1be5-0a7e-091d-8ed31eecc88c.png" width=300>

## 2. パスを少し内側に

パスを中心に線は引かれるので、
`UIBezierPath(roundedRect: self.bounds, cornerRadius: cornerRadius)`
のパスに線を描くと、 `lineWidth` の半分はビューをはみ出ることになります。
なので `lineWidth / 2` の分だけ内側にパスを描きます。

## 3. zPosition

値が大きいほど手前に表示されます。
基本的に全てのレイヤーの `zPosition` はデフォルト `0` となっています。
Viewのサイズが変更されると `borderLayer` が埋もれてしまうようなので、`1` を指定しておきます。

## 4. UILabel の padding

この記事で伝えたいことから少しずれてしまうので、下記の記事をご参照にどうぞ。
https://qiita.com/ryokosuge/items/eb64ed8e6d718378f770

## その他

`CAShapeLayer` ,  `CABasicAnimation` , `UIBezierPath` 等の知識を前提に書いています。
コードからでも Interface Builder 上からでも問題なく使用でき、
テキスト変更などによるサイズ変更にも追従できるよう設計しています。

細かなカスタマイズ性の拡充など、ご自由にどうぞ！

# 環境

- Swift 4.2