Cocoa練習帳 -12ページ目

[OSX][iOS]Sprite Kit(その4)

『Sprite Kit Programming Guide』を参考に物理シミュレートを組み込んでみた。

宇宙船に物理的な実態を与えてみよう。

@implementation SpaceshipScene
...
- (SKSpriteNode *)newSpaceship
{
    /* 宇宙船を生成 */
    SKSpriteNode *hull = [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32)];
    
    /* 宇宙船にライトをつける */
    SKSpriteNode *light1 = [self newLight];
    light1.position = CGPointMake(-28.0, 6.0);
    [hull addChild:light1];
    
    /* 宇宙船にライトをつける */
    SKSpriteNode *light2 = [self newLight];
    light2.position = CGPointMake(28.0, 6.0);
    [hull addChild:light2];
    
    /* 宇宙船に実体を与える */
    hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];
    
    /* 宇宙船に重力の影響が与えられないようにする */
    hull.physicsBody.dynamic = NO;
	
    /* 宇宙船を動かす */
    SKAction *hover = [SKAction sequence:@[
                                           [SKAction waitForDuration:1.0],
                                           [SKAction moveByX:100 y:50.0 duration:1.0],
                                           [SKAction waitForDuration:1.0],
                                           [SKAction moveByX:-100.0 y:-50 duration:1.0]]];
    [hull runAction: [SKAction repeatActionForever:hover]];
    
    return hull;
}
...
@end

ランダムに岩石を生成して、それを落としてみよう。

static inline CGFloat skRandf(void)
{
    return rand() / (CGFloat) RAND_MAX;
}
 
static inline CGFloat skRand(CGFloat low, CGFloat high)
{
    return skRandf() * (high - low) + low;
}
 
...
@implementation SpaceshipScene
...
- (void)createSceneContents
{
    self.backgroundColor = [SKColor blackColor];
    self.scaleMode = SKSceneScaleModeAspectFit;
    
    /* 宇宙船を配置 */
    SKSpriteNode *spaceship = [self newSpaceship];
    spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) - 150);
    [self addChild:spaceship];
    
	
    SKAction *makeRocks = [SKAction sequence: @[
                                                [SKAction performSelector:@selector(addRock) onTarget:self],
                                                [SKAction waitForDuration:0.10 withRange:0.15]
                                                ]];
    [self runAction: [SKAction repeatActionForever:makeRocks]];
}
...
- (void)addRock
{
    /* 岩石を作り出す */
    SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor brownColor] size:CGSizeMake(8,8)];
    rock.position = CGPointMake(skRand(0, self.size.width), self.size.height-50);
    rock.name = @"rock";
    rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size];
    rock.physicsBody.usesPreciseCollisionDetection = YES;   /* 衝突判定を正確に */
    [self addChild:rock];
}
...
@end

下に落ちて見えなくなった岩石を削除する。

@implementation SpaceshipScene
...
-(void)didSimulatePhysics
{
    /* 物理シミュレート後の実行 */
    [self enumerateChildNodesWithName:@"rock" usingBlock:^(SKNode *node, BOOL *stop) {
        /* 見えなくなった岩石を削除 */
        if (node.position.y < 0)
            [node removeFromParent];
    }];
}
...
@end

spiritus


ソースコード GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Spiritus - GitHub
関連情報 iOSヒューマンインターフェイスガイドライン
iOS Technology Overview
UIKit Dynamics Catalog
Sprite Kit Programming Guide
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[OSX][iOS]Sprite Kit(その3)

基本的に『Sprite Kit Programming Guide』の内容をサンプルコードに組み込んでみた。

シーンを追加することにする。SKSceneの子クラスSpaceshipSceneを生成する。

前回ラベルを動かすのに呼んだメソッド - runAction: には、終了時に実行するBlocksを渡せる - runAction:completion: というメソッドがあるので、これでSpaceshipSceneシーンを呼び出すようにする。

