【Swift】ScrollViewがカクつく時の対処法

下記ブログの転載です！
https://rc-code.info/ios/post-273/

iOSにて、**`UIScrollView`** のカクつきを改善する必要があったので備忘録。
今回は **`UIScrollViewDelegate.scrollViewDidScroll`** を利用している時の対処法を紹介します。
&nbsp;


## UIScrollViewがカクつく理由

まず *UIScrollView* に限らず描画がカクつく場合、その理由は `mainthread` を圧迫していることがほとんどです。

`mainthread` については、[こちら](https://rc-code.info/?p=282)を参考にしていただければと思います。

## scrollViewDidScroll内の処理を見直す

さて、今回解決したいのは `UIScrollView` のカクつきですが、`scrollViewDidScroll` という `UIScrollView.Delegate` の処理が重たくなりすぎているというケースがよく見られます。

後述する対策は `scrollViewDidScroll` だけでなく、他の `UIScrollView.Delegate` の関数にも有効な場合があるので、参考にしてみてください。

### UIScrollView のカクつきを再現する負荷試験

下記は　`extension` で `scrollViewDidScroll` を定義したサンプルになります。

```swift


extension ViewController: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
            
        var count: Int = 0
        var array: [Int] = []
        repeat {
            array.append(count)
            array.reverse()
            count += 1
        } while(count < 200)
    }
}
```

上記のソースでは、スクロールされる度に 0〜199 までの数字を配列に格納してはリバースするという再帰処理を行なっています。

count を array に格納しリバース、countに+1 (array=[0]、count=1 になる)
count を array に格納しリバース、countに+1 (array=[1,0]、count=2 になる)
　　以下、199まで繰り返し、、、
	
この処理が少しでもスクロールされる度に行われるので、なかなか重いタスクとなります。
`scrollViewDidScroll` という関数は、`スクロール` というUI操作に紐づいているので、この重たい処理は `mainthread` にて処理されてしまいます。
つまり、`scrollViewDidScroll` 関数内に負荷がかかると画面更新に影響するということです。
実際にこの処理を加えてUIScrollViewをスクロールしていただくと、カクつくことが分かると思います。

### カクつきを解消する方法

```swift

class ViewController: UIViewController {
    
		//スクロール処理用Queue
    private let scrollSessionQueue = DispatchQueue(label: "ScrollSessionQueue")
		
		...以下省略
}

extension ViewController: UIScrollViewDelegate {
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        // mainthread を圧迫しないように、別スレッドで処理
        scrollSessionQueue.async {
            
            var count: Int = 0
            var array: [Int] = []
            repeat {
                array.append(count)
                array.reverse()
                count += 1
            } while(count < 200)
        }
    }
}
```

上記のソースでは、`ViewController` に `scrollLogSessionQueue` というQueueを新たに呼び出しています。

そして `scrollViewDidScroll` 内の処理を `scrollSessionQueue.async` で囲むことで、処理を `mainthread` から別threadに移譲しています。
すなわち、処理は `mainthread` ではなく、UIに関連しない別の `thread` にて処理されることになります。

実際に動かしてみると、スクロールが滑らかになっていることが分かると思います。
`DispatchQueue` の詳しい生成方法については [こちら](https://developer.apple.com/documentation/dispatch/dispatchqueue/2300059-init) を参考にしていただければと思います。


### 修正後の注意点

ただ、別threadにて処理を行う際には注意が必要です。
主に挙げられる注意点は

 **①別threadにすれば100%画面更新に影響がなくなるわけではない
 ②別threadに移譲するので、実行タイミングがズレる**
 
という２点です。

①についてですが、 `scrollLogSessionQueue.async` で別スレッドを使ってもCPUやGPUが圧迫されることは免れませんし、別スレッド内の処理によっては再度 `mainthread` への移譲が行われるという事もあり得ます。
なので100%影響がなくなるというわけではありません。
サンプルコード `scrollLogSessionQueue.async` の中で行われている処理の負荷を上げていくと、この事が分かるかと思います。
 
②について、指定したthreadに処理を移譲するので、当然ながら処理は画面と同期しません。
画面的にはスムーズにスクロールしたり、スクロールが既に完了していても、`scrollLogSessionQueue.async` 内の処理が同様に完了しているとは限らないので、注意が必要です。
&nbsp;

## 検証Playground

[検証Playground Git](https://github.com/rickeycode/UINSamplePlaygrounds/tree/master/UIScrollView/ThreadUpdate)
&nbsp;

## 検証環境
**Mac:** 10.14.4
**XCode:** 10.2
**Swift:** 5.0
&nbsp;

## 参考ドキュメント
[公式 DispatchQueue ドキュメント](https://developer.apple.com/documentation/dispatch/dispatchqueue)
[公式 UIScrollView ドキュメント](https://developer.apple.com/documentation/uikit/uiscrollview)
&nbsp;
&nbsp;