ベータコンピューティングの活動や技術、開発のこだわりなどを紹介するブログです。



端末管理アプリ ~ 技術仕様 ~

この記事について

こんにちは、Beta Computing株式会社ソフトウェア開発者の尾崎です。
前の記事に引き続き、この記事では端末管理アプリの技術仕様についてお話しいたします。

blog.betacomputing.co.jp

システム構成

本アプリは以下の3つの要素から構成されます。

  • iOSアプリ
    • 端末情報を登録、貸出、返却するためのUIを提供
  • Firebase(クラウドバックエンド)
    • 端末情報、貸出状況等を保存するためのデータサーバ
  • NFCタグ
    • 端末識別のための物理タグ
    • 各端末に貼り付け、iOSアプリで読み取り可能とする
    • 書き込む情報は変動しないデータのみ(書き込み操作を初回のみとするため)
      • デバイスID、モデル名、シリアル番号

システム構成図

シーケンス図

例1. 端末情報更新 + NFCタグ登録

sequenceDiagram
  participant User as ユーザ
  participant App as iOSアプリ
  participant Firebase as Firebase

  User ->> App: 端末一覧タブをタップ
  App ->> User: 端末一覧画面を表示
  App ->> Firebase: 端末情報リストを読み込み
  Firebase ->> App: 読み込み成功レスポンス
  App ->> User: 端末情報リストを表示
  User ->> App: 端末情報セルをタップ
  App ->> User: 端末詳細情報画面を表示
  App ->> Firebase: 更新履歴を読み込み
  Firebase ->> App: 読み込み成功レスポンス
  App ->> User: 更新履歴を表示
  User ->> App: 編集ボタンをタップ
  App ->> User: 端末情報入力画面を表示
  User ->> App: 状態、OSバージョンなど入力
  App ->> App: 入力状況をチェックして保存ボタンを活性制御
  User ->> App: 保存ボタンをタップ
  App ->> Firebase: 端末情報を書き込み
  Firebase ->> App: 書き込み成功レスポンス
  App ->> User: UI更新

  User ->> App: NFCスキャンボタンをタップ
  App ->> User: NFCモーダルを表示
  User ->> NFC: NFCタグをスキャン
  NFC ->> App: 読み込み成功レスポンス
  App ->> App: タグ内のデータをチェック
  App ->> User: 書き込み準備完了ダイアログを表示
  User ->> App: 書き込み実行ボタンをタップ
  App ->> User: NFCモーダルを表示
  User ->> NFC: NFCタグをスキャン
  NFC ->> App: 書き込み成功レスポンス
  App ->> Firebase: 該当端末をNFC登録済に更新
  Firebase ->> App: 更新成功レスポンス
  App ->> User: UI更新

例2. 端末貸出申請

sequenceDiagram
  participant User as ユーザ
  participant App as iOSアプリ
  participant Firebase as Firebase
  participant NFC as NFCタグ

  User ->> App: 貸出状況タブをタップ
  App ->> User: 貸出中端末一覧画面を表示
  App ->> Firebase: 貸出状況リストを読み込み
  Firebase ->> App: 読み込み成功レスポンス
  App ->> User: 貸出状況リストを表示
  User ->> App: 申請ボタンをタップ
  App ->> User: 貸出返却処理メニューを表示
  User ->> App: NFCスキャンボタンをタップ
  App ->> User: NFCモーダルを表示
  User ->> NFC: NFCタグをスキャン
  NFC ->> App: 読み込み成功レスポンス
  App ->> App: タグ内のデータをチェック
  App ->> Firebase: 該当端末の貸出状況を読み込み
  Firebase ->> App: 読み込み成功レスポンス
  App ->> User: 貸出申請画面を表示
  App ->> Firebase: ユーザ情報リストを読み込み
  Firebase ->> App: 読み込み成功レスポンス
  App ->> User: ユーザ情報をUIに反映
  App ->> App: ユーザ選択状況をチェックして申請ボタンを活性制御
  User ->> App: ユーザを選択して申請ボタンをタップ
  App ->> Firebase: 貸出状況を更新
  Firebase ->> App: 更新成功レスポンス
  App ->> User: UI更新

使用フレームワーク・ライブラリ

名称 説明
CoreNFC NFCタグの読み取り/書き込みを行うためのフレームワーク
Network ネットワーク接続状況を監視するためのフレームワーク
FirebaseCore Firebaseの基盤ライブラリで、アプリ起動時の初期化処理を行う
FirebaseFirestore Cloud Firestoreとの通信・データ操作を行うためのライブラリ
RxSwift 非同期処理や状態管理のためのライブラリ
RxCocoa UIイベントとRxSwiftの連携を行うライブラリ
XCGLogger ログ出力(開発・デバッグ用)の管理ライブラリ
SwiftLint Swiftコードのスタイルチェックと静的解析ツールライブラリ

アーキテクチャ設計

