Tag Archives: Ui

NewImage

UISearchBarまとめ | SpiriteK Blog より

iOSアプリの検索バーUIである、UISearchBarですがあまり機能的なカスタマイズは用意されていないようで、
横に表示できるボタンもCacelButtonだけとなっています。 

キャンセルだけではなく、入力した内容をそのまま使うなど、
補完と入力の両方を実現したい場合には、入力内容をそのまま使うボタンをCancelの位置におくようなUIが欲しくなると思います。

今回は、UISearchBarで補完候補を表示できる jcoleman/JCAutocompletingSearch · GitHub ライブラリのUISearchBarの見た目をカスタマイズしてみたいと思います。

サンプルのプロジェクトは azu/SearchBarCustomize · GitHub においてあります。

今時のプロジェクトならCocoaPodsを使ってライブラリ管理していると思うので、 JCAutocompletingSearchをcocoapodsでインストールします。
(cocoapodsを使ってないなら  JCAutocompletingSearch のファイルをそのままプロジェクトに入れればいいと思います)

platform :ios, '5.0'

pod 'JCAutocompletingSearch'

で `$ pod install` しておきます。(開くプロジェクトはSearchBarCustomize.xcworkspaceの方)

JCAutocompletingSearchのデフォルトで用意されている検索UIもそのまま検索バーとしてしか使えないようになっています。

IOSシミュレータ  iPhone  Retina 3 5 inch iOS 6 1  10B141 2013 02 18 11 11 19

この検索画面のJCAutocompletingSearchViewControllerのサブクラスを作って、補完と入力ができるUIを作ってみたいと思います。

作りたいUIは以下のような感じで、Cancelの位置にそのまま入力を使う決定ボタン、キャンセルを入力補完画面を閉じるボタンという感じにしたいと思います。

IOSシミュレータ  iPhone  Retina 3 5 inch iOS 6 1  10B141 2013 02 18 11 33 50

これを作るためには実際にサンプルを開いて見るとわかるのですが、UIToolBarの中にUIBarButtonのViewとして、UISearchBarを入れて、
同じように決定ボタンもUIBarButtonとして配置します。 

SearchBarCustomize xcworkspace  AZAutCompletingViewController storyboard 2013 02 18 11 36 52

recursiveDescription でダンプしてみると以下の様な構造ですね。

<UIToolbar: 0x917b690; frame = (0 44; 320 44); autoresize = W+BM; layer = <CALayer: 0x917b770>>
   | <_UIToolbarBackground: 0x917bf90; frame = (0 0; 320 44); autoresize = W; userInteractionEnabled = NO; layer = <CALayer: 0x917c040>>
   | <UIImageView: 0x917c4c0; frame = (0 -3; 320 3); opaque = NO; autoresize = W+BM; userInteractionEnabled = NO; layer = <CALayer: 0x917c520>>
   | <UIView: 0x917bae0; frame = (12 0; 247 44); autoresize = RM; layer = <CALayer: 0x917bb40>>
   |    | <AZCustomSearchBar: 0x9176710; baseClass = UISearchBar; frame = (0 0; 247 44); text = ''; autoresize = W+BM; layer = <CALayer: 0x91767c0>>
   |    |    | <UISearchBarBackground: 0x9177e80; frame = (0 0; 247 44); userInteractionEnabled = NO; layer = <CALayer: 0x9177f40>>
   |    |    | <UISearchBarTextField: 0x9178450; frame = (0 0; 0 0); text = ''; clipsToBounds = YES; opaque = NO; gestureRecognizers = <NSArray: 0x9178d60>; layer = <CALayer: 0x9178600>>
   | <UIToolbarTextButton: 0x917c7f0; frame = (270 0; 44 44); opaque = NO; layer = <CALayer: 0x917d9e0>>
   |    | <UIButton: 0x917c910; frame = (0 8; 44 30); opaque = NO; layer = <CALayer: 0x917c9d0>>
   |    |    | <UIImageView: 0x917df20; frame = (0 0; 44 30); clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x917df80>>
   |    |    | <UIButtonLabel: 0x917cc20; frame = (10 7; 24 15); text = '決定'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x917cd10>>

ただし、上記のスクリーンショットでもわかるように、UIToolbarの背景と、UISearchBarの背景が重なって一部が出っ張って見えてしまいます。
これの見た目を治すために、 UISearchBarの背景画像を透明にしてしまうのが簡単な方法です。

サンプルでは以下のように透明の画像を作りUIAppearanceを使って背景画像に適応しています。
(範囲をこのViewControllerだけに限定するために、appearanceWhenContainedInを使います) 

