iOSの最近のブログ記事

本文はQiitaにあります。

本文はQiitaにあります。
Qiitaページへ

weak var delegate: DelegateProtocol?


これはよく見られる1つだけ登録可能なdelegateプロパティです。

対象のライフサイクルに干渉しない様、weak属性となっています。


複数登録可能なdelegateプロパティを実装する際、Array型を使うと上記と同じ仕様になりません。

Array型に要素を追加することにより、retain countが増加してしまい強参照となってしまう為です。


弱参照でありつつ複数登録可能なArray(Set)を作るには、NSHashTableを用います。


var delegateTable = NSHashTable.weakObjectsHashTable()


delegateTableに追加した要素は弱参照で登録され、参照先が解放された際自動的に要素は削除されます。


要素の追加は次の通りです。


delegateTable.addObject(ObjA())


NSHashTable内の要素はAnyObject型で管理される為、Array型と違いあらかじめ要素の型を指定することはできません。

登録されている各要素のdelegateメソッドを正しく実行する為には、次の手順を実行します。


for case let target as DelegateProtocol in self.delegateTable.allObjects {

target.sampleFunc()

}

Firebaseを利用して、手軽にアプリにユーザフォロー関連の機能を追加できるライブラリを公開しました。


MITライセンスです。
IF_FirebaseFollowHelperを導入する事で、次の機能が実装されます。
 ・ユーザをフォローする
 ・ユーザをブロックする
 ・他ユーザに自分がフォローやブロックされた事をリアルタイムに受け取る

Firebase内のデータ処理についてはライブラリでカプセル化されているので、機能を利用する際にFirebaseのデータ構造について気にする必要はありません。
例えば自分がフォローしているユーザ一覧を受け取る場合、結果は [(uid, timestamp)] といった単純なArray返されます。

CocoaPodsからの導入も可能です。

詳細な仕様は次のドキュメントを参照してください。

導入方法やサンプルの動かし方などはgithubで説明していますので、そちらをご覧ください。
複数行対応のUITableViewCellを使用するときなど、自動的にセル高さを決定してくれるUITableViewAutomaticDimensionは非常に便利です。
便利なのですが画面回転などによりUITableViewの(内部Cellの)frameが変わってしまう場合、正しい高さを求めてくれなくなってしまいます。
UITableView.frameの変更に合わせて次のおまじないを唱えることで、簡単に対応することが可能です。

[UITableView beginUpdates];

[UITableView endUpdates];


UITapGestureRecognizerを登録したviewに対してUITableViewをaddSubviewした場合、セルをタップしてもUITapGestureRecognizerが優先されてしまいdidSelectRowAtIndexPathがコールされなくなってしまいます。
このような競合を解消するにはUIGestureRecognizerの次のプロパティを設定します。

UITapGestureRecognizer* viewTap = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(view_Tap:)] autorelease];

viewTap.cancelsTouchesInView = NO;

[self.view addGestureRecognizer:viewTap];

bringSubviewToFrontなどのメソッドでsubviewの表示順を操作することは可能ですが、subviewの数や配置の複雑度が増してくると処理が煩雑になりがちです。


例えば

 ・背景(2views)

 ・情報表示(5views)

 UIボタン群(5views~10views)

 ・広告を上下に1つずつ配置(2views?)

という構造を考えてみましょう。


背景は常に一番下に配置されますが、状況によりviewの表示順を入れ替える必要があります。

情報表示画面は常に最重要な画面を前面に表示する必要があり、頻繁に表示順の入れ替えが行われます。

UIボタン群は最前面に表示している情報表示画面を操作するためのもの、また情報表示画面を切り替えるためのものがあり、viewの数や表示順は状況により変わります。

UIボタンは常に情報表示画面より上に配置し、隠れないことが求められます。

広告はUIボタンを隠さない位置に表示され、常に最前面に配置します。サードパーティ製の広告SDKを用いるため、どのような実装がされているかは不明です。


このような複雑度を持つアプリの場合、もはやbringSubviewToFrontでなんとかしようとするのは現実的ではありません。

insertSubviewexchangeSubviewAtIndexを使うことで多少コード量は減らすことができますが、それでも煩雑であるという印象は解消できないでしょう。

ある情報表示画面を前面に配置したい場合、次の条件でsubviewsから挿入箇所を検索する必要があります。

 ・情報表示画面群の最前面

 ・UIボタン群の1つ背面である

UIボタン群の中で常に最背面にあるボタンが固定なら楽かもしれませんんが、UIボタンは状況により変わります。

もしかすると検索対象としたUIボタンインスタンスそのものがsubviewsに含まれていないかもしれません。


背景の入れ替えについても安易にsendSubviewToBackを用いるのは危険です。

なぜなら今後の仕様追加により背景viewが追加された場合に不具合の原因となる可能性が高くなるからです。


UIボタンの入れ替えも大変です。

