0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【iOS個人開発】未経験からKPT習慣化アプリをリリースするまでの全記録①|Fat View脱却とデータ設計

0
Posted at

こんにちは!
先日、KPT法(Keep/Problem/Try)を取り入れた目標管理・習慣化アプリ「HabitSpark」を開発し、無事にApp Storeへリリースすることができました!🎉

👇 今回完成したアプリはこちらです!
HabitSpark-振り返りで続くシンプル習慣化アプリ

スクリーンショット 2026-04-23 13.26.47.png

スクリーンショット 2026-04-23 13.27.07.png

Swiftのチュートリアルを終えて、「さあオリジナルアプリを作るぞ!」と意気込んだものの、そこからリリースまでは壁の連続でした…。

この記事では、これから全4回にわたり「チュートリアルを終えたばかりの初心者が、どうやって1つのアプリを完成させ、世に出したのか?」というリアルな開発プロセスを包み隠さず共有します。

記念すべき第1回は、SwiftUI初心者が最初に絶望するFat Viewからの脱却と、すべての土台となるデータ(Model)の設計について、どこよりも分かりやすく解説します!


1. Fat View(巨大なファイル)からの脱却

SwiftUIの学習を始めて、いざ自分のアプリを作り始めると、最初はすべての画面デザインや処理を ContentView.swift という1つのファイルに書いてしまいがちです。

しかし、これは洋服も、本も、食器も、ぜんぶ1つの巨大なダンボール箱に投げ込んでいる状態と同じです。
機能が増えると、あっという間にコードが数千行を超え、「どこでエラーが起きているのか全く分からない!」というパニックに陥ります。これがいわゆる Fat View です。

これを防ぐため、HabitSparkでは役割ごとにファイルを分けることを徹底しました。

📁 HabitSparkの実際のファイル構成

  • Models.swift: データの設計図
  • CoreDataService.swift: データ保存・読み込み
  • GoalViewModel.swift: ロジック(アプリの頭脳)
  • ContentView.swift: 画面の土台
  • MainViews.swift: メイン画面のUI
  • Components.swift: 再利用UIパーツ

このように役割を分けておくことで、問題が起きたときに「どこを見るべきか」が明確になります。


2. すべての基本は「データ(Model)」から

開発を始めると、つい画面のデザイン(View)から作り始めたくなりますが、一番最初にやるべきはこのアプリはどんなデータを扱うのか?という設計図(Model)を作ることです。ここが決まれば、開発の8割は終わったと言っても過言ではありません。

実際の Models.swift のコードを抜粋し、初心者が絶対に知っておくべきテクニックと、Swiftの基礎知識を1行ずつ丸裸にして解説します。

① タスクの「種類」は enum(選択肢)で守る

HabitSparkのタスクには、「普通のタスク」「日次目標」など複数の種類があります。これを安全に管理するために enum(イーナム:列挙型)を使います。

// タスクの種類を定義する
enum TaskType: String, Codable, Equatable {
    case normal           // 普通のタスク
    case dailyGoal        // カレンダーで設定した日次目標
    case tryCarryOver     // 昨日の振り返り(Try)から引き継いだタスク
}

💡 コードの徹底解剖!

  • enum: 「この中からしか選べません」という“選択肢のセット”を作るイメージです。レストランのメニューみたいなものです。
  • TaskType: そのメニュー(enum)に付けた名前です。
  • : String, Codable, Equatable: これはenumに持たせている性質(機能)です。
    • String: 値を文字列として扱う
    • Codable: 後でiPhoneのデータベースに保存できるように、データを「翻訳(エンコード)」していいよ、という許可証です。
    • Equatable: 「Aのタスク種類と、Bのタスク種類は同じかな?」と、後で比較(=)できるようにするための能力です。
  • case(ケース): 実際の選択肢の中身です。例えば case normal は「通常タスク」、case dailyGoal は「日次目標」といった意味になります。

種類をただの文字で "normal" と書くこともできますが、もし "nomal"(rが抜けている)と打ち間違えてもエラーにならず、気づきにくいバグの原因になります。enum を使うと、「あらかじめ決めた選択肢の中からしか選べない」状態になるので、Xcodeの補完も効き、こういったミスを防ぎやすくなります。

② タスクの設計図 struct を作る

次に、上で作った TaskType を組み込んで、タスク1つ1つが持つべき「プロフィール項目」を定義します。

// 1つのタスクが持つ情報の「設計図」
struct Task: Identifiable, Equatable, Codable {
    let id: UUID             // タスクのマイナンバー(絶対被らないID)
    var title: String        // タスクの名前
    var isCompleted: Bool    // 完了したかどうか(true/false)
    var type: TaskType       // タスクの種類(さっき作ったenum)
    var categoryId: String   // カテゴリー(色など)
    
