Hatena::ブログ(Diary)

Kazzzの日記 このページをアンテナに追加 RSSフィード

2013-11-09

[][]UITableViewCellのビュー階層iOS6以前とiOS7で違う

iOSアプリケーションでUITableViewCell中のUIViewを取得するのは難しくない。方法は幾つかあるが、例えばInterfaceBuilderで予めターゲットのビュー(コントール)にタグを打っておけばUITableViewCellのデリゲート中で以下のように取得できる。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //cellにはInterfaceBuilderでcellIdentifier:"cell1"をセットしている
    NSString* cellIdentifier = @"cell1";
    UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier ];

    //txtInputにはInterfaceBuilderでタグ:1をセットしている
    UITextField* txtInput = (UITextField*)[cell viewWithTag:1];
}

簡単だ。

では、逆にUITableViewCellが分からない状況でUITableViewCellを取得するにはどうしたら良いのだろう。
例えばBlocksを使ってボタンタップイベントを処理する時に、そのボタンがどのUITableViewCellに属するのかを取得するようなケースだ。

[button1 addBlockForAction: //UIButtonのExtensionをBlocksで予め用意してあるとする
^{
     UITableViewCell* cell = (UITableViewCell*)button1.superView.superView;  
 } forControlEvents:UIControlEventTouchUpInside];

これで取得できる。
というののもこの時のUITableViewCellをルートとするビュー階層は以下のようになっていることが経験上分かっているからだ。

  • UITableViewCellのビュー階層(iOS6)
<UITableViewCell>
   | <UITableViewCellContentView>
   |    | <UIButton>

ところが、iOS7ではこのコードではUITableViewCellの参照を取得することはできない。2つ上の階層はnilではないが何か他のクラスが格納されているようだ。

じゃあどうなっているのかと調べてみると、iOS7ではUITableViewCell下のビュー階層が以下のように変わっていることが判明した。

  • UITableViewCellのビュー階層(iOS7)
<UITableViewCell>
   | <UITableViewCellScrollView>
   |    | <UITableViewCellContentView>
   |    |    | <UIButton>

(実際にはデフォルトUIのための他の階層もあるが、今回の話には関係ないので省略している)
という訳で、今までのように2階層上と決め打ちする手はiOS7では使えないようだ。

ならどうする? 3階層上にする? また変更されたらどうする?

この辺のクラスの構成はいわゆる"Hidden"であり、仕様が公開されていない。つまりは「予告なしに変更される」可能性がある訳で、元々固定の階層で取得するべきものではないのだ。

ということで取得は以下のように汎用的なものにすることにした。

  • iOS6、iOS7の両方で使えるUITableViewCellの取得
__weak MyViewController* weak_ref = self;
[button1 addBlockForAction: //UIButtonのExtensionをBlocksで予め用意してあるとする
^{
     UITableViewCell* cell = [weak_ref tableCellForControll:btnOrder];
 } forControlEvents:UIControlEventTouchUpInside];

- (UITableViewCell*)tableCellForControll:(UIView*) targetView
{
    UIView* parent = targetView.superview;
    if ( !parent || [parent isMemberOfClass:[UITableView class]])
    {
        return nil;
    }
    else
    {
       if ( [parent isMemberOfClass:[UITableViewCell class]])
       {
           return (UITableViewCell*)parent;
       }
       else
       {
           return [self tableCellForControll:parent]; //再起
       }
    }
}

完璧ではないかもしれないが、これでiOSのバージョン依存したコードは回避できる。

2013-09-27

[][]iOS6iOS7で違うTintColorプロパティ意味

iOS6からiOS7への移行で困った点の第二弾。

バー系ビューの透過設定のデフォルトとTint(色合い)の違い

iOSアプリケーション重要役割を占めるバー系のビュー(ナビゲーションバー、ステータスバー、ツールバー等)の背景は透過するのがデフォルトとなった。これはiOS7の基本的なデザインの考え方が透過、又は半透過のレイヤを重ね合わせることを前提にしているからだと思われる。

