【Swift UI】旅行で使える割り勘アプリを自作！アプリ構造と流れ

Swift UIを使った「**iOSアプリ個人開発第2弾**」ということでリリースした「**割り勘アプリ**」の概要と作り方をまとめておきたいと思います。


:::note info
旅行で使える割り勘アプリ-bill-
:::

![536EC57E-CB00-49DF-B255-C4BDB9B67774.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2264957/c425ecca-511b-99dc-f687-624749eb8b4d.png)


◇この記事を読んで欲しい人
+ iOSアプリ開発の流れを知りたい
+ 個人開発ってどんな感じ？
+ Swift UIを使った開発の流れ

# アプリの概要
今回作っていきたいアプリは「**旅行の際に使える割り勘アプリ**」です。

具体的には...
+ 複数人の旅行
+ 1人がまとめて支払う
+ 何人かがバラバラに支払う
+ 旅費の割り勘がややこしいor面倒
+ 旅行の履歴を残しておきたい

これらを解決できるようなアプリを開発していきたいと思います。

アプリ開発をする際の流れは以下の通りです。まずは**企画して要件をまとめ、ターゲットやアプリの主軸となる役割、実装方法など**を考えますす。


◇アプリ開発の流れ
+ 要件定義
+ 設計
+ 制作
+ テスト
+ 公開

また**初心者の個人開発**なので勘違いや至らぬ点もあると思いますのでもし何かありましたら教えていただけると嬉しいです。

