iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
iOSでは開発者が独自のフレームワークを作成することは推奨されていない。フレームワークの利用の利点となる動的リンクやバージョン管理の恩恵を受けることはできないが、外部に対してライブラリを提供する方法としてフレームワークを選択することには利点があるので、試してみることにした。
最も簡単なフレームワークのディレクトリ構成は以下のとおり。
MyFramework.framework/
Frameworks -> Versions/Current/Frameworks
Headers -> Versions/Current/Headers
MyFramework -> Versions/Current/MyFramework
Resources -> Versions/Current/Resources
Versions/
A/
Frameworks/
OtherFramework.framework
Headers/
MyHeader.h
MyFramework
Resources/
English.lproj/
Documentation
InfoPlist.strings
Info.plist
Current -> A
Frameworksに参照しているフレームワークを配置する。
Headersに公開するヘッダーファイルを配置する。
MyFrameworはライブラリ本体。
Resourcesはリソースを格納しているディレクトリ。
DocumentationにHTMLまたはPDFの文書を配置する。
Info.plistにフレームワークの設定を記述する。
iOSではXcodeにフレームワーク用にひな形が用意されていないため、このディレクトリ構成になるように自分で対応を記述することになる。
それでは、著者がGitHubで公開しているグラフ描画プログラムSimpleChartをフレームワーク化してみよう。
SimpleChartは一つのプロジェクトファイルに、グラフ描画ビューとサンプルコードが納められているが、グラフ描画ビューはフレームワーク用プロジェクトに、サンプルコードも別プロジェクトに分けて、それをまとめるワークスペースの用意することにした。このプロジェクトの分割とワークスペースの用意については、フレームワーク化とは関係ないので説明は割愛する。
まずは、フレームワーク用プロジェクトを生成する。Static Libraryのひな形を選べばよい。
フレームワークのディレクトリ構成を生成しやすいようにする為、HeadersとClasses、Resourcesのディレクトリを用意して、ソースファイルを移動する。
フレームワーク用のProperty ListをResourcesディレクトリ内に、ファイル名はInfo.plistで生成する。
Info.plistの内容は以下のとおり。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleIdentifier</key>
<string>jp.co.bitz.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string>${PRODUCT_NAME}</string>
<key>NSPrincipalClass</key>
<string></string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (C) Bitz Co., Ltd. All rights reserved.</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
</dict>
</plist>
フレームワーク用のターゲットを追加する。追加するのはAggregateという種類だ。
ターゲットの名前は、分かりやすいように例えば、SimpleChart.frameworkとフレームワーク用だと分かる名前がよいと思う。
追加したターゲットのBuild Phasesにフレームワーク作成用のスクリプトを追加する。
スクリプトの内容は、以下のとおり。
#!/bin/sh
# ==============================
# 変数設定
# ==============================
#FRAMEWORK_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" ${INFOPLIST})
FRAMEWORK_NAME='SimpleChart'
INFOPLIST="${FRAMEWORK_NAME}/Resources/Info.plist"
BUILD_TARGET_NAME=${FRAMEWORK_NAME}
#FRAMEWORK_BUILD_CONFIGURATION= ${CONFIGURATION}
FRAMEWORK_BUILD_CONFIGURATION="Release"
FRAMEWORK_VERSION_NUMBER=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${INFOPLIST})
FRAMEWORK_VERSION=A
FRAMEWORK_BUILD_PATH="Framework"
FRAMEWORK_DIR="${FRAMEWORK_BUILD_PATH}/${FRAMEWORK_NAME}.framework"
PACKAGENAME="${FRAMEWORK_NAME}.${FRAMEWORK_VERSION_NUMBER}.zip"
# ==============================
# ビルド
# ==============================
echo xcodebuild -configuration ${FRAMEWORK_BUILD_CONFIGURATION} -project ${PROJECT_NAME}.xcodeproj -target ${BUILD_TARGET_NAME} clean
xcodebuild -configuration ${FRAMEWORK_BUILD_CONFIGURATION} -project ${PROJECT_NAME}.xcodeproj -target ${BUILD_TARGET_NAME} clean
echo xcodebuild -configuration ${FRAMEWORK_BUILD_CONFIGURATION} -project ${PROJECT_NAME}.xcodeproj -target ${BUILD_TARGET_NAME} -sdk iphonesimulator${IPHONEOS_DEPLOYMENT_TARGET}
xcodebuild -configuration ${FRAMEWORK_BUILD_CONFIGURATION} -project ${PROJECT_NAME}.xcodeproj -target ${BUILD_TARGET_NAME} -sdk iphonesimulator${IPHONEOS_DEPLOYMENT_TARGET}
[ $? != 0 ] && exit 1
echo xcodebuild -configuration ${FRAMEWORK_BUILD_CONFIGURATION} -project ${PROJECT_NAME}.xcodeproj -target ${BUILD_TARGET_NAME} -sdk iphoneos${IPHONEOS_DEPLOYMENT_TARGET}
xcodebuild -configuration ${FRAMEWORK_BUILD_CONFIGURATION} -project ${PROJECT_NAME}.xcodeproj -target ${BUILD_TARGET_NAME} -sdk iphoneos${IPHONEOS_DEPLOYMENT_TARGET}
[ $? != 0 ] && exit 1
# ==============================
# ディレクトリ作成
# ==============================
[ -d "${FRAMEWORK_BUILD_PATH}" ] && rm -rf "${FRAMEWORK_BUILD_PATH}"
mkdir -p ${FRAMEWORK_DIR}
mkdir -p ${FRAMEWORK_DIR}/Versions
mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}
mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}/Resources
mkdir -p ${FRAMEWORK_DIR}/Versions/${FRAMEWORK_VERSION}/Headers
ln -s ${FRAMEWORK_VERSION} ${FRAMEWORK_DIR}/Versions/Current
ln -s Versions/Current/Headers ${FRAMEWORK_DIR}/Headers
ln -s Versions/Current/Resources ${FRAMEWORK_DIR}/Resources
ln -s Versions/Current/${FRAMEWORK_NAME} ${FRAMEWORK_DIR}/${FRAMEWORK_NAME}
# ==============================
# framework作成
# ==============================
lipo -create \
build/${FRAMEWORK_BUILD_CONFIGURATION}-iphoneos/lib${FRAMEWORK_NAME}.a \
build/${FRAMEWORK_BUILD_CONFIGURATION}-iphonesimulator/lib${FRAMEWORK_NAME}.a \
-o "${FRAMEWORK_DIR}/Versions/Current/${FRAMEWORK_NAME}"
cp -Rf ${BUILD_TARGET_NAME}/Headers/* ${FRAMEWORK_DIR}/Headers/
cp ${BUILD_TARGET_NAME}/Resources/* ${FRAMEWORK_DIR}/Resources/
cp ${INFOPLIST} ${FRAMEWORK_DIR}/Resources/
cd ${FRAMEWORK_BUILD_PATH}
chmod -fR 777 "${FRAMEWORK_NAME}.framework"
zip -ry ${PACKAGENAME} $(basename $FRAMEWORK_DIR)
# End Of File
今回、既存のプロジェクトからソースコードを抜き出してフレームワーク化したということで、フレームワーク化によって、既存のソースコードにも手を加えることにある。例えば、ヘッダーファイルのインクルードも、フレムワーク名/ヘッダーファイル名というパスとなる。
#import <SimpleChart/SimpleChartView.h>
この程度の対応だけで大丈夫だと思うが、今回、既存のプロジェクトからソースファイルをフレームワークのプロジェクトに移動したからだと思うが、InterfaceBuilderが、フレームワーク内のクラスの認識に失敗するという問題が発生した。
対応方法を試行錯誤した結果、以下のコードを埋め込むとうまくいったのだが、少し気持ちが悪い。
- (void)viewDidLoad
{
[super viewDidLoad];
[self _init];
/* InterfaceBuilderでフレームワークのクラスを認識できない問題の対処 */
[SimpleChartView class];
:
継続して本格対処方法の調査を行っている。
今回は松戸で開催。個人的に気になって点を中心にまとめてみる。
『タブインターフェイスをドキュメントベースアプリケーションに適応 』。PSMTabBarControlを利用。タブはウィンドウと独立した存在。ドキュメントベースの場合とそう出ない場合について発表。
『Xcode Server〜みんなで使ってみる』。参加している皆でOSX ServerのXcodeサービスを使ってみた。
『iOS向けのフレームワーク化に挑戦』。著者の発表。困っていたことがあったのだがアドバイスを貰って助かった。嬉しい。
『iOSアプリのデジタル署名』。ビルドのプロセスを調べ内容を詳細に説明して貰って、とても得るものがあった。
『Multipeer Connectivityを試してみよう!』。BonjourとCoreBluetoothを組み合わせた機能が欲しいと思っていたが、iOS7から、こんな便利なフレームワークが追加されたなんて。もっと前に知りたかった!
Core Data について調べたことをまとめてみる。
管理オブジェクトコンテキスト(NSManagedObjectContext)がレコード(管理オブジェクト)を管理。Core Dataではレコードをエンティティと呼ぶ。
レコードを格納するファイルは永続オブジェクトストア(Persistent Object Store)が管理し、管理オブジェクトコンテキストとは永続ストアコーディネータ(Persistent Store Coordinator)を経由して関係付けられる。例えば、SQLiteのファイル"demo.sqlite"にデータを読み書きする、管理オブジェクトコンテキストを生成するサンプルコードは、以下のとおり。
/* 例えば、Demo.xcdatamodeldを読み込む */
NSManagedObjectModel *mom = [NSManagedObjectModel mergedModelFromBundles:nil];
/* 永続ストアコーディネータ */
NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
/* 永続オブジェクトストア */
NSArray *documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDiretory, NSUserDomainMask, YES);
NSString *documentDirectory = [documentDirectories objectAtIndex:0];
NSString *path = [documentDirectory stringByAppendingPathComponent:@"demo.sqlite"];
NSURL *storeURL = [NSURL fileURLWithPath:path];
NSError *error = nil;
if (![psc addPersistentStoreWithType:NSSQLiteStireType
configuration:nil
URL:storeURL
options:nil
error:&errir]) {
[NSException raise:@"Open failed"
format:@"Reason: %@", [error localizedDescriptoin]];
}
/* 管理オブジェクトコンテキスト */
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoodinator:psc];
/* Undo Managerを利用しない */
[moc setUndoManager:nil];
保存するサンプルコードは、以下のとおり。
NSError *error = nil;
BOOL result = [moc save:&error];
if ((! result) || (error)) {
NSLog(@"Error: %@", [error localizedDescriptoin]);
}
管理オブジェクトコンテキストに対してデータを検索するのに必要なのがフェッチ要求。
フェッチ要求には、エンティティ名(必須)と述語オブジェクト(Predicate)、整列記述子オブジェクト(Sort Descriptor)の配列(Sort Orderings)が指定できる。例えば、全レコードを"num"順に整列して取得するサンプルコードは、以下のとおり。
/* フェッチ要求 */
NSRetchRequest *request = [[NSRetchRequest alloc] init];
/* エンティティ名 */
NSEntityDescription *ed = [[mom entitiesByName] objectForKey:@"DemoItem"];
[request setEntity:ed];
/* 整列記述子オブジェクト */
NSSortDescriptor *sd = [NSSortDescriptor sortDescriptorWithKey:@"num"
ascending:YES];
[request setSortDescriptors[NSArray arrayWithObject:sd];
/* フェッチ */
NSError *error = nil;
NSArray *response = [moc executeFetchRequest:request error:&error];
if (! response) {
[NSException raise:@"Fetch failed"
format:@"Reason: %@", [error localizedDescriptoin]];
}
NSArray *demoItemMutableArray = [[NSMutableArray alloc] initWithArray:response];
UITableViewを使っている場合、フェッチはNSFetchedResultsControllerが利用できる。
NSError *error = nil;
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:moc
sectionNameKeyPath:nil
cacheName:@"Root"];
if (![fetchedResultsController performFetch:&error]) {
[NSException raise:@"Fetch failed"
format:@"Reason: %@", [error localizedDescriptoin]];
}
管理オブジェクト(NSManagedObjectのサブクラス)の初期化は、initWithEntity:insertIntoManagedObjectContext:、awakeFromInsert、awakeFromFetchのいずれかをオーバーライドする。
- (void)awakeFromInsert
{
[super awakeFromInsert];
/* 独自の初期化コード */
}
- (void)awakeFromFetch
{
[super awakeFromFetch];
/* 独自の初期化コード */
}
管理オブジェクトの追加と削除は、以下の感じ。
/* 追加 */
DemoItem *item = [NSEntityDescription insertNewObjectForEntityForName:@"DemoItem"
inManagedObjectContext:moc];
[demoItemMutableArray addObject:item];
/* 削除 */
[moc deleteObject:item];
[demoItemMutableArray removeObjectIdenticalTo:item];
管理オブジェクトコンテキスト内では、管理オブジェクトは一意なので、フェッチ要求したオブジェクトに対応する管理オブジェクトが管理オブジェクトコンテキストに存在していたら、それをフェッチ結果として返す。
管理オブジェクトは、フェッチされたら管理オブジェクトコンテキストに存在する。管理オブジェクトの使用をやめれば、自動的に解放される。
複数の管理オブジェクトコンテキストが、一つの永続ストアコーディネータを介して、複数の永続オブジェクトストアとつながるということも可能。
NSPersistentDocumentはNSDocumentのサブクラスで、自動で管理オブジェクトコンテキストと永続オブジェクトストアを生成する。
管理オブジェクト(NSManagedObjectのサブクラス)とは、管理オブジェクトモデルで定義できる。エンティティを記述したものをメタデータと呼んでいる。
InterfaceBuilderでフレームワーク内のクラスの認識に失敗する問題が解決したので報告する。
SampleCodeのOther Linker Flagsに-ObjCと-all_loadを指定したら解決した。