@implementation MyScene
 ...
 
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    SKNode *myLabel = [self childNodeWithName:@"myLabel"];  /* ノードを取得する */
    if (myLabel != nil) {
        myLabel.name = nil;
        SKAction    *moveUp = [SKAction moveByX: 0 y: 100.0 duration: 0.5]; /* 上昇 */
        SKAction    *zoom = [SKAction scaleTo: 2.0 duration: 0.25];         /* 拡大 */
        SKAction    *pause = [SKAction waitForDuration: 0.5];               /* 停止 */
        SKAction    *fadeAway = [SKAction fadeOutWithDuration: 0.25];       /* フェードアウト */
        SKAction    *remove = [SKAction removeFromParent];                  /* 消滅 */
        SKAction    *moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove]];
        [myLabel runAction:moveSequence completion:^{
            /* SpaceshipSceneに遷移 */
            SKScene *spaceshipScene  = [[SpaceshipScene alloc] initWithSize:self.size];
            SKTransition *doors = [SKTransition doorsOpenVerticalWithDuration:0.5];
            [self.view presentScene:spaceshipScene transition:doors];
        }];
    }
}
 
 ...
@end

SpaceshipSceneの内容は以下の通り。ほぼ、参考資料のままだ。

@interface SpaceshipScene ()
@property (assign, nonatomic) BOOL  contentCreated;
@end
 
@implementation SpaceshipScene
 
- (void)didMoveToView:(SKView *)view
{
    if (!self.contentCreated) {
        [self createSceneContents];
        self.contentCreated = YES;
    }
}
 
- (void)createSceneContents
{
    self.backgroundColor = [SKColor blackColor];
    self.scaleMode = SKSceneScaleModeAspectFit;
    
    /* 宇宙船を配置 */
    SKSpriteNode *spaceship = [self newSpaceship];
    spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) - 150);
    [self addChild:spaceship];
}
 
- (SKSpriteNode *)newSpaceship
{
    /* 宇宙船を生成 */
    SKSpriteNode *hull = [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32)];
    
    /* 宇宙船にライトをつける */
    SKSpriteNode *light1 = [self newLight];
    light1.position = CGPointMake(-28.0, 6.0);
    [hull addChild:light1];
    
    /* 宇宙船にライトをつける */
    SKSpriteNode *light2 = [self newLight];
    light2.position = CGPointMake(28.0, 6.0);
    [hull addChild:light2];
	
    /* 宇宙船を動かす */
    SKAction *hover = [SKAction sequence:@[
                                           [SKAction waitForDuration:1.0],
                                           [SKAction moveByX:100 y:50.0 duration:1.0],
                                           [SKAction waitForDuration:1.0],
                                           [SKAction moveByX:-100.0 y:-50 duration:1.0]]];
    [hull runAction: [SKAction repeatActionForever:hover]];
    
    return hull;
}
 
- (SKSpriteNode *)newLight
{
    /* ライトを生成 */
    SKSpriteNode *light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(8,8)];
    
    /* 点滅させる */
    SKAction *blink = [SKAction sequence:@[
                                           [SKAction fadeOutWithDuration:0.25],
                                           [SKAction fadeInWithDuration:0.25]]];
    SKAction *blinkForever = [SKAction repeatActionForever:blink];
    [light runAction: blinkForever];
    
    return light;
}
 
@end

spiritus


『Sprite Kit Programming Guide』では、-(id)initWithSize:(CGSize)sizeを使わないで、contentCreatedプロパティで初回起動の判定しているのは何故だろう?

ソースコード GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Spiritus - GitHub
関連情報 iOSヒューマンインターフェイスガイドライン
iOS Technology Overview
UIKit Dynamics Catalog
Sprite Kit Programming Guide
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[OSX][iOS]Sprite Kit(その2)

『Sprite Kit Programming Guide』を参考に、サンプルコードを拡張していきたいと思う。

インスタンス変数を使って保持していなくても取得できるように、最初に表示するラベルに名前を付ける。

@implementation MyScene
 
-(id)initWithSize:(CGSize)size
{
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
        
        SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];        
        myLabel.name = @"myLabel";  /* ノードに名前を付ける */
        myLabel.text = @"Hello, World!";
        myLabel.fontSize = 30;
        myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                       CGRectGetMidY(self.frame));
        [self addChild:myLabel];
    }
    return self;
}
 
 ...
@end

名前を付けたノードをタッチされた際に取得して、アニメーションさせる。

