シャクトリムシみたいにニョロっと動く、どっかで見たことありそうなProgressBar【Swift 5】

皆さんこんにちは。@Zhalen といいます。

読み方は「ツァーレン」です。今まで200記事くらいはてなブログの記事を書きましたが、誰も見なかったのでブログを消しました。これからは、これから作るこういったものに加えてその以前のブログに書いていたことも多分書きます。

この記事では、こんな感じでどっかで一度は見たことのありそうなプログレスバー（名前は不明）を実現します。私が現在制作途中のSNSアプリにこれを実装しました。

![index1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540507/718f7ca7-ddbd-7e57-ed07-3fa824c35d8f.gif)

#名前は「イモムシ進捗棒」です
お使いのプロジェクトに実装したい場合は、コードを丸ごとコピペして下さい。
GitHubをまだ使ったことがないので、ライブラリとして作成する方法がわかりません。なのでコピペになりますがご了承ください。

###カスタムクラスとして使用する場合

```swift
//MARK:- シャクトリムシProgressBar
enum Speed: Int {
    case slowest = 0
    case slow = 1
    case normal = 2
    case fast = 3
    case fastest = 4
}
class WormProgressBar: UIView {
    
    private let progressBarView: UIView = UIView()
    private var timer: Timer?
    
    var trackColor: UIColor = .lightGray
    var progressTintColor: UIColor = .black
    var duration: TimeInterval = 1
    var cornerRadius: CGFloat = 0
    var curveEase: (left: Speed, right: Speed) = (left: .slow, right: .fast)
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setView()
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setView() {
        
        if curveEase.left.rawValue >= curveEase.right.rawValue {
            fatalError("Left speed needs set at less than right one.")
        }
        
        self.addSubview(progressBarView)
        
        progressBarView.snp.makeConstraints { (make) in
            make.width.equalTo(0)
            make.height.equalToSuperview()
            make.top.equalToSuperview()
            make.left.equalToSuperview()
            make.bottom.equalToSuperview()
        }
        
        self.backgroundColor = .clear
        progressBarView.backgroundColor = .clear
        progressBarView.layer.cornerRadius = cornerRadius
    }
    
    func start() {
        self.backgroundColor = trackColor
        progressBarView.backgroundColor = progressTintColor
        progressBarView.layer.cornerRadius = cornerRadius
        
        let dt: CGFloat = 0.01 / CGFloat(duration)
        let w: CGFloat = self.frame.size.width
        var t: CGFloat = 0
        var m: CGFloat = 0
        var M: CGFloat = 0
        
        guard timer == nil else { return }
        timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { _ in
            t += dt ; t = t % 1
            m = w * self.leveledCurve(variable: t, level: self.curveEase.left)
            M = w * self.leveledCurve(variable: t, level: self.curveEase.right)
            
            self.progressBarView.snp.remakeConstraints { (make) in
                make.width.equalTo(M-m)
                make.height.equalToSuperview()
                make.top.equalToSuperview()
                make.left.equalTo(m)
                make.bottom.equalToSuperview()
            }
        })
    }
    private func leveledCurve(variable: CGFloat, level: Speed) -> CGFloat {
        switch level {
        case .slowest:
            return variable ** 4
        case .slow:
            return variable ** 2
        case .normal:
            return variable
        case .fast:
            return sqrt(variable)
        case .fastest:
            return sqrt(sqrt(variable))
        }
    }
    func end() {
        self.timer?.invalidate()
        self.timer = nil
        self.backgroundColor = .clear
        progressBarView.backgroundColor = .clear
        self.progressBarView.snp.remakeConstraints { (make) in
            make.width.equalTo(0)
            make.height.equalToSuperview()
            make.top.equalToSuperview()
            make.left.equalTo(0)
            make.bottom.equalToSuperview()
        }
    }
}

//一応汎用的に冪乗を定義したいため、効率の良い冪乗演算子を定義。可読性は無視。
infix operator ** : BitwiseShiftPrecedence
func ** (lhs: CGFloat, rhs:Int) -> CGFloat {
    var s = lhs, t = rhs, i = 1, A = [s]
    if t == 0 {s = 1}
    repeat {
        if i <= t / 2 {
            s *= s ; A.insert(s, at: 0) ; i *= 2
        } else {
            t -= i
            for j in 1..<A.count {
                i /= 2
                if i <= t {
                    s *= A[j] ; t -= i
                }
            }
        }
    } while t > 0
    
    return s
}
```

