Swiftで1か月の家計を費目別にまとめてみよう(総合演習)
ここまでの Swift レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。struct・enum・辞書・オプショナル・guard let・sorted——すでに練習したものだけで組み立てます。
作るのは1か月の家計の集計です。費目ごとに合計し、予算と比べ、金額の大きい支出を並べて、レポートにまとめます。
この演習には仕掛けが2つあります。——予算を決めていない費目と、メモの無い支出です。どちらも「値が無い」状況で、Swiftではこれをオプショナルで表します。
ここがSwiftを学ぶ意味のいちばん出るところです。「無いかもしれない」を型に書かせ、使う前に必ず確かめさせる——だからguard letや??が、覚えるべき記法ではなく必要だから使う道具として出てきます。
このページではコードを実行していません。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際のSwiftで走らせて得た出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== 今月の家計 ===== --- 金額の大きい順 --- 19日 食費 6,120円 (来客の買い出し) 3日 食費 4,280円 12日 趣味 3,800円 (写真展) --- 費目別 --- 食費 使用 13,050円 / 予算 12,000円 超過 日用品 使用 2,800円 / 予算 3,000円 注意 交通費 使用 3,600円 / 予算 6,000円 余裕あり 趣味 使用 3,800円 / 予算 —— 予算未設定 -------------------------------------------- 支出合計: 23,250円 1費目が予算を超えています。
STEP 1費目をenumで、支出をstructで表す
🎯 このステップのゴール: 取りうる費目を限定し、支出1件を値型でまとめる
まずデータの形を決めます。費目から始めましょう。
費目をStringにすると、"食費"と"食品費"が混ざります。集計したときに別の費目として数えられて、合わない——ありがちな失敗です。
enum Category: Stringと書くと、この4つ以外は存在できません。: Stringを付けているのはrawValue(生の値)を持たせるためで、Category.food.rawValueが"食費"になります。
この書き方の効き目は、表示を変えたくなったときに出ます。「食費」を「食料品費」にしたい——直すのはenumの1行だけです。
支出1件はstruct Expenseです。Swiftではclassよりstructを先に考えます。
違いは「渡したときに何が起きるか」です。structは値型——他の変数に入れると複製されます。片方を書き換えても、もう片方は変わりません。classは参照型で、同じ実物を指すので、片方を変えるともう片方も変わります。
家計簿の1件のような「ただのデータ」は、複製されるほうが安全です。知らないうちに別の場所の値が変わっていた、という事故が起きません。
let memo: String?の?がオプショナルです。メモの無い支出のほうが多いので、nilを許す形にしています。
支出件数: 8件 1件目: 3日 食費 4280円 予算を決めた費目: 3個
STEP 2辞書で費目ごとに合計する
🎯 このステップのゴール: default付きの添字で、初回の場合分けを消す
費目ごとに合計します。使うのは辞書——キーと値の対応表です。
var totals: [Category: Int] = [:]で空の辞書を作ります。[:]が空の辞書、[]が空の配列です。見た目が似ているので注意してください。
集計の中身がこの演習でいちばん覚えて帰ってほしい1行です。
totals[e.category, default: 0] += e.amount
default: 0は「そのキーがまだ無ければ0として扱う」という意味です。だから初回でも0 + 4280になって、正しく積み上がります。
これが無いとどうなるか。辞書から取り出した値はInt?——キーが無ければnilなので、そのままでは足せません。if letで場合分けして、無ければ代入、あれば加算……と4行くらい書くことになります。
default:はその4行を1行に畳んでくれます。Swiftらしい、実用的な短縮です。
戻り値の型を[Category: Int]と書いているのにも意味があります。キーがCategoryに限られるので、totals["食費"]のような書き方は通りません。ステップ1でenumにしておいた効果が、ここまで効いています。
引数の_ expenses:の_は「呼ぶときにラベルを書かせない」という指定です。totalByCategory(expenses)と素直に書けます。
食費: 13050円 日用品: 2800円 交通費: 3600円 趣味: 3800円
STEP 3guard letで「予算が無い」を先に片付ける
🎯 このステップのゴール: 早めに抜けて、その先はオプショナルを気にせず書く
予算と比べて、状態を一言で表します。ただし趣味には予算を決めていません——budgets[.hobby]はnilです。
ここでguard letを使います。
guard let budget = budget else { return "予算未設定" }——読み方は「budgetが取り出せなければ、ここで抜ける」です。
if letとの違いは「その後の書きやすさ」です。if letで書くと、取り出した値が使えるのは波括弧の中だけ。判定が続くと、どんどん入れ子が深くなります。
guard letは逆で、この行より下ではbudgetがIntとして使えます。?が外れた状態で、最後まで素直に書けます。
「うまくいかない場合を先に全部片付けて、残りを本筋として書く」——これがguardの考え方です。Swiftのコードが浅く読みやすいのは、この書き方が根付いているからです。
guardのelseでは必ず抜けなければなりません。returnやthrowを書かないとコンパイルが通りません。「抜けたつもりで抜けていない」が起きないようになっています。
注意の判定はDouble(spent) >= Double(budget) * warnRatioです。整数どうしのままでは0.9を掛けられません——Swiftは型の混在に厳しいので、明示的にDouble()で変換します。
面倒に見えますが、気づかないうちに小数が切り捨てられていた、という事故が起きません。
食費 13050 / 12000 → 超過 日用品 2800 / 3000 → 注意 交通費 3600 / 6000 → 余裕あり 趣味 3800 / 未設定 → 予算未設定
STEP 4決まった順番で費目を並べる
🎯 このステップのゴール: 辞書の順序に頼らず、自分で決めた並びで回す
ステップ2で作った辞書を、そのままforで回して表示すると——実行するたびに順番が変わります。
これはバグではありません。辞書はキーから値を素早く引くための入れ物で、順番を保つようには作られていないのです。
SwiftでもKotlinでもJavaでもPythonでも、多かれ少なかれ同じ問題があります。「手元では正しく見えたのに、本番で並びが変わった」——言語を問わず、必ず一度は踏みます。
直し方は決まっています。表示する順番を、自分で持つ。
static let all: [Category] = [.food, .daily, .transport, .hobby]——enumの中に並び順を書いておきます。あとはこの配列を回せば、いつでも同じ順で出ます。
enumの中に置くのが要点です。費目を1つ足したとき、同じファイルの数行下に並び順があるので、直し忘れにくくなります。別の場所に離して置くと、まず忘れます。
totals[category] ?? 0の??は、「nilなら右の値を使う」という意味です。1件も支出の無い費目は辞書にキーが無いので、0として表示します。
budget.map { yen($0) } ?? "——"は少し込み入っています。mapは「中身があるときだけ、その中身を加工する」——nilなら加工せずnilのままです。そこに?? "——"で受け止めています。
「あるときだけ整形し、無ければ別のものを出す」が1行で書けます。
食費: 使用 13050円 / 予算 12000円 日用品: 使用 2800円 / 予算 3000円 交通費: 使用 3600円 / 予算 6000円 趣味: 使用 3800円 / 予算 ——
STEP 5金額の大きい順に並べて上位を出す
🎯 このステップのゴール: sortedに比較の条件を渡し、prefixで先頭だけ取る
「何にいちばん使ったか」を出します。金額の大きい順に並べて、上位3件だけ表示します。
expenses.sorted { $0.amount > $1.amount }——並べ替えの条件を渡すだけです。
$0と$1は「比べる2つ」を表す省略記法です。$0.amount > $1.amountは「1つ目の金額が2つ目より大きいなら、1つ目を先に」——つまり降順という意味になります。
<にすれば昇順です。不等号の向きが、そのまま並び順になります。
sortedは元の配列を変えません。並べ替えた新しい配列を返します。元を直接並べ替えたいときはsort()——末尾にedが付くほうが「新しく作って返す」という命名の決まりがSwiftにはあります。
.prefix(3)で先頭3件を取ります。件数が3件未満でも落ちません——あるだけ返ってきます。sorted[0..<3]と書くと、3件未満のときに落ちます。
金額の整形はNumberFormatterです。numberStyle = .decimalとすると3桁ごとにカンマが入ります。
?? String(amount)で受けているのは、formatter.string(from:)が失敗しうるためです。まず失敗しませんが、Swiftは「失敗しうる」と宣言されたものを、必ず扱わせます。
メモの表示がe.memo.map { " (\($0))" } ?? ""です。ステップ4と同じ形——あるときだけ括弧を付け、無ければ何も出しません。
--- 金額の大きい順 --- 19日 食費 6,120円 (来客の買い出し) 3日 食費 4,280円 12日 趣味 3,800円 (写真展)
STEP 6ひとつの関数にまとめて仕上げる
🎯 このステップのゴール: reportにまとめ、桁を揃えて締めの一言まで出す
最後の仕上げです。ここまでの部品をreportという1つの関数にまとめます。
並べる順番に意味を持たせます。まず金額の大きい支出、次に費目別、最後に合計と結論——目を引くものを先に、まとめを最後にという流れです。
桁揃えにString(format:)を使っています。ここに Swift ならではの注意点があります。
%@という指定子と、as NSStringという変換が出てきます。String(format:)はもともと Objective-C 時代からある仕組みで、文字列を渡すときは%sではなく%@を使い、NSString に変換して渡すのが確実です。
見慣れない書き方ですが、Swiftが iOS/macOS の土台と地続きであることの現れです。文字列補間\(...)だけで済むなら、そちらのほうが Swift らしい書き方になります。
String(repeating: "-", count: 44)で区切り線を引きます。引数に名前が付いているのが Swift らしいところで、String("-", 44)のような曖昧な呼び方はできません。
締めはif over == 0で分けます。「予算を超えた費目はありません」か「〇費目が超えています」か——数字を並べて終わりにせず、「それで、どうなの?」に答えるのが道具としての親切さです。
完成です。budgetsに.hobby: 5000を足してみてください。「予算未設定」が「余裕あり」に変わります——ステップ3でguard letを書いておいた分岐が、実際に切り替わるところが見られます。
warnRatioを0.5にすると、「注意」の付く費目が増えます。1つの数字を変えるだけで判定が変わる——定数に切り出しておいた効果です。
===== 今月の家計 ===== --- 金額の大きい順 --- 19日 食費 6,120円 (来客の買い出し) 3日 食費 4,280円 12日 趣味 3,800円 (写真展) --- 費目別 --- 食費 使用 13,050円 / 予算 12,000円 超過 日用品 使用 2,800円 / 予算 3,000円 注意 交通費 使用 3,600円 / 予算 6,000円 余裕あり 趣味 使用 3,800円 / 予算 —— 予算未設定 -------------------------------------------- 支出合計: 23,250円 1費目が予算を超えています。
よくある質問
このページではコードを実行しないのですか?
していません。Swiftはコンパイルが必要な言語で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際のSwiftで走らせた出力を、そのまま載せています。
guard let と if let は、どちらを使えばいいですか
取り出した値をその後ずっと使うならguard letです。
if letで取り出した値は波括弧の中でしか使えません。判定が2つ3つと続くと、その分だけ入れ子が深くなり、本筋がどんどん右へ寄っていきます。
guard letならその行より下でずっと使えます。「うまくいかない場合を先に片付けて、残りを素直に書く」という形になり、コードが浅く保てます。
if letが向くのは、その場だけで使って終わりのときです。
struct と class は、どう使い分けますか
迷ったらstructから始めてください。Swiftの標準ライブラリも、配列も文字列も辞書もすべてstructです。
structは値型で、他の変数に入れると複製されます。知らないうちに別の場所の値が変わっていた、という事故が起きません。
classが要るのは、同じ実物を複数の場所から指したいときや、継承したいときです。画面の状態や、アプリ全体で1つだけ持つ設定などが当てはまります。
辞書の順番が実行するたびに変わります
辞書は順番を持たない入れ物だからです。キーから値を素早く引くことに特化していて、並び順は保証されません。
決まった順で表示したいなら、ステップ4のように並び順を自分で配列として持ちます。Category.allを回せば、何度実行しても同じ順です。
キーを並べ替える方法もありますが、「食費・日用品・交通費・趣味」のように意味のある順に並べたいときは、自分で書いたほうが素直です。
🚩 次はこのカテゴリへ
Swiftのオプショナルと、Kotlinのnull安全はほとんど同じ考え方です。guard letにあたるものが?:と早期return、??はそのまま?:——片方を知っていると、もう片方はすぐ読めます。Kotlinカテゴリの最終演習では、記録し忘れのある歩数データを集計します。