3
4

More than 1 year has passed since last update.

Realm に手書きの線を一本保存して取り出す

Last updated at Posted at 2022-01-06

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

本編スタート

お絵かき機能の実装

ではまずはお絵かき機能を実装します。

お絵かき機能用のファイルを新規作成する

今回は線の色などは保存しない、シンプルな形にします。
先ほどご紹介したお絵かきソフトの実装のサイトのコードを少し編集して、
以下のようなコードにします。

DrawView.swift

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をストーリーボードに追加します。
image.png

クラスをDrawViewにします。
image.png

ViewControllerと先ほど作成したViewをOutlet接続をしdrawViewという名前にします。

image.png

線の保存に使うSaveボタンと取り出しに使うRetrieveボタンを作り、
適当に制約をつけます。
image.png

それぞれのボタンのIBActionも設定します。saveButtonPressedretrieveButtonPressedという名前にします。TypeはUIButtonにします。
image.png

お絵かき機能完成

ここまででお絵かき機能は追加できると思います。シミュレーターで確認してください。

ただこの状態だと、シミュレーターを終了すると線は消えてしまいます。

Realmの設定を行う

Realmのインストール

線のデータを保存するためにRealmをインストールします。
CocoaPods等でRealmSwiftをインストールしてください。
先ほどご紹介したサイトなどを参考にしてやってみてください。

何を保存すれば良いのか?

さてRealmも無事にインストールできたところで、何をRealmに保存すれば良いのかを考えてみましょう。

もちろん線を保存するためにやっているのですが、「線」ってなんでしょう。

線の正体を探るために、手書きの線のデータをデバッグエリアに出力してみましょう。
線のデータはDrawViewクラスの中のfinishedDrawingsに入っているので、
print(finishedDrawings)を追加して中をみてみます。

DrawView.swift
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)
        }
    }

もう一度シミュレーターを実行して、線を書いてみましょう。
するとデバッグエリアに線のデータが表示されるはずです。

image.png

数字がたくさん表示されていますが、ちょっと分かりづらいので、
線のデータの設計図であるstruct Drawingも確認しておきましょう。
DrawView.swiftの一番下に書いています。

DrawView.swift
struct Drawing {
    var color = UIColor.black
    var points = [CGPoint]()
}

pointsという変数の中にCGPointの配列が入っていますね。
さらにしつこいようですがCGPointの定義も見てみましょう。

CGPointCGFloat型のxCGFloat型の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という名前でファイルを新規作成して、
こちらのコードを記述してください。

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クラスはIDpointsというプロパティを持ちます。

IDは線を1本だけ保存するときには必要ないかもしれませんが、
2本以上保存するときに必要になりますので、一応入れておきましょう。
IDは自動で重ならないように採番されます。

pointsプロパティはPointという子クラスの配列を持ちます。

Pointクラスはx座標を格納するxPositionプロパティと
y座標を格納するyPositionプロパティを持ちます。
これらはFloat型にして初期値を設定しています。

最後のlineプロパティは親となるLineクラスへの関係を定義するものになります。

ViewControllerにRealmのコードを追加

Modelが終わりましたので、ViewControllerにRealmを使用するためのコードを追加します。

まずは線の先頭にある点の座標を保存しましょう。

ViewController.swift
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で確認してみると、きちんと点の座標が保存されたようです。

image.png

次にRetrieveボタンを押すと無事に点が表示されました。

*Realmに保存されたデータは適宜RealmStudioで削除してください。
削除したいデータを選んでcontrolを押しながらクリックすれば削除できます。

複数の点を保存して取り出す

1つの点の保存と取り出しができましたので、
複数の点の保存と取り出しをしましょう。

SaveとRetrieveのボタンを押した時のメソッドを変更します。

ViewController.swift
@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でループさせることで、点の座標を順番に保存しています。
取り出す時もループで順番に取り出しています。

シミュレーターを起動させて、線を書いてみましょう。

Saveボタンを押します
image.png

点の座標が保存できましたね。

Retrieveボタンを押して戻しましょう

無事に取り出すことができました。

かなり長くなってしまいましたが、
無事に線を1本保存して取り出すことができました。

ぜひこれを参考に複数の線の保存などいろいろと試してみてください。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4