広告のview構成が不明であるため、広告viewの1つ背面にinsertするのは危険です。(広告が読み込まれたタイミングでview構成が変わり、もしかしたらviewのインスタンスすら変わっているかもしれません)

そのため現在前面に表示されている情報表示画面を検索する必要が出てきます。


仕様変更も視野に入れて設計することを考えただけでウンザリしてきたのではないでしょうか。


今回はこのような複雑なsubview構成に対応するため、階層構造でsubviewを管理できるUIViewControllerを設計します。

階層は好きな数を自由に追加でき、階層に追加したsubviewはその階層内でのみ表示順の入れ替えが可能です。

たとえbringSubviewToFrontを使ったとしても、その階層より上の階層に存在するsubviewを上書きすることはありません。


先ほどの例を階層構造を用いて実装した場合のイメージを下図に示します。


Hierarchy.png


それぞれのsubviewは対応した階層にaddされます。

先ほどと同じくある情報表示画面を前面に配置したい場合の処理を考えてみましょう。

情報表示階層にaddされている情報表示画面はその上階層であるUIボタン階層を上書きすることはできません。

つまり情報表示画面を前面に表示したいのであれば、単純にbringSubviewToFrontを使うだけで実装することができます。


背景も前面に出したいsubviewbringSubviewToFrontするだけです。

bringSubviewToFrontによる実装であれば、今後背景viewが増えたとしても不具合は発生しません。


UIボタンの入れ変えはどうでしょうか。

UIボタン階層に存在するsubviewを一掃し、新しいUIボタンをUIボタン階層にaddするだけで完了です。


広告がどのようなview構造で実装されていたとしても、それは広告階層の中の問題です。

他の階層が気にする必要はなくなり、実装におけるリスクも随分軽減されます。


階層はUIViewで実装しますが、そのままUIViewを使ってしまうとタッチイベントがブロックされてしまいます。

UIボタン階層の上に広告階層が存在するため、すべてのタッチイベントは広告階層にブロックされUIボタンを押すことはできません。

この問題を解決するため、一切のタッチイベントをスルーするUIView(TouchTransmissionView)を実装します。


TouchTransmissionView.h

@interface TouchTransmissionView : UIView


@property (nonatomic, readonly) NSUInteger hierarchy;


- (id)initWithFrame:(CGRect)frame hierarchy:(NSUInteger)hierarchy;

- (NSComparisonResult)compareHierarchyAscending:(TouchTransmissionView*)view;


@end


TouchTransmissionView.m

@interface TouchTransmissionView ()

{

NSUInteger _hierarchy;

}


@end


@implementation TouchTransmissionView


@synthesize hierarchy = _hierarchy;


- (id)initWithFrame:(CGRect)frame hierarchy:(NSUInteger)hierarchy

{

    self = [super initWithFrame:frame];

    if (self) {

self->_hierarchy = hierarchy;

    }

    return self;

}


- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event

{

UIView* eventView = nil;

UIView *hitView = [super hitTest:point withEvent:event];

if (hitView != self) eventView = hitView;

return eventView;

}


- (NSComparisonResult)compareHierarchyAscending:(TouchTransmissionView*)view

{

if (self.hierarchy > view.hierarchy) return NSOrderedDescending;

else if (self.hierarchy < view.hierarchy) return NSOrderedAscending;

else return NSOrderedSame;

}


@end


subviewを階層構造で管理するためのUIViewController(HierarchyViewController)を実装します。


HierarchyViewController.h

@interface HierarchyViewController : UIViewController


- (void)addSubview:(UIView*)view hierarchy:(NSUInteger)hierarchy;


- (void)bringSubviewToFront:(UIView*)view;


- (NSArray*)subviews;

- (NSArray*)subviewsOfHierarchy:(NSUInteger)hierarchy;


@end


HierarchyViewController.m

#import "TouchTransmissionView.h"


@interface HierarchyViewController ()


- (TouchTransmissionView*)addLayerViewWithHierarchy:(NSUInteger)hierarchy;


@property (nonatomic, retain) NSMutableArray* layerViewList;


@end


@implementation HierarchyViewController


@synthesize layerViewList;


- (TouchTransmissionView*)addLayerViewWithHierarchy:(NSUInteger)hierarchy

{

TouchTransmissionView* newLayerView = [[[TouchTransmissionView alloc] initWithFrame:self.view.bounds hierarchy:hierarchy] autorelease];

newLayerView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin |

UIViewAutoresizingFlexibleLeftMargin |

UIViewAutoresizingFlexibleRightMargin |

UIViewAutoresizingFlexibleTopMargin |

UIViewAutoresizingFlexibleHeight |

UIViewAutoresizingFlexibleWidth ;

[self.view addSubview:newLayerView];

[self.layerViewList addObject:newLayerView];

NSArray* sortedLayerViewList = [self.layerViewList sortedArrayUsingSelector:@selector(compareHierarchyAscending:)];

for (UIView* view in sortedLayerViewList) {

[self.view bringSubviewToFront:view];

}

self.layerViewList = [[sortedLayerViewList mutableCopy] autorelease];

return newLayerView;

}


