jarinosuke blog

about software engineering, mostly about iOS

Xcode 5 で作る Universal Static Library

なぜ Static Library なのか

全てのソースコードGitHub にアップロードし、Cocoapods に podspec をマージしてもらって、

他のデベロッパー達とのコラボレーションを楽しむのが現在の iOS 界隈の Social Coding の主流になっているのかなと思います。

しかし、以下のような要因などによっては上記のようなアクションを取り辛い場合があると思います。

  • コード自体は自由に使ってもらいたいが、実装ファイルは見られたくない

  • ファイルが山ほどあるので、それらを使いやすくパッケージ化したうえで共有したい

そんなときに有効な手段の一つとなるのが Static Library です。

Static Library は指定した header file とコンパイル済みの archive file (.a 拡張子)がインターフェースとなります。

そのため、実装部分が利用者には見えなくなり隠蔽できるうえ、利用者にも扱いやすくなります。

プロジェクトの作成

では実際に Xcode 5 を用いて Static Library を作成してみましょう。

まずは Static Library 作成用のプロジェクトを新規に作成します。

Xcode を起動し、メニューから File -> New… -> Project を選択します。

左ペインの iOS セクションから Framework & Library を選択し、以下の Cocoa Touch Static Library を選択します。

f:id:jarinosuke0808:20140208184830p:plain

適当な Product Name を付けたら完成です、早速ビルドしてみましょう。

Product フォルダに libProject.a という archive file ができていると思います。

これを選択状態にし、 Show in Finder を選択して実際にどんなファイルが生成されているかを確認してみましょう。

Product フォルダ内にある lib.a という archive file が生成されているので、

それを選択して右クリックメニューから Show in Finder です。

上記でも説明した、header file と archive file の2種類が生成されています。

手順としてはこれだけです。

あとは生成された Static Library を、利用したいプロジェクトに追加し適切にリンクするだけです。

Universal Library

実は上記の手順だけだと、一つ問題があります。

このままだとビルドした時に選択している Architecture 用にコンパイルされた Static Library しか生成されないからです。

例えば iOS Simulator を選択してビルドした Static Library は iPhone など実機のArchitecture とは異なるため使用できません。

Xcode が Universal Library 用のテンプレートなりを用意してくれても良い気がするのですが、そんなものはありません。

なので lipo コマンドを使い、各 Architecture 毎に生成した Static Library を結合する処理を追加します。

手順をみていきましょう。

まず先ほど作成した Static Library 作成用プロジェクトに Aggregate ターゲットを追加します。

f:id:jarinosuke0808:20140208184910p:plain

Aggregate ターゲットは、複数のターゲットを一度にビルドしてくれ、コマンドラインスクリプトを記述できるターゲットです。

ソースコードコンパイルしたり、ライブラリをリンクするなど普通のターゲットとは違うので、ターゲット同士を結合するための非常にシンプルかつ透明性のあるターゲットという位置付けになっています。

ターゲットが作成できたら、下記のようにメニューを辿ってスクリプトを追加するための項目を以下のようにして追加します。

f:id:jarinosuke0808:20140208184947p:plain

スクリプトは以下のようなものを追加しましょう。

# Universal Libraryを配置する場所
UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal
 
# 実機用ライブラリ生成
xcodebuild -target TestTarget ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}

# iOS Simulator用ライブラリ生成
xcodebuild -target TestTarget -configuration ${CONFIGURATION} -sdk iphonesimulator -arch i386 BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}"
 
# Universal Library用フォルダ生成
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
 
# lipo コマンドで実機・iOS Simulatorライブラリを結合
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/lib${PROJECT_NAME}.a" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/lib${PROJECT_NAME}.a" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/lib${PROJECT_NAME}.a"
 
# lipoコマンドにより生成されたUniversal Libraryをコピー
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/include" "${UNIVERSAL_OUTPUTFOLDER}/" 

簡単に言うと、iOS Simulator 向けの i386, iOS Device の向けの arm の Architecture を生成し、それらを lipo で結合してアウトプットを作っています。

