I have been working on an app that uses Pencil Kit and I am trying to store a drawing on the canvas into a sqlite3 database. In order to do so, I had to convert the drawing(type: Data) into an UnsafeRawPointer. However, after the conversion, when I try to access (print) the drawing via the pointer, it returns 0 bytes instead of 42 bytes. I have added some print statements and what they return in the code below, I hope that helps.
// Function that converts drawing data to UnsafeRawPointer
func dataToPtr(drawing: Data) -> UnsafeRawPointer {
let nsData = drawing as NSData
print(nsData) // shows 42 bytes
let rawPtr = nsData.bytes
print(rawPtr.load(as: Data.self))// Shows 0 bytes
return rawPtr
}
// Drawing before conversion
print(canvas.drawing) // Prints: 42 bytes
let drawingPtr = dataToPtr(drawing: canvas.drawing)
// Drawing when accessing the pointer
print(drawingPtr.load(as: Data.self)) // shows 0 bytes
I am a beginner in making iOS app and have struggle to understand pointers in swift. Thank you in advance.
Edit: The save drawing method:
func save(canvas: Canvas) {
connect()
// prepare
var statement: OpaquePointer!
// update the drawing given the row id
if sqlite3_prepare_v2(database, "UPDATE drawings SET drawing = ? WHERE rowid = ?", -1, &statement, nil) != SQLITE_OK {
print("Could not create (update) query")
}
// bind place holders
print("DRAWING SAVED: \(canvas.drawing)") // shows 42 bytes
let drawingPtr = dataToPtr(drawing: canvas.drawing)
sqlite3_bind_blob(statement, 1, drawingPtr, -1, nil)
sqlite3_bind_int(statement, 2, Int32(canvas.id))
// execute
if sqlite3_step(statement) != SQLITE_DONE {
print("Could not execute update statement")
}
// finalise
sqlite3_finalize(statement)
}
The method where I wanted to convert the pointer to data using .load():
// Function to check if canvas for a certain date is already in the database, if exists, return canvas
func check(selectedDate: Date) -> [Canvas] {
connect()
var result: [Canvas] = []
// prepare
var statement: OpaquePointer!
if sqlite3_prepare_v2(database, "SELECT rowid, date, drawing FROM drawings WHERE date = ?", -1, &statement, nil) != SQLITE_OK {
print("Could not create (select) query")
return []
}
// bind
sqlite3_bind_text(statement, 1, NSString(string:dateToStringFormat(dateDate: selectedDate)).utf8String, -1, nil)
// executes
while sqlite3_step(statement) == SQLITE_ROW {
// change string date into Date date
let Date_date = stringToDateFormat(stringDate: String(cString: sqlite3_column_text(statement, 1)))
// if canvas is not empty
if sqlite3_column_blob(statement, 2) != nil {
let drawingPtr = sqlite3_column_blob(statement, 2)
result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawingPtr!.load(as: Data.self)))
print("DRAWING NOT NIL")
}
else {
let drawing = Data.init()
result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawing))
print("DRAWING IS NIL")
}
}
// finalise
sqlite3_finalize(statement)
return result
}
What you need to do here is wrap your function body in a withUnsafeBytes
:
func save(canvas: Canvas) {
connect()
let drawingData = canvas.drawing.dataRepresentation()
drawingData.withUnsafeBytes { drawingBuffer in
let drawingPtr = drawingBuffer.baseAddress!
// ... In here you can use drawingPtr, for example:
sqlite3_bind_blob(statement, 1, drawingPtr, Int32(drawingBuffer.count), nil)
// ...
}
}
Inside of the withUnsafeBytes
block you must not refer to drawingData
itself. Outside of the block, you must not refer to drawingPtr
.
The point of withUnsafeBytes
is that it ensures there is a contiguous representation of the bytes (making copies if needed), and then provides you a pointer to those bytes that is valid for the duration of the block. This pointer is not valid outside of the block. You must not return it or let it otherwise escape. But within the block, you may use it as a void *
. This means you must make sure that sqlite3 does not store drawingPtr
past the end of this block, which is why you must put the withUnsafeBytes
around the entire prepare/finalize sequence, not just the bind_blob
statement.
As a rule, you cannot pass around UnsafeRawPointers to things that you did not allocate yourself. There is no promise that the thing they point to continues to exist when you think it does. In the case of Data, there isn't even a promise that it represents a single block of memory (Data may be backed from a dispatch_data, for example). The way that you access a Data's bytes is using withUnsafeBytes
.
Your check
function has some mistakes, as well. First, your NSString conversions are unnecessary. This line:
sqlite3_bind_text(statement, 1, NSString(string:dateToStringFormat(dateDate: selectedDate)).utf8String, -1, nil)
Can be written as just:
sqlite3_bind_text(statement, 1, dateToStringFormat(dateDate: selectedDate), -1, nil)
Swift will automatically convert String to C-string when passed to a C function that takes a char *
.
This code is simply wrong, and may be why you're getting zero bytes:
let drawingPtr = sqlite3_column_blob(statement, 2)
result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawingPtr!.load(as: Data.self)))
A pointer to a Blob is not a Data. You can't just load
it this way. You need to know how long it is. This is the code you need there:
// Get the pointer
let drawingPtr = sqlite3_column_blob(statement, 2)!
// Get the length
let drawingLength = Int(sqlite3_column_bytes(statement, 2))
// Copy the bytes into a new Data
let drawing = Data(bytes: drawingPtr, count: drawingLength)
// Now construct your Canvas.
result.append(Canvas(id: Int(sqlite3_column_int(statement, 0)), date: Date_date, drawing: drawing))