#pragma mark - public

- (void)addSubview:(UIView*)view hierarchy:(NSUInteger)hierarchy

{

if (self.layerViewList == nil) {

self.layerViewList = [NSMutableArray array];

}

UIView* targetLayerView = nil;

for (TouchTransmissionView* layerView in self.layerViewList) {

if (layerView.hierarchy == hierarchy) {

targetLayerView = layerView;

break;

}

}

if (targetLayerView == nil) {

targetLayerView = [self addLayerViewWithHierarchy:hierarchy];

}

[targetLayerView addSubview:view];

}


- (NSArray*)subviewsOfHierarchy:(NSUInteger)hierarchy

{

TouchTransmissionView* targetLayerView = nil;

for (TouchTransmissionView* layerView in self.layerViewList) {

if (layerView.hierarchy == hierarchy) {

targetLayerView = layerView;

break;

}

}

return targetLayerView.subviews;

}


- (NSArray*)subviews

{

NSMutableArray* subViewList = [NSMutableArray array];

for (UIView* layerView in self.layerViewList) {

[subViewList addObjectsFromArray:layerView.subviews];

}

return subViewList;

}


- (void)bringSubviewToFront:(UIView*)view

{

for (UIView* layerView in self.layerViewList) {

for (UIView* subView in layerView.subviews) {

if (subView == view) {

[layerView bringSubviewToFront:subView];

break;

}

}

}

}


@end



最後にサンプルとしてHierarchyViewControllerを継承したSampleViewControllerを実装し、動作を確認してみましょう。


SampleViewController.h

@interface SampleViewController : HierarchyViewController

@end


SampleViewController.m

typedef enum {

ViewHierarchy_BackGround = 0,

ViewHierarchy_Back,

ViewHierarchy_Middle,

ViewHierarchy_Front,

}eViewHierarchy;


@interface SampleViewController ()


- (void)handleBackgroundView_Tap:(UITapGestureRecognizer*)sender;


@end


@implementation SampleViewController


- (void)viewDidLoad {

    [super viewDidLoad];

UIView* view2 = [[[UIView alloc] initWithFrame:CGRectMake(30, 30, 200, 200)] autorelease];

view2.backgroundColor = [UIColor blueColor];

[self addSubview:view2 hierarchy:ViewHierarchy_Front];

UIView* view1 = [[[UIView alloc] initWithFrame:CGRectMake(90, 90, 200, 200)] autorelease];

view1.backgroundColor = [UIColor redColor];

[self addSubview:view1 hierarchy:ViewHierarchy_Back];

UIView* backgroundView = [[[UIView alloc] initWithFrame:self.view.bounds] autorelease];

backgroundView.backgroundColor = [UIColor greenColor];

[self addSubview:backgroundView hierarchy:ViewHierarchy_BackGround];

UIView* middleView1 = [[[UIView alloc] initWithFrame:CGRectMake(60, 130, 250, 40)] autorelease];

middleView1.backgroundColor = [UIColor yellowColor];

[self addSubview:middleView1 hierarchy:ViewHierarchy_Middle];

UIView* middleView2 = [[[UIView alloc] initWithFrame:CGRectMake(70, 150, 250, 40)] autorelease];

middleView2.backgroundColor = [UIColor grayColor];

[self addSubview:middleView2 hierarchy:ViewHierarchy_Middle];

[backgroundView addGestureRecognizer:[[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleBackgroundView_Tap:)] autorelease]];

}


- (void)handleBackgroundView_Tap:(UITapGestureRecognizer*)sender

{

NSArray* subViews = [self subviewsOfHierarchy:ViewHierarchy_Middle];

UIView* backView = [subViews firstObject];

[self bringSubviewToFront:backView];

}


@end


2014-12-02 04.57.22.png

各subviewを配置するための階層はeViewHierarchyで管理しています。

subviewは最背面から順にaddSubviewをしているわけではなく、まったくのチグハグです。

しかし階層により表示順が管理されているため、正しい表示順が保持されます。

背景view(緑色のview)をタップすると、Middle階層に存在するviewの表示順が入れ替わります。

表示順の入れ替えにbringSubviewToFrontを用いていますが、階層内の最前面に移動するだけで決してFront階層を上書きすることはありません。


あまり多くの階層を作ってしまうとパフォーマンスの悪影響が考えられますので、その点には注意してください。

アプリを開発するにあたり、いきなり各機能が複雑に干渉しあう本番機能を実装する事は非常にリスキーです。
まずは各機能が仕様を満たせるか、技術的に問題は無いかを確認する目的で単機能の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に項目を追加してください。
Twitterボタン
Twitterブログパーツ