これでビルドするターゲットを Aggregate に設定すれば Universal ビルドを作成出来るようになります。

終わりに

ここまで読んで頂いた方は気付いたかもしれませんが、私たちが SDK 普段使っているなどで使用している Static Library は凄い簡単に作れます。

次回はもう一つ先を行った Framework の作り方を書こうと思います。

参考

Xcode Build Settings Reference

xcodebuild man page

Creating a Static Library in iOS Tutorial

メモリ管理・レイアウトの観点からみた UIViewController の view の扱い

self.view

iOS 開発において、UIViewController の view の振る舞いは一番理解しておきたい点の一つです。

今回はその view に対して、メモリ管理とレイアウトの2つの視点を交えてアプローチを行い、

UIViewController の subclass を作成する上で、

UIViewController の各メソッドにどんな処理を書くべきか、そして何を書くべきでない

を説明出来ればなと思っています。

iOS 6 以降からを対象として考えていますので、 iOS 5 以前は取り扱いません。

self.view の振る舞い

扱いを学ぶには、まず対象の振る舞いを把握する事からです。

ライフサイクルとレイアウトサイクルの2点から簡単に復習します。

self.view のライフサイクル

UIViewController の view がどのようなタイミングで生成されメモリ上に展開し、

どのようにメモリから消されていくか、を Apple が提供している図を元に簡単に把握しましょう。

https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/Art/loading_a_view_into_memory_2x.png

では上記の要点を簡単に。

view はアクセスされたタイミングで初めて生成される

アプリケーションからビューを要求されてはじめて、UIViewController は view をメモリ上に展開し、

自身の view プロパティにも保持するようになります。

UIViewController の提供されているメソッドを用いたロードサイクルは上記の図の通りです。

システムの命令により、UIViewController は自身の view を window からアンロード

UIViewController は自身が破棄されるまでは retain 属性の view プロパティを保持し続けるのがデフォルトの動作です。

メモリ警告が起きると、以下のメソッドが呼ばれてシステムにより UIViewController の view が

何らかの理由で window にアタッチされている場合にはアンロードされてしまいます。

(2014/02/01 修正 - ブックマークコメントで指摘を頂いた箇所を修正しました。)

(2014/02/03 追加 - ブックマークコメントの詳しい修正を以下のブログに書いて頂きました。ありがとうございます! )

Viewのリソースの話。

何らかの理由で window からデタッチされている場合にはアンロードされてしまいます。

- (void)didReceiveMemoryWarning;

詳しくは以下が参考になると思います。

viewDidUnloadがdeprecatedになった理由を考察

self.view のレイアウトサイクル

UIViewController の view の frame は、様々な外部要因によって決定されます。

簡単に例を挙げると以下の通りです。

UIWindow の rootViewController

  • 表示されている window の frame

  • ステータスバーの有無、通話中など Background Mode が作動時の高さの変更

  • デバイスの傾き

UITabBarController, UINavigationController などの Container ViewController

上記の Child ViewController は、親の Container ViewController の frame に従い設定されます。

開発者はこれらを意識したレイアウトの実装を UIViewController に行い、

それぞれに適したレイアウトを、 UIViewController の外部からは意識せずとも実現させるのが理想です。

では肝心の UIViewController は、上記の frame 決定プロセスにどのように関与できるのでしょうか?

大まかなメソッドが呼ばれる流れは以下の通りです。

簡略化のため、UIViewAutoresizing と Auto Layout は省いています。

///(1) self.view の frame がシステムによって変更される

///(2) 以下が呼ばれる
- (void)viewWillLayoutSubviews;

///(3) self.view の layoutSubviews が呼ばれる

///(4) 以下が呼ばれる
- (void)viewDidLayoutSubviews;

駆け足で UIViewController のメモリ管理とレイアウトの流れを復習しました。

では実際に、それらに関した処理をどのメソッドに書くべきかを見ていきましょう。

実装メソッド

- (id)init

initializer では、その UIViewController が適切な振る舞いを行うための全ての状態の設定を書くべきです。

具体的にはカレンダーの画面を表示する ViewController だったら NSCalendar、