これも例を見てもらうと分かりやすい。

Xcode5でデフォルトで生成されたナビゲーションコントローラにおける、ナビゲーションバーのプロパティは以下のようになっている。
f:id:Kazzz:20130928001407p:image:w300
Navigation Bar/Bar TintとView/Tintに着目して欲しい。
この状態で実行するとiOS6、iOS7ではそれぞれ以下のようになる。

実行結果 (Translucent=YES, Bar Tint=Default, Tint=Default)
iOS6iOS7
f:id:Kazzz:20130928001408p:image:w300f:id:Kazzz:20130928001409p:image:w300

これは双方のデフォルトであり、特に問題は無いだろう。

ここでiOS6の標準のバーの色が地味でつまらない(よくあることだ)ので、青に変更したいとしよう。
Interface-Builderで新設されたBar Tintを青色に変える。
f:id:Kazzz:20130928002004p:image:w300

実行結果 (Translucent=YES, Bar Tint=Blue, Tint=Default)
iOS6iOS7
f:id:Kazzz:20130928002003p:image:w300f:id:Kazzz:20130928002002p:image:w300

iOS6は全く変わらない。Bar Tintは新設だけにiOS6では無視されるようだ。iOS7では色が着いたが透過なので微妙な感じになっている。※

なので今度は透過を外してみよう。
f:id:Kazzz:20130928002337p:image:w300

実行結果 (Translucent=NO, Bar Tint=Blue, Tint=Default)
iOS6iOS7
f:id:Kazzz:20130928002336p:image:w300f:id:Kazzz:20130928002335p:image:w300

iOS7は真っ当な色目になったが、iOS6はやはり全く変わらない。(iOS7ではタイトルバー上の文字の色が黒いため見づらくなっているが、これは別途修正できる)

今度はViewカテゴリのTint色を変えてみよう。これはiOS6まではナビゲーションバーの色を変えていたプロパティだ。
f:id:Kazzz:20130928002550p:image:w300

実行結果 (Translucent=NO, Bar Tint=Blue, Tint=Blue)
iOS6iOS7
f:id:Kazzz:20130928002549p:image:w300f:id:Kazzz:20130928002548p:image:w300

iOS6では漸く期待通りのTint(色合い)になった。ツールバーも同様に変更すれば良いだろう。(本来このように基本GUIのTintを変えるには、Appearance Proxyを使って一括で変更するものだろう)
iOS7も一見良さそうだがよく見てみるとナビゲーションバーの右端に配置したボタン(UIBarButtonItem)の色がナビゲーションバーと同じになったせいで、全く判別できなくなっている。

ここまでで分かったことだが、Bar TintとTintプロパティの意味がiOS6とiOS7で違う。

  • Bar Tint
    • iOS6 : 無視される
    • iOS7 : ナビゲーションバーのTint色を設定する

  • Tint
    • iOS6 : ナビゲーションバーのTint色を設定する
    • iOS7 : ナビゲーションバー上に配置されたコントロールのTint色を設定する

ここで問題なのは、iOS6にとってナビゲーションバーやツールバーのTintを決定しているプロパティがiOS7ではそれ自身ではなく上に配置された他のコントロールの背景や文字のTintを決定していること。
従って前回同様、iOS6のデザインに合わせるとiOS7では期待通りの見栄えにならないのだ。

今回の問題に対応するコード書くならばさしずめ以下のようになるだろうか。

- (int)osMajorVersion
{
    return [[[[UIDevice currentDevice] systemVersion]
             componentsSeparatedByString:@"."][0] intValue];
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    UIColor* barTintColor = [UIColor blueColor];
    UIColor* barButtonTintColor = [UIColor whiteColor];

    if ( [self osMajorVersion] >= 7 )
    {
        self.navigationController.navigationBar.barTintColor = barTintColor;
        self.navigationController.navigationBar.tintColor = barButtonTintColor;
    }
    else
    {
        self.navigationController.navigationBar.tintColor = barTintColor;
    }
}

