Realm
(レルム)にお絵かき機能で描いた線を保存して取り出す方法を説明します。
お絵かきアプリの作り方はググるととても分かりやすいサイトが出てきたのですが、
それを画像では無く、後から編集できる状態でRealmに保存して取り出す方法が分からず試行錯誤しました。
一応保存はできるようになりましたが、
我流なところもあると思いますので、
間違いや他に良いやり方があればぜひご教授ください。
#この記事でわかること
・Realmを使って手書きの線を1本保存して、それを取り出す方法
#動作確認環境など
・Xcode 12.4
・Realm 10.20.1
・MongoDB Realm Studio 11.1.0
・UIKit
・Storyboard
#この記事の対象読者
・RealmとRealmStudioの使い方がなんとなくわかる初心者の方
#事前にやっておいた方が良いこと
お絵かきの機能とRealmの基本的な使い方はあまり詳しく解説できませんので、
下記サイトなどを確認してみて、いろいろ試してみてください。
[お絵かきソフトの実装 〜 タッチ系イベント処理とベジェ曲線の描画]
https://swift-ios.keicode.com/ios/drawing-touch-events.php
[Realmを使ってTODOアプリを作ってみよう!/ Swift(Realmの使い方、初級編)]
https://qiita.com/pe-ta/items/616e0dbd364179ca284b
#本編スタート
#お絵かき機能の実装
ではまずはお絵かき機能を実装します。
##お絵かき機能用のファイルを新規作成する
今回は線の色などは保存しない、シンプルな形にします。
先ほどご紹介したお絵かきソフトの実装のサイト
のコードを少し編集して、
以下のようなコードにします。
import UIKit
class DrawView: UIView {
var currentDrawing: Drawing?
var finishedDrawings = [Drawing]()//ここに画面に描画される線のデータが入ります
var currentColor = UIColor.black
override func draw(_ rect: CGRect) {
for drawing in finishedDrawings {
drawing.color.setStroke()
stroke(drawing: drawing)
}
if let drawing = currentDrawing {
drawing.color.setStroke()
stroke(drawing: drawing)
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
currentDrawing = Drawing()
currentDrawing?.color = currentColor
currentDrawing?.points.append(location)
setNeedsDisplay()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first!
let location = touch.location(in: self)
currentDrawing?.points.append(location)
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if var drawing = currentDrawing {
let touch = touches.first!
let location = touch.location(in: self)
drawing.points.append(location)
finishedDrawings.append(drawing)
}
currentDrawing = nil
setNeedsDisplay()
}
func clear() {
finishedDrawings = []
setNeedsDisplay()
}
func refresh(){
setNeedsDisplay()
}
func stroke(drawing: Drawing) {
let path = UIBezierPath()
path.lineWidth = 10.0
path.lineCapStyle = .round
path.lineJoinStyle = .round
let begin = drawing.points[0];
path.move(to: begin)
if drawing.points.count > 1 {
for i in 1...(drawing.points.count - 1) {
let end = drawing.points[i]
path.addLine(to: end)
}
}
path.stroke()
}
}
struct Drawing {
var color = UIColor.black
var points = [CGPoint]()
}
##ストーリーボードでViewを追加
次にUIViewをストーリーボードに追加します。
ViewController
と先ほど作成したViewをOutlet接続
をしdrawView
という名前にします。
線の保存に使うSave
ボタンと取り出しに使うRetrieve
ボタンを作り、
適当に制約をつけます。
それぞれのボタンのIBActionも設定します。saveButtonPressed
とretrieveButtonPressed
という名前にします。TypeはUIButtonにします。
##お絵かき機能完成
ここまででお絵かき機能は追加できると思います。シミュレーターで確認してください。
ただこの状態だと、シミュレーターを終了すると線は消えてしまいます。
#Realmの設定を行う
##Realmのインストール
線のデータを保存するためにRealmをインストールします。
CocoaPods
等でRealmSwift
をインストールしてください。
先ほどご紹介したサイトなどを参考にしてやってみてください。
##何を保存すれば良いのか?
さてRealmも無事にインストールできたところで、何をRealmに保存すれば良いのかを考えてみましょう。
もちろん線を保存するためにやっているのですが、「線」
ってなんでしょう。
線の正体を探るために、手書きの線のデータをデバッグエリアに出力してみましょう。
線のデータはDrawView
クラスの中のfinishedDrawings
に入っているので、
print(finishedDrawings)
を追加して中をみてみます。
override func draw(_ rect: CGRect) {
for drawing in finishedDrawings {
drawing.color.setStroke()
stroke(drawing: drawing)
}
//これを追加
print(finishedDrawings)
if let drawing = currentDrawing {
drawing.color.setStroke()
stroke(drawing: drawing)
}
}
もう一度シミュレーターを実行して、線を書いてみましょう。
するとデバッグエリアに線のデータが表示されるはずです。
数字がたくさん表示されていますが、ちょっと分かりづらいので、
線のデータの設計図であるstruct Drawing
も確認しておきましょう。
DrawView.swift
の一番下に書いています。
struct Drawing {
var color = UIColor.black
var points = [CGPoint]()
}
points
という変数の中にCGPoint
の配列が入っていますね。
さらにしつこいようですがCGPoint
の定義も見てみましょう。
CGPoint
はCGFloat型のx
とCGFloat型のy
というプロパティを持っていますね。
先ほどのデバックエリアの表示にラベルをつけて見やすくするとこんな感じに書けると思います。
Drawing(
color: UIExtendedGrayColorSpace 0 1,
points: [CGPoint(x: 181.5, y: 60.5), CGPoint(x: 181.5, y: 62.0),//以下略
])
線はたくさんの点が集まったもので、
デバッグエリアの数字はそれぞれの点のX座標
とY座標
を表しています。
こんな短い線なのにたくさんの点でできているんですね。
Realmにはこの
線を構成する一つ一つの点のx座標とy座標
を保存します。
##RealmにCGFloat型を保存するのは非推奨
線を保存するための方針はかなり固まってきました。
ここで一つ残念なお知らせです。RealmではCGFloat型の保存は非推奨
となっています。
理由はググっていただければと思います。
CGFloat型のx座標とy座標をRealmに保存しようと思ったのに、それは非推奨。
ただ普通のFloat型の保存は問題ないようなので、
保存するときにCGFloat型をFloat型に変換して、Realmに保存し、
取り出す時はFloat型をCGFloat型に変換して線を描画することにします。
少し面倒ですがしょうがありません。
##RealmのModelを書く
座標を保存するためのRealmのModelはどうなるでしょう。
Model.swift
という名前でファイルを新規作成して、
こちらのコードを記述してください。
import Foundation
import RealmSwift
class Line: Object {
@objc dynamic var ID = UUID().uuidString
var points = List<Point>()
override static func primaryKey() -> String? {
return "ID"
}
}
class Point: Object {
@objc dynamic var xPosition: Float = 0.0
@objc dynamic var yPosition: Float = 0.0
let line = LinkingObjects(fromType: Line.self, property: "points")
}
少し詳しくみていきます。
一つの線は複数の点で構成されています。そしてそれぞれの点はx座標とy座標を持ちます。
線が1に対して、点は多数、つまり1対多の関係となります。
親となるLine
クラスはID
とpoints
というプロパティを持ちます。
ID
は線を1本だけ保存するときには必要ないかもしれませんが、
2本以上保存するときに必要になりますので、一応入れておきましょう。
ID
は自動で重ならないように採番されます。
points
プロパティはPoint
という子クラスの配列を持ちます。
Point
クラスはx座標を格納するxPosition
プロパティと
y座標を格納するyPosition
プロパティを持ちます。
これらはFloat型
にして初期値を設定しています。
最後のlineプロパティは親となるLineクラスへの関係を定義するものになります。
##ViewControllerにRealmのコードを追加
Modelが終わりましたので、ViewControllerにRealmを使用するためのコードを追加します。
まずは線の先頭にある点の座標を保存しましょう。
import UIKit
import RealmSwift //追加
class ViewController: UIViewController {
@IBOutlet var drawView: DrawView!
//追加 realmのインスタンスを生成
let realm = try! Realm()
override func viewDidLoad() {
super.viewDidLoad()
//Realmファイルの場所を確認するために追加
print(Realm.Configuration.defaultConfiguration.fileURL)
}
@IBAction func saveButtonPressed(_ sender: UIButton) {
//finishedDrawingに入っている一番最初の線のデータを取り出す。
if drawView.finishedDrawings.count == 0 {
//finishedDrawingsに線のデータがなければ何もしない
} else {
//まずはLineとPointのインスタンスを生成
let newLine = Line()
let newPoint = Point()
//最初の線の一つ目の点のx座標をCGFloat型から
//Float型に変換してnewPointのxPositionに入れる
newPoint.xPosition = Float(drawView.finishedDrawings[0].points[0].x)
//同じくy座標をCGFloat型からFloat型に変換して
//newPointのyPositionに入れる
newPoint.yPosition = Float(drawView.finishedDrawings[0].points[0].y)
//newLineオブジェクトのpointsプロパティにnewPointを追加
newLine.points.append(newPoint)
//newLineオブジェクトをRealmに保存
do {
try realm.write{
realm.add(newLine)
}
} catch {
print("Error saving in Realm")
}
}
//Realmに保存したら、一旦今表示されている線を削除。finishedDrawingsを空にする
drawView.clear()
}
@IBAction func retrieveButtonPressed(_ sender: UIButton) {
//今表示されてる線を削除する
drawView.clear()
//realmからデータを取り出す
let line = realm.objects(Line.self)
//新しくDrawingのインスタンスを生成
var newDrawing = Drawing()
//lineの中に入っているFloat型のxy座標をCGFloat型に変換する。
let xInCGFloat = CGFloat(line[0].points[0].xPosition)
let yInCGFloat = CGFloat(line[0].points[0].yPosition)
//newDrawingのpointsプロパティにCGFloat型の座標を入れる
newDrawing.points.append(CGPoint(x: xInCGFloat, y: yInCGFloat))
//点を表示させるためにx座標に1を追加したコードも入れる
newDrawing.points.append(CGPoint(x: xInCGFloat + 1, y: yInCGFloat))
//finishedDrawingにnewDrawingを追加する
drawView.finishedDrawings.append(newDrawing)
//viewを更新
drawView.refresh()
}
}
以下の部分はxy座標が一つだと点が表示されなかったので、
x座標にのみ1を足した点を追加しています。
//newDrawingのpointsプロパティにCGFloat型の座標を入れる
newDrawing.points.append(CGPoint(x: xInCGFloat, y: yInCGFloat))
//点を表示させるためにx座標に1を追加したコードも入れる
newDrawing.points.append(CGPoint(x: xInCGFloat + 1, y: yInCGFloat))
シミュレーターを起動して線を書いてみます。
saveボタンを押して保存しましょう。
RealmStudioで確認してみると、きちんと点の座標が保存されたようです。
次にRetrieveボタンを押すと無事に点が表示されました。
*Realmに保存されたデータは適宜RealmStudioで削除してください。
削除したいデータを選んでcontrolを押しながらクリックすれば削除できます。
##複数の点を保存して取り出す
1つの点の保存と取り出しができましたので、
複数の点の保存と取り出しをしましょう。
SaveとRetrieveのボタンを押した時のメソッドを変更します。
@IBAction func saveButtonPressed(_ sender: UIButton) {
//finishedDrawingに入っている一番最初の線のデータを取り出す。
if drawView.finishedDrawings.count == 0 {
//finishedDrawingsに線のデータがなければ何もしない
} else {
//Lineのインスタンスを生成
let newLine = Line()
//for文を追加 線を構成する全ての点の座標を順番に保存する
for i in 0..<drawView.finishedDrawings[0].points.count{
let newPoint = Point()
//最初の線の点のx座標をCGFloat型からFloat型に変換してnewPointのxPositionに入れる
newPoint.xPosition = Float(drawView.finishedDrawings[0].points[i].x)
//同じくy座標をCGFloat型からFloat型に変換してnewPointのyPositionに入れる
newPoint.yPosition = Float(drawView.finishedDrawings[0].points[i].y)
//newLineオブジェクトのpointsプロパティにnewPointを追加
newLine.points.append(newPoint)
}
//newLineオブジェクトをRealmに保存
do {
try realm.write{
realm.add(newLine)
}
} catch {
print("Error saving in Realm")
}
}
//Realmに保存したら、一旦今表示されている線を削除。finishedDrawingsを空にする
drawView.clear()
}
@IBAction func retrieveButtonPressed(_ sender: UIButton) {
//今表示されてる線を削除する
drawView.clear()
//realmからデータを取り出す
let line = realm.objects(Line.self)
//新しくインスタンスを生成
var newDrawing = Drawing()
for i in 0..<line[0].points.count{
//lineの中に入っているFloat型のxy座標をCGFloat型に変換する。
let xInCGFloat = CGFloat(line[0].points[i].xPosition)
let yInCGFloat = CGFloat(line[0].points[i].yPosition)
//newDrawingのpointsプロパティにCGFloat型の座標を入れる
newDrawing.points.append(CGPoint(x: xInCGFloat, y: yInCGFloat))
}
//点を表示させるためにx座標に1を追加したコードも入れる このコードは必要なくなったので削除
// newDrawing.points.append(CGPoint(x: xInCGFloat + 1, y: yInCGFloat))
//finishedDrawingにnewDrawingを追加する
drawView.finishedDrawings.append(newDrawing)
//viewを更新
drawView.refresh()
}
for-inでループさせることで、点の座標を順番に保存しています。
取り出す時もループで順番に取り出しています。
シミュレーターを起動させて、線を書いてみましょう。
点の座標が保存できましたね。
Retrieveボタンを押して戻しましょう
無事に取り出すことができました。
かなり長くなってしまいましたが、
無事に線を1本保存して取り出すことができました。
ぜひこれを参考に複数の線の保存などいろいろと試してみてください。