メモ帳の詳細画面だったらメモの内容はここで設定するのが良いでしょう。

逆に、 view やそれの subview の初期化は行うべきではありません。理由は後述。

- (void)loadView

loadView はシステムによって UIViewController の view プロパティにアクセスされ、

まだ view プロパティが nil の場合に発火します。

override している loadView 内で

[super loadView];

を行うことで初めて view プロパティに何も設定されていない UIView が入ります。

ここでは self.view と、その subviews の load の一点だけを行うべきです。

self.view の frame を用いて、 subviews の frame 調整は行うべきではありません。理由は後述。

- (void):viewDidLoad

loadView が完了されると呼ばれるメソッドです。

名前の通り、UIViewController が管理する全ての view がロード済みであることが保証されています。

なので、ここでは view に対して初期化以外の処理を行うべきです。

具体的には view へのデータセットや Target-Action などの通知設定などがあたります。

- (void):viewDidLayoutSubviews

loadView の項で frame 調整は行うべきではない と書きましたが、それはここで行うべきだからです。

self.view の subviews.frame の調整、すなわちレイアウト処理は全てここで記述するべきです。

なぜなら、このメソッドが発火するタイミングは、self.view の layoutSubviews の後、

要するにシステムや Container ViewController による self.view の frame 調整の後だからです。

このメソッド内で得られる self.view.frame がそのまま画面上に描画されるので、

Navigation Bar があるとか、Tab Bar が無いとか、デバイスが横向きとか全てが反映された後の frame になっています。

- (void):didReceiveMemoryWarning

メモリ警告が発生した場合に呼ばれるメソッドです。

ここでは不要なプロパティや self.view を解放するべきです。

このメソッド内で self.view に nil をセットする事で、

次回以降この UIViewController の view にアクセスがあった際に loadView が発火します。

loadView では view の初期化処理のみを行うべきと書いたのはそれが理由です。

サンプル

最後に GoodViewController と BadViewController という名前でサンプルを書いてみました。

BadViewController は「べきでない」を実践している最悪な UIViewController、

そして GoodViewController は上記の「べき」を実践した UIViewController。

サンプルはあくまでイメージですので、動作や値の保証は致しません。

viewDidLoad 全記述原理主義を目撃した際は、このブログを教えてあげて下さい。

参考

view | UIViewController Class Reference

Resource Management in View Controllers | View Controller Programming Guide

Resizing the View Controllers’s Views | View Controller Programming Guide

コーディング規約要らずの Objective-Clean

コーディング規約

プログラミングを行う上でコーディング規約は一つの指針になり、

定める事でそれをもとにコードレビューなどの場で指摘などがし易くなります。

Objective-C で、しかも公開されているだけでも、ざっと探してみてこれだけの量があります。

企業

開発者

これだけの量のコーディング規約があることからも分かりますが、

それぞれの中で定められている事柄には、結構なばらつきがあるものも多いです。

そのような環境の中で、以下のような事象が発生してしまうのも事実です。

  • コーディング規約を定めたものの、守れないコードが生まれてしまう
  • コードレビュー内でコーディング規約に沿わないコードを見つけるコストが高い
  • コーディング規約に沿わないだけのコードに対して PR をするのが億劫
  • コーディング規約の陳腐化

今回紹介する Objective-Clean はそのようなコーディング規約のイチ文書化を、

技術的視点から解決してくれるツールです。

Objective-Clean

http://a4.mzstatic.com/us/r30/Purple4/v4/86/e3/4d/86e34d5b-a352-95cd-497e-54647420bd88/Icon.512x512-75.png

Objective-Clean が行ってくれるのは以下の2つです。

  • コーディング規約設定ファイルの生成
  • ビルド時に上記ファイルに反するコードへの警告

これによって、今までとは違い、定義したコーディング規約となるファイルが

毎回のビルド時に反映され、Xcode 内で警告を表示してくれるようになります。

具体的には設定ファイルは plist 形式で生成されるので、それをプロジェクト内に組み込み、

一度インストールすると、Project Settings に Run Script が追加されるので、

