iOS

UITextFieldがキーボードに隠れないようにオフセットする


iPhoneでアプリの画面にたくさんTextFieldを配置した時、画面下の方のTextFieldを選択するとキーボードが下からニョキッと出てきてTextFieldを隠してしまう。もちろんそのままでも入力は可能だが、キーボードを仕舞うまでどう入力されているか分からないので不便である。
そこで、TextFieldを選択したときに隠れてしまう場合は表示をずらして隠れないようにしたい。ネットでサンプルを探したのだが、これというものが見当たらなかったので作ってみた。

InterfaceBuilderでまず全体にUIScrollViewを敷き詰めて、そこにUITextFieldを並べていく。
まず、(隠れてしまう)全てのTextFieldについて「Editing Did Begin」と「Editing Did End」の2つのイベントハンドラを定義しておく。この際、どのTextFieldに対しても同じメソッドを適用する。例えばそれぞれ – (void)textEditingDidBegin および - (void)textEditingDidEnd とする。
この時、InterfaceBuilderを使って普通にイベントハンドラを定義しようとすると両方とも「Editing Did End」イベントに結合してしまうので、ConnectionsInspectorで「Editing Did Begin」イベントから – (void)textEditingDidBegin メソッドに結合した方が良い。
また次のフィールドを宣言しておく。

1
2
3
4
5
6
7
@interface ViewController () {
    float _keyboardOffset; // 表示のオフセット(どれだけずらすか)
    float _offsetMargin; // 実際にオフセットを適用し始めるマージン
}
    :
    :
@end

そしてそれぞれ次のように初期化する。

1
2
3
4
5
6
7
8
- (void)viewDidLoad
{
    [super viewDidLoad];
    :
    :
    _keyboardOffset = 0.0f;
    _offsetMargin = 120.0f; // 4インチディスプレイの場合は 210.0f 程度
}

_offsetMargin というのは、例えば画面の上の方にあるTextFieldを選択したときは表示をずらす必要がないので、じゃあY座標がどこまでの範囲内ならオフセットしないかというのを決めておく必要がある。3.5インチの場合はだいたい120ピクセルぐらいまではずらさなくても大丈夫。4インチの場合は3.5インチの場合と90ピクセルくらい差があるのでマージンもそれだけ増やして大丈夫だ。

次に最初に定義したイベントハンドラを実装する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (IBAction)textEditingDidBegin:(UITextField *)sender {
    // オフセットすべきサイズを計算する
    CGRect textFieldRect = [sender frame];
    _keyboardOffset = MAX(0, textFieldRect.origin.y - _offsetMargin);
    // アクティブなTextFieldが表示されるようにオフセットする
    CGRect viewFrame = [self.scrollView frame];
    viewFrame.origin.y -= _keyboardOffset;
    self.scrollView.frame = viewFrame;
}

- (IBAction)textEditingDidEnd:(UITextField *)sender {
    // ScrollViewのオフセットを元の値に戻す
    CGRect viewFrame = [self.scrollView frame];
    viewFrame.origin.y += _keyboardOffset;
    self.scrollView.frame = viewFrame;
    _keyboardOffset = 0.0f;
}

尚、UITextFieldDelegateとかオフセット時のアニメーションとかは本題から外れるので適宜実装してください。

UITableViewの一覧にCora Dataで取得した内容を反映する


 Core Dataのサンプルとか、UITableViewのサンプルとかはネット上でも書籍でもかなり豊富にあるのだが、それらの組み合わせの例は意外と少ない。先日見つけた書籍はかなり詳しく書いてあって「これはいけるか!?」と思われたが、実は肝心なコードが抜けていた。それはMaster-Detail型で言うところのMaster画面でCore Dataから取得したデータの一覧をUITableViewに表示する際、一覧の見出しにデータの内容が反映されないのだ。おそらくその書籍の著者にとってはあまりに自明の事なので記載するのを忘れてしまったのだろう。あるいはそれくらいは自分で考えろという事なのかもしれない。

 まあここで毒づいても仕方が無いので対処方法をメモしておく。

 一覧に内容を反映させるには MasterViewController.m の 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
メソッドにコードを記述する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell;
    // iOSのバージョンによりセルの取得方法が変わる
    if (6.0 <= [[[UIDevice currentDevice] systemVersion] floatValue]) {
        cell = [tableView dequeueReusableCellWithIdentifier:@"AnyId" forIndexPath:indexPath];
        [self configureCell:cell atIndexPath:indexPath];
    }
    else {
        cell = [tableView dequeueReusableCellWithIdentifier:@"AnyId"];
        if (!cell) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"AnyId"];
        }
    }
    // ここでセルの表示を変更
    NSManagedObject* managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = managedObject.anyProperty;
    return cell;
}


 尚、iOSのOSバージョンを判別する方法は以下のサイトを参考にした。
