2014年10月アーカイブ

UIViewやUIViewを継承するクラス(UIButtonやUIImageViewなど)は座標の設定にframeプロパティを用います。

例えばユーザが自由に配置した画像の下にボタンが追従するようにしたいなど、UIViewの大きさは変えずに座標のみをいじりたいという事はよくあります。
CGRectOffsetという座標を変えるためのメソッドもありますが、相対的な移動方法である為、絶対値として設定したい場合には不向きです。

絶対値として座標を設定したい場合frame.orizinを変えれば良いのですが、frameはプロパティでありoriginはCGRect構造体のメンバという差があるため
 xxxView.frame.origin = CGPointMake(10, 100);
といった設定はできません。

一度frameプロパティをCGRect構造体として取り出し、originを書き換えたのちにframeプロパティに設定するという面倒な手順が必要となります。
 CGRect newFrame = xxxView.frame;
 newFrame.origin = CGPointMake(10, 100);
 xxxView.frame = newFrame;

ただ座標を変えるだけのためにこのコードを毎回書くのは面倒ですので、座標に直接アクセスできるプロパティを追加します。
以下のコードはUIViewを拡張してpositionというプロパティを追加しています。

【UIView+Position.h】

// positionプロパティを追加したUIView拡張クラス

@interface UIView (Position)


@property (nonatomic) CGPoint position;


@end


【UIView+Position.m】

@implementation UIView (Position)


@dynamic position;

- (CGPoint)position

{

return self.frame.origin;

}

- (void)setPosition:(CGPoint)position

{

self.frame = CGRectMake(position.x, position.y, self.frame.size.width, self.frame.size.height);

}


@end


内部の処理は至極単純で、自身のframeプロパティを書き換えているのみです。
UIView+Position.hをインポートするだけでUIView及びUIViewを継承しているクラスでpositionプロパティが使用できます。

xxxView.position = CGPointMake(10, 100);
アプリを開発するにあたり、いきなり各機能が複雑に干渉しあう本番機能を実装する事は非常にリスキーです。
まずは各機能が仕様を満たせるか、技術的に問題は無いかを確認する目的で単機能のViewControllerを実装する方も多いのではないでしょうか。
例えば録音機能と取得した音量に合わせてマスコットがアニメーションする機能を実装したい場合、録音機能用のViewControllerとアニメーション用のViewControllerを実装するといった具合になります。

確認用のViewControllerが少ないうちはAppDelegateのviewControllerプロパティに見たい画面を毎度設定し直せば済みますが、画面が多くなってくるとそれも煩雑になってきます。
また確認したい画面が変わるだけで毎回ビルドし直すというのも面倒です。できれば各画面に遷移する事ができる初期画面が欲しい所です。

今回は簡単に実装でき、且つ新しいViewControllerも簡単に追加できる初期画面の実装を紹介します。

各ViewControllerはplistで管理します。
SampleViewControllers.plistを追加し、次のようにデータを入力します。
SampleViewControllers.plist
Rootの属性をArrayに変更します。
Rootの下にDictionary型のアイテムを追加し、Dictionary型アイテムの下に「title」と「class」というキーを持つString型のアイテムを追加します。
titleアイテムの値にはViewControllerのタイトルとなる文字列を設定します。
classアイテムの値には表示したいViewControllerのクラス名を設定します。
上図では6つのViewControllerを追加しています。

UITableViewControllerを継承したクラス「SampleManagerViewController」を追加します。

@interface SampleManagerViewController : UITableViewController


@end


上で作成したplistから読み込んだデータソースを管理するプロパティを追加します。

@interface SampleManagerViewController ()


@property (nonatomic, retain) NSArray* dataSource;


@end


viewDidLoadでSampleViewControllers.plistから情報を読み込みます。

- (void)viewDidLoad {

    [super viewDidLoad];

    

NSString* path = [[NSBundle mainBundle] pathForResource:@"SampleViewControllers" ofType:@"plist"];

self.dataSource = [NSArray arrayWithContentsOfFile:path];

}


tableViewのセクション数は1を返します。

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

    return 1;

}


行数はSampleViewControllers.plistから読み込んだViewControllerの数を返します。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return self.dataSource.count;

}


各Cellにはtitleを表示します。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

static NSString *CellIdentifier = @"Cell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

if (cell == nil) {

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];

}

NSDictionary* dic = [self.dataSource objectAtIndex:indexPath.row];

cell.textLabel.text = [dic objectForKey:@"title"];

return cell;

}


Cellタップ時の動作として、NSClassFromString()を用いclassに設定された文字列からClass型オブジェクトを求め、そのViewControllerへ遷移させます。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

{

NSDictionary* dic = [self.dataSource objectAtIndex:indexPath.row];

NSString* className = [dic objectForKey:@"class"];

Class sampleClass = NSClassFromString(className);

UIViewController* sampleView = [[[sampleClass alloc] init] autorelease];

[self.navigationController pushViewController:sampleView animated:YES];

}


これで初期画面の実装は終了です。
後は初期画面をAppDelegateのviewControllerに設定すれば完了です。
画面遷移はpushViewControllerで行っているため、UINavigationControllerのrootViewControllerに初期画面を追加します。

#import "SampleManagerViewController.h"

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

UIViewController* rootView = [[[SampleManagerViewController alloc] init] autorelease];

self.viewController = [[[UINavigationController alloc] initWithRootViewController:rootView] autorelease];

    self.window.rootViewController = self.viewController;

    [self.window makeKeyAndVisible];

    return YES;

}


@end


新しく画面を追加したい場合は、SampleViewControllers.plistに項目を追加してください。
ExtAudioFileを使って録音のファイル書き込みを行う際に
 1. 録音終了したファイルAを閉じる
 2. すぐ新規録音を開始し、録音データをファイルBに書き込む(ExtAudioFileWriteAsync)
このようなケースでAudioRingBuffer::GetTimeBoundsからEXC_BAD_ACCESSが発生する事があります。

ファイルへの書き込みはExtAudioFileWriteAsyncによって非同期に書き込まれる為、古いファイル(A)への処理が完結する前に新しいファイル(B)への書き込みが行われる事で発生するものと考えられます。

解決策として、排他制御(mutex)を用います。
 ※mutexを用いる為にpthread.hをimportする必用があります。
pthread_mutex_init() mutexの初期化
pthread_mutex_lock() mutexのロック
pthread_mutex_trylock() ブロックしないで行うmutexのロック
pthread_mutex_unlock() mutexのロック解除
pthread_mutex_destroy() mutexの削除

排他制御のキーとなるmutexはpthread_mutex_t型で宣言します。
static pthread_mutex_t fileMutex;

録音を始めるより前にmutexを初期化します。
pthread_mutex_init(&fileMutex, nil);

後は排他制御が必用な箇所をロックとアンロック処理で挟み込みます。
ファイルへの書き込みとファイルを閉じる箇所について挟み込むと問題が解決しました。
例)
// ファイルへの書き込み
if (pthread_mutex_trylock(&fileMutex)) {
    ExtAudioFileWriteAsync(...);
}
pthread_mutex_unlock(&fileMutex);

// ファイルを閉じる
pthread_mutex_lock(&fileMutex);
ExtAudioFileDispose(...);
pthread_mutex_unlock(&fileMutex);

最後に必用の無くなったmutexを削除します。
pthread_mutex_destroy(&fileMutex);