恐らくそれが plist の内容を読み取り、Clang の静的解析 を拡張してくるのかなと思っています。

では実際の使い方は以下のページを観て頂くとして、

objClean

面白かったコーディング規約の生成方法をちょっと触れます。

コーディング規約生成方法

Objective-Clean は現時点(2014/01/29 v1.4)で、

コーディング規約の取得に際して3つの方法を提供しています。

f:id:jarinosuke0808:20140127225451p:plain

Custom

Objective-Clean には Survey 機能があり、

一問一答形式で数十個の質問に答えていくと、自分にあったコーディング規約のファイルが生成されます。

少し面倒ですが、普段の自分がどういう意識でコードを書いているかが明確になるような気がするのでオススメです。

Stackoverflow

Objective-Clean は Survey 作成に際して、

Facebook, Twitter, Stackoverflow でのログイン機能を提供しています。

その中でもコーディングに関して著しく親和性の高い Stackoverflow でログインしたユーザの作った

コーディング規約の平均を取ったコーディング規約を使用する事が出来ます。

User Average

上記と似ていますが、こちらは Survey を行ったユーザ全ての平均になります。

おわりに

コンパイラの設定ファイルとして常にビルド時に参照する事で、

コーディング規約にワンランク上のステージが訪れたようにも思います。

まだまだ発展途上のツールですが、最近は Cocoapods 対応など入り盛り上がっているようなので使い続けてみようと思います。

ちなみに現在半額セール中なので、買うなら今ですね!

Objective-Clean | App Store

そして僕のコーディング規約ファイルは以下ですので、

興味がある方はのぞいてみて、あれやこれや言って下さい。

StyleSettings.plist

CGGeometry 啓蒙活動

コードを用いてのレイアウト処理

iOS 6 から Auto Layout が導入され、レイアウトに関するコードを書く機会が ずいぶんと減ってきていると思いつつもなかなか減らない今日この頃。

最後の砦となるのはやはり以下の2メソッドでしょう。

//UIView
- (void)layoutSubviews;

//UIViewController
- (void)viewDidLayoutSubviews;

上記2メソッド内でコードを用いて様々なレイアウトを行うわけですが、

今回はその中でのコーディング作法として CGGeometry をもっと使って読みやすくできるよ!

という事を知って欲しくて書きました。

CGGeometry

CGGeometry というと聞き慣れないですが、CGRect や CGPoint, CGSize などの構造体と、それを取り巻くマクロ関数を含めた総称です。

CGGeometry Reference

読み辛いレイアウトコード

早速ですが、僕が読み辛いと思うレイアウトコードを例として以下に挙げました。

CGRect targetFrame = self.targetView.frame;
targetFrame.origin.x = self.frame.origin.x + self.frame.size.width - 10.0;
targetFrame.origin.y = (self.frame.origin.y + self.frame.size.height - targetFrame.size.height) / 2;
self.targetView.frame = targetFrame; 

何が辛いかと言うと、targetFrame に代入される値を一つずつ読んでいられない点です。

ドット記法が4つ連なっていて、しかも間の2つ(frame.origin, frame.size)があまり重要な情報でないのが理由な気がします。

これを解決する手段の一つとして、たまに見かけるのが独自のカテゴリを実装して簡単に操作できるように行う方法があります。

例えば以下。

UIView拡張カテゴリによるUIコーディングの簡略化

しかし、個人的にこのようなカテゴリ拡張は好きではありません。

理由はプロジェクトに入ってきた人が毎度実装部分をみないと動作が掴み辛い点と、

デフォルトでCGGeometry などがあるなかで、 カテゴリ拡張を便利機能として使うのはちょっと腰が引けるのが理由です。

では、その CGGeometry を使うと上記のコードがどう読みやすくなるのかというと、

CGRect targetFrame = self.targetView.frame;
targetFrame.origin.x = CGRectGetMaxX(self.frame) - 10.0;
targetFrame.origin.y = (CGRectGetMaxY(self.frame) - CGRectGetHeight(targetFrame)) / 2;
self.targetView.frame = targetFrame; 

