🦆で🦈を釣る

Swift

DuckDBを用いたサメのトラッキングアプリ🦈

どりー_ハックツ

推しアイデア

サメのトラッキングデータ(CSV)をサメの種類ごとに表示!

作った背景

縛りのDuckDBの特徴を活かすために、そこそこ規模の大きいデータをMap上に表示してSQLを叩きたかった

推し技術

DuckDB DuckDB-Swift Map Kit TabularData SwiftUI

プロジェクト詳細

概要

シャークトラッキングデータをもとにDuckDBのテーブルを作成し、ローカルでSQLを叩くことで10種類のサメのトラッキングデータの表示を切り替えています。 表示にはMapKitを使用し、実際のMap上に表示をしています。

DuckDBとは

(DuckDBに入門してみたより抜粋) DuckDBはOLAP(オンライン分散処理)分析に特化したデータベースです。 特に高速な処理を得意とし、かつ無料およびOSSライセンスで動作します。

DuckDBの強み

・シンプル DuckDBは、コンパイル時も実行時も外部依存関係はありません。 DuckDBの構築に必要なのは、動作するC++11コンパイラーのみです。 ・豊富な機能 DuckDB は、本格的なデータ管理機能を提供します。 大規模な関数ライブラリ、ウィンドウ関数などを使用してSQLの複雑なクエリを広範囲にサポートしています。 DuckDB は Python および R に深く統合されており、効率的な対話型データ分析が可能です。 DuckDB は、Java、C、C++ などの API を提供します。 ・処理の速度 DuckDB は、オンライン分析処理 (OLAP)とも呼ばれる分析クエリ ワークロードをサポートするように設計されています。 これらのワークロードは、テーブル全体の集計や複数の大きなテーブル間の結合など、 格納されたデータセットの重要な部分を処理する複雑で比較的長時間実行されるクエリによって特徴付けられます。 データへの変更もかなり大規模になることが予想され、複数の行が追加されたり、テーブルの大部分が同時に変更または追加されたりします。 ・無料 DuckDB はオープンソースであり、ソース コード全体は GitHub で無料で入手できます。 私たちは、当社の行動規範を遵守する限り、誰からの貢献も歓迎します。

OLAPについて

🦆🦆🦆🦆🦆🦆DuckDB入門🦆🦆🦆🦆🦆🦆 に違いについてまとめられている。

DuckDB for Swift

  1. Swift上でSQLが書ける!
  2. JSON,CSV,Parquet ファイルのインポートとエクスポートができる!
  3. 厳密に型指定されたResultsが手に入る!
  4. 並列処理可能!!!

詳しくは↓ Introducing DuckDB for Swift

今回使用したDB

使用データ

シャークトラッキングデータ

作成したテーブル・使用SQL

全件検索

