【SwiftUI】ふるさと納税管理アプリ「ふるログ」の開発の全て！個人開発の流れ

SwiftUIを使って個人開発した「**ふるさと納税管理アプリ**」の概要と作成の流れをまとめていきます。

＼インストールはこちら／
[ふるさと納税管理アプリ-ふるログ-](https://apps.apple.com/jp/app/%E3%81%B5%E3%82%8B%E3%83%AD%E3%82%B0/id1644963031)

# ふるさと納税管理アプリ-ふるログ-の概要

「ふるログ」は**寄付情報を管理するためのメモアプリ**です。
単純な登録、閲覧、削除、計算を行うだけの簡素なアプリとなっています。

●機能
+ 寄付情報を登録
+ 寄付上限金額を保持
+ 購入URLを登録可能
+ ワンストップ申請の送付済の識別
+ お気に入りの登録
+ 年ごとにリスト化


![2CCE7DA7-EAA5-46F7-B5A5-0843E4639077.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2264957/c45de85c-2ed1-4325-f897-564823010009.jpeg)

![2981E3C5-924A-412E-84B7-4BB43999F341.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2264957/7e6ba2ff-5379-efb1-61db-468a9e728a4e.jpeg)

## 作ろうと思ったきっかけ

ふるさと納税をしていく中で購入するオンラインストアがポイントの有無や取り扱い、キャンペーンなどでバラバラになることがあり、**寄付情報の一元管理ができなくなった**ので管理用のアプリが欲しかった。

## 開発環境

+ 言語：Swift
+ フレームワーク：Swift UI
+ マシン：MacBook Air
+ 開発環境：Xcode

ふるログは**Swift UIをベースにして開発**しました。
エディタはAppleから**公式に提供されている統合開発環境である「Xcode」を使用**しました。

[Xcodeのインストール方法と使い方！SwiftとSwiftUIの違い](https://appdev-room.com/swift-xcode-install)

## ふるログの開発手順と裏側

### データの管理方法
最初にデータの管理方法を決めます。
今回はメモアプリなのでユーザーが入力したデータを永続的に保持しないといけません。

Swiftではアプリを停止させてもデータを保持させる方法がいくつかありますが今回は**JSONを使ったファイル管理**にしました。

SwiftではJSONデータを簡単に構造体に変換できるので簡素なデータ構造で有れば簡単に操作が可能です。

データを保存する機能でいえばUserDefaults
やRealm Swiftなどもあるので興味があればご覧ください。

[【SwiftUI】Realm Swiftとは？導入方法とCRUD処理のやり方](https://appdev-room.com/swift-realm)

[【SwiftUI】@AppStorageとは？UserDefaultsにデータを保持する方法](https://appdev-room.com/swift-appstorage)

### アプリ名やアイコンなど
+ アプリ名

「**ふるログ**」
「ふるさと納税」の「ログ(履歴)」

+ アイコン
ふるさと納税ということで日本地図

+ テーマカラー
ふるさと＝古風なイメージ＝茶色系統

### 制作手順
私のアプリ開発の流れは以下の通りでした。

1. モデルデータを定義
1. データ管理ロジックを構築
1. モデルを表示させるビューの構築
1. ビュー同士の紐付け(画面遷移)
1. リファクタリング

最初にアプリ内で扱うことになるデータから定義しました。(命名を失敗したなと思ってます…)

+ FuluLog構造体：寄付情報構造
+ UserDonationInfo構造体：ユーザーの年ごとの寄付上限金額
+ AllFuluLogクラス：プロパティに「FuluLog構造体」形式の配列を保持


```swift:FuluLogModels.swift
import Foundation


struct FuluLog: Identifiable,Codable,Equatable {

    var id = UUID()             // 一意の値
    var productName:String      // 商品名
    var amount:Int              // 金額情報
    var municipality:String = ""// 自治体
    var url:String = ""         // URL
    var memo:String = ""        // メモ
    var request:Bool = false    // ワンストップ申請
    var time:String = ""        // 日付
}


struct UserDonationInfo:Identifiable, Codable,Equatable {
    var id = UUID()             // 一意の値
    var year:String             // 年
    var limitAmount:Int         // 上限金額

}
// MARK: -

class AllFuluLog:ObservableObject{
    
    // MARK: - プロパティ
    @Published var allData:[FuluLog] = []  // 全情報
    // フィルタリング用
    @Published var timeArray:[String] = [] // 保存されている全年情報 ["2022","2023"]
    @Published var donationLimit:[UserDonationInfo] = [] // 年ごとの寄付金額の上限配列
    @Published var allFavoriteData:[FuluLog] = []  // お気に入り全情報
    
    init(){
        self.setAllData()
        self.createTimeArray()
        self.setAllDonationLimit()
        self.setAllFavoriteData()
    }
    
    // MARK: - プロパティセットメソッド
    func setAllData(){
        let f = FileController()
        self.allData = f.loadJson()
    }
    func setAllDonationLimit(){
        let f = FileController()
        self.donationLimit = f.loadDonationLimitJson()
    }
    func setAllFavoriteData(){
        let f = FileController()
        self.allFavoriteData = f.loadFavoriteJson()
    }
    // MARK: - プロパティセットメソッド
    
    // MARK: - メソッド
    // 寄付金上限リスト用
    func sumYearAmount(_ year:String) -> Int{
        var sum = 0

        let filterData = allData.filter({$0.time.prefix(4) == year })
        for data in filterData {
            sum += data.amount
        }
        return sum
    }
    
    // 登録制限
    func countAllData() -> Int{
        return self.allData.count
    }
    
    // timeArrayに保存されている全年情報を抽出した重複のない配列を構築
    func createTimeArray(){
        var array:[String] = ["all"]
        for item in allData {
            array.append(String(item.time.prefix(4)))
        }
        let timeSet = Set(array) // 重複値を除去
        self.timeArray = Array(timeSet).sorted().reversed()
    }
    // MARK: - メソッド
    
    // MARK: -　CRUD
    func removeData(_ item:FuluLog) {
        guard let index = allData.firstIndex(of:item) else { return }
        allData.remove(at: index)
    }
    func updateData(_ item:FuluLog,_ id:UUID){
        guard let index = allData.firstIndex(where: { $0.id == id }) else { return }
        self.allData[index] = item
    }
    // MARK: -　CRUD
  
    // MARK: - Donation CRUD
    func updateDonationLimit(_ item:UserDonationInfo,_ year:String){
        let f = FileController()
        guard let index = donationLimit.firstIndex(where: { $0.year == year }) else {
            // 今年未保存の場合 = 今年の年が無い場合
            // 新規データを保存処理
            f.saveDonationLimitJson(item)
            return
        }
        // 今年分保存済み = 今年の年がある場合
        // Update処理
        self.donationLimit[index] = item
        f.updateDonationLimitJson(self.donationLimit)
    }
    // MARK: - Donation CRUD
    
    // MARK: - Favorite CRUD
    func removeFavoriteData(_ item:FuluLog) {
        guard let index = allFavoriteData.firstIndex(of:item) else { return }
        allFavoriteData.remove(at: index)
    }
    func updateFavoriteData(_ item:FuluLog,_ id:UUID){
        guard let index = allFavoriteData.firstIndex(where: { $0.id == id }) else { return }
        self.allFavoriteData[index] = item
    }
    // MARK: - Favorite CRUD
}
```

## データ管理ロジック
データを永続的に保存するためにファイルを扱うことになるので**FileManagerクラスを用いたファイル操作**が必要になりました。


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


デバイス内(Docmentsフォルダ)にファイルを生成しそこにJSON形式で追記していくことでデータを保持していきます。


またSwiftではJSONEncoderクラスやJSONDecoderクラスを使用して**構造体⇆JSON**の変換が簡単に行えます。


[【Swift】JSONデータをエンコードする方法！JSONEncoderクラスの使い方](https://tech.amefure.com/swift-json-encoder)


[【Swift】JSONデータをデコードする方法！JSONDecoderクラスの使い方](https://tech.amefure.com/swift-json-decoder)


これらの処理を1つのファイル(クラス)としてまとめて管理できるようにしておきました。

## モデルを表示させるビューの構築
アプリ画面は**iOSアプリによくある下側にタブが分かれている感じ**にしました。

ContentViewが**大元となる親ビュー**になりそこ子供にビューをいくつか構築しています。
```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    // MARK: - View
    @State var selectedTag:Int = 1      //  タブビュー
    
    @ObservedObject var allFulu = AllFuluLog()
    
    init() {
           // リストの背景色を変更
           UITableView.appearance().backgroundColor = UIColor(named: "BaseColor")
       }
    
    var body: some View {
        TabView(selection: $selectedTag){
            
            // MARK: - Entry
            EntryFuluLogView().environmentObject(allFulu).tabItem{
                Image(systemName:"plus.circle")
            }.tag(1)
            
            // MARK: - List
            ListFuluLogView().environmentObject(allFulu).tabItem{
                Image(systemName:"list.bullet")
            }.tag(2)
            
            // MARK: - Favorite
            FavoriteFuluLogView().environmentObject(allFulu).tabItem{
                Image(systemName:"star.fill")
            }.tag(3)
            
            // MARK: - Setting
            SettingView().environmentObject(allFulu).tabItem{
                Image(systemName:"gear")
            }.tag(4)
            
        }.preferredColorScheme(.light)
        .accentColor(.orange)
        .ignoresSafeArea()
    }
}
```
●作成した子ビュー
+ 寄付情報登録ページ
+ 寄付情報リスト表示ページ
+ お気に入りリストページ
+ 設定ページ

## リファクタリング
リファクタリングとは「**振る舞いを変えずにコードを変更すること**」です。
要するに無駄を省いたり、コードを綺麗にしたりしていくことです。

ビューは出来るだけ**細かい分割させた方がコードの見通しが良くなる**と教わったので素人なりに分けていきました。

とはいえ使い回せるビューがあったり、ただ分割しただけのビューがあったりとどこまで拡張性を持たせれば良いか色々と難儀しました。

ふるログのソースコードはGitHubに公開していますので興味があればご覧ください！

[GitHub-ふるログ](https://github.com/amefure/FuluLog)

## 個人開発の流れの感想
iOSアプリの個人開発は以下のような流れで行います。

1. アプリの概要を決める
1. 用意するビューを決める
1. 必要な機能をリフトアップ
1. 実際に制作
1. テスト
1. 公開

1人でやると全て自分でやらなければいけないので大変な部分は大きいですが今回の開発はとても楽しかったです。

自分の欲しいアプリを自分で作れるなんてそんな嬉しいことはないですよね。

どうしても公開するのに費用はかかりますし、私の場合元が取れるかというと知名度が低すぎるので致し方なしですが経験値がとても上がったと思います。

個人開発してる方ってすごいんだなと思いました。