またここではSwift開発環境の解説は省略します。iOSアプリ開発の統合開発環境である**Xcode**をインストールするだけで簡単に始められますので是非インストールしてみてください。
[Xcodeのインストール方法と使い方！終わらない時の対処法！](https://appdev-room.com/swift-xcode-install)
![FDCFFC33-6F2B-4B77-BEE6-C5048C2948E6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2264957/505fd554-c82b-caed-523d-a161c3663438.png)

## 要件定義

◇ターゲット
+ 大学生
+ イベンター
+ 旅行好き
+ 管理したい人

◇アプリの概要
+ 割り勘をスムーズに
+ 旅行履歴を保存

◇実装するために
+ リスト機能
+ データ保存

自分が旅行好きというのもあって**簡単に割り勘できるかつ旅行内容をメモできるアプリ**が欲しかったので作ってみることにしました。

## 設計
Swiftでは**MVCアーキテクチャに倣ってアプリ構造**を設計していきます。
+ **Model(モデル)**：データそのもの
+ **View(ビュー)**：表示される画面
+ **Controller(コントローラ)**：MとVを操作する

Swift UIでのMVCアーキテクチャの紐付けがまだよく分かっていないですができる限り切り分けて行きたいと思います。

## Model(モデル)
まずは**扱うデータ本体と管理方法**を考えます。

◇必要となるデータ
+ **1件ずつ管理される旅費情報**
+ **1件ずつ管理される旅行情報**

今回の保存方法は**JSON形式のファイルとしてDocumentsフォルダの中に蓄積**していくことにします。
```ruby:iOSアプリのサンドボックス構造
├── AppData
│ ├── Documents ☆
│ ├── Library
│        ├── Application Support
│        ├── Caches
│        ├── Preferences
│        ├── Saved Application State
│        └── SplashBoard
│ ├── SystemData
│ └── tmp
```

[【Swift】FileManagerでファイルを保存！操作方法や格納場所](https://appdev-room.com/swift-file-manager)

## View(UIデザイン)

アプリのビュー、いわゆる UI部分やデザインなどを決めておきます。


+ テーマカラー：緑色
+ ベースカラー：無色
+ アプリアイコン：フリーアイコンを使用
+ アプリ名：割り勘アプリ-bill-(請求書の意)
+ キャッチコピー：「旅行で使える割り勘アプリ」


## Controller(画面構成)？
これはControllerとは少し逸れるかも知れませんがデータを表示させる画面構成を考えます。
大枠となるのは2枚、「**旅費を登録する画面**」と「**イベントリストを閲覧する画面**」です。

◇旅費を登録する画面
設けたい機能
+ 旅費の登録
+ リスト表示
+ 割り勘機能
+ イベントの登録/リセット

この画面から遷移させるページ
+ 旅費リストページ
+ 割り勘ページ


◇イベントリストを閲覧する画面
設けたい機能
+ イベントリストの表示
+ 各イベントの詳細表示

この画面から遷移させるページ
+ 旅費リストページ

## 制作
Swift UIベースでXcodeを使ってアプリ開発を進めて行きます。プロジェクトを作成し各コードを記述していきます。最終的なファイル構造は以下のようになりました。

```ruby:プロジェクトのファイル構造
├── bill
│ ├── Models
│        ├── AdMobViewModels.swift
│        ├── CashData.swift
│        ├── EventData.swift
│        └── FileController.swift
│ ├── ViewComponent
│        ├── ButtonView.swift
│        ├── EventNameView.swift
│        ├── MemberView.swift
│        ├── PickerMemberView.swift
│        ├── RewardButtonView.swift
│        ├── RowCashView.swift
│        └── RowEventView.swift
│ ├── CalcBillView.swift
│ ├── ContentView.swift
│ ├── CreateEvent.swift
│ ├── EntryCashView.swift
│ ├── EntryEventView.swift
│ ├── InputEventView.swift
│ ├── HistoryEventView.swift
│ └── ListCashView.swift
├── bill.xcodeproj

```

**Viewをできるだけ分割しつつ、使い回せるように制作していくことの大事さと難しさを痛感**しました。

少しだけここにコードの中身を貼っておきます。
```swift:CashData.swift
import Foundation

struct CashData: Identifiable,Codable,Equatable {
    // Identifiable：List表示のため
    // Codable：JSONエンコード/デコード
    // Equatable；firstIndexを使用可能にするため
    // キャッシュ情報を統括して管理する構造体

    var id = UUID()             // 一意の値
    var cash:Int                // 金額情報
    var memo:String = ""        // MEMO
    var member:String = ""      // メンバー
    var time:String = { // 初期値に現在の日付
        
        let df = DateFormatter()
        df.calendar = Calendar(identifier: .gregorian)
        df.locale = Locale(identifier: "ja_JP")
        df.timeZone = TimeZone(identifier: "Asia/Tokyo")
        df.dateStyle = .none
        df.timeStyle = .short
        
        return df.string(from: Date())

    }()
}

// -------------------------------------------------------------

class AllCashData:ObservableObject{
    // 全キャッシュ情報をデータとして持つクラス
    //ObservableObjectプロトコル→プロパティの値を監視
    
    // プロパティ-------------------------------------------------
    @Published var allData:[CashData] = [] // 全情報
    @Published var bill:Int = 0  // 請求金額の合計
    
    // プロパティに値をセット---------------------------------------
    init(){
        // 初期値を入れていないとメソッドは実行できないためプロパティでも初期値有
        self.setAllData()
        self.sumBill()
    }
    
    
    // メソッド---------------------------------------------------
    
    // JSONファイルに格納されている全キャッシュ情報をプロパティにセット
    func setAllData(){
        let f = FileController()
        self.allData = f.loadJson()
    }
    
    // 現在の合計金額を格納
    func sumBill(){
        var result:Int = 0
        for item in self.allData{
            result += item.cash
        }
        self.bill = result
    }
    
    // Equatableを準拠
    // Referencing instance method 'firstIndex(of:)' on 'Collection' requires that 'CashData' conform to 'Equatable'
    func removeCash(_ item:CashData) {
        guard let index = allData.firstIndex(of:item) else { return }
        allData.remove(at: index)
    }
}
```


```swift:ContentView.swift
import SwiftUI
import GoogleMobileAds


struct ContentView: View {
    
    // MARK: - View
    
    @State var selectedTag:Int = 1      //  タブビュー
    
    // MARK: - AppStorage
    @AppStorage("eventName") var eventName:String = "" // イベント名
    @AppStorage("member") var member:String = ""  // 配列が格納できないため文字列で保存 "Tom,Johnny,Joseph"
    // memberArrayに配列形式で保持
    @State var memberArray:[String] = [""]
    
    // MARK: - インスタンス
    // ファイルコントローラークラスをインスタンス化
    let fileController = FileController()
    // 全キャッシュ情報をデータとして持つクラスをインスタンス化
    
    @ObservedObject var allCashData = AllCashData()
    @ObservedObject var allEventData = AllEventData()
    
    // MARK: - Boolプロパティ
    @State var isEvent:Bool = false // 旅行イベントを作成したかどうか

    
    // MARK: - メソッド
    func storageReset(){
        eventName = ""
        member = ""
        memberArray = [""]
        isEvent = false
    }
    
    var body: some View {
        TabView(selection: $selectedTag){
            
            // MARK: - タブ1 イベント作成画面 or 登録画面
            Group{
                if !isEvent {
                   CreateEvent(isEvent: $isEvent, memberArray: $memberArray)
                }else{
                    EntryCashView(eventName:$eventName,memberArray:$memberArray,parentStorageResetFunction:storageReset).environmentObject(allCashData).environmentObject(allEventData)
                }
            }
            .tabItem{
                
                Image(systemName: "pencil.circle")
                Text("Event")
                
            }.tag(1)
            
            // MARK: - タブ2　割り勘計算View
            HistoryEventView().environmentObject(allEventData).tabItem{
                Image(systemName: "list.bullet.circle.fill")
                Text("History")
            }.tag(2)
        }
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .navigationViewStyle(.stack)
        .ignoresSafeArea()
        .accentColor(.orange)
        .onAppear{
            if member != "" {
                memberArray = member.components(separatedBy: ",")
                isEvent = true
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```




## テスト
Swift UIではシミュレーターを使って常時テストしながら開発ができるのでアプリ公開前に実機で試したり、さまざまな状況を試しながら異常がないか確認しました。

## 公開
アプリ制作が完了したらいよいよ公開する時です。公開までの詳細な手順はここでは記載しませんので以下を参考にしてください。

[【Swift】iOSアプリをAppStoreで公開する方法！前準備とXcodeのビルドのやり方](https://appdev-room.com/swift-appstore-public)

◇公開までの流れ
1. Xcodeで開発
1. Apple Developer Programへの登録
1. AppStore Connectを使って審査申請
1. 審査に通過
1. アプリ配信開始

# 割り勘アプリの宣伝

私が作ったアプリを改めて紹介させてください。

:::note info
旅行で使える割り勘アプリ-bill-
:::

◇概要
このアプリは**友達との旅行でかかる費用を割り勘**できるアプリです。
料金がかかるたびに登録していき、**最終的な金額と1人いくらなのか、誰がいくら払い誰に払えばスムーズかが表示**されます。

![3062B615-94CC-4016-8815-E02E33530CF5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2264957/953e7721-893e-6e39-35f8-2cf292ba53b9.png)

インストールはこちら
[割り勘アプリ-bill-](https://apps.apple.com/jp/app/%E5%89%B2%E3%82%8A%E5%8B%98%E3%82%A2%E3%83%97%E3%83%AA-bill/id1642039167)

またこのアプリはGitHub上にコードを全て公開しています。「ここはこんなコードになっているんだ〜」みたいに確認してみてください。

[GitHub-bill](https://github.com/amefure/Bill)