TABLE sharkData AS SELECT * FROM read_csv_auto('\(csvFileURL.path)');`

Shark(種類)、Depth(深さ)、Latitude(緯度),Longitude(経度)

SELECT Shark, Depth, Latitude, Longitude FROM sharkData;

image

Shark(種類)指定 & サンプル数指定

SELECT Shark, Depth, Latitude, Longitude FROM sharkData WHERE Shark = 'WS\(number)' USING SAMPLE 2000;

検索後Latitude, LongitudeをCLLocationCoordinate2Dの形式に

var frame = try await sharkStore.sharkTrackingNumber(number: 2) frame.combineColumns("Latitude", "Longitude", into: "location") { (latitude: Double?, longitude: Double?) -> CLLocationCoordinate2D? in guard let latitude = latitude, let longitude = longitude else { return nil } return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) }

image

SwiftUIで座標をマッピング

MapUI

これだけでMapが出る!

import SwiftUI import MapKit struct MapStyleSample: View { var body: some View { Map() } }

ピン(アノテーション)を追加してみる

簡単にカスタマイズしたアノテーションを追加できる!

import SwiftUI import MapKit struct MapStyleSample: View { var body: some View { Map() { Annotation(coordinate: .sharkIsland ) { //.sharkIsland = どっかの座標 ZStack{ RoundedRectangle(cornerRadius: 5) .fill(.background) RoundedRectangle(cornerRadius: 5) .stroke(.secondary, lineWidth: 5) Text("🦈") .padding(5) } } .annotationTitles(.hidden) } } }

こう書くと↓こんなのが出る image

座標データにこのアノテーションを配置!

マイグレーションした座標データをマッピング! ついでにDuckDBによるマイグレーションのLoadingも!

import Charts import SwiftUI import MapKit @preconcurrency import TabularData extension CLLocationCoordinate2D { //通称シャークアイランドの座標 static let defaultPoint = CLLocationCoordinate2D(latitude: 29.14015, longitude: -118.2867) } struct ContentView: View { private enum ViewState { case fetching(Error?) case loaded(DataFrame) } let sharkStore: SharkStore @State private var state = ViewState.fetching(nil) @State var sharkNum:Int = 1; var body: some View { Group { switch state { case .loaded(let dataFrame): VStack { Text("SharkTrackingData") .font(.title) .fontDesign(.default) .fontWeight(.bold) .multilineTextAlignment(.center) Maping(dataFrame: dataFrame) Button("🦈") { Task { do { sharkNum += 1 if sharkNum > 10 { sharkNum = 1 } var frame = try await sharkStore.sharkTrackingNumber(number: sharkNum) frame.combineColumns("Latitude", "Longitude", into: "location") { (latitude: Double?, longitude: Double?) -> CLLocationCoordinate2D? in guard let latitude = latitude, let longitude = longitude else { return nil } return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } self.state = .loaded(frame) print(frame) } catch { self.state = .fetching(error) } } } } case .fetching(nil): ProgressView { Text("Fetching Data") } case .fetching(let error?): ErrorView(title: "Query Failed", error: error) } } .padding() .task { do { var frame = try await sharkStore.sharkTrackingNumber(number: 1) frame.combineColumns("Latitude", "Longitude", into: "location") { (latitude: Double?, longitude: Double?) -> CLLocationCoordinate2D? in guard let latitude = latitude, let longitude = longitude else { return nil } return CLLocationCoordinate2D(latitude: latitude, longitude: longitude) } self.state = .loaded(frame) print(frame) } catch { self.state = .fetching(error) } } } } struct Maping: View { let dataFrame: DataFrame var body: some View { Map { ForEach(0..<dataFrame.Shark.count) { num in Annotation("shark\(num)", coordinate: dataFrame.location[num] as! CLLocationCoordinate2D) { ZStack{ RoundedRectangle(cornerRadius: 5) .fill(.background) RoundedRectangle(cornerRadius: 5) .stroke(.secondary, lineWidth: 5) Text("🦈") .padding(5) } } .annotationTitles(.hidden) //Marker("defaultPoint", monogram: Text("🦈"), coordinate: .defaultPoint) } } .mapStyle(.standard(elevation: .realistic)) } }

↓メインビュー

import SwiftUI @main struct HackathonDuckDBSharkApp: App { private enum ViewState { case loading(Error?) case ready(SharkStore) } @State private var state = ViewState.loading(nil) var body: some Scene { WindowGroup { Group { switch state { case .ready(let sharkStore): ContentView(sharkStore: sharkStore) case .loading(nil): ProgressView { Text("Loading Shark") } case .loading(let error?): ErrorView(title: "Failed To Load Shark", error: error) { Task { await prepareSharkStore() } } } } .task { await prepareSharkStore() } } } private func prepareSharkStore() async { guard case .loading(_) = state else { return } self.state = .loading(nil) do { self.state = .ready(try await SharkStore.create()) } catch { self.state = .loading(error) } } }

出力画面

image

まとめ

Swift完結でSQLが書けるのは革命的! データ量が大規模でなければローカル(デバイス内の)ファイルにすることでオフラインでもデータベースマイグレーションが可能に!!! 並列処理もできて割と実用的?!?!

どりー_ハックツ

@Friedrich_buryu