2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【個人開発】実装紹介 -CoreDataをSQLで集計する方法- (Swift)

Last updated at Posted at 2022-09-26

はじめに

メモ魔向けのメモアプリを作りました(最下部にリンクあり)。その時にタイトルの内容を苦労して調べたので紹介します。

開発環境

Xcode Version 14.0

機能

前提としてアプリのイメージを最低限紹介したく、画像を2枚貼ります。
Group 182.png

左図)メモ一覧画面です。横線でメモが区切られています。薄い紫色の四角がタグです
右図)画面の下半分に、タグの組み合わせごとのメモ数が表示されています。この計算にタイトルの実装を利用しました。

データ構造

データ構造は下図の通りです。1行が1つのメモで、「ZLABELS」列にカンマ区切りでラベルidを入れてます。
※Xcode上では「labels」ですが実際には「ZLABELS」で入ってるようです
image.png
(CoreDataの中身の確認方法はこちらの記事で知りました)

Select文でこのZLABELSをGroup Byのキーにしてレコード数を集計します。

実装:CoreDataをSQLのSelect文でGroupBy集計

 このやり方を調べるのが一番苦労しました。自分はデータ分析屋なので「データベースはselect文で集計するためのもの」みたいな認識を持っちゃってるんですが、CoreDataは集計する用途では全くないみたいですね。
 でも中身がSQLiteというのは知っていたので絶対できるはずだ(別にswiftでも計算できるけどめんどくさい)と、調べてみました。そのままヒットする記事は見つけられなかったのですが、いくつか繋ぎ合わせると実現できました。ミニマムにしたサンプルコードがこちらです。

import SwiftUI
import CoreData
import SQLite3

var db: OpaquePointer?
private var labelsCount:[String:Int] = [:]

struct ContentView: View {
    init(){
        // dbに接続
        openDB()
        // group byで集計
        selectCntGroupByLabels()
        // 集計結果をコンソール出力
        print(labelsCount)
    }
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
    @State private var textInput:String = ""
    @State private var labelsInput:String = ""

    var body: some View {
        let frameWidth:CGFloat = 100
        let spaceWidth:CGFloat = 20
        VStack{
            Text("データ登録").font(.title2)
            HStack{
                TextField("text", text: $textInput)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .frame(width: frameWidth)
                Spacer().frame(width: spaceWidth)
                TextField("labels", text: $labelsInput)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .frame(width: frameWidth)
            }
            Button("登録"){ addNewMemo() }

            Divider()
            Text("データ表示").font(.title2)
            ForEach(items){ item in
                if let text = item.text{
                    if let labels = item.labels {
                        HStack{
                            Text(text).frame(width:frameWidth)
                            Spacer().frame(width: spaceWidth)
                            Text(labels).frame(width:frameWidth)
                        }
                    }
                }
            }
        }
    }
    
    func addNewMemo() {
        withAnimation {
            // 新規データ登録
            let newItem = Item(context: viewContext)
            newItem.text = textInput
            newItem.labels = labelsInput
            newItem.timestamp = Date()
            do {
                try viewContext.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
            // 表示を初期化
            textInput = ""
            labelsInput = ""
        }
    }
}

func openDB() {
    let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.libraryDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
    let filePath:String = "\(paths[0])/Application Support/qiita_coredata.sqlite" // 要確認。アプリによって異なる。環境でも変わるかも。
    print(filePath)
    if sqlite3_open(filePath, &db) != SQLITE_OK {
        print("DBファイルが見つからず、生成もできません。")
    } else {
        print("DBファイルが生成できました。(対象のパスにDBファイルが存在しました。)")
    }
}

func selectCntGroupByLabels(){
    // ラベルセットごとの個数を取得
    let queryString = """
        SELECT
            ZLABELS, count(*)
        FROM
            ZITEM
        where
            ZLABELS LIKE '%,%'
        group by
            ZLABELS
    """
    var stmt:OpaquePointer?
    // クエリを準備する
    if sqlite3_prepare(db, queryString, -1, &stmt, nil) != SQLITE_OK{
        let errmsg = String(cString: sqlite3_errmsg(db)!)
        print("error preparing insert: \(errmsg)")
        return
    }
    // クエリを実行し、取得したレコードをループして結果を格納
    labelsCount = [:] // 初期化
    while(sqlite3_step(stmt) == SQLITE_ROW){
        labelsCount[String(cString: sqlite3_column_text(stmt, 0))] = Int(sqlite3_column_int(stmt, 1))
    }
}
// コピペ部分がありますが元記事のリンクを見失ってしまいました、すみません。。。

このサンプルではinit()内で集計してますが、onAppearなど好きなタイミングで実行できます。
シミュレーターとコンソールのキャプチャはこちら:
image.png

少し誤算だったのは、カンマを展開して集計することはSQLiteでは出来なさそうで、結局Swift側でも地道な集計が必要だったことです。とはいってもGroupByで前処理できて楽だったので良かったです。

以上「CoreDataをSQLのSelectのGroupByで集計する」方法のご紹介でした。

(参考)そもそも何故この機能が必要だったのか

「どの組み合わせでラベル使ってたっけ?」と全部覚えてないので、何個あるか眺めてからポチポチ選びたかったからです。詳しくはnoteに書いてます。ご興味あれば。

(参考)タグ管理に全振りしたメモアプリ「ラベルメモ」

https://apps.apple.com/jp/app/label-99-note/id1644589126
※iOS版のみ
※タグ管理なのになぜ「ラベルメモ」なのかですが、「タグ」だとSNSのハッシュタグのイメージが強すぎて違和感あったからです。ちなみにMacのFinderはtagと呼んでいて、GoogleのGmailはlabelと呼んでますね。labelが圧倒的少数派な感じです。labelだと音楽レーベルと同じ単語で紛らわしいからかも。ぶっちゃけ今はTagの方が良かったと思ってますが、アプリアイコンを「L」で作っちゃったからラベルのままにしました。。。

2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?