このように直していくのは簡単なのだが、怖いのはApple側の気まぐれで急に元の振る舞いに戻ることである。それをされるとこのようなアドホックなコードは破綻する。
にしても、どうして同名のプロパティで意味を変えてしまうのか理解に苦しむ。

※前回のエントリで対策されたViewControllerを使用しているため、iOS6とiOS7でコンテンツのY軸はずれない。

2013-09-23

[][]iOS7ステータスバーが被る問題への対応

先日のエントリでも書いたが、iOS6→iOS7への移行で最も酷いなと感じたのはステータスバーの問題である

ステータスバーのレイヤの違いと画面に与える影響

f:id:Kazzz:20130922211218p:image:w300
iOS7においてはステータスバーはコンテンツとは完全に独立したレイヤとして扱うことになったようで、基本的に透過レイヤであり座標系もビューのルートと共有していない。つまりはiOS6までのアプリケーションをiOS7上で動かすとこのようにステータスバーがビューに覆い被さるように描画されてしまう。

この問題はステータスバーを表示している、つまりフルスクリーンを使う画面以外全ての画面が影響を受けてしまう凶悪なものだ。

ステータスバーをiOS6同様に非透過レイヤとすることで回避できそうなものだが、iO7ではステータスバーを非透過にする事は(今のところ)方法は無いので、これに合わせるしかない。ということは...iOS6まででステータスバーを透過にデザインしていないアプリケーションはこのままでは全てデザインし直しである。

例外としてナビゲーションバーを表示することでiOS7でもビューを期待する位置に描画することもできるが、この場合ナビゲーションバーを非透過にする必要がある。
f:id:Kazzz:20130922211220p:image:w300
なので、iOS7でデフォルトである透過コントロールを使う場合はやはり調整が必要だ。

幸いなことにステータスバーの高さはアプリケーション内で取得出来る。なので、画面を表示する際にiOS7であればビューのY座標をその分ずらしてやれば良いことになる。

サンプルコードの前提として、ずらしたい他のコンテンツが乗ったビューのルートをInterfaceBuilderによってcontentViewという名のプロパティアウトレットしておく必要がある。

iOS6とiOS7で同一デザインの画面を使うためのコード
- (void)viewDidLoad
{
    [super viewDidLoad];

    if ([[[[UIDevice currentDevice] systemVersion]
         componentsSeparatedByString:@"."][0] intValue] >= 7) //iOS7 later
    {
        CGRect statusBarViewRect = [[UIApplication sharedApplication] statusBarFrame];
        float heightPadding = statusBarViewRect.size.height + self.navigationController.navigationBar.frame.size.height;
        
        CGRect original = self.contentView.frame;
        
        CGRect new = CGRectMake(original.origin.x,  original.origin.y + heightPadding,
                                original.size.width,  original.size.height - heightPadding);
        
        self.contentView.frame = new;
    }
}

なにをしているかはコードを見れば一目瞭然だろう。既に書いたがステータスバーの下にはナビゲーションバーがあることも考慮にいれている。(それにしてもUIKitでビューのサイズを変更するのはいちいちCGRect変数を入れ替えねばならず面倒だ)

実行結果
iOS6iOS7
f:id:Kazzz:20130922214127p:image:w300f:id:Kazzz:20130922211219p:image:w300

このようにコードに手を入れる必要があるが、ナビゲーションバーを透過にした状態であってもiOS6、iOS7で共通のデザインで表示することができる。

にしても、これはSDK後方互換性としては最低の部類の対応な訳で、上記のようなアドホックなコードも醜いことこの上ない。
AppleはこうまでしてiOS6以前のデザインを排除したかったのだろうか。もとい、AppleはiOS6以前のデザインをよほど使ってほしくないのだろうな。

2013-09-22

[][]簡単ではないiOS7への移行