@implementation MyScene
 ...
 
 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    SKNode *myLabel = [self childNodeWithName:@"myLabel"];  /* ノードを取得する */
    if (myLabel != nil) {
        myLabel.name = nil;
        SKAction    *moveUp = [SKAction moveByX: 0 y: 100.0 duration: 0.5]; /* 上昇 */
        SKAction    *zoom = [SKAction scaleTo: 2.0 duration: 0.25];         /* 拡大 */
        SKAction    *pause = [SKAction waitForDuration: 0.5];               /* 停止 */
        SKAction    *fadeAway = [SKAction fadeOutWithDuration: 0.25];       /* フェードアウト */
        SKAction    *remove = [SKAction removeFromParent];                  /* 消滅 */
        SKAction    *moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove]];
        [myLabel runAction:moveSequence];
    }
}
 
 ...
@end
ソースコード GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Spiritus - GitHub
関連情報 iOSヒューマンインターフェイスガイドライン
iOS Technology Overview
UIKit Dynamics Catalog
Sprite Kit Programming Guide
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[OSX][iOS]Sprite Kit(その1)

最近の流れはプログラマでもUIを他人任せにしないということだろうか。iOS7のUIやのUIKit Dynamics、そして、今回取り上げるSprite Kitから、そんなメッセージを感じた。

Sprite Kitは、2Dゲーム開発をサポートしてくれるフレームワークで、OS標準のフレームワークに含まれていることからゲーム以外にも利用の用途はあると思う。

主なクラスは、描画領域となるSKViewと、ゲームの1画面に対応するSKScene、画面に置かれる要素となるSKNodeとそのサブクラス、画面遷移のアニメーションを担当するSKTransition、ノードの動作を担当するSKActionだ。

今回は、XcodeのiOSのひな形SpriteKit Gameで生成されるプロジェクトの中身を見て行こうと思う。

SpriteKit Gameのひな形から、Spiritusという名前でプロジェクトを作成(GitHubにサンプルコードを用意したので、そちらを参照してほしい)。

ひな形から生成されたプロジェクトでは、SpriteKit.frameworkがリンクされ、以下のようにSpriteKit.hがインポートされているが、最新の開発環境ではLLVM clangのModulesに対応されているので、以下のインポート分を記述するだけで対応できるが、ひな形は以前の最新の記述を採用していないようだ。

@import SpriteKit;

一個だけ生成されたビューコントローラで、画面全体に配置してSKViewに、デバッグ用の情報表示を設定して、シーンを一個追加している。

@implementation ViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
	
    // Configure the view.
    SKView * skView = (SKView *)self.view;
    skView.showsFPS = YES;          /* frame rateを表示 */
    skView.showsNodeCount = YES;    /* ノード個数を表示 */
    skView.showsDrawCount = YES;    /* 描画個数を表示 */
    
    // Create and configure the scene.
    SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
    scene.scaleMode = SKSceneScaleModeAspectFill;
    
    // Present the scene.
    [skView presentScene:scene];
}
 :
@end

つぎに、ひな形から生成されたシーンMySceneの中身を見てみよう。

シーンが開かれたら、中央に『Hello, World!』と記述されたラベルのノードを追加している。


@implementation MyScene


-(id)initWithSize:(CGSize)size {    
    if (self = [super initWithSize:size]) {
        /* Setup your scene here */
        
        self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
        
        SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
        
        myLabel.text = @"Hello, World!";
        myLabel.fontSize = 30;
        myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
                                       CGRectGetMidY(self.frame));
        
        [self addChild:myLabel];
    }
    return self;
}
 :
@end

そして、このシーンが表示されている画面がタッチされると、宇宙船の画像が貼られた、回転するアクションを追加したノードを追加している。

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    /* Called when a touch begins */
    
    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
        
        SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"Spaceship"];
        
        sprite.position = location;
        
        SKAction *action = [SKAction rotateByAngle:M_PI duration:1];
        
        [sprite runAction:[SKAction repeatActionForever:action]];
        
        [self addChild:sprite];
    }
}

次回は、このサンプルポードをカスタマイズしてみたいと思っている。

ソースコード GitHubからどうぞ。
https://github.com/murakami/workbook/tree/master/ios/Spiritus - GitHub
関連情報 iOSヒューマンインターフェイスガイドライン
iOS Technology Overview
UIKit Dynamics Catalog
Sprite Kit Programming Guide
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[OSX]CUPS Webインタフェースの有効化

