Objective-Cで連絡先を整理してみよう(総合演習)
ここまでの Objective-C レッスンで学んだことを全部つないで、動くものを1つ完成させます。新しい構文は出てきません。クラス・プロパティ・NSArray・NSDictionary・for-in・ブロック——すでに練習したものだけで組み立てます。
作るのは連絡先の整理です。ごぶさたしている相手を見つけ、分類ごとに数えて、一覧にまとめます。
Objective-Cで最初に慣れるべきは、角括弧の書き方です。[object method]——これは関数を呼んでいるのではなく、「オブジェクトにメッセージを送っている」という考え方です。この演習では、それが自然に手に馴染むところまで進みます。
このページではコードを実行していません。Objective-Cはコンパイルが必要で、ブラウザの中では走らせられないためです。かわりにお手本と同じコードを書けたかを、その場で判定します(判定はあなたのブラウザの中だけで行われ、どこにも送信されません)。
表示している実行結果は本物です。このページを作るときに、実際にclangでコンパイルして走らせた出力をそのまま載せています。
完成イメージ
最後まで進むと、このコードが完成します。下の実行結果は、このページを作るときに開発機で実際にコードを走らせて得たものです(このページ上では実行していません)。
===== 連絡先 ===== 中村 友人 070-9999-0000 120日前 ごぶさた 佐野 友人 080-3333-4444 47日前 ごぶさた 兄 家族 090-5555-6666 21日前 取引先窓口 仕事 03-7777-8888 9日前 母 家族 090-1234-5678 3日前 山田課長 仕事 03-1111-2222 1日前 ---------------------------------------------- 家族: 2件 仕事: 2件 友人: 2件 登録件数: 6件 30日以上連絡していない相手: 2件 いちばんごぶさた: 中村(120日前)
STEP 1連絡先をクラスで表す
🎯 このステップのゴール: @interfaceで宣言し、@implementationで実装を書く
まずデータの形を決めます。連絡先1件が持つのは「名前・電話番号・分類・最後に連絡した日」の4つです。
Objective-Cのクラスは2つに分かれています。@interfaceが「外から何ができるか」の宣言、@implementationが「実際にどうするか」の実装です。
この分け方はC言語のヘッダファイルから来ています。実務では.hと.mという別々のファイルに書きますが、ここでは1つにまとめてあります。
@propertyの括弧の中が大事です。
copy——NSStringには必ずこれを付けます。渡された文字列が後から書き換えられる可能性があるからです。NSMutableStringを渡されて、あとで中身を変えられたら、この連絡先の名前も勝手に変わります。copyなら複製を持つので安全です。
assignはNSIntegerのような数値に使います。オブジェクトではないので、複製も参照管理も要りません。
readonlyは外から書き換えられないという意味です。連絡先の記録なので、作ったあと変わっては困ります。
初期化メソッドのself = [super init];はお決まりの形です。親クラスに初期化させ、その戻り値を自分に代入し直します。if (self)で確かめるところまで含めて、この4行はそのまま覚えてしまってください。
中で_nameとアンダースコア付きで書いているのは、@propertyが自動で作る変数です。初期化の中だけは、この形で直接入れます。
登録件数: 6件 1件目: 母 090-1234-5678 3日前
STEP 2NSLogではなくprintfで出す
🎯 このステップのゴール: NSStringを組み立て、UTF8Stringでprintfに渡す
Objective-Cの入門でまず教わるのはNSLogです。この演習では使いません。理由があります。
NSLogは出力の先頭に日時とプロセス名を付けます。——2026-08-18 13:04:00.586 a.out[75024:25908553] 合計: 1234のような形です。
だから実行するたびに出力が変わります。このページに載せた結果と、あなたの手元の結果が一致しません。教材として困ります。
かわりにprintfを使います。ただしNSStringをそのまま渡せません——printfはC言語の関数で、NSStringを知らないからです。
[line UTF8String]でC言語の文字列に変換してから渡します。
組み立てには[NSString stringWithFormat:@"..."]を使います。%@がオブジェクト用の書式指定で、NSStringもそのまま埋め込めます。
整数の書式にひとつ落とし穴があります。NSIntegerを%dで出すと、環境によってはおかしな値になります。64ビットではNSIntegerがlongだからです。%ldを使い、(long)でキャストするのがお決まりの形です。
この(long)は省かないでください。32ビット環境ではNSIntegerがintになるので、キャストしないと環境によって壊れます。
@autoreleasepoolは一時的に作られたオブジェクトを片付ける仕組みです。mainを囲むのがお決まりです。
母 (090-1234-5678) 3日前 山田課長 (03-1111-2222) 1日前 佐野 (080-3333-4444) 47日前 兄 (090-5555-6666) 21日前 取引先窓口 (03-7777-8888) 9日前 中村 (070-9999-0000) 120日前
STEP 3NS_ENUMとswitchで分類を扱う
🎯 このステップのゴール: 型の付いた列挙を作り、switchで名前に変える
分類をNSStringで持つと、@"家族"と@"かぞく"が混ざります。集計が合わなくなります。
typedef NS_ENUM(NSInteger, ContactGroup)——これがObjective-Cの列挙の書き方です。
ただのenumと何が違うのか。NS_ENUMは「値の型」も一緒に決めます。ここではNSInteger。型が決まっていると、コンパイラが「この変数にはこの列挙の値しか入らない」と分かります。
効き目はswitchで出ます。caseを1つ書き忘れると、「この値が扱われていない」と警告が出ます。ただのenumでは出ません。
名前の付け方にも作法があります。ContactGroupFamily——列挙の名前を頭に付けます。
Objective-Cには名前空間がありません。だからFamilyという短い名前を作ると、他のライブラリのFamilyとぶつかります。NSStringやUIViewのNSやUIも、同じ理由で付いている接頭辞です。
switchの各caseですぐreturnしているので、breakを書き忘れて次に流れ込む事故は起きません。
末尾のreturn @"その他";は保険です。NS_ENUMのおかげで通常は届きませんが、数値をキャストして無理やり渡された場合に備えています。
家族: 母 仕事: 山田課長 友人: 佐野 家族: 兄 仕事: 取引先窓口 友人: 中村
STEP 4NSDictionaryで分類ごとに数える
🎯 このステップのゴール: 数値をNSNumberで包み、可変の辞書に積み上げる
分類ごとの件数を数えます。使うのはNSMutableDictionaryです。
ここでObjective-Cの大きな制約に出会います。——コレクションにはNSIntegerのような素の数値を入れられません。入れられるのはオブジェクトだけです。
だから@(c.group)と書いてNSNumberに包みます。@()は「これをオブジェクトにする」という短い書き方です。
取り出すときは逆に.integerValueで中身の数値に戻します。包んで、出して、また包む——手間ですが、Objective-Cのコレクションを使うかぎり必ず付いてきます。
そして、この面倒がありがたい場面があります。——まだ無いキーを読んでも落ちません。counts[key]はnilを返します。
nilにメッセージを送っても落ちないのがObjective-Cの特徴です。current.integerValueはcurrentがnilなら0を返します。だから「キーが無ければ0を入れる」という前処理が要りません。
これは他の言語と大きく違う性質です。JavaならNullPointerException、Swiftなら型エラー——Objective-Cは黙って0やnilを返します。
便利ですが、裏返すと危険でもあります。変数がnilのまま処理が進んでもエラーにならず、静かに間違った結果が出ます。「なぜか0になる」の原因は、たいていこれです。
取り出す順番はorderという配列で持っています。辞書は順番を保証しないからです。
家族: 2件 仕事: 2件 友人: 2件 無いキー: 0件
STEP 5ブロックで並べ替える
🎯 このステップのゴール: 比較の手順を無名関数として渡す
最後に連絡した日が古い順に並べ替えます。
[contacts sortedArrayUsingComparator:^NSComparisonResult(Contact *a, Contact *b) { ... }]
^で始まるのがブロック——他の言語でいうラムダ式や無名関数です。その場に書いた小さな関数を、そのまま引数として渡しています。
読み方に慣れが要ります。^NSComparisonResult(Contact *a, Contact *b)——^のあとが戻り値の型、括弧が引数。C言語の関数宣言に^を付けた形だと思ってください。
返す値は3つのうちのどれかです。NSOrderedAscendingなら1つ目が先、NSOrderedDescendingなら2つ目が先、NSOrderedSameなら同じ。
名前が直感に反するので注意してください。「Ascending(昇順)」は「昇順に並べる」ではなく「1つ目のほうが小さい」という意味です。だから大きい順に並べたいときにNSOrderedAscendingを返す——ここは何度書いても迷うところです。
この演習ではa.lastCallDay > b.lastCallDayのときNSOrderedAscending——つまり日数の大きい(=古い)ほうを先に持ってきています。
sortedArrayUsingComparatorは元の配列を変えません。並べ替えた新しい配列を返します。NSArrayはそもそも変更できないので、これが自然な形です。
元を直接並べ替えたいならNSMutableArrayのsortUsingComparatorですが、「呼んだら元のデータが変わる」関数は扱いに注意が要ります。
--- ごぶさたな順 --- 中村 120日前 佐野 47日前 兄 21日前 取引先窓口 9日前 母 3日前 山田課長 1日前 元の1件目: 母
STEP 6ひとつの関数にまとめて仕上げる
🎯 このステップのゴール: Reportにまとめ、日本語の幅を揃えて表にする
最後の仕上げです。ここまでの部品をReportという1つの関数にまとめます。
桁揃えにPadRightを自分で書いています。「山田課長」は4文字ですが、等幅フォントでは英数字8文字ぶんの幅——文字数で数えると揃いません。
[s characterAtIndex:i]で1文字ずつ見て、0x80より小さければ1、そうでなければ2として数えています。
足すところでNSMutableStringを使っています。NSStringは変更できないので、繰り返し足すなら可変のほうです。[s mutableCopy]で変更できる複製を作ってから足していきます。
この「不変と可変が別クラス」という考え方は、Objective-Cのいたるところに出てきます。NSArrayとNSMutableArray、NSDictionaryとNSMutableDictionary——すべて同じ対です。
既定は不変のほうを選んでください。変更できないものは知らないうちに書き換わる心配がありません。可変が必要になってから切り替えれば十分です。
static const NSInteger kStaleDays = 30;で「ごぶさた」の境目を定めます。先頭のkは定数を表す慣習で、Appleのコードでも広く使われています。
sorted.firstObjectで先頭を取っています。sorted[0]とは違って、空の配列でも落ちません——nilが返るだけです。「空かもしれない」ときはfirstObjectを使ってください。
完成です。kStaleDaysを10に変えてみてください。「ごぶさた」の付く相手が増えます。200にすれば0件になり、締めの行が消えます。
1つの数字を変えるだけで判定が全部連動する——定数に切り出しておいた効果が確かめられます。
===== 連絡先 ===== 中村 友人 070-9999-0000 120日前 ごぶさた 佐野 友人 080-3333-4444 47日前 ごぶさた 兄 家族 090-5555-6666 21日前 取引先窓口 仕事 03-7777-8888 9日前 母 家族 090-1234-5678 3日前 山田課長 仕事 03-1111-2222 1日前 ---------------------------------------------- 家族: 2件 仕事: 2件 友人: 2件 登録件数: 6件 30日以上連絡していない相手: 2件 いちばんごぶさた: 中村(120日前)
よくある質問
このページではコードを実行しないのですか?
していません。Objective-Cはコンパイルが必要で、ブラウザの中では走らせられないためです。このページではお手本と同じコードを書けたかどうかを判定する形にしています(判定はあなたのブラウザの中だけで行われ、コードはどこにも送信されません)。
ただし表示している実行結果は本物です。このページを作るときに実際にclangでコンパイルして走らせた出力を、そのまま載せています。
なぜ NSLog を使わないのですか
出力に日時とプロセス名が混ざるからです。
NSLog(@"合計: %d", 1234)と書くと、実際には2026-08-18 13:04:00.586 a.out[75024:25908553] 合計: 1234のように出ます。実行するたびに変わるので、教材に載せた結果と一致しません。
この演習ではprintfとUTF8Stringに統一しています。
実務ではNSLogで構いません。日時が付くのは、ログとしてはむしろ利点です。
NSInteger を %d で出してはいけないのですか
環境によって壊れます。64ビットではNSIntegerはlong、32ビットではintになるためです。
%ldを使い、(long)でキャストするのがお決まりの形です。printf("%ld日", (long)c.lastCallDay)のように書きます。
キャストを省くと、環境を変えたときだけ壊れます。手元で動いていたものが別の機械で崩れる——見つけにくい種類の不具合です。
nil にメッセージを送っても落ちないのは便利ですか
便利ですが、危なくもあります。
便利な面は、nilチェックを省けることです。この演習でも、辞書に無いキーを読んでnil.integerValueが0を返すおかげで、「キーが無ければ0を入れる」という前処理が要りません。
危ない面は、間違いが表に出ないことです。変数がnilのまま処理が進んでも例外にならず、静かに0や空文字が返り続けます。「なぜか0になる」「なぜか何も表示されない」の原因は、たいていこれです。
大事な場面では、自分でnilを確かめてください。
🚩 次はこのカテゴリへ
SwiftはObjective-Cの後継として作られた言語です。この演習で書いた@interfaceと@implementationの分離も、@(...)で数値を包む手間も、Swiftではどれも要りません。そしてnilは型で管理され、黙って0を返すことがなくなりました。Swiftカテゴリの最終演習で、その違いを確かめてみてください。