UIImage *(^createImageFromUIColor)(UIColor *) = ^(UIColor *color) {
    CGRect rect = CGRectMake(0, 0, 1, 1);
    UIGraphicsBeginImageContext(rect.size);
    CGContextRef contextRef = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(contextRef, [color CGColor]);
    CGContextFillRect(contextRef, rect);
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return img;
};

- (void)awakeFromNib {
    [super awakeFromNib];
    // 背景画像を透明にする
    [[UISearchBar appearanceWhenContainedIn:[AZAutCompletingViewController class], nil]
            setBackgroundImage:createImageFromUIColor([UIColor clearColor])];
    // 検索アイコンを変更(透明にする)
    [[UISearchBar appearanceWhenContainedIn:[AZAutCompletingViewController class], nil]
            setImage:createImageFromUIColor([UIColor clearColor]) forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
}

 

利用できる、UIAppearanceはUISearchBar Class Referenceにまとまっています。
サブクラスで直接subViewsのプロパティをいじって変更することもできますが、将来的な変更などで壊れる可能性があるので、UIAppearanceを使った見た目の変更をしたほうがいいです。

これで、以下の様な見た目を作ることができます。
決定ボタンやキャンセルボタンは通常のUIBarButtonなので、普通にイベントをつけたりすれば自由に扱えるようになるはずです。
(サンプルではJCAutocompletingSearchViewControllerDelegateにsubmitのdelegateを追加して決定ボタンの挙動を作ってあります) 

IOSシミュレータ  iPhone  Retina 3 5 inch iOS 6 1  10B141 2013 02 18 11 33 50

これで、UISearchBarに決定ボタンを持たすような見た目のものが出来上がりました。

今回利用したJCAutocompletingSearchIMOAutocompletionViewControllerと違い非同期での補完候補を絞込みができるため、
大量の補完単語を用意した場合でも問題無く動きます。
また日本語等の入力も問題なく動作すると思います。

今回のように既存のライブラリの機能は欲しいけど見た目や機能を少し付け加えたいと思った時に、こういうサブクラスなどで機能を引き継いで
見た目や機能を追加するアプローチは結構重要だと思います。
cocoapodsを使ってインストールしたライブラリに対して直接ライブラリをいじるのは行儀が良くないので、サブクラスだとライブラリ本体と分離して実装することができるため、色々使い所が出てくると思います。 

StoryBoard等ではUITableViewではコンテンツのタイプで”Dynamic Prototypes”と”Static Cells”が選べるようになっています(iOS5から)

NewImage

“Static Cells”はiOS5から使うことができ、名前の通りInterface builder(GUI)上でTableViewの見た目を作ることができます。
そのため、複数の入力UIを併せ持つような 設定画面 などでとても効果を発揮します。

NewImage

設定の見た目をGUIで作って、後はUIViewControllerと同じようにoutletを結んで処理を書くだけで済むようになります。
今までの”Dynamic Prototypes”の方式ではコードで X section の Y rowは何を表示する等といったdataSourceの定義が必要になり、
見た目以上にコード量が増えてしまうので、”Static Cells” で十分な場合は”Static Cells”を使ったほうが圧倒的に楽になります。

逆に、”Static Cells”で済まない場合というのは、staticではないということなので、セルの増減が入るようなTableViewの場合は、
“Static Cells”では行う事ができません(やる方法があったら知りたい)

設定画面のように入力UIが複数あって、なおかつセルを動的に追加したりするようなUIが必要な場合は、
“Dynamic Prototypes”を使って行わないといけません。

例えば、以下のように”Static Cells”で作れたら楽な部分と動的にセルの追加が必要なものが混ざったデザインを考えみます。

実際に”Dynamic Prototypes”で作ったものは下記にあります。

DynamicUI

このUIを”Dynamic Prototypes”で以下に楽に作るかというのが今回の本題です。

Dynamic Prototypesでできる事

Dynamic Prototypesの場合でも実はInterface builder上でセルの見た目を作成することができます。

ただし、この”Dynamic Prototypes”上で追加したUITableViewCellは、その配置のまま表示されてる訳ではなく、
UITableViewCellにidentifierを振り、それをコード上で取得して表示するためのカスタムセルを作っているのと同じです。

Static cell xcodeproj  MainStoryboard storyboard

以前、シンプルなカスタムセルの作り方とセル内のボタンへのイベント設定方法 | Technology-Gymで紹介したカスタムセルの作り方では、
xibに単独の UITableViewCell を作ってそれをUITableViewControllerでregisterNib:を使い読み込んで使用していました。

SimpleCustomCell xcodeproj  SmartCell xib

今回の、”Dynamic Prototypes”上で作るカスタムセルはregisterNib:が既にされている状態のような感じなので、
後は [tableView dequeueReusableCellWithIdentifier:cellIdentifier];という感じで呼び出せば扱えるようになっています。

“Dynamic Prototypes”上のカスタムセルと別ファイルとして作るカスタムセルの使い分けとしては、
別ファイルとして作るカスタムセルの方が汎用性は高いので色んなクラスで利用できますが、セルごとにバラバラで保存されているのでまとまりがないです。
(一つのカスタムセルを並べてリスト表示するようなUIにはコチラのほうが汎用性が出る気がします)
逆に “Dynamic Prototypes”上のカスタムセル は その定義してるUITableViewController以外では使わないので、
一箇所で複数のカスタムセルを管理できるので利点です。

今回は”Dynamic Prototypes”上のカスタムセルを作っていきます。
目的のUIではセルの種類は4種類となっているので、以下のように4つの UITableViewCell を作って、それぞれにidentifierを付けていきます。

Static cell xcodeproj  MainStoryboard storyboard 1

後は、[tableView dequeueReusableCellWithIdentifier:@"kCellName"]; というような感じでカスタムセルのインスタンスが取得できますが、
identifierを文字列で指定するのは避けたいと思います。

そこでUser Defined Runtime Attributesを利用します。(iOS5以上から利用可能)
User Defined Runtime AttributesはInterface builder上から、クラスのプロパティに特定の値を実行時に代入する機能です。

今回の例では先にTableViewControllerのクラスのプロパティを宣言しておいて、user defined runtime attributes経由でそこに
それぞれのカスタムセルのidentifierを代入するようにします。


@interface MyTableViewController : UITableViewController  {

}
// ...
#pragma mark - user defined runtime attributes
@property(nonatomic, strong) NSString *idNameCell;
@property(nonatomic, strong) NSString *idTimeCell;
@property(nonatomic, strong) NSString *idAddCell;
@property(nonatomic, strong) NSString *idSwitchCell;
// ...
@end

user defined runtime attributesは以下のように、KeyPathにプロパティへのKeyPath、Valueにカスタムセルのindetifierを入れておきます。
こうすると、実行時にはself.idNameCellは@”kCellName”という値が入るようになるので、コード上に文字列を書く必要はなくなります。

Static cell xcodeproj  MainStoryboard storyboard 2

これで”Dynamic Prototypes”上のカスタムセルの扱いを少しマシにできました。

次にセルの配置について考えてみます。

“Static Cells”では、セルの配置セクションの構成などもInterface builder上で配置できるので何も考えなくても見た目を作成できます。
しかし、”Dynamic Prototypes”では<UITableViewDataSource>でセルの配置を考えたりするため、コードで配置を定義しないといけません。

Objective-Cのliteralを使って、配置だけ配列と辞書を使って作っていきます。
今回は次のようなイメージで、sectionが配列で、それぞれのセルが辞書という感じにして、
辞書のkCellIdentifierの値がカスタムセルの種類を指定するという感じにしました。


    @[
        // section
        @[
            // cell
            @{
                kCellIdentifier : @"cell id"
            },
        ],
        // section
        @[
            // cell
            @{
                kCellIdentifier : @"cell id",
            },
            // cell
            @{
                kCellIdentifier : @"cell id",
            },
        ],
    ];

実際には次のように、dataSourceを更新するメソッドを作って、
毎回固定(static)な部分と、メソッドを呼び出して動的にsectionを作る部分を混ぜたdataSourceを作ります。


- (void)updateDataSource {
    self.dataSource = [[NSMutableArray alloc] initWithArray:@[
    // static
    @[
        @{
        kCellIdentifier: self.idNameCell
        }
    ],
    // dynamic
    [self timeStampAddDataSource],
    // static
    @[
        @{
        kCellIdentifier: self.idSwitchCell
        }
    ]
    ]];
}

timeStampAddDataSourceメソッドは、タイムスタンプのカスタムセル(self.idTimeCell)は動的に追加されるので、
タイムスタンプのカスタムセルの配列の末尾に、タイムスタンプを追加するためのセル(self.idAddCell)を追加した配列を返すメソッドです。

NewImage


- (NSArray *)timeStampAddDataSource {
    NSArray *array = [self.timeStampDataSource copy];
    return [array arrayByAddingObject:@{
    kCellIdentifier:self.idAddCell
    }];
}

[self updateDataSource];すれば、配置の情報を持ったdataSourceが作られるので、
後はそのdataSourceに従って<UITableViewDataSource>を実装していくだけになります。

dataSourceと作られるUIの対応を見ると以下のようなイメージ

Skitched 20121113 175841

この辺の実装についてはソースなどを参照するといいかと思います。

これで、以下の要素を使って”Static Cells”で作る時程は楽では無いですが、動的にセルの増減するTableViewをある程度扱い易く作れるようになると思います。
(配置のdataSourceとかをもう少し上手くやりたいですが…)

  • Dynamic Prototypes
  • user defined runtime attributes
  • Custom Cell