iOSアプリの開発を初めてから一年半というところだが、その間に一度OSメジャーバージョンアップを体験している。その時はiOS5iOS6であり、画面サイズも3.5インチから4インチであり互換性に心配したのだが、Xcodeシミュレータとデザイナ(Interface Builder)で画面サイズの違いをきちんと使い分けることが出来たため、必要アセット(リソース)を用意すれば良くあとは細かいAPIの違い程度だった。

で、今回はiOS6→iOS7である。また今回もXcode側で違いを吸収してくれるものだと思っていたので、正直あまり心配はしていなかったのだが、いざ既存プロジェクトを新しい開発環境であるXcode5に移行するに辺り、それほど簡単ではないことが分かって困惑しているところだ。

iOS6以前とiOS7のデザインの違い

これは色々な所で語られているので、敢えてここで取り上げなくても良いだろう。iOS7はそれまでのデザインをがらりと変えて、よりフラットで白を基調にしたデザインになっている。(チーフデザイナがジョナサン・アイブに変わったところが大きい)

iOS6のGUIデザインiOS7のGUIデザイン
f:id:Kazzz:20130922002224p:image:w300f:id:Kazzz:20130922002222p:image:w300

iOS7のスイッチが歴代ipod shuffleスライドスイッチを思いおこさせる。それにしてもボタンはボーダー位は付けるかホバー時には強調するなどしてほしかった。これじゃラベルと変わらない...

当然ながら全ての基本的なGUIのデザインも変わっておりUIKitが提供しているビュー、コントローラを使う分には一貫した変更がされているので良いのだが、問題は自作のビューやコントロールである。iOS6までのデザインを意識して書いたものはiOS7上で使う場合には違和感が出ることが多いため、デザインを変えることを余儀なくされることだろう。今回はそれ位にドラスティックな変更である。

バー系ビューの透過設定のデフォルトとTint(色合い)の違い

iOSアプリケーション重要役割を占めるバー系のビュー(ナビゲーションバー、ステータスバー、ツールバー等)の背景は透過するのがデフォルトとなった。これはiOS7の基本的なデザインの考え方が透過、又は半透過のレイヤを重ね合わせることを前提にしているからだと思われる。

iOS6以前とiOS7で分離されたInterfaceBuilderのプレビュー

以上のようにGUIの基本的なデザイナが変わったことにより、iOS6までとiOS7にはこれまでには無いデザインの相違があるため、XcodeのInterface-Builder上でも一つのプレビュー画面で済ますことが出来なくなってしまった。
Xcode5のInterface-Builder、StoryboardはiOS6以前とiOS7で表示とレイアウトを切換えることができるようになっている。これにより、ターゲットとしているiOSに寄せた画面デザインを確認できるという訳だ


iOS6以前表示iOS7以降表示
f:id:Kazzz:20130922002219p:image:w400f:id:Kazzz:20130922002217p:image:w400
ステータスバーのレイヤの違いと画面に与える影響

iOS6までのステータスバーはアプリケーションのコンテンツを表示するビューと基本的には分離しており、ある条件を満たした場合にのみステータスバーを透過にしてコンテンツにオーバラップして表示することが出来た。

iOS7においてはステータスバーはコンテンツとは完全に独立したレイヤとして扱うことになったようで、基本的に透過レイヤであり座標系もビューのルートと共有していない。つまりはiOS6までのアプリケーションをiOS7上で動かすとこのようにステータスバーがビューに覆い被さるように描画されてしまう。これが一番やばそうだ※


iOS6用にデザインされた画面はiOS7ではステータスバーが被ってしまう
f:id:Kazzz:20130922002223p:image:w300

というわけで、iOS5→iO6では画面サイズ以外はあまり違いを意識せずに済んだ画面デザインだが、iOS7ではデザインの違いをはっきりと意識して、明示的に対応する必要があるということだ。
次は上に挙げた中でも特に注意するべき点について言及していこうと思う。

※ただし、ある特定の条件下(画面の遷移にナビゲーションバーやタブバーが使われている場合、)においてはビューとは被らないように描画される。

2013-09-15

[][]ダイアログ風ポップアップビューをUIAlertViewに似せる

