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

`Realm`（レルム）にお絵かき機能で描いた線を保存して取り出す方法を説明します。

お絵かきアプリの作り方はググるととても分かりやすいサイトが出てきたのですが、
それを画像では無く、後から編集できる状態でRealmに保存して取り出す方法が分からず試行錯誤しました。

一応保存はできるようになりましたが、
我流なところもあると思いますので、
間違いや他に良いやり方があればぜひご教授ください。

#この記事でわかること
・Realmを使って手書きの線を１本保存して、それを取り出す方法


#動作確認環境など
・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

#本編スタート

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

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

```swift: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]()
}
```
ファイル構成です
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/b6811bf7-57ea-3c4a-8628-13a304cede6a.png">

##ストーリーボードでViewを追加
次にUIViewをストーリーボードに追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/4b6901fe-e383-2352-4f2e-a0fa0411c1cb.png)

クラスを`DrawView`にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/35966bba-307f-c7af-b478-4d98fc24c090.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/e7acfbb6-7743-53c4-28dc-ec66a7c9bb90.png)

線の保存に使う`Save`ボタンと取り出しに使う`Retrieve`ボタンを作り、
適当に制約をつけます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/7080f081-3379-1695-58cb-f22e9ac46e05.png)



それぞれのボタンのIBActionも設定します。`saveButtonPressed`と`retrieveButtonPressed`という名前にします。TypeはUIButtonにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/3305a6c1-800c-1585-5f26-35c1ebace4df.png)


##お絵かき機能完成
ここまででお絵かき機能は追加できると思います。シミュレーターで確認してください。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/fd2cf8cd-574a-8dad-397f-b01ccd0e35e9.png">


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

#Realmの設定を行う

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

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

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

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


```swift: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/6da2be1e-8b82-3ee4-66eb-c52e42fc5bfb.png)

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

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

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

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/475ef6e3-79f5-8a4b-58f7-e343e9427364.png">

`CGPoint`は`CGFloat型のx`と`CGFloat型のy`というプロパティを持っていますね。

先ほどのデバックエリアの表示にラベルをつけて見やすくするとこんな感じに書けると思います。

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

```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座標を持ちます。
線が１に対して、点は多数、つまり１対多の関係となります。

親となる`Line`クラスは`ID`と`points`というプロパティを持ちます。

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

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

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

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

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

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

```swift: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を足した点を追加しています。

```swift
  //newDrawingのpointsプロパティにCGFloat型の座標を入れる
        newDrawing.points.append(CGPoint(x: xInCGFloat, y: yInCGFloat))
        
        //点を表示させるためにx座標に1を追加したコードも入れる
        newDrawing.points.append(CGPoint(x: xInCGFloat + 1, y: yInCGFloat))
```

シミュレーターを起動して線を書いてみます。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/f8d34164-505d-0a02-a4d4-6f435f92a307.png">

saveボタンを押して保存しましょう。
RealmStudioで確認してみると、きちんと点の座標が保存されたようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/f0e5387c-bad0-79b9-c5c7-67ef29d2fadb.png)

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

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/ff2c5821-86f5-f1a3-4bc1-febf4d4230e2.png">

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

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

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

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

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

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/8addfeb7-8081-ff7b-7b6f-7ffbc14c116d.png">


Saveボタンを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/b6e384a0-b9ce-d0ad-9ace-51d0e8e20977.png)

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

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

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1402909/c44fffc7-2f6c-cbcb-bda2-79e0fccb523e.png">

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

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

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