Building app based on Apple Pencil require much cumbersome process but since the introduction of PencilKit in WWDC 20 it now easier to add Canvas and Apple Pencil Support to your app. In this article we gonna take a look Apple PencilKit in action along with we’ll also add core data functionality to store our drawing inside the device and for creating the interface we will using Swift UI.
Our application look somewhat like this after we complete the code.
In order to create this application new start a new Xcode Project. Select iOS App and then select Application icon. Then type your project name and check out we are using fully SwiftUI with SwiftUI Lifecycle and we also use core data so check mark on it also. Then create the project. Xcode automatically create some boilerplate codes so lets just delete some codes.
Then inside our xc data model file we need to create a new entity called as Drawing. Then inside it we need to create attributes like ID which of type U U I D. Then Title which is of type String. And canvas Data which is of type Binary Data this will hold our drawing data.
Then inside our Content View file, we need to fetch the data from the core data using Fetch Request function for this you should need to import Core Data inside the function we need to add our entity which we just created and then we can add variable as drawing which help to refer inside our view this is of type Fetched Result which hold our Drawing Entity.
import CoreData
struct ContentView: View {
...
@FetchRequest(entity: Drawing.entity(), sortDescriptors: []) var drawings: FetchedResults<Drawing>
...
}
Then inside our View we can crate For Each function this will take our fetched drawings as input and for each drawing it will layout then as Text which will have title as a value.
ForEach(drawings){drawing in NavigationLink(destination: DrawingView(id: drawing.id, data: drawing.canvasData, title: drawing.title), label: { Text(drawing.title ?? "Untitled") }) }
Then we need to create a Button this button will help us to open a sheet view inside which we create a new canvas for the label we add H Stack inside it we add image and text
Button(action: {
self.showSheet.toggle()
}, label: {
HStack{
Image(systemName: "plus")
Text("Add Canvas")}
})
.foregroundColor(.blue)
.sheet(isPresented: $showSheet, content: { AddNewCanvasView().environment(\.managedObjectContext, viewContext) })
Then for action we just need to toggle our show sheet state which we need to create as State private var show sheet which have initial value as false
@State private var showSheet = false
Create a new SwiftUI File as Add New Canvas View. Inside the Canvas View we need to require Environment managed Object Context and Presentation Mode from previous view.
// AddNewCanvasView.Swift
@Environment (\.managedObjectContext) var viewContext @Environment (\.presentationMode) var presentationMode
Then we wrap our view inside Navigation View and add Form inside it. Then add section inside it add Text Field. Create a State for our Text Field which will be empty string.
// AddNewCanvasView.Swift
@State private var canvasTitle = ""
var body: some View {
NavigationView{
Form{
Section{
TextField("Canvas Title", text: $canvasTitle)
}
}
.navigationViewStyle(StackNavigationViewStyle()) .navigationTitle(Text("Add New Canvas"))}
}
Then we add Navigation Bar Item both will have Button on the leading we add cancel button this will does action presentation Mode wrapped value dismiss this will close our sheet then add button image as label. While on the trailing side we add create button this will perform saving task. Inside its action we check if the canvas title is not empty the we perform this function. Inside the function we create our drawing context then provide the appropriate data to it then we wrapped our sheet using dismiss function.
// AddNewCanvasView.Swift
Form{
Section{
TextField("Canvas Title", text: $canvasTitle) } }
.navigationViewStyle(StackNavigationViewStyle()) .navigationTitle(Text("Add New Canvas")) .navigationBarItems(leading: Button(action: { presentationMode.wrappedValue.dismiss() }, label: { Image(systemName: "xmark") }),
trailing: Button(action: {
if !canvasTitle.isEmpty{
let drawing = Drawing(context: viewContext) drawing.title = canvasTitle
drawing.id = UUID()
do {try viewContext.save()}
catch{print(error)} self.presentationMode.wrappedValue.dismiss() } }, label: {
Text("Save")
}))
Then on the Content View file we add our Add New Canvas View inside our sheet content.
// ContentView.swift
.sheet(isPresented: $showSheet, content: { AddNewCanvasView().environment(\.managedObjectContext, viewContext) })
So finally we can see that our data is saving now let tackle the our Canvas for Drawing. We need to create new Folder called as Canvas inside it create a Swift File and named it as Drawing Canvas View Controller. Inside the File import Swift Ui and Pencil Kit.
import SwiftUI
import PencilKit
Then create the class same as file name the class will conform to UI View Controller. Then we create some variable canvas which conform to PK Canvas View Class inside the we create a constant view which will initialize the Pk Canvas View class.
Then our view will have input policy as any input. Then we can add ability to zoom our canvas to only 1 so we can not zoom in our using pinch gesture. Then finally we negate the translate Auto resizing of the constraint. Then we will return our View. Then create new variable tool Picker which conform to Pk Tool Picker. Then create a constant tool pick which initialize Pk Tool Picker Class . We set the tool picker observer to self.
As all the view controller have View Did Load Function which will load on view start we add this to super view Did Load function. Before it we need to create the a drawing Data which will initialize our canvas with data. Then then create a variable drawing change which will conform to Data and return to Void data.
// DrawingCanvasViewController.swift
class DrawingCanvasViewController: UIViewController {
lazy var canvas: PKCanvasView = {
let view = PKCanvasView()
view.drawingPolicy = .anyInput
view.minimumZoomScale = 1
view.maximumZoomScale = 1 view.translatesAutoresizingMaskIntoConstraints = false
return view }()
lazy var toolPicker: PKToolPicker = {
let toolPicker = PKToolPicker()
toolPicker.addObserver(self)
return toolPicker }()
var drawingData = Data()
var drawingChanged: (Data) -> Void = {_ in}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(canvas)
NSLayoutConstraint.activate([ canvas.leadingAnchor.constraint(equalTo: view.leadingAnchor), canvas.trailingAnchor.constraint(equalTo: view.trailingAnchor), canvas.topAnchor.constraint(equalTo: view.topAnchor), canvas.bottomAnchor.constraint(equalTo: view.bottomAnchor)]) toolPicker.setVisible(true, forFirstResponder: canvas) toolPicker.addObserver(canvas)
canvas.delegate = self
canvas.becomeFirstResponder()
if let drawing = try? PKDrawing(data: drawingData){ canvas.drawing = drawing
}
}
}
We need to extent our Drawing Canvas View Controller Delegate to Pk Tool Picker Observer and PK Canvas View Delegate. Inside it we add out canvas View Drawing Did Change which will take care of all the changes happen inside the view and gives us output which help to know that data has change so we can save the data on the go. Inside the this function we add out Drawing Changed variable inside it will take our canvas drawing data.
// DrawingCanvasViewController.swift
extension DrawingCanvasViewController:PKToolPickerObserver, PKCanvasViewDelegate{
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) { drawingChanged(canvasView.drawing.dataRepresentation())
}}
Then create a new Swift which will contain our Canvas View as UI Representable View.Then create a struct which is conform to UI View Controller Representable. Inside it we will create our managed Object Context as view Context. Then we create some boiler plate function update Ui View and make UI View. Inside the update UI View Controller we set our Ui view controller drawing data to data variable.
Then create a struct which is conform to UI View Controller Representable. Inside it we will create our managed Object Context as view Context. Then we create some boiler plate function update Ui View and make UI View. Inside the update UI View Controller we set our Ui view controller drawing data to data variable. We make our UI Controller Type as Type alias of Drawing Canvas View Controller. Then we create some variable Data and ID which data require more upper hierarchy view. Then inside our make UI View Controller we can set our View controller to Drawing Canvas View Controller
Inside it we make request to NS Fetch Request then put inside our Drawing Entity which equals to Drawing dot fetch function. The we can predicate which will matches the id we provide to id inside our core data and then set our NS Fetch request to predicate. Then inside our do try closure we get our result. The result will output as object we set first value which we will get. Then we save the value of only the canvas Data attribute. Then again inside the do try closure we save our changes. Or we catch any error print that error. Then finally return our view controller.
// DrawingCanvasView.swift
import SwiftUI
import CoreData
import PencilKit
struct DrawingCanvasView: UIViewControllerRepresentable { @Environment(\.managedObjectContext) private var viewContext
func updateUIViewController(_ uiViewController: DrawingCanvasViewController,context: Context) { uiViewController.drawingData = data }
typealias UIViewControllerType = DrawingCanvasViewController
var data: Data
var id: UUID
func makeUIViewController(context: Context) -> DrawingCanvasViewController {
let viewController = DrawingCanvasViewController()
viewController.drawingData = data viewController.drawingChanged = {data in let request: NSFetchRequest<Drawing> = Drawing.fetchRequest()
let predicate = NSPredicate(format: “id == %@”, id as CVarArg) request.predicate = predicate do{ let result = try
viewContext.fetch(request) let obj = result.first
obj?.setValue(data, forKey: “canvasData”)
do{ try viewContext.save() } catch{ print(error) } }
catch{ print(error) } }
return viewController
}}
Finally we add our DrawingCanvasView.swift inside our Drawing View Swift file. And when we run our app inside the simulator we can see that our app is working fine.
Code and Video are mentioned below 👇
Top comments (0)