昨日のMJPopupViewControllerによるダイアログ風のポップアップビュー、そのままでも良いのだがせっかくなのでデザインにも凝ってみたい。流行のフラットデザインにするのも勿論良いが、まずは現在のアプリケーションにデザインを寄せるためにUIAlertViewに似せてみることにした。

具体的には

1. ビューの縁を丸める
2. ボーダーの色を変える

以上を実装する。ビューの外観を変えるのは大変そうだが、UIKitはこの辺が上手く分離されておりQuarzcoreを使ってUIViewのレイヤを操作するだけで良い。

ビューの縁を丸めてボーダーの色を白にする

HogeViewController.xib

f:id:Kazzz:20130915210908p:image:w480

HogeViewController.h
#import <UIKit/UIKit.h>
@interface HogeViewController : UIViewController
@property (strong, nonatomic) IBOutlet UIView* contentView;
@end

InterfaceBuilderでデザインしたビューはcontentViewにアウトレットにしておくこと。

HogeViewController.m
#import <QuartzCore/QuartzCore.h>
#import HogeViewController.h
#define RGBA(r, g, b, a) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a]

@implementation HogeViewController
- (void)viewDidLoad
{
    //ビューの枠を丸くしてボーダーを白に
    [self.contentView.layer setCornerRadius:10.0f];
    [self.contentView setClipsToBounds:YES];
    
    [self.contentView.layer setBorderColor:RGBA(255, 255, 255, 0.7).CGColor];
    [self.contentView.layer setBorderWidth:2.0f];
}
- (void)dealloc
{
    [self setContentView:nil];
}
@end

以上だ。Quarzcoreの機能を使えばこのように外観を簡単に変えることができる。

あとは昨日紹介したMJPopupViewControllerでビューをロードしてポップアップ表示させる。

MainViewController.m
#import "UIViewController+MJPopupViewController.h"
〜
HogeViewController *hogeViewController = [[HogeViewController alloc] initWithNibName:@"HogeViewController" bundle:nil];
[self presentPopupViewController:detailViewController animationType:MJPopupViewAnimationFade];

presentPopupViewController:detailViewControllerメッセージを使うことでポップアップ表示を開始する。

実行結果

f:id:Kazzz:20130915210910p:image
これでちょっとiOSぽくなった。(iOS7でまた変わるが..)更にグラデーションを付ければ完璧だろうが、そこまではしないでおくことにする。
このように外観に幾らでも凝ることができるのも、通常のUIViewで構成されているメリットである

2013-09-14

[][]MJPopupViewControllerを使ってポップアップ・ダイアログを簡単に作る

前に書いたことがあるが、iOSプログラミングにおいてちょっとしたポップアップ/ダイアログを作るのには結構難儀する。

カスタマイズされたダイアログのベースになるのはUIAlertViewを使うケースが多いのだが、元々このクラス、テキストボックスを増やす位のカスタマイズしか考慮しておらず、通常のUIViewを配置することなど考えていないようで、実際に書いたことがある開発者ならば分かると思うが、継承してちょっと凝ったものを作ろうとするとかなり苦労する。

martinjuhasz/MJPopupViewController
martinjuhasz/MJPopupViewController

Martin Juhasz氏作のMJPopupViewControllerはUIViewControlerのカテゴリと半透明の背景ビューにより、通常のビューを恰もポップアップ/ダイアログのように表示してくれる優れものだ。 その素晴らしさは文で書いてもよくわからないので、興味のある方はデモ用のプロジェクトを実行してみると良い

MJPopupViewControllerのデモプロジェクトによるポップアップの表示

f:id:Kazzz:20130914214050p:image

ポップアップとして表示されているのは、通常のUIViewControllerと組み合われた通常のUIViewであり、UIAlertViewのような特殊なコントロールではない。それっぽく見えるのは半透明でグラデーションのかかった背景ビューと、ビューを表示する際のアニメーションのお蔭である