###エクステンションとして利用する場合は上記 + これ
```swift

extension UIView {
    func makeWormProgressBar() {
        let safeFrame: CGRect = self.safeAreaLayoutGuide.layoutFrame
        let wormProgressBar: WormProgressBar = WormProgressBar(
            frame: CGRect(
                x: safeFrame.minX-2.5,
                y: safeFrame.minY,
                width: safeFrame.width+5,
                height: 5
            )
        )
        let UTMI_random: (row: Int, column: Int) = upperTriangularMatrixIndex_random(n: 5)
        
        self.addSubview(wormProgressBar)
        
        wormProgressBar.trackColor = .clear
        wormProgressBar.progressTintColor = ThemeColor.normal
        wormProgressBar.cornerRadius = 2.5
        wormProgressBar.duration = Double.random(in: 0.5...2)
        wormProgressBar.curveEase = (left: Speed.init(rawValue: UTMI_random.row)!, right: Speed.init(rawValue: UTMI_random.column)!)
        wormProgressBar.start()
    }
    func hideWormProgressBar() {
        var wormProgressBar: WormProgressBar!
        self.subviews.forEach {
            if type(of: $0) == WormProgressBar.self { wormProgressBar = $0 as? WormProgressBar }
        }
        guard wormProgressBar != nil else { return }
        wormProgressBar.end()
        wormProgressBar.removeFromSuperview()
    }
}

//上三角行列のインデックス_ランダム
private func upperTriangularMatrixIndex_random(n: Int) -> (row: Int, column: Int) {
    let row: Int = Int.random(in: 0..<n-1)
    let section: Int = Int.random(in: row+1..<n)
    return (row: row, column: section)
}
```

#Usage
###カスタムクラスで使う場合の例
```swift
class ViewController: UIViewController {
    
    private var wormProgressBar: WormProgressBar!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        //１、フレームを指定します
        wormProgressBar = WormProgressBar(frame: CGRect(x: 0, y: 200, width: 100, height: 5))

        //２、背景色を指定します
        wormProgressBar.trackColor = .white

        //３、バーの色を指定します
        wormProgressBar.progressTintColor = .black

        //４、角丸の大きさを指定します
        wormProgressBar.cornerRadius = 2.5

        //５、何秒ごとにするか指定します
        wormProgressBar.duration = 1

        //６、末尾（左）のスピードと先端（右）のスピードを指定します。
        //もし左のスピードを右と同じかそれ以上に設定してしまうとfatalErrorになりま
        //すのでご注意ください。
        wormProgressBar.curveEase = (left: .slow, right: .fast)
    }
}
```

###エクステンションとして簡潔に使用する場合の例
実際に使用する場合は、こんな感じで簡単に作ったり消したりします。非同期処理とかで待ち時間を作っている場合には、本当に便利です。

```swift:UIViewController
self.view.makeWormProgressBar()//作る
self.view.hideWormProgressBar()//消す
```

これを用いた場合、```duration```や```curveEase```がランダムとなり、毎回異なる挙動を示します。
毎回同じだとなんとなく「<b>あれ？　こいつ本当は読み込みを反映してるフリしてるけどただの動く棒じゃん</b>」となりますがランダムにすることによって手抜きしている事がバレることはありません。

デフォルトではセーフエリアのトップに出現するようにしましたが、もし場所を変更したい場合はエクステンションのフレームを指定している部分で調整して下さい。



#仕組み

![スクリーンショット 2020-11-17 16.23.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540507/f3f64c63-ef89-f48e-a1fa-d1cc3f0739f8.png)

横軸を時間、縦軸を棒のleftMarginとしてそれぞれ標準化すると、このような図を考える事ができます。上記に```Speed```クラスがあると思いますが、
```.slowest```の時は 4√x、
```.slow```の時は √x、
```.normal```の時は x、
```.fast```の時は x^2
```.fastest```の時は x^4 
をそれぞれ表します。0.01秒ごとにその差分（＝長さ）を計算して反映させます。

従ってこの図で言うと、例えば赤は```wormProgressBar.curveEase = (left: .fast, right: .fastest)```の場合で、最初急激に長くなりその後緩やかに減少していきます。

こんな時もありますし、
![スクリーンショット 2020-11-17 16.42.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540507/593658f5-45e6-5b2f-c027-f95d4fde1250.png)

こんな時もあります。
![スクリーンショット 2020-11-17 16.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540507/619bb984-2f9b-92f1-4500-27a02489f85f.png)



#まとめ
今気がつきましたが、これ円弧にしないと本質的じゃないですね。計算量を考えると何か悪い予感がしますが、暇な時に改良します。

閲覧ありがとうございました。これ含めたこれからの５回くらいの記事は、今まで作ってきたいろんなやつをドバドバ書いていくと思います。