    // 新しくタスクを作る時の「初期値」を設定
    init(id: UUID = UUID(), title: String, isCompleted: Bool = false, type: TaskType = .normal, categoryId: String = "none") {
        self.id = id
        self.title = title
        self.isCompleted = isCompleted
        self.type = type
        self.categoryId = categoryId
    }
}

💡 コードのポイント解説

  • struct
    データの「設計図(テンプレート)」を作るためのものです。

  • Identifiable
    SwiftUIでリスト表示するときに、「それぞれをちゃんと区別できるようにする」ための仕組みです。
    そのため、中に一意な id を持たせる必要があります。


👇 letvar の違い

どちらもデータを入れる「箱」ですが、役割が違います。

  • let(定数)
    一度入れたら変更できない値。
    例:id のように途中で変わると困るもの

  • var(変数)
    後から変更できる値。
    例:titleisCompleted など、ユーザー操作で変わるもの


👇 型(UUIDやBool)って何?

その変数に「どんな種類のデータを入れるか」を決めるものです。

  • UUID:一意なID(重複しない識別子)
  • String:文字列
  • Booltrue / false の2択

👇 init(初期化)とは?

新しいデータを作るときのルールです。

isCompleted: Bool = false

のように書くことで、「特に指定がなければ false(未完了)で始める」という初期値を設定できます。

💡 初心者がつまずきやすいポイント

  • IdentifiableUUID
    SwiftUIでリスト表示をする際、同じ名前のデータがあると区別がつきません。
    そこで Identifiable を使い、id(一意な識別子)を持たせることで、それぞれを判別できるようにします。
    UUID はそのための「重複しないID」を自動で生成してくれる仕組みです。

  • Codable
    データを保存・読み込みできる形に変換するための仕組みです。
    JSONなどに変換できるようになるので、データ保存を行う場合はよく使います。

③ データ自身に計算させる(Computed Property)

最後に、「未来の自分」画面で使用している FutureVision というデータです。ここでは、データ自身に自分の進捗率を計算させるというテクニックを使っています。

struct SubTask: Identifiable, Codable, Equatable {
    let id: UUID
    var title: String
    var isCompleted: Bool
}

struct FutureVision: Identifiable, Codable, Equatable {
    let id: UUID
    var title: String
    var isCompleted: Bool
    var subTasks: [SubTask] = []  // 達成するための小さなステップ(サブタスク)
    
    // ここに注目!進捗率(0.0〜1.0)を自動で計算してくれる変数
    var progress: Double {
        // もしサブタスクが無いなら、完了していれば100%(1.0)、未完了なら0%(0.0)
        guard !subTasks.isEmpty else { return isCompleted ? 1.0 : 0.0 }
        
        // 完了しているサブタスクの数を数えて、全体の数で割る
        let completed = subTasks.filter { $0.isCompleted }.count
        return Double(completed) / Double(subTasks.count)
    }
}

💡 コードのポイント解説

  • Double
    1.5 や 0.8 のような「小数」を扱う型です。
    進捗率は 0.0〜1.0 の値で扱うことが多いので、ここでは Double を使っています。

  • var progress: Double { ... }
    普通の var と違い、{ } がついているのがポイントです。
    これは値を保存するのではなく、**呼ばれたときに計算して結果を返すプロパティ(Computed Property)**です。
    エクセルの関数みたいに、その場で計算してくれるイメージです。


👇 中の処理をざっくり見ると

  • guard !subTasks.isEmpty else
    サブタスクが空かどうかをチェックしています。
    空の場合は、完了状態に応じて 1.0 または 0.0 を返して終了します。

  • filter
    条件に合うデータだけを取り出します。
    ここでは「完了しているサブタスク」だけを抽出しています。

  • .count
    抽出した要素の数を数えます。

最後に「完了数 ÷ 全体数」で進捗率を計算しています。


💡 なぜ値として保存しないの?

進捗率をそのまま保存すると、タスクが更新されるたびに毎回計算し直して保存する必要があります。
この方法なら、必要なときにだけ計算するので、コードがシンプルになりバグも減らしやすくなります。


3. おわりに:まずは「箱」を作ろう

「アプリを作るぞ!」と思った時、いきなり画面にボタンやテキストを配置するのではなく、まずは Models.swift というファイルを作り、どんな情報(タイトル、達成したかどうか、カテゴリ等)が必要かを紙やテキストに書き出してみてください。

データという「箱」の形がしっかり決まっていれば、後からデータベースに保存したり、画面に表示したりする作業が驚くほどスムーズになります!

次回は、iOS開発で一番の鬼門と言われるCoreData(データ保存)とViewModel(脳みそ)の連携について、私が実際につまずいた設定の罠などを交えながら解説します!お楽しみに!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?