こんな感じになります。求めたい値がマクロ名に、対象が引数になることで かなりノイズが減った事が分かると思います。

実用例

上記までだけでも可読性とコード量の面などから充分に有益だと思います。

以下から実際の開発ユースケースに合わせて、いくつか紹介します。

ビュー座標移動

読み辛い例

CGRect frame = self.view.frame;
frame.origin.x = 10.0;
frame.origin.y = 20.0;
self.view.frame = frame;

読みやすい例

CGRect frame = self.view.frame;
self.view.frame = CGRectOffset(frame, 10.0, 20.0);

子ビューのサイズ調整

以下の様にやってしまいがちですが、驚くほどコード量が減らせるんです。

読み辛い例

CGRect childFrame = self.parentView.frame;
CGFloat horizontalPadding = 5.0; 
CGFloat verticalPadding = 10.0;
childFrame.origin.x = horizontalPadding;
childFrame.origin.y = verticalPadding;
childFrame.size.width -= horizontalPadding * 2;
childFrame.size.height -= verticalPadding * 2;
self.childView.frame = childFrame;

読みやすい例

self.childView.frame = CGRectInset(self.parentView.frame, 5.0, 10.0);

ログ出力

CGRect はデバッグ対象になる割合が非常に多いです。 その時に以下のような事をいちいち書いていたら、何をしていたか忘れかねません。

読み辛い例

NSLog(@“x:%f, y:%f, width:%f, height:%f”, self.view.frame.origin.x, self.view.frame.origin.y, self.view.frame.size.width, self.view.frame.size.height);

これも便利なマクロが提供されています。 CGRect, CGPoint, CGSize の他にも NSRange や UIEdgeInsets などたくさんの構造体に対してあります。

読みやすい例

NSLog(@“frame: %@”, NSStringFromCGRect(self.view.frame));

オブジェクトとしての扱い

CGRect などの構造体で困るのが配列などを用いて、一度に操作したいときです。 しかし、以下のようなことをしてしまうのは非常に残念な事です。

読み辛い例

NSNumber *x = [NSNumber numberWithFloat:self.view.frame.origin.x];
NSNumber *y = [NSNumber numberWithFloat:self.view.frame.origin.y]; 
NSNumber *width = [NSNumber numberWithFloat:self.view.frame.size.width]; 
NSNumber *height = [NSNumber numberWithFloat:self.view.frame.size.height];  
NSDictionary *frameDic = @{@“x”: x,
                                               @“y": y,
                                               @“width": width,
                                               @“height”: height};

CGRect を NSValue に変換する事で簡単に対応できます。

読みやすい例

NSValue *frameValue = [NSValue valueWithCGRect:self.view.frame];
CGRect frameFromValue = [frameValue CGRectValue];

参考

他にも様々な CGGeometry があります。

詳しくは末尾にある参考資料を読んでみてください。

CGGeometry : NSHipster

CGRect Tricks | Cocoanetics

追記 (2014/02/03)

ささやかな啓蒙活動の結果、各界の著名人から CGGeometry やレイアウトに関するお声を頂きました。

嬉しい限りです。

カテゴリ使用等による移植性・可読性の低下への警鐘

CGGeometry の非等価性

AutoLayout への応用

モダンなインスタンス変数の扱いとプロパティ宣言

導入

最近、最近ふと 詳解 Objective-C 2.0 初版 を手に取る(電子化しているので実際に手に取るのは Kindle ですが)機会がありました。

もちろん最新の3版も持っているのですが、初版と比べて読み進めてみると色んな事を振り返る事ができて楽しいんですね、もう歴史です。

言うまでもなく、Objective-C は僕が関わってきた約5年近くだけでも凄まじい発展を遂げています。

コミュニティも WWDC や Developer Forum を初めとして、最近では CocoaPods など 3rd party のものまで活発になっています。

そのような環境の中でも、たまにそれらの範囲外では数年前の「構造」のままのコードを見る事があったりもします。

「構造」と少々分かり辛く書いているのは、以下のような「コーディング規約」を超えた考え方だと思ったので分けて書いています。

