DEV Community

Cover image for Creating a Drawing App using Pencil Kit and Core Data in SwiftUI
Haaris Iqubal for Recoding

Posted on • Originally published at Medium

Creating a Drawing App using Pencil Kit and Core Data in SwiftUI

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.

Drawing App SwiftUI

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>
...
}
Enter fullscreen mode Exit fullscreen mode

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")                        })                    }

Enter fullscreen mode Exit fullscreen mode

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)                    })
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"))}
}
Enter fullscreen mode Exit fullscreen mode

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")            
}))
Enter fullscreen mode Exit fullscreen mode

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)                    })

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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        
}    
}
}
Enter fullscreen mode Exit fullscreen mode

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())    
}}
Enter fullscreen mode Exit fullscreen mode

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 
}}
Enter fullscreen mode Exit fullscreen mode

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 👇

Discussion (0)