下記はPencilKitのSampleソースからの引用である。
struct DataModel および class DataModelControllerはデータ保存・読込用structおよびclassである。このclass DataModelControllerを実装することで、キャンバスの描画を保存・読込できる。
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
The app's data model for storing drawings, thumbnails, and signatures.
*/
/// Underlying the app's data model is a cross-platform `PKDrawing` object. `PKDrawing` adheres to `Codable`
/// in Swift, or you can fetch its data representation as a `Data` object through its `dataRepresentation()`
/// method. `PKDrawing` is the only PencilKit type supported on non-iOS platforms.
/// From `PKDrawing`'s `image(from:scale:)` method, you can get an image to save, or you can transform a
/// `PKDrawing` and append it to another drawing.
/// If you already have some saved `PKDrawing`s, you can make them available in this sample app by adding them
/// to the project's "Assets" catalog, and adding their asset names to the `defaultDrawingNames` array below.
import UIKit
import PencilKit
import os
/// `DataModel` contains the drawings that make up the data model, including multiple image drawings and a signature drawing.
struct DataModel: Codable {
/// Names of the drawing assets to be used to initialize the data model the first time.
static let defaultDrawingNames: [String] = ["Notes"]
/// The width used for drawing canvases.
static let canvasWidth: CGFloat = 768
/// The drawings that make up the current data model.
var drawings: [PKDrawing] = []
var signature = PKDrawing()
}
/// `DataModelControllerObserver` is the behavior of an observer of data model changes.
protocol DataModelControllerObserver {
/// Invoked when the data model changes.
func dataModelChanged()
}
/// `DataModelController` coordinates changes to the data model.
class DataModelController {
/// The underlying data model.
var dataModel = DataModel()
/// Thumbnail images representing the drawings in the data model.
var thumbnails = [UIImage]()
var thumbnailTraitCollection = UITraitCollection() {
didSet {
// If the user interface style changed, regenerate all thumbnails.
if oldValue.userInterfaceStyle != thumbnailTraitCollection.userInterfaceStyle {
generateAllThumbnails()
}
}
}
/// Dispatch queues for the background operations done by this controller.
private let thumbnailQueue = DispatchQueue(label: "ThumbnailQueue", qos: .background)
private let serializationQueue = DispatchQueue(label: "SerializationQueue", qos: .background)
/// Observers add themselves to this array to start being informed of data model changes.
var observers = [DataModelControllerObserver]()
/// The size to use for thumbnail images.
static let thumbnailSize = CGSize(width: 192, height: 256)
/// Computed property providing access to the drawings in the data model.
var drawings: [PKDrawing] {
get { dataModel.drawings }
set { dataModel.drawings = newValue }
}
/// Computed property providing access to the signature in the data model.
var signature: PKDrawing {
get { dataModel.signature }
set { dataModel.signature = newValue }
}
/// Initialize a new data model.
init() {
loadDataModel()
}
/// Update a drawing at `index` and generate a new thumbnail.
func updateDrawing(_ drawing: PKDrawing, at index: Int) {
dataModel.drawings[index] = drawing
generateThumbnail(index)
saveDataModel()
}
/// Helper method to cause regeneration of all thumbnails.
private func generateAllThumbnails() {
for index in drawings.indices {
generateThumbnail(index)
}
}
/// Helper method to cause regeneration of a specific thumbnail, using the current user interface style
/// of the thumbnail view controller.
private func generateThumbnail(_ index: Int) {
let drawing = drawings[index]
let aspectRatio = DataModelController.thumbnailSize.width / DataModelController.thumbnailSize.height
let thumbnailRect = CGRect(x: 0, y: 0, width: DataModel.canvasWidth, height: DataModel.canvasWidth / aspectRatio)
let thumbnailScale = UIScreen.main.scale * DataModelController.thumbnailSize.width / DataModel.canvasWidth
let traitCollection = thumbnailTraitCollection
thumbnailQueue.async {
traitCollection.performAsCurrent {
let image = drawing.image(from: thumbnailRect, scale: thumbnailScale)
DispatchQueue.main.async {
self.updateThumbnail(image, at: index)
}
}
}
}
/// Helper method to replace a thumbnail at a given index.
private func updateThumbnail(_ image: UIImage, at index: Int) {
thumbnails[index] = image
didChange()
}
/// Helper method to notify observer that the data model changed.
private func didChange() {
for observer in self.observers {
observer.dataModelChanged()
}
}
/// The URL of the file in which the current data model is saved.
private var saveURL: URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths.first!
return documentsDirectory.appendingPathComponent("PencilKitDraw.data")
}
/// Save the data model to persistent storage.
func saveDataModel() {
let savingDataModel = dataModel
let url = saveURL
serializationQueue.async {
do {
let encoder = PropertyListEncoder()
let data = try encoder.encode(savingDataModel)
try data.write(to: url)
} catch {
os_log("Could not save data model: %s", type: .error, error.localizedDescription)
}
}
}
/// Load the data model from persistent storage
private func loadDataModel() {
let url = saveURL
serializationQueue.async {
// Load the data model, or the initial test data.
let dataModel: DataModel
if FileManager.default.fileExists(atPath: url.path) {
do {
let decoder = PropertyListDecoder()
let data = try Data(contentsOf: url)
dataModel = try decoder.decode(DataModel.self, from: data)
} catch {
os_log("Could not load data model: %s", type: .error, error.localizedDescription)
dataModel = self.loadDefaultDrawings()
}
} else {
dataModel = self.loadDefaultDrawings()
}
DispatchQueue.main.async {
self.setLoadedDataModel(dataModel)
}
}
}
/// Construct an initial data model when no data model already exists.
private func loadDefaultDrawings() -> DataModel {
var testDataModel = DataModel()
for sampleDataName in DataModel.defaultDrawingNames {
guard let data = NSDataAsset(name: sampleDataName)?.data else { continue }
if let drawing = try? PKDrawing(data: data) {
testDataModel.drawings.append(drawing)
}
}
return testDataModel
}
/// Helper method to set the current data model to a data model created on a background queue.
private func setLoadedDataModel(_ dataModel: DataModel) {
self.dataModel = dataModel
thumbnails = Array(repeating: UIImage(), count: dataModel.drawings.count)
generateAllThumbnails()
}
/// Create a new drawing in the data model.
func newDrawing() {
let newDrawing = PKDrawing()
dataModel.drawings.append(newDrawing)
thumbnails.append(UIImage())
updateDrawing(newDrawing, at: dataModel.drawings.count - 1)
}
}
下記ソースは上記を実装した、キャンバスを実装した描画ビューである。
/*
See LICENSE folder for this sample’s licensing information.
Abstract:
`DrawingViewController` is the primary view controller for showing drawings.
*/
///`PKCanvasView` is the main drawing view that you will add to your view hierarchy.
/// The drawingPolicy dictates whether drawing with a finger is allowed. If it's set to default and if the tool picker is visible,
/// then it will respect the global finger pencil toggle in Settings or as set in the tool picker. Otherwise, only drawing with
/// a pencil is allowed.
/// You can add your own class as a delegate of PKCanvasView to receive notifications after a user
/// has drawn or the drawing was updated. You can also set the tool or toggle the ruler on the canvas.
/// There is a shared tool picker for each window. The tool picker floats above everything, similar
/// to the keyboard. The tool picker is moveable in a regular size class window, and fixed to the bottom
/// in compact size class. To listen to tool picker notifications, add yourself as an observer.
/// Tool picker visibility is based on first responders. To make the tool picker appear, you need to
/// associate the tool picker with a `UIResponder` object, such as a view, by invoking the method
/// `UIToolpicker.setVisible(_:forResponder:)`, and then by making that responder become the first
/// Best practices:
///
/// -- Because the tool picker palette is floating and moveable for regular size classes, but fixed to the
/// bottom in compact size classes, make sure to listen to the tool picker's obscured frame and adjust your UI accordingly.
/// -- For regular size classes, the palette has undo and redo buttons, but not for compact size classes.
/// Make sure to provide your own undo and redo buttons when in a compact size class.
import UIKit
import PencilKit
class DrawingViewController: UIViewController, PKCanvasViewDelegate, PKToolPickerObserver, UIScreenshotServiceDelegate {
@IBOutlet weak var canvasView: PKCanvasView!
@IBOutlet var undoBarButtonitem: UIBarButtonItem!
@IBOutlet var redoBarButtonItem: UIBarButtonItem!
@IBOutlet var backLayerButton: UIBarButtonItem!
@IBOutlet var drawBarButtoniitem: UIBarButtonItem!
var toolPicker: PKToolPicker!
var signDrawingItem: UIBarButtonItem!
/// On iOS 14.0, this is no longer necessary as the finger vs pencil toggle is a global setting in the toolpicker
var pencilFingerBarButtonItem: UIBarButtonItem!
/// Standard amount of overscroll allowed in the canvas.
static let canvasOverscrollHeight: CGFloat = 500
/// Data model for the drawing displayed by this view controller.
var dataModelController: DataModelController!
/// Private drawing state.
var drawingIndex: Int = 0
var hasModifiedDrawing = false
// MARK: View Life Cycle
/// Set up the drawing initially.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Set up the canvas view with the first drawing from the data model.
canvasView.delegate = self
canvasView.drawing = dataModelController.drawings[drawingIndex]
canvasView.alwaysBounceVertical = true
// Set up the tool picker
if #available(iOS 14.0, *) {
toolPicker = PKToolPicker()
} else {
// Set up the tool picker, using the window of our parent because our view has not
// been added to a window yet.
let window = parent?.view.window
toolPicker = PKToolPicker.shared(for: window!)
}
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
toolPicker.addObserver(self)
updateLayout(for: toolPicker)
canvasView.becomeFirstResponder()
// Add a button to sign the drawing in the bottom right hand corner of the page
signDrawingItem = UIBarButtonItem(title: "Sign Drawing", style: .plain, target: self, action: #selector(signDrawing(sender:)))
navigationItem.rightBarButtonItems?.append(signDrawingItem)
// Before iOS 14, add a button to toggle finger drawing.
if #available(iOS 14.0, *) { } else {
pencilFingerBarButtonItem = UIBarButtonItem(title: "Enable Finger Drawing",
style: .plain,
target: self,
action: #selector(toggleFingerPencilDrawing(_:)))
navigationItem.rightBarButtonItems?.append(pencilFingerBarButtonItem)
canvasView.allowsFingerDrawing = false
}
// Always show a back button.
navigationItem.leftItemsSupplementBackButton = true
// Set this view controller as the delegate for creating full screenshots.
parent?.view.window?.windowScene?.screenshotService?.delegate = self
}
/// When the view is resized, adjust the canvas scale so that it is zoomed to the default `canvasWidth`.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let canvasScale = canvasView.bounds.width / DataModel.canvasWidth
canvasView.minimumZoomScale = canvasScale
canvasView.maximumZoomScale = canvasScale
canvasView.zoomScale = canvasScale
// Scroll to the top.
updateContentSizeForDrawing()
canvasView.contentOffset = CGPoint(x: 0, y: -canvasView.adjustedContentInset.top)
}
/// When the view is removed, save the modified drawing, if any.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Update the drawing in the data model, if it has changed.
if hasModifiedDrawing {
dataModelController.updateDrawing(canvasView.drawing, at: drawingIndex)
}
// Remove this view controller as the screenshot delegate.
view.window?.windowScene?.screenshotService?.delegate = nil
}
/// Hide the home indicator, as it will affect latency.
override var prefersHomeIndicatorAutoHidden: Bool {
return true
}
// MARK: Actions
/// Action method: Turn finger drawing on or off, but only on devices before iOS 14.0
@IBAction func toggleFingerPencilDrawing(_ sender: Any) {
if #available(iOS 14.0, *) { } else {
canvasView.allowsFingerDrawing.toggle()
let title = canvasView.allowsFingerDrawing ? "Disable Finger Drawing" : "Enable Finger Drawing"
pencilFingerBarButtonItem.title = title
}
}
/// Helper method to set a new drawing, with an undo action to go back to the old one.
func setNewDrawingUndoable(_ newDrawing: PKDrawing) {
let oldDrawing = canvasView.drawing
undoManager?.registerUndo(withTarget: self) {
$0.setNewDrawingUndoable(oldDrawing)
}
canvasView.drawing = newDrawing
}
/// Action method: Add a signature to the current drawing.
@IBAction func signDrawing(sender: UIBarButtonItem) {
// Get the signature drawing at the canvas scale.
var signature = dataModelController.signature
let signatureBounds = signature.bounds
let loc = CGPoint(x: canvasView.bounds.maxX, y: canvasView.bounds.maxY)
let scaledLoc = CGPoint(x: loc.x / canvasView.zoomScale, y: loc.y / canvasView.zoomScale)
signature.transform(using: CGAffineTransform(translationX: scaledLoc.x - signatureBounds.maxX, y: scaledLoc.y - signatureBounds.maxY))
// Add the signature drawing to the current canvas drawing.
setNewDrawingUndoable(canvasView.drawing.appending(signature))
}
// MARK: Navigation
/// Set up the signature view controller.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
(segue.destination as? SignatureViewController)?.dataModelController = dataModelController
}
// MARK: Canvas View Delegate
/// Delegate method: Note that the drawing has changed.
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
hasModifiedDrawing = true
updateContentSizeForDrawing()
}
/// Helper method to set a suitable content size for the canvas view.
func updateContentSizeForDrawing() {
// Update the content size to match the drawing.
let drawing = canvasView.drawing
let contentHeight: CGFloat
// Adjust the content size to always be bigger than the drawing height.
if !drawing.bounds.isNull {
contentHeight = max(canvasView.bounds.height, (drawing.bounds.maxY + DrawingViewController.canvasOverscrollHeight) * canvasView.zoomScale)
} else {
contentHeight = canvasView.bounds.height
}
canvasView.contentSize = CGSize(width: DataModel.canvasWidth * canvasView.zoomScale, height: contentHeight)
}
// MARK: Tool Picker Observer
/// Delegate method: Note that the tool picker has changed which part of the canvas view
/// it obscures, if any.
func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
updateLayout(for: toolPicker)
}
/// Delegate method: Note that the tool picker has become visible or hidden.
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
updateLayout(for: toolPicker)
}
/// Helper method to adjust the canvas view size when the tool picker changes which part
/// of the canvas view it obscures, if any.
///
/// Note that the tool picker floats over the canvas in regular size classes, but docks to
/// the canvas in compact size classes, occupying a part of the screen that the canvas
/// could otherwise use.
func updateLayout(for toolPicker: PKToolPicker) {
let obscuredFrame = toolPicker.frameObscured(in: view)
// If the tool picker is floating over the canvas, it also contains
// undo and redo buttons.
if obscuredFrame.isNull {
canvasView.contentInset = .zero
navigationItem.leftBarButtonItems = []
}
// Otherwise, the bottom of the canvas should be inset to the top of the
// tool picker, and the tool picker no longer displays its own undo and
// redo buttons.
else {
canvasView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: view.bounds.maxY - obscuredFrame.minY, right: 0)
navigationItem.leftBarButtonItems = [undoBarButtonitem, redoBarButtonItem]
}
canvasView.scrollIndicatorInsets = canvasView.contentInset
}
// MARK: Screenshot Service Delegate
/// Delegate method: Generate a screenshot as a PDF.
func screenshotService(
_ screenshotService: UIScreenshotService,
generatePDFRepresentationWithCompletion completion:
@escaping (_ PDFData: Data?, _ indexOfCurrentPage: Int, _ rectInCurrentPage: CGRect) -> Void) {
// Find out which part of the drawing is actually visible.
let drawing = canvasView.drawing
let visibleRect = canvasView.bounds
// Convert to PDF coordinates, with (0, 0) at the bottom left hand corner,
// making the height a bit bigger than the current drawing.
let pdfWidth = DataModel.canvasWidth
let pdfHeight = drawing.bounds.maxY + 100
let canvasContentSize = canvasView.contentSize.height - DrawingViewController.canvasOverscrollHeight
let xOffsetInPDF = pdfWidth - (pdfWidth * visibleRect.minX / canvasView.contentSize.width)
let yOffsetInPDF = pdfHeight - (pdfHeight * visibleRect.maxY / canvasContentSize)
let rectWidthInPDF = pdfWidth * visibleRect.width / canvasView.contentSize.width
let rectHeightInPDF = pdfHeight * visibleRect.height / canvasContentSize
let visibleRectInPDF = CGRect(
x: xOffsetInPDF,
y: yOffsetInPDF,
width: rectWidthInPDF,
height: rectHeightInPDF)
// Generate the PDF on a background thread.
DispatchQueue.global(qos: .background).async {
// Generate a PDF.
let bounds = CGRect(x: 0, y: 0, width: pdfWidth, height: pdfHeight)
let mutableData = NSMutableData()
UIGraphicsBeginPDFContextToData(mutableData, bounds, nil)
UIGraphicsBeginPDFPage()
// Generate images in the PDF, strip by strip.
var yOrigin: CGFloat = 0
let imageHeight: CGFloat = 1024
while yOrigin < bounds.maxY {
let imgBounds = CGRect(x: 0, y: yOrigin, width: DataModel.canvasWidth, height: min(imageHeight, bounds.maxY - yOrigin))
let img = drawing.image(from: imgBounds, scale: 2)
img.draw(in: imgBounds)
yOrigin += imageHeight
}
UIGraphicsEndPDFContext()
// Invoke the completion handler with the generated PDF data.
completion(mutableData as Data, 0, visibleRectInPDF)
}
}
@IBAction func backLayer(_ Sender: Any){
}
}
“Swift PencilKitを触ってみました” への3件のフィードバック
PKCanvasViewを実装することで、描画に関する処理を一任できる
PKCanvasViewを複数実装し、透過を管理し、描画層を管理すればトレース描画ができるかも
storyboardのコードをswiftuiにするときはUIViewRepresentableが使えます