こんにちは、アイスタイルでiOS @cosmeの開発をしている小笠原です。
普段はチームリーダーとして働いています。
趣味ではFF14のiOS版タイマーアプリをほそぼそと開発しています。
この記事は アイスタイル Advent Calendar 2019 8日目の記事です。
PencilKitについて
今回はiOS13から使えるPencilKitを使い、ページャーつきのお絵かきアプリを作ってみましたので、こんなときはどうしたらいいの?というときのヒントになれば幸いです。
サンプルコード
こちらにサンプルアプリを用意しました。
このアプリでできること
以下の項目はページ単位で指定が可能です
1. 単一の画面内で絵を書きながらページ移動
2. ページ追加
3. キャンバスに背景色/背景画像を指定
4. キャンバスの描画情報と背景色/背景画像を合成して、1枚の画像に変換
5. キャンバスのデータをDataに変換
6. 他で生成されたキャンバス情報をロードして画面に表示(Undo/Redo状態も維持)
7. キャンバス上のスクロールに対応
8. ToolPickerの出し入れ
9. ページごとに編集・閲覧モードにできる
単一の画面内で絵を書きながらページ移動
画面に前と次へというボタンを追加して、それぞれでキャンバスを切り替える場合にはPKDrawing
を配列で持たせ、ボタンが押されるたびにPKCanvasView.drawing
へ設定することで、ページごとのキャンバスの状態を復元できます。
var pageDraws: [Draw] = []
private func prevDrawing() {
saveCurrentPage()
if currentPageIndex > 0 {
currentPageIndex -= 1
if let prevDrawingData = pageDraws[currentPageIndex].drawingData, let prevDrawing = try? PKDrawing(data: prevDrawingData) {
canvasView.drawing = prevDrawing
}
}
updateUI()
}
private func nextDrawing() {
saveCurrentPage()
if currentPageIndex < maxPageNum - 1 {
currentPageIndex += 1
if let nextDrawingData = pageDraws[currentPageIndex].drawingData, let nextDrawing = try? PKDrawing(data: nextDrawingData) {
canvasView.drawing = nextDrawing
}
}
updateUI()
}
ページ追加
ページ追加はページ切り替えと同じで、PKDrawingを配列に追加した後、PKCanvasView.drawing
に設定することで実現できます。
addPageButton.rx.tap.asDriver().drive(onNext: { [weak self] in
let draw = Draw()
draw.thumbnailImage = UIImage(named: "thumbnail")
// draw.backgroundColor = .red
self?.appendPage(draw)
}).disposed(by: disposeBag)
private func appendPage(_ draw: Draw) {
saveCurrentPage()
pageDraws.append(draw)
changePage(at: pageDraws.endIndex - 1)
}
private func changePage(at index: Int) {
templateImageView?.image = pageDraws[index].thumbnailImage
if let drawingData = pageDraws[index].drawingData, let drawing = try? PKDrawing(data: drawingData) {
canvasView.drawing = drawing
} else {
canvasView.drawing = PKDrawing()
}
currentPageIndex = index
}
キャンバスに背景色/背景画像を指定
背景色を設定する場合はPKCanvasView.Opaque
をfalse
にした上で、backgroundColor
を指定すると反映されます。
背景画像の指定は標準で用意されていないので、UIImageView
をPKCanvasView
にaddSubView
することで背景画像を表示した状態で描くことができます。
var canvasViewBackgroundColor: UIColor = .clear {
didSet {
if pageDraws[currentPageIndex].thumbnailImage != nil {
canvasView.backgroundColor = UIColor.clear
} else {
canvasView.backgroundColor = self.canvasViewBackgroundColor
}
}
}
private func initUI() {
if let firstDrawing = pageDraws.first {
canvasViewBackgroundColor = firstDrawing.backgroundColor
templateImageView?.image = firstDrawing.thumbnailImage
}
if let firstDrawingData = pageDraws.first?.drawingData, let firstDrawing = try? PKDrawing(data: firstDrawingData) {
canvasView.drawing = firstDrawing
}
templateImageView = UIImageView()
templateImageView?.clipsToBounds = true
templateImageView?.contentMode = .scaleAspectFill
if let defaultThumbnailImageView = templateImageView {
canvasView.addSubview(defaultThumbnailImageView)
canvasView.sendSubviewToBack(defaultThumbnailImageView)
}
canvasView.allowsFingerDrawing = true
}
キャンバスの描画情報と背景色/背景画像を合成して、1枚の画像に変換
背景色の場合はPKCanvasView
そのものを画像に変換するだけです。
背景画像と描画情報を合成して1枚の画像に変換する場合には、CGContext
上に背景画像と描画情報を合成して1枚にしています。
private func saveCurrentPage() {
pageDraws[currentPageIndex].drawingData = canvasView.drawing.dataRepresentation()
delay(0.3) { [weak self] in
guard let `self` = self else { return }
if let templateImage = self.pageDraws[self.currentPageIndex].thumbnailImage {
// PKCanvasView上のレンダリング情報とテンプレート画像を合成して一枚絵にする。
// キャンバスのスクロールが有効の場合にはScrollView全域を対象とするためにContenSizeを使用する
let contentSize = self.canvasView.contentSize
let contentRect = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
let drawImage = templateImage.composite(image: self.canvasView.drawing.image(from: contentRect, scale: 1.0))
self.pageDraws[self.currentPageIndex].imageData = drawImage.jpegData(compressionQuality: 0.7)
UIImageWriteToSavedPhotosAlbum(drawImage, self, #selector(self.didFinishSavingImage(_:didFinishSavingWithError:contextInfo:)), nil)
} else {
let drawImage = self.canvasView.getImage()
self.pageDraws[self.currentPageIndex].imageData = drawImage.jpegData(compressionQuality: 0.7)
UIImageWriteToSavedPhotosAlbum(drawImage, self, #selector(self.didFinishSavingImage(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
}
extension UIImage {
func composite(image: UIImage) -> UIImage {
UIGraphicsBeginImageContextWithOptions(self.size, false, 0)
draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
extension UIView {
func getImage() -> UIImage {
let rect = self.bounds
UIGraphicsBeginImageContextWithOptions(rect.size, false, 2.0)
let context: CGContext = UIGraphicsGetCurrentContext()!
layer.render(in: context)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}
キャンバスのデータをDataに変換
描画情報を他デバイス、サーバーに保存、ローカルストレージに保存できる形式に変換する場合にはPKDrawing.dataRepresentation()
を使います。
pageDraws[currentPageIndex].drawingData = canvasView.drawing.dataRepresentation()
他で生成されたキャンバス情報をロードして画面に表示(Undo/Redo状態も維持)
PKDrawing
をData
化したものはPKCanvasView.drawing
に設定できます。
そのため、デバイス間をまたがって描画情報を復元したり、APIで描画情報をDBに保存してから、好きなタイミングで取得して画面に状態ごと復元できます。
保存後に描いた線を消したりすることが簡単にできるようになるというのもメリットの一つです。
キャンバス上のスクロールに対応
PKCanvasView
はUIScrollView
を継承しているので、縦スクロールも可能になっています。
縦を大きく使う場合にはcontentSize
を調整することでスクロールしながら描くということができます。
var isEnabledScoll: Bool = false {
didSet {
canvasView.isScrollEnabled = self.isEnabledScoll
}
}
ToolPickerの出し入れ
ToolPickerの出し入れは以下のように実装しています
private func enableEditCanvas() {
canvasView.drawingGestureRecognizer.isEnabled = true // ペンでかけるようにする
if let window = UIApplication.shared.windows.first, let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
toolPicker.addObserver(self)
canvasView.becomeFirstResponder()
}
}
private func disableEditCanvas() {
canvasView.drawingGestureRecognizer.isEnabled = false // ペンでかけないようにする
if let window = UIApplication.shared.windows.first, let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.setVisible(false, forFirstResponder: canvasView)
toolPicker.removeObserver(canvasView)
toolPicker.removeObserver(self)
}
}
ページごとに編集・閲覧モードにできる
PKCanvasView
の大本はUIView
なので、Gesture.isEnabled
を切り替えることでできます。これとセットでToolPicker
の出し入れも制御すると閲覧・編集モードみたいなUIにできます。
canvasView.drawingGestureRecognizer.isEnabled = true // ペンでかけるようにする
参考にした記事
おわりに
これまで絵描き機能を作る際には一手間かかっていたものが、PencilKitを使うことにより、少ないコードでApple標準のメモ機能と同じことがアプリ内で簡単に実装できるようになったのは嬉しいことですね。
さらにサーバーにデータを保存して、アプリでロードした際に描いた情報を消すことができるというのも非常に便利でした。
最後まで読んでいただきありがとうございます。