OS Xの印刷機能のCUPSにWebブラウザでアクセスしたい場合は、以下のURLとなる。

http://localhost:631

ただし、最近のOS Xでは、このWebインタフェースは無効となっているので、事前にターミナルで以下のコマンドを実行しておく必要がある。

$ cupsctl WebInterface=yes
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[OSX]Mountain Lion以降でWeb共有の有効化

Mountain Lionからシステム環境設定の共有にWeb共有がなくなった。

そのため、以前のようにWeb共有を有効したい場合は、ターミナルで以下のコマンドを実行する必要がある。

$ sudo launchctl load -w /System/Library/LaunchDaemons/org.apache.httpd.plist

Web共有を無効にしたい場合は、以下のコマンドを実行する。

$ sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist

launchctlコマンドにloadオプションを指定すると、launchd.plistのDisabledキーを削除してロードされ、そのため、OS起動時に自動起動されるようになるということらしい。

関連情報 OS X 10.8 Mountain Lion での Web 共有の有効化
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]Frameworkについて(その2)

InterfaceBuilderでフレームワーク内のクラスの認識に失敗する問題が解決したので報告する。

SampleCodeのOther Linker Flagsに-ObjCと-all_loadを指定したら解決した。

ソースコード GitHubからどうぞ。
https://github.com/murakami/SimpleChart - GitHub
関連情報 Framework Programming Guide
Technical Q&A QA1490: Building Objective-C static libraries with categories
Cocoaの日々:[Mac][iOS] Frameworkとは?
[iPhoneアプリ開発]自作Frameworkをつくる
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[OSX][iOS]Core Data についてのメモ

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)) {
	
}

管理オブジェクトコンテキストに対してデータを検索するのに必要なのがフェッチ要求。

フェッチ要求には、エンティティ名(必須)と述語オブジェクト(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のサブクラス)とは、管理オブジェクトモデルで定義できる。エンティティを記述したものをメタデータと呼んでいる。

ソースコード GitHubからどうぞ。
https://github.com/murakami/SimpleChart - GitHub
関連情報 Core Dataプログラミングガイド
Core Data Model Versioning and Data Migration
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[OSX][iOS]関東第63回Cocoa勉強会

今回は松戸で開催。個人的に気になって点を中心にまとめてみる。

『タブインターフェイスをドキュメントベースアプリケーションに適応 』。PSMTabBarControlを利用。タブはウィンドウと独立した存在。ドキュメントベースの場合とそう出ない場合について発表。

『Xcode Server~みんなで使ってみる』。参加している皆でOSX ServerのXcodeサービスを使ってみた。

『iOS向けのフレームワーク化に挑戦』。著者の発表。困っていたことがあったのだがアドバイスを貰って助かった。嬉しい。

『iOSアプリのデジタル署名』。ビルドのプロセスを調べ内容を詳細に説明して貰って、とても得るものがあった。

『Multipeer Connectivityを試してみよう!』。BonjourとCoreBluetoothを組み合わせた機能が欲しいと思っていたが、iOS7から、こんな便利なフレームワークが追加されたなんて。もっと前に知りたかった!

関連情報 Cocoa勉強会
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)

[iOS]Frameworkについて

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>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
</dict>
</plist>

フレームワーク用のターゲットを追加する。追加するのはAggregateという種類だ。

Aggregate

ターゲットの名前は、分かりやすいように例えば、SimpleChart.frameworkとフレームワーク用だと分かる名前がよいと思う。

追加したターゲットのBuild Phasesにフレームワーク作成用のスクリプトを追加する。

addscript

script

スクリプトの内容は、以下のとおり。

#!/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];
	

継続して本格対処方法の調査を行っている。

ソースコード GitHubからどうぞ。
https://github.com/murakami/SimpleChart - GitHub
関連情報 Framework Programming Guide
Cocoaの日々:[Mac][iOS] Frameworkとは?
[iPhoneアプリ開発]自作Frameworkをつくる
【Cocoa練習帳】 http://www.bitz.co.jp/weblog/
http://ameblo.jp/bitz/(ミラー・サイト)