通常のUIViewControllerで扱うUIViewなので、InterfaceBuilderを使ってxibやStoryboardから起こしたビューがそのまま使えるのだ。 ビューの表示のアニメーションエフェクトも上→下、左→右、フェードインなどが使えて見栄えも良いし、背景をタッチすることでポップアップを閉じることもできる。 

ダイアログとしてデザインされたビューをMJPopupViewControllerでポップアップ風に表示した例

f:id:Kazzz:20130914221014p:image
このように、まるでカスタマイズされたUIAlertViewのように、それっぽくデザインしたダイアログのように使える。 勿論ビュー上に配置した各コントロールのイベントを処理することもできる。だって通常のUIViewControllerと組み合われた通常のUIViewだから。

ランドスケープでもレイアウトが崩れない

f:id:Kazzz:20130914221015p:image
通常のUIViewControllerなので勿論ランドスケープ時のレイアウトにも対応する。 UIAlertViewの拡張クラスではこうはいかなかなった。

UIAlertViewのカスタマイズに疲れたプログラマはMJPopupViewControllerの使用を検討してみることをお勧めする。

2013-09-08

[][]UIコントロールの対照性


AndroidとiOSは一般的には似たような画面を持ったアプリケーションを開発していると思われがちだが※、実際には両プラットホームでの画面を構成する部品にはかなりの違いがある。

その違いは画面を構成するUIコントロール(View)において顕著であり、一通り汎用的なUI部品を提供するAndroidに対して、iOSは一貫した画面の見栄えや操作性を実現するために提供されており、互いに同じ用途のUIコントロールを使えるとは限らない。

そこで互いの移植においては悩まないために両プラットホームのUIの対照表を作ってみたのが下の表。ただし、全ての用途を対象にすると凄く多くなるので、アプリケーションでなにか情報を入力、選択するために使用するUIコントールに限っている。

UIコントロール対照一覧

UIコントロールAndroidiOS(UIKit)
ラベルandroid.widget.TextViewUILabel
ボタンandroid.widget.ButtonUIButton
テキストボックスandroid.widget.EditTextUITextField
チェックボックスandroid.widget.CheckBoxUISwitch
ラジオボタンandroid.widget.RadioButtonUISegmentedControl
スピナ(コンボボックス)android.widget.SpinnerUIPickerView
メニュー(メニューバー)android.app.ActionBarUIActionSheet
ダイアログandroid.app.Dialog派生クラスUIAlertDialog派生クラス?

このほかにもパネル等のコンテナビュー上に上記のUIコントロールを複数載せて、特定の情報を入力させるコンポジットコントロール(TableView中の編集行の実装もこれにあたる)もあるが、複合的なものは省いている。

大方、対照となるUIコントロールが提供されているように見えるのだが、AndroidとiOS、特にAndroid→iOSへのポーティングは注意する点がある。

チェックボックス

コンベンショナルなCheckBoxに対してUISwitchはデザインの自由度は少ない。

ラジオボタン

UISegmentedControlはラジオボタンというよりは、トグルボタンの集合のため、やはりデザインの自由度は少ない。特に縦に並べることはできない。

スピナ

この用途でのUIPickerViewはかなり用途が限定される。予め提供されていない型(年月日や時間以外)を扱う場合は別途開発するか、カスタムコントロールの利用を考慮する必要がある。

メニュー

UIActionSheetは基本的には縦にボタンを並べただけなので、通常のメニューとは情報の表現に制限がある。例えば、メニューでは普通に実装できる階層構造を表現するのは難しい。

ダイアログ

AndroidのDialogはActivity同様にレイアウトを流し込むので自由度が高いが、iOSのUIAlertDialogは自身でレイアウトするのが困難なため、下手に継承クラスをこね回す位ならば、他のモーダルビューの利用を検討すべきだろう。

その他

今回は入力コントロールということで対象とはしなかったが、逆にiOSのUINavigationControllerとUINavigationBarを使った戻るボタン込みの画面遷移の実装はAndroidにポーティングするのはかなり困難であり、すべきではない。

ゆめゆめ互いの画面デザインを同じにしよう等とは思わないことだ。