http://syszr.com/s14.html

“The model used to open the store is incompatible with the one used to create the store” に対処する


Core Dataを使った開発をしていたら次のようなエラーが出てシミュレータで起動しなくなったので、対処方法をメモしておく。

The model used to open the store is incompatible with the one used to create the store

プロジェクトをクリーンしてもダメで、このエラーメッセージで検索したところ、sqliteのファイルを削除しないとダメなのだそうだ。
そのsqliteファイルは以下の場所。ターミナルを開いてrmコマンドで削除する。

/Users/{ユーザー名}/Library/Application Support/iPhone Simulator/{iOSバージョン}/Applications/{英数字}/Documents/{プロジェクト名}.sqlite

参考にしたページはこちら
http://u2k772.blog95.fc2.com/blog-entry-167.html

No iOS Development certificate was found に対処する


iOSでの開発を久しぶりに、しかも新しい機種で行おうとしたらxcodeで「No iOS Development certificate was found」というエラーになり、デバイスが登録できないという現象に陥ったので、対処方法を忘れないようにメモしておく。

単にProvisioning Profileの有効期限切れなのだが、どうやったかすっかり忘れてしまっている。これまた久しぶりに iTunes Connect へアクセスし、「Certificates, Identifiers & Profiles」というメニューに入る。左メニューの「Provisioning Profiles」にて[+]マークをクリックすると新しいプロファイルを作成する手順に移行する。

新しいプロファイルの作成過程で、証明書も期限が切れていると証明書(ios_development.cer)も作り直すことができる。この時別の証明書(CertificateSigningRequest.certSigningRequest)が必要になる。もしこれも有効なものが無ければキーチェーンアクセスから認証局に証明書を要求する。これで無事にプロファイル(*.mobileprovision)が作成され、ダウンロードが可能になる。

最後にxcodeのOrganizerでダウンロードしたプロファイルを追加すればデバイスを登録できるようになる。

UIButtonに任意の画像を使ってタップ時にハイライトさせる


 今更ではあるがメモしておく。

 画面上にボタンを用意するにはUIButtonを使うが、標準の外観では物足りない場合はボタンに見える画像を用意することが多い。ところがHTMLのつもりでImageプロパティに画像を指定してTitleプロパティにラベルを指定…とやってしまうと画像だけ表示されてラベルが表示されない。

 その場合はラベルも含んだボタン画像を用意するというのも一つの方法だが、ボタンの種類が多い場合は用意すべき画像が多くなってしまう。背景画像(BackgroundImageプロパティ)に画像を設定する。

 ただ、単純に背景画像を設定してもいいが、より細かく表現するにはボタンの状態別に画像を用意する。非活性状態、有効状態、選択状態、押下状態の4種類あればよいだろう。それぞれ次のようなコードになる。

1
2
3
4
[button setBackgroundImage:[UIImage ...] forState:UIControlStateDisabled];
[button setBackgroundImage:[UIImage ...] forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage ...] forState:UIControlStateSelected];
[button setBackgroundImage:[UIImage ...] forState:UIControlStateHighlighted];

 これを利用してトグルボタンのような機能を持たせることもできる。タッチイベントを捕捉してOFF状態であればONにし、ON状態であればOFFにするという具合だ。しかし、これがうまく動作してくれない。いや、トグルは動作するのだが、OFF状態のボタン押下中のハイライト画像が表示されないのだ。

 まず、adjustsImageWhenHighlightedプロパティはfalseにしておこう。これがtrueだとOFF状態のボタンを押下した時にハイライト画像ではなく、ボタン全体が暗転してしまう。だがそれでもまだ不充分。

 どうやらUIControlState~はビット和を指定することが可能らしい。つまりON状態とOFF状態だけでなく、OFF状態のボタンを押下中やON状態のボタンを押下中という表現ができる。それを改良したのが次のコード。

1
2
3
4
5
[button setBackgroundImage:[UIImage ...] forState:UIControlStateDisabled];
[button setBackgroundImage:[UIImage ...] forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage ...] forState:UIControlStateSelected];
[button setBackgroundImage:[UIImage ...] forState:(UIControlStateHighlighted|UIControlStateNormal)];
[button setBackgroundImage:[UIImage ...] forState:(UIControlStateHighlighted|UIControlStateSelected)];

 これでボタンの状態に関係なく、ボタン押下中はハイライト画像を表示させることができた。

“Unknown class ‘class name’ in Interface Builder file” に対処する


