UITableViewのキャプチャ（UIImage）を取得する


# TL;DR
UITableViewのコンテンツ領域全体をキャプチャしてUIImageとして取得する方法です。Swift 3.0 で書いてます。

*同僚と話していて「UIViewのキャプチャは取れるけれど、UITableViewのコンテンツ全体のキャプチャって取れるんですかね？」みたいな話になって、ググっても出てこなかったので書いてみました。*

ソース全体はGistに上げています。
https://gist.github.com/YusukeHosonuma/5235d14f20077573c031dd780e2dbef0

# ソース

## UIView

まずは`UIView`のキャプチャを。

```swift
extension UIView {
    
    func captureImage() -> UIImage? {
        
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)

        guard let context: CGContext = UIGraphicsGetCurrentContext() else { return nil }
        
        self.layer.render(in: context)
        
        let capturedImage: UIImage? = UIGraphicsGetImageFromCurrentImageContext()
        
        UIGraphicsEndImageContext()
        
        return capturedImage
    }
}
```

## UITableView

次に`UITableView`のキャプチャ。

仕組みとしては難しいことはしていなくて、`captureImages`で`UITableView`の高さ分スクロールを繰り返してキャプチャを取って`UImage`の配列として返しています。

そして、`captureImage`で取得した`UIImage`の配列を1つの`UIImage`に合成しています。

```swift
extension UITableView {

    var contentBottom: CGFloat {
        return contentSize.height - bounds.height
    }
    
    override func captureImage() -> UIImage? {

        let images = captureImages()
        
        // Concatenate images
        
        UIGraphicsBeginImageContext(contentSize);
        
        var y: CGFloat = 0
        for image in images {
            image.draw(at: CGPoint(x: 0, y: y))
            y = min(y + bounds.height, contentBottom) // calculate layer diff
        }
        let concatImage = UIGraphicsGetImageFromCurrentImageContext();
        
        UIGraphicsEndImageContext();
        
        return concatImage
    }
    
    func captureImages() -> [UIImage] {
        
        var images: [UIImage?] = []
        
        while true {
            
            images.append(superview?.captureImage()) // not work in self.view
            
            if contentOffset.y < (contentBottom - bounds.height) {
                contentOffset.y += bounds.height
            } else {
                contentOffset.y = contentBottom
                images.append(superview?.captureImage()) // not work in self.view
                break
            }
        }
        
        return images.flatMap{ $0 } // exclude nil
    }
}
```

## 使用例

60個分のセルを持つ`UITableView`のコンテンツ領域をキャプチャして、カメラロールに保存しています。

```swift
class ViewController: UIViewController, UITableViewDataSource {
    
    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        if let image = self.tableView.captureImage() {
            UIImageWriteToSavedPhotosAlbum(image, self, #selector(didFinishWriteImage(_:error:contextInfo:)), nil)
        }
    }
    
    // MARK: - UIImageWriteToSavedPhotosAlbum
    
    func didFinishWriteImage(_ image: UIImage, error: NSError?, contextInfo: UnsafeMutableRawPointer) {
        if let error = error {
            print("Image write error: \(error)")
        }
    }

    // MARK: - UITableViewDataSource
    
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 60
    }
    
    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") else { return UITableViewCell() }
        cell.textLabel?.text = "Index: \(indexPath.row)"
        return cell
    }
}
```

以下のような感じでキャプチャがとれます。（長い・・・）
![IMG_0179.jpg](https://qiita-image-store.s3.amazonaws.com/0/29653/6a414201-f115-a78a-804b-61f84e4af81c.jpeg "IMG_0179.jpg")

# 終わり

どれだけ役に立つケースがあるか分かりませんが、参考になる人もいるかもと思って上げてみました。