SwiftUIでRunlogger(風)を作ってみた

,

以前Objective-Cで作成していたRunLoggerをSwiftUIの調査・勉強の為にテスト的に基本部分を作ってみました。

SeiftUI

1)状況とSwiftUI

前回はFlutterを使用してRunlogger(風)を作ってみましたので、同様にSwiftUIでも作ってみました。知らない言語でも「1週間も有れば大体使える」と、色々な人が言ってますし私も昔はそう思って居ましたので、今でも出来るか?との確認も含めてやってみました。

以前Objective-Cで作って居た時にSwiftが出てきて、サンプルアプリを少し弄っただけなので「初めて」とは違いますが、似た様な物でしょう。その時はObjective-Cより「行数が少なく書ける」程度の認識でした。

iOS開発はSwiftUIが出る前はObjective-C時代のUIKitとインターフェースビルダーを使用して居ましたので、Swiftになったとしても面倒でした(=部品配置などGUIでできるので一見使いやすいのですが、位置の調整などすると結構時間がかかります)。

が、SwiftUIになり流行りの画面をコードで指定する方法になり、細かな調整をしないなら簡単に画面が作成されます。この辺はFlutterと同じですが、Flutterよりも簡易に作成できそうです。

2)作成したファイル・フォルダー

実際に修正・作成したのは以下のファイルとフォルダーでした。
画面(View)から作成していったのですが、機能分割しているとどうしてもMVVM形式に引きずられてしまいました。

SwiftUIを使用する場合、MVVM形式は不要とのNet記事もありました。内容的にはViewModelsがModelとViewの仲介ですが、2段階callになって効率が悪い。。。との感じも受けますがそれなりに便利なので使って居ます。

コード的には改善点が多々あると思いますが1週間ではこの程度でしょう。

│  RunLoggerSWUIApp.swift
│  ContentView.swift
│  
├─ViewModels
│      EnvViewModel.swift
│      
├─Models
│      gpsLogData.swift
│      gps.swift
│      debugData.swift
│      
└─Views
        RunView.swift
        MapView.swift
ファイル名内容など
RunLoggerSWUIApp.swiftプログラムメイン

ContentView()を呼ぶだけですが、共通に使用する「環境オブジェクト」の初期化を含みます
例)
ContentView()
.environmentObject(EnvViewModel()) 
ContentView.swift画面全体を定義します。Runlogger(風)では
TabViewを使用して画面切り替えをしています。
Views/MapView.swiftmap(地図)画面プログラムソース
地図表示と現在位置にピンを立てる。
Views/RunView.swiftrun(地図)画面プログラムソース
走行速度・距離・標高を表示する
ViewModels/EnvViewModel.swiftViewで共通に使用するタイマーやGpsデータの加工を行うModelsとの仲介機能ソース
通常のMVVMではMapViewModelとRunViewModelに相当する。
環境オブジェクトとしてグローバル変数の様な使い方をしている(=誰かに、問題がある使い方と言われるかもしれませんね)
Models/gps.swiftGPSの関連情報を取得するクラス.
別々な関数や値をまとめるためのクラスで、staticで全てアクセスできる様にした物です(ある意味ではシングルトーンパターンですね)。
Models/gpsLogData.swiftrunloggerの「logger」部分のデータを管理するクラスです。「Start」「Stop」間の位置や高さ、スピード、距離などを保存しており「Stop」時にcsvファイルとして保存します。
Models/gpsLogData.swift動作検証用の座標値リストデータファイルです。Debugモードの時に使用されます。

3)Map(地図)画面

Runloggerを起動すると地図画面が表示されます。ログが開始されて居ませんので、走行時間・走行距離・「stopボタン」などは灰色表示です。

「Startボタン」でログを開始します。

「Stopボタン」でログを終了します。終了時にcsvファイルにログを保存します。

「RunTypeボタン」は未実装です。歩き・走り・自転車などでカロリー計算パラメータなどを切り替える為に使用予定です。

「+」地図を拡大します

「ー」地図を縮小します。

「L」地図・衛生画像を切り替えます(未実装)。

現在位置に赤いピンを配置します。以前の位置がわかる様に20個のピンを巡回配置します。

4)Run(走行)画面

地図画面で「Run」タブを押すとRun画面になります。

「Startボタン」でログを開始します。

「Stopボタン」でログを終了します。終了時にcsvファイルにログを保存します。

「RunTypeボタン」は未実装です。歩き・走り・自転車などでカロリー計算パラメータなどを切り替える為に使用予定です。

速度・標高をグラフ表示します。

「<<ボタン」はログ開始から20個のグラフを表示します。

「>>ボタン」はログ最後から20個のグラフを表示します。

「allボタン」はログ全体グラフを表示します。

5)開発メモ

いくつかSwiftUIで開発する上で困った事が有りますのでメモとして残しておきます。

1。なかなかサンプルプログラムが動かない
 これは私の環境問題とも言えますがXcode14.3.1を使用して居ます(=Xamarinの開発もありXcode15.4に上げれないためのトラブルかもしれません)
 サンプルがiOS17などの最新バージョンを対象にしていたり、SwiftUI自体が新しくて機能更新が続いている様でなかなか素直に動いてくれません。

 現在、地図形式を普通・衛星に切り替えるための「L」ボタンが未実装です。MapのmapStyleを切り替えるだけの単純な機能ですが、どうしてもコンパイルエラーが取れないのでそのままです。

mapStyle部分をコメントアウトするとコンパイルエラーがなくなります(???)

2。XcodeがVSCodeと比べると機能がイマイチ
 VSCodeなら次々とソースコードを提案してくれるので実際のコード入力が非常にすくなくて済むのが、決まりきったコードを一々いれるのは非常に面倒です。。。。と思ったら、GitHub CopilotのXcodeバージョンがありました。 別アプリですがXcodeと共に使うと少しはマシですが、まだまだVSCodeの様には動きません。今後に期待です。

参照:GitHub Copilot を Xcode で使う(GitHub 公式の Xcode 機能拡張)

3。TABのアイコン画像は基本の物で十分だった
 SwiftUIで使用するアイコンなどの画像は「SF Symbols」アプリで簡単に見つけれました。

参照:SF Symbols 6

例えばTABアイコンを設定しているソースでは、Mapは「globe」Runは「bicycle」を使用して居ます。

struct ContentView: View {
    var body: some View {
        TabView {
            MapView()
                .tabItem {
                    Label("Map", systemImage: "globe")
                    
                }
            RunView()
                .tabItem {
                    Label("Run", systemImage: "bicycle")
                }
            Text("Set")
                .tabItem {
                    Label("Set", systemImage: "gearshape")
                }
        }
    }
}
SF Symbols画面

6000を超えるシンボルがある様なので、大体は間に合いそうです。

4。画面の再描画タイミング
 画面全体は「Viewのbody」として記述しますが、画面の更新は「
@EnvironmentObject」で定義したクラス内の変数が更新されることにより、関連するViewが更新されます。なので、ボタンが押されたり、1秒タイマーで1秒毎に現在座標を取得して、地図のピンを移動する。。などの処理が実現できます。

// @EnvironmentObjectのregionを@Publishedとして定義している。
//
    // Mapの変更用変数定義
    @Published var region:MKCoordinateRegion = MKCoordinateRegion (
        center: CLLocationCoordinate2D (
            latitude: 35.70521248,
            longitude: 139.5728176
        ),
        latitudinalMeters: 50,
        longitudinalMeters: 50
    )

[拡大ボタン]拡大ボタンを押した場合の実行関数例: regionの範囲を0.5倍している
regionが変更されたのでregionを使用しているMapViewのbuildが実行され、拡大地図が表がされる。
    /// <summary>
    /// 地図を拡大する
    /// </summary>
    func zoomIn() {
        self.region.span.latitudeDelta *= 0.5
        self.region.span.longitudeDelta *= 0.5
    }

タイマーで1秒毎に現在座標を取得している。取得した座標をregionに設定するので、 [拡大ボタン]と同様にMapViewのbuildが実行され、地図位置が更新される


    /// <summary>
    /// タイマーを開始する
    ///  1秒毎にカウントアップし、現在の緯度経度を取得して、以下の処理を行う
    ///  速度、標高、走行時間、走行距離を取得する
    ///  また、マーカーを追加する
    ///  速度、標高の表示文字を作成する
    ///  速度、標高のグラフの表示データを作成する
    ///  DEBUGの場合、内部テストデータの座標を取得する
    ///  </summary>
    func startTimer() {
        self.markTerable.removeAll()
        self.markerNo = 0
        /// gpsデータ取得を開始する
        gps.gpsStart()
        /// タイマーを開始する
        cancellable = Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { _ in
                self.count += 1
                //現在地取得:Map位置更新
                let location = self.gps.getLocation()
                self.region.center = location.coordinate
                // 表示文字列作成
                self.timeStr = self.gps.getGpsLogTime()
                self.distanceStr = self.gps.getGpsLogDistance()
                // マーカーを追加する
                let mak = MarkerData(
                    lat: location.coordinate.latitude, long: location.coordinate.longitude)
                // マーカーを描画データテーブルに追加する。最大20個までで、巡回する
                self.markTerable.append(mak)
                self.markerNo += 1
                if (self.markerNo >= 20) {
                    self.markTerable.remove(at: 0)
                    self.markerNo = 20;
                }
                // グラフのタイトル:表示文字を作成する
                let oneCod = self.gps.getCurrentCoordnaet()
                self.speedStr = "\(String(format: "%.2f", oneCod.dltSpeed))m/s"
                self.altStr = "\(String(format: "%.2f", oneCod.altitude))m"
            }
    }

5。地図上のボタン配置
 画面をコードで記述する場合、ボタン配置などは縦(Y)方向、横(X)方向でスタック的に定義する方法やグリッドなどで位置を指定する方法など色々ありましたが、重ねて表示する場合の配置を高さ(Z)方向としえ定義するのはSwiftUIの良い点と思います。SwiftUIのZStackを使用すると地図画面上の操作ボタンが以下の様に簡単に記述できました。

//
//  MapView.swift
//  MyMap
//
//注意: Xcode 14.3を使用しているため、CameraPositionを使えない?。
//  その為、以前のregionを使用して、表示位置を指定している。

import SwiftUI
import MapKit

/// <summary>
/// RunLoggerSWUIApp->ContentView->MapView
/// マップを表示する画面で以下の機能を提供する
/// ログの開始・終了、走行時間、走行距離、地図の切り替え、拡大・縮小を行う
/// </summary>
struct MapView: View {
    //環境オブジェクトをviewModelの様に使用する
    //アプリケーション全体で共有される状態を管理する
    //  この変数が変化すると関連するビューが更新される
    @EnvironmentObject var envViewModel: EnvViewModel
    
    var body: some View {
        ZStack {
            // マップを表示
            Map(coordinateRegion: $envViewModel.region ,
                annotationItems: envViewModel.markTerable)
                    { marker in
                        MapAnnotation(coordinate: marker.location) {
                            Image(systemName: "mappin")
                                .foregroundColor(Color.red)
                        }
                    }
                //.mapStyle($envViewModel.mapType) //xcode14.3では、使用で
          // きない??
                .ignoresSafeArea()
                .edgesIgnoringSafeArea(.bottom)
                .onAppear {
                    envViewModel.startTimer()
                }
            VStack(spacing:10)
            {
                Button("走行時間: \(envViewModel.timeStr)") {
                    print("時間:ボタンタップ時の処理")
                }
                .buttonStyle(.borderedProminent)
                .disabled(!envViewModel.isLogStart())

                Button("走行距離: \(envViewModel.distanceStr)") {
                    print("距離:ボタンタップ時の処理")
                }
                .buttonStyle(.borderedProminent)
                .disabled(!envViewModel.isLogStart())

                HStack
                {
                    Spacer()
                    Button("start") {
                        print("start:ボタンタップ時の処理")
                        envViewModel.gpsLogStart()
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(envViewModel.isLogStart())
                    
                    Spacer()
                    Button("stop") {
                        print("stop:ボタンタップ時の処理")
                        envViewModel.gpsLogStop()
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(!envViewModel.isLogStart())
                    
                    Spacer()
                    Button("runtype") {
                        print("runtype:ボタンタップ時の処理")
                    }
                    .buttonStyle(.borderedProminent)
                    Spacer()
                }
                Spacer()
                HStack{
                    Spacer()
                    VStack {
                        Spacer()
                        Button("L") {
                            print("地図切り替え:ボタンタップ時の処理")
                        }
                        .buttonStyle(.borderedProminent)
                        Button("+") {
                            print("拡大:ボタンタップ時の処理")
                            envViewModel.zoomIn()
                        }
                        .buttonStyle(.borderedProminent)
                        Button("-") {
                            print("縮小:ボタンタップ時の処理")
                            envViewModel.zoomOut()
                        }
                        .buttonStyle(.borderedProminent)
                    }
                }
            }
            .padding()
        }
    } // body ここまで
} // MapView ここまで

 struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

6)雑感

 Runloggerの主要機能を作成してみてSwiftUIの使い方が少し理解できました。
ほとんど知らない状態から一応は動くアプリまで作成するのに1週間でしたが、flutterよりも簡単に感じました。これでAndroidなども開発出来るならばSwiftUIが良いのですが、現時点ではマルチプラットーフォーム用ではFluttrerなのでしょうね。

7)今後

 ここまで来るとReact NativeでもRunlogger(風)を作って見るかとも思います。

 また、前回のFlutterでのRunlogger(風)は、今回のSwiftUI版とファイル構成が異なるので、FlutterバージョンもMVVM風にして、互いを比較する事を考えて居ます(=ソースなどの記述量や機能比較がやりやすくなるはずです)。

9)いずれGitHubに公開するか?

SwiftUIでの地図アプリサンプルは沢山あるので公開する意味は少ないと思いますが、ネットのサンプルが動かなかった場合の修正方法の参考になるかもしれませんので、mapStyle設定問題が解決したら公開の意味が有るかもしれません。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

PAGE TOP