もちろんどちらが重要とか、そういう話では全く無いです。

そのような「構造」のズレや遅れを少しでも解決できればなぁと思い、この記事を書いています。

今回はそのような「構造」の中でも、OOP では欠かす事の出来ないインスタンス変数に関するものを3つ挙げました。

どれも僕の今までの iOS 開発による経験が主ですので、批判など大歓迎です。

1.インスタンス変数の定義とアクセス方法

いきなりですが、以下のようにヘッダファイルにインスタンス変数を定義してるなら今すぐやめましょう。

///JRNCustomClass.h

@interface JRNCustomClass : NSObject {
    NSNumber *numberOne;
}
@end

上記の記述には以下に挙げた二重の問題があると思っています。

結論となる解決策から言いますと、

@property ディレクティブを用いてプロパティ定義を行い、そのプロパティにはメッセージ式なりドット記法で必ずアクセスする。

(アクセス方法は「コーディング規約」の領域まで入ってきそうなので、曖昧にしてます。) 書くとしたら、こんな感じです。

///JRNCustomClass.m

@interface JRNCustomClass ()
@property (nonatomic) NSNumber *numberOne;
@end

@implementation JRNCustomClass

- (NSNumber *)numberOne
{
     if (!_numberOne) {
          _numberOne = [NSNumber numberWithInteger:1];
     }
     return _numberOne;
}

@end 

こう書くと、出てくる反論としては以下のようなものがあると思います。

これらの問いは以下の3つの利点によって、ほぼ無意味になると僕は考えます。

確かに getter/setter 経由でプロパティにアクセスする事で、CPU cycle を9倍消費するがそれは無いに等しいし、何回それを行ったとしてもボトルネックにはならないでしょう。

  • 2.一貫性

getter/setter 経由か、インスタンス変数への直接のアクセスか、どちらかによる副作用などのデバッグを行う必要が無くなります。

プロパティにアクセスする時に breakpoint を打ちたい時は getter/setter 内に打てば良いんです。無闇矢鱈に breakpoint を打つ必要が無くなります。

こう見ると、マジでインスタンス変数を定義したり、synthesize 経由で直接インスタンス変数にアクセスする必要なんて無いです。(もちろん getter/setter は除いて)

initializer と dealloc に関しては神経質になるなら、synthesize 経由でも良いと思います。そういう人は以下を参照して下さい。

Advanced Memory Management Programming Guide : Don’t Use Accessor Methods in Initializer Methods and dealloc

2.ヘッダでの読み取り専用プロパティの宣言

プロパティ宣言には属性指定ができ、そのなかでも読み取り専用を指定する readonly 属性はオブジェクト間での隠蔽性を保つ上で有用です。

しかし、上記で「必ずプロパティ宣言で生成された getter/setter 経由でアクセスする」と挙げているので、どうやれば?となりますね。

読み取り専用プロパティを定義しつつ getter/setter からのみのアクセスを維持するには、 以下のように実装ファイルにも同様のプロパティを readwrite 属性として宣言する事で実現可能です。

///JRNCustomClass.h

@interface JRNCustomClass
@property (nonatomic, readonly) NSNumber *numberOne;
@end


///JRNCustomClass.m
@interface JRNCustomClass
@property (nonatomic, readwrite) NSNumber *numberOne;
@end

3.適切な BOOL 型プロパティ宣言

少し「コーディング規約」にまでリーチする話ですが。

正直これは僕もたまに忘れてしまいます、BOOL 型のプロパティ宣言には適切な getter を設定しましょうというものです。

@property (nonatomic, getter = isHuman) BOOL human;

まとめ

大まかに3つ、問題提起と解決方法を記述しました。 既に知っている人達には当たり前だと思う事がほとんどだと思います。

今後も他に「構造」について思うところがあれば書いていきます。 このような記事を書こうと思った詳解 Objective-C 2.0には感謝感謝です。

詳解 Objective-C 2.0 第3版

詳解 Objective-C 2.0 第3版

参考

Structuring Modern Objective-C