久しぶりにiOSアプリの開発環境をいじったら久しぶりにハマったのでメモしておく。

現象としてはXcodeからプロジェクトをビルドしてシミュレータで起動しようとすると、起動できずにアプリは終了し、以下のエラーメッセージを吐くというもの。実際には「class name」には作成したクラス名が入る。

Unknown class ‘class name’ in Interface Builder file

対処方法は次の通り。

  1. XcodeのProject Navigatorでプロジェクトのルートを選択して設定画面を開く。
  2. Standard editorの左ペインで TARGETS 以下の項目を選択する。
  3. Standard editorの右ペインの Build Phases タブを選択する。
  4. Compile Sources のグループを開き、不足しているクラスのソース本体(.m)を追加する。
  5. ビルド及び実行する。

これはInterface Builderから特定のクラスが見えないという状況なのだが、例えば新規にプロジェクトを作成した時に、他のプロジェクトで作成したクラスをファイルのコピーによって追加したような場合に発生する。(クラスを新規作成で追加した場合は自動でBuild Phaseに追加されるようだ。)

絵文字を含むNSStringの正確な文字数をカウントする(2)


 ちょっと前に絵文字の混在した文字列のカウント方法について書いたら、ちゃんとカウントできない文字があると指摘された。
なるほど、確かにカウントできない文字がある。いったいどんな規則性があるのかと思っていたら、絵文字の文字コードをまとめていたサイトを教えてもらった。
それがこれ↓
iOS Emoji

 う~ん、まるで規則性が見当たらないと思っていたらピンときた。前は UTF8String とかやってたけど、内部の文字コードはもしかしてUTF-16ではなかろうか?と思って調べたらやっぱりUTF-16だった。それならわざわざUTF-8に変換しないでUTF-16のまま処理した方が良い。

 UTF-16といえば主要なトピックはサロゲートペアだ。0xD800-0xDBFFが上位サロゲート、0xDC00-0xDFFFが下位サロゲート。( Wikipedia#Unicode 参考)なので上位サロゲートを検出したら1文字スキップする。(1)

 さらに、国旗の絵文字には Regional Indicator という専用のシンボルが2つの組み合わせで使われている。( Unicode.orgのこのページ の最下段に記載されている。)なのでRegional Indicatorを検出して次のサロゲートペアもRegional Indicatorだったら3文字スキップする。(2)

 あと、ガラケーではお馴染みの四角で囲まれた数字には通常の数字の後に COMBINING ENCLOSING KEYCAP という専用の文字が配置されている。だがこれは単純に無視すればいいだろう。(3)

 そうすると、こんな感じで書けばいいかな?ちゃんと実装するには文字のインデックスが out of bounds にならんようにした方がいいかも。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
- (NSUInteger) actualNSStringSize:(NSString *) txt {
    int len = [txt length];
    NSUInteger count = 0;
    for (NSUInteger i = 0; i < len; i++) {
        unichar c = [txt characterAtIndex:i];
       
        if (0xD83C == c) {
            unichar c1 = [txt characterAtIndex:i+1];
            if ((0xDDE6 <= c1) && (c1 <= 0xDDFF)) {
                unichar c2 = [txt characterAtIndex:i+2];
                if (0xD83C == c2) {
                    unichar c3 = [txt characterAtIndex:i+3];
                    if ((0xDDE6 <= c3) && (c3 <= 0xDDFF)) {
                        // 国旗なのでスキップ・・・(2)
                        i += 3;
                        ++count;
                        continue;
                    }
                }
            }
            i++;
            ++count;
        }
        else if (0xD800 <= c && c <= 0xDBFF) {
            // 上位サロゲート・・・(1)
            i++;
            ++count;
        }
        else if (0xDC00 <= c && c <= 0xDFFF) {
            // 下位サロゲートなので念のため無視
        }
        else if (0x20E3 == c) {
            // 囲みなので無視・・・(3)
        }
        else {
            ++count;
        }
    }
    return count;
}

 ちなみに、これらの絵文字はiOS独自の実装かと思っていたらそうではなく、Unicode6.0 というれっきとした国際規格なのだそうだ。ガラケーに端を発した日本発の絵文字が知らない間に国際規格に…っていうか Unicode のバージョンって知らない間にこんなに上がっていたんだ。

絵文字を含むNSStringの正確な文字数をカウントする


 iPhone/iPad のキーボード設定で「絵文字」というキーボードを追加すると、ガラケーよろしく絵文字を入力できるようになる。なので、何も考えないとUITextFieldなどに絵文字を入力されてしまう可能性がある。ここでアプリの仕様上、絵文字を許容するかしないかという議論は当然起こるが、許容したくないというケースはひとまず置いといて、許容する場合のトピックを取り上げる。

 絵文字も他の文字と同様UTF-8であるようだが、絵文字の中には4バイトで表現されるものもあるようで、その4バイトの絵文字を含んだ文字列は [str length] で得られる文字数が狂ってしまうのだ。そうなるとバリデーションとして文字数制限を設ける場合にうまくない。

 この現象はどうやらNSStringクラスのUTF-8の取り扱いに原因があるようだが、不具合だと認識されていれば将来的にSDKの方で対応される可能性がある。それまでは次のような関数で凌ぎたいと思う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (NSUInteger) actualUtf8StringSize:NSString str {
     const char* cstr = [str UTF8String];
     int len = strlen(cstr);
     NSUInteger count = 0;
     unsigned char c;
     for (int i = 0; i < len; i++) {
          c = (unsigned char)cstr[i];
          if (0x00 <= c && c <= 0x7F) {
               ;
          }
          else if (0xC2 <= c && c <= 0xDF) {
               i += 1;
          }
          else if (0xE0 <= c && c <= 0xEF) {
               i += 2;
          }
          else if (0xF0 <= c && c <= 0xF7) {
               i += 3;
          }
          else {
               continue;
          }
          ++count;
     }
     return count;
}

 これはUTF-8の性質を利用して文字数をカウントしている。UTF-8は文字の1バイト目でその文字が何バイトかを示している。なので、バイト列を先頭から調べて行き、文字のバイト数分スキップしながらカウンタを増加している。ひとつ留意したいのは、バイト列から1バイト取得する時に char 型ではなく unsigned char 型の変数に代入することだ。C言語では常識の範疇だが、慣れていないと見落としやすい。

複数行のUILabelを内容に合わせて自動縮小


 UILabelは文字列を表示する部品だが、固定した内容を表示する時はもちろん、状況に応じて内容を変更することもできる。この部品にはInterface Builderで見るとFont指定欄下に Autoshrink という属性(プロパティとしては adjustsFontSizeToFitWidth )があって、ここにチェックを入れていると文字列が長くてはみ出てしまうときは、自動でフォントサイズを小さくして全体が収まるようにしてくれる。(際限なく小さくすると読めなくなってしまうため、 MinimumSize という属性で最小サイズを指定できる。)もちろんチェックを外せば、はみ出る場合に例えば末尾が「…」という風に省略される。

 ところが、公式リファレンスにも記載されている通り、この Autoshrink という属性は Lines 属性が 1 の時しか有効にならないという制限がある。そうは言っても長い文字列の時は折り返して見やすく表示したいし、行数も分からないけど表示枠だけは決まっているというケースもあるはずだ。そんな時、自動でフォントサイズを小さくして決められた枠内に収まるような機能があれば嬉しい。

 例えば、Interface Builder上でUILabel部品を貼り付けて、labelという名前でOutletを作成したとする。まずは。Size Inspector を開いてサイズを決める。次に Attributes Inspector を開き、フォントとフォントサイズを決めてしまおう。そして今回は Autoshrink のチェックを外しておく。そして行数は動的に変わって欲しいので Lines =0 としておく。(こうすると自動で行数を増減してくれる。)そして次のようなコードを書いて、本来のサイズに収まるまでフォントサイズを小さくしていけば良い。

1
2
3
4
5
6
7
8
9
float orgHeight = self.label.frame.size.height;    // ラベルの高さの初期値を保持しておく
self.label.text = @"...";                          // 充分に長い文字列
[self.label sizeToFit];                            // 内容に合わせてラベルの大きさ(高さ)を変更
// 本来の高さになるまでフォントを小さくしていく
while (orgHeight < self.label.frame.size.height)
{
    self.label.font = [UIFont fontWithName:self.label.font.fontName size:self.label.font.pointSize - 0.1f];
    [self.label sizeToFit];
}

UIWebViewをスクロールさせない


UIWebViewクラスはUIScrollViewの派生クラスなので基本的にはWEBブラウザと同様に内容がはみ出る場合には自動でスクロールする。
ところが、場合によってはUIWebViewを使いたいがスクロールはさせたくないということもある。
そういう場合は userInteractionEnabled プロパティをNOにしてしまえばよい。これでタップ等のイベントを拾わなくなるのでスクロールすることもない。
(ちなみに、この方法だとハイパーリンクもタップ出来なくなるので注意!)

尚、Interface Builderを使用する場合は、Attribute Inspectorを開き、 View > Interaction > User Interaction Enabled のチェックを外せば良い。

アーカイブ