設計はSwiftUI + MVVM(Model-View-ViewModel)アーキテクチャを採用しています。

  • View
    • ユーザーインターフェースの定義(SwiftUI)
  • ViewModel
    • 状態管理とロジック処理
    • 各Viewに対して1つのViewModel
  • Model
    • データ構造
  • 共通ユーティリティクラス
    • Firestoreとのやり取り
    • NFCタグの読み取り/書き込み
    • ネットワーク接続状況の監視

View-ViewModelの構成

ViewとViewModelは1対1とし、機能ごとに責務を分離して保守性とテスト容易性を高めることを目的としています。

実装例

struct RentalView: View {
    @StateObject var viewModel = RentalViewModel()
    // UIの定義
}
final class RentalViewModel: ObservableObject {
    @Published var rentals: [RentalModel] = []
    func fetchRentals() {...}
}

共通ユーティリティクラスについて

複数のViewModelやViewから共通的に利用される機能を、以下3つのユーティリティクラスとして分離し、それぞれシングルトンパターンで管理しています。

DataManagerクラス

  • Firebase/Firestore関連の操作を集約
  • CRUD操作の共通化、エラーハンドリングの一元化が目的
final class DataManager {
    static let sharedInstance = DataManager()
    private let db = Firestore.firestore()
    private init() {}

    func fetchDevices() -> [DeviceModel] { ... }
    func saveDevice(...) { ... }

    // 他にも貸出返却処理・更新履歴保存など...
}

NFCManagerクラス

  • CoreNFCを使用してNFCタグの読み取り/書き込みを管理
  • ViewModelとは独立して動作し、デリゲートパターンで結果を返す
final class NFCManager: NSObject, ObservableObject, NFCNDEFReaderSessionDelegate {
    static let sharedInstance = NFCManager()
    private override init() {}
    weak var delegate: NFCManagerDelegate?

    func readerSession(...) {...}
}

NetworkMonitorクラス

  • ネットワーク接続状況の監視を担当
  • オフライン時にサーバへの書き込みを制限するため使用
  • NWPathMonitorを使用してリアルタイムで監視を行う
final class NetworkMonitor {
    static let sharedInstance = NetworkMonitor()
    private let monitor = NWPathMonitor()
    private let queue = DispatchQueue(label: "NetworkMonitor")
    private(set) var isConnected: Bool = false

    private init() {
        monitor.pathUpdateHandler = { [weak self] path in
            self?.isConnected = path.status == .satisfied
        }
        monitor.start(queue: queue)
    }
}

これらのクラスはアプリ全体にわたって一貫した状態・動作が求められるため、final class+private init()により、インスタンスを1つに限定することで整合性を実現しています。

データモデル設計

Cloud Firestoreでは端末情報・更新履歴・貸出情報・貸出履歴・ユーザ情報を各コレクションに分けて管理しています。アプリ側のModelは各ドキュメントに対応するモデル設計としています。
独立したデータとして保存することで、端末やユーザ情報を削除しても履歴には残るという仕様にしています。

DeviceModel(端末情報)

struct DeviceModel: Codable, Identifiable, Hashable {
    @DocumentID var id: String?
    var group: String           // グループ(Androidスマホ、iPhoneなど)
    var model: String           // 端末名
    var serialNumber: String    // シリアル番号
    var osVersion: String       // OSバージョン
    var status: String          // 状態(正常、故障、廃棄)
    var connectorType: String   // コネクタ形状
    var registeredAt: Date      // 登録日
    var isRental: Bool          // 現在貸出中ならtrue
    var note: String?           // 備考
    var isNFCRegistered: Bool? = false  // NFCタグ登録済みならtrue
}

DeviceLogModel(更新履歴)

struct DeviceLogModel: Codable, Identifiable {
    @DocumentID var id: String?
    var deviceId: String        // 端末ID
    var operationType: String   // 操作種別(新規追加、更新、削除)
    var field: String           // 操作対象
    var oldValue: String?       // 変更前の値
    var newValue: String?       // 変更後の値
    var registeredAt: Date      // 登録日
}

RentalModel(貸出情報、貸出履歴)

struct RentalModel: Codable, Identifiable {
    @DocumentID var id: String?
    var deviceId: String        // 端末ID
    var model: String           // 端末名
    var osVersion: String       // OSバージョン
    var serialNumber: String    // シリアル番号
    var userId: String          // ユーザID
    var userName: String        // ユーザ名
    var rentedAt: Date          // 貸出日
    var returnedAt: Date?       // 返却日
}

UserModel(ユーザ情報)

struct UserModel: Codable, Identifiable {
    @DocumentID var id: String?
    var name: String            // ユーザ名
    var registeredAt: Date?     // 登録日
}

おわりに

今回作成したアプリはクラウドデータベースとNFC技術を活用することで、シンプルかつ正確な端末管理を目指した設計となっています。実装面でも機能ごとに切り分けてコードを記述することで、今後の拡張性やメンテナンス性も意識した開発ができたかと思います。

前の記事でも触れましたが、ViewをSwiftUIで実装したことで、ダークテーマや画面回転にも対応した柔軟なデザインが簡潔に記述できるのは非常に良い点だと感じました。

今後も継続してアプリ開発に尽力し、より良いサービスを開発できるよう努めたいと思います。
ここまでお読みいただき、ありがとうございました。