Tag Archives: Ios

OpenFileInWebView というシンプルなライブラリを書きました。

これはWebView指定したファイルパスを開くViewControllerを返すという単機能のライブラリで主にデバッグ等に使う感じです。

以下のように使えば、file.pdfというファイルを表示してるWebViewを見ることが出来ます。(WebViewはPDFやdocなども表示出るので簡単なプレビュー側がわりになります)


UIViewController *controller = [OpenFileInWebView viewControllerToOpenFile:[[NSBundle mainBundle] pathForResource:@"file" ofType:@"pdf"]];
[self.navigationController pushViewController:controller animated:YES];

使用例としては、PDFを生成してメールで送るような機能をつけるときにシミュレータ上でPDFを直接確認する用途などに使えます。

system("open ~")でMac OSXの方で開いてもいいのですが、Releaseビルドに残ってたりすると面倒なので、WebViewで開いてます。

最近APIとのやり取りにデータモデルとしてMantle/Mantleを使っています。

APIにデータを送るときにNSJSONSerialization等を使ってNSDictionaryからJSON文字列にシリアライズしてから送るのが普通です。

しかし、その過程で以下のような真偽値を持つDictionaryをシリアライズすると

@{
    @"key" : @YES
}

数値に変換されてしまう事があります。(主にCoreData -> JSONとしていくような時)

{
    "key" : 1
}

これを回避するために泥臭いコードが必要になったりすることがあります。

Mantleではそういうよくある事は大体想定されていてそれようの変換する仕組みを持っています。

Mantleではプロパティに対応した名前で、プロパティ名JSONTransformerといいう`NSValueTransformer*`を返すメソッドを定義して置くと自動的に変換してくれます。

例えば、

@property(nonatomic) BOOL isBool;

というプロパティなら以下のようにメソッドを定義します。

+ (NSValueTransformer *)isBoolJSONTransformer {
    return [NSValueTransformer valueTransformerForName:MTLBooleanValueTransformerName];
}

[NSValuetransformer valueTransformerForName:MTLBooleanValueTransformerName]; を使うと自動的にJSONでもBOOLとなるように強制的に変換してくれます。
これで安全にAPIにデータを送れるようになります。

Lupinus

LupinusHTTP は直感的に扱いやすい事を目的にしたHTTPライブラリです。

NSURLSessionをベースにしているのでiOS7以降のみ対応です。

インストール

Cocoapodsからインストールできます。

pod "LupinusHTTP"

使い方

基本的には以下の2ステップで送信と受信ができます。

  1. LupinusHTTPRequest でどこにリクエストするのかを決める。
  2. 返り値のLupinusHTTPresponseJSONresponseString などどういう風にレスポンスを受け取るかを決める。

GET Request

http://httpbin.org/get?key=value にGETリクエストする例です

LupinusHTTPRequest *httpRequest = [LupinusHTTP request:LupinusMethodGET URL:@"http://httpbin.org/get" query:@{
    @"key" : @"value"
}];
[httpRequest responseJSON:^(NSURLRequest *request, NSURLResponse *response, id JSON, NSError *error) {
    NSLog(@"JSON = %@", JSON);
}];

レスポンスをどういう形式で受け取るかはどのresponse*を呼ぶかによって決まります。送信する段階ではなく、受信する段階でJSONやテキストで受け取るのかを決定出来ます。両方で受け取ることも出来ます。

以下はJSONで受け取る場合です。

LupinusHTTPRequest *httpRequest = [LupinusHTTP request:LupinusMethodGET URL:@"http://httpbin.org/get"];
[httpRequest responseJSON:^(NSURLRequest *request, NSURLResponse *response, id JSON, NSError *error) {
    NSLog(@"JSON = %@", JSON);// => JSON Object(NSDictionary or NSArray)
}];

普通にNSStringで受け取る事もできます。

LupinusHTTPRequest *httpRequest = [LupinusHTTP request:LupinusMethodGET URL:@"http://httpbin.org/get"];
[httpRequest responseString:^(NSURLRequest *request, NSURLResponse *response, NSString *string, NSError *error) {
    NSLog(@"string = %@", string);// => NSString
}];

POSTリクエスト

POSTリクエストも送ることが出来ます。

e.g) http://httpbin.org/post?key=value

body
    [1,2,3]

bodyの中身は自動でJSONシリアライズして送信されます。

[LupinusHTTP request:LupinusMethodPOST URL:@"http://httpbin.org/post" query:@{
    @"key" : @"value"
} body:@[@1, @2, @3]];
[httpRequest responseJSON:^(NSURLRequest *request, NSURLResponse *response, id JSON, NSError *error) {
    NSLog(@"JSON = %@", JSON);// => JSON Object(NSDictionary or NSArray)
}];

GETやPOSTなどのレスポンスを受け取る共通の挙動としてresponse.statusCode >= 400 の時は自動的にerrorとなります。
コールバックのerrorに値が入った状態でレスポンスが帰ってきます。

LupinusHTTPRequest *httpRequest = [LupinusHTTP request:LupinusMethodGET URL:@"http://httpbin.org/status/403"];
// response status code is 403
[httpRequest responseJSON:^(NSURLRequest *request, NSURLResponse *response, id JSON, NSError *error) {
    // error is not nil
    if(error){
        NSLog(@"%@", error);
    }
}];

NSURLSessionではNSURLSessionConfigurationをつけうことで独自のHTTPヘッダ等を付加することが出来ますが、LupinusHTTPRequestを呼ぶときにそれを渡すことが出来ます。

// default : [NSURLSessionConfiguration defaultSessionConfiguration]
+ (instancetype)httpWithSessionConfiguration:(NSURLSessionConfiguration *) sessionConfiguration;

リクエストのキャンセル

LupinusHTTPRequest#cancelを呼ぶことで送信したリクエストをキャンセル出来ます。
この場合コールバックは呼ばれません。

LupinusHTTPRequest *httpRequest = [LupinusHTTP request:LupinusMethodGET URL:@"http://httpbin.org/get"];
[httpRequest responseJSON:^(NSURLRequest *request, NSURLResponse *response, id JSON, NSError *error) {
  // this callback doens't call!
}];
// cancel request
[httpRequest cancel];

Swiftで書かれたAlamofireに影響を受けて作られているので似ている部分は多いです。

Credit

Photo by Tatu Väyrynen

UITabBarControllerはタブバーのコントローラーですが、
タブバーは細かいデザインの調整を行う場合はUITabBarControllerのサブクラスを作って実装するパターンが多いと思います。

一つのStoryBoardに全てのタブの参照がある場合はInterface Builderなどでもある程度はデザインを設定できますが、StoryboardをViewController毎やタブ毎等に分けていた場合はInterface BuilderだけだとIBOutletを結べなくなります。

運用的にはタブやViewControllerでstoryboardファイルを分けた方が扱いやすいと感じるので、タブを関連付けるにはUITabBarControllerのサブクラスを作ってコードで結んでいく必要があります。

しかし、UITabBarControllerをそのまま実装すると、UITabItemのテキスト、アイコン…等意外とややこしいです。

タブバーの構成要素は以下のような感じですね。

  • タブバー背景画像
  • 選択してるタブの背景画像
  • 各タブのアイコン
  • 各タブのテキスト

実際にやることとしては以下のようになりますね。

  1. self.viewControllers に 各タブTopのViewControllerを入れる
  2. self.tabBar.items を走査して、各タブ(UITabBarItem)にタイトルやアイコンを設定
  3. タブバー全体の細かい調整

これを直に書くとforループ等がでてきてしまい余りキレイにはならないと思います。

- (void)setupTabs {
	// 1. 各タブのViewControllerを設定
    self.viewControllers = @[
        self.tabFactory.calendarNavigationController,
        self.tabFactory.graphNavigationController,
        self.tabFactory.reporterNavigationController,
        self.tabFactory.karadaNoteNavigationController,
        self.tabFactory.settingNavigationController,
    ];
	// 2. TabBarItemにタイトルなどの設定
    NSArray *tabBarItems = [[self tabBar] items];
    for (NSUInteger i = 0; i < [tabBarItems count]; i++) {
        MenuTabType menuTabType = (MenuTabType)i;
        UITabBarItem *tabBarItem = tabBarItems[i];
        tabBarItem.title = [self tabItemTitle:menuTabType];
        UIImage *tabBarItemSelectedIcon = [self selectedTabItemImage:menuTabType];
        UIImage *tabBarItemUnselectedIcon = [self unselectedTabItemImage:menuTabType];
        [self setTabBarItem:tabBarItem tabBarItemSelectedIcon:tabBarItemSelectedIcon tabBarItemUnselectedIcon:tabBarItemUnselectedIcon];
        [self setTitleTextColorWithTabBarItem:tabBarItem];
    }
	// 3. タブバー全体の設定
    [[self tabBar] setSelectionIndicatorImage:[UIImage imageNamed:@"tab_select_image.png"]];
    [[self tabBar] setBackgroundImage:[UIImage imageNamed:@"tab_backgroundimage.png"]];
    [[self tabBar] setSelectionIndicatorImage:[UIImage imageNamed:@"tab_select_image.png"]];
}

という感じになってしまいました。

そこで、これをパターン化して解決出来るようにするRoleTabBarControllerというライブラリを書きました。

RoleTabBarController

このライブラリが扱うパターンと言うのは<UITableViewDataSource>、UITableViewのDataSourceと基本的に同じです。

$ pod try RoleTabBarController で試すことが出来ます。

詳しくはExampleを見てもらいたいのですが、先ほどの例はUITabBarController内に全て書いていましたが、UITableViewDataSourceと同じようにModelクラスに情報を出しやすくなります。

@implementation AppTabBarController
- (AppTabBarControllerModel *)model {
    if (_model == nil) {
        _model = [[AppTabBarControllerModel alloc] init];
    }
    return _model;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataSource = self;
}


#pragma mark - RoleTabBarController

- (NSUInteger)numberOfRoleTabBarController:(RoleTabBarController *) tabBarController {
    return 4;// 実際にはこれもモデルに出せる
}

- (UIViewController *)roleTabBarController:(RoleTabBarController *) tabBarController viewControllerAtIndex:(NSUInteger) index {
    return [self.model viewControllerAtType:(AppTabBarControllerType)index];
}

- (RoleTabBarItemObject *)roleTabBarController:(RoleTabBarController *) tabBarController tabBarItemObjectAtIndex:(NSUInteger) index {
    return [self.model tabBarItemObjectAtIndex:index];
}
@end

モデルクラスの方は、indexに応じたViewControllerやUITabBarItemの設定オブジェクトを返すだけですね。

 

typedef NS_ENUM(NSUInteger, AppTabBarControllerType) {
    AppTabBarControllerType_MAIN,
    AppTabBarControllerType_CALENDAR,
    AppTabBarControllerType_POST,
    AppTabBarControllerType_RANKING,
};

@implementation AppTabBarControllerModel {
}
- (UIViewController *)viewControllerAtType:(AppTabBarControllerType) type {
    switch (type) {
        case AppTabBarControllerType_MAIN:
            return [RoleViewController viewController:@"main"];
        case AppTabBarControllerType_CALENDAR:
            return [RoleViewController viewController:@"calendar"];
        case AppTabBarControllerType_POST:
            return [RoleViewController viewController:@"post"];
        case AppTabBarControllerType_RANKING:
            return [RoleViewController viewController:@"ranking"];
    }
    return nil;
}

- (RoleTabBarItemObject *)tabBarItemObjectAtType:(AppTabBarControllerType) type {
    switch (type) {
        case AppTabBarControllerType_MAIN:
            return [[RoleTabBarItemObject alloc] initWithTitle:@"MAIN" selectedImage:nil unselectedImage:nil];
        case AppTabBarControllerType_CALENDAR:
            return [[RoleTabBarItemObject alloc] initWithTitle:@"CALENDAR" selectedImage:nil unselectedImage:nil];
        case AppTabBarControllerType_POST:
            return [[RoleTabBarItemObject alloc] initWithTitle:@"POST" selectedImage:nil unselectedImage:nil];
        case AppTabBarControllerType_RANKING:
            return [[RoleTabBarItemObject alloc] initWithTitle:@"RANKING" selectedImage:nil unselectedImage:nil];
    }
    return nil;
}

- (RoleTabBarItemObject *)tabBarItemObjectAtIndex:(NSUInteger) index {
    return [self tabBarItemObjectAtType:(AppTabBarControllerType)index];
}
@end

RoleTabBarItemObjectはTabBarItemのタイトルと選択時のアイコン、非選択時のアイコンを設定出来るオブジェクトです。

基本的なデザインならこれで足りますが、TabBarItemのテキスト位置などを調整したい場合は、

- (void)roleTabBarController:(RoleTabBarController *) tabBarController willShowTabBar:(UITabBarItem *) tabBarItem atIndex:(NSUInteger) index;

 を実装して、そこでUITabBarItemを更新するようにするといいです。

RoleTabBarControllerは<UITableViewDataSource>のパターンを使ってUITabBarControllerを実装するパターンを提供するライブラリです。

ライブラリも100行行かない程度のシンプルなものなので、試してみるといいかもしれません。

前回お知らせしていたように第1回 Tech-Gym byプラスアール @SMS : ATNDというiOS勉強会を開催しました。


HealthKit.framework概要 - azu

スライド: HealthKit.framework概要

私はHealthKit.framework概要というテーマで発表しました。

基本的にはブログで紹介していたものをまとめた感じになっています。

またスライドではコードはあまり含めていませんが、実際に動くものとして以下にそれぞれサンプルコード、アプリを用意してあります。 

HealthKitは非同期APIのインターフェイスの勉強にもなるので興味を持ってるひとは見てみるといいですね。

以下は、私がメモしたものとなっています。


ViewはModelの写像であるということ – akuraru

  • MVCの概要
    • 機能ごとに分離する事で読理性を確保できるパターン
      • それぞれのライブラリを使って実装する 
    • 再利用性を高める
    • ControllerはModelからデータを取ってきて、Viewを作って表示する
      • コントールはモデルを読み込む
      • Viewを生成する(Xib,Storyboardでやる)
      • ViewにModelを反映させる
    • TableViewとModel
      • 1Cell = 1Model でやることが多い
    • View に Modelを渡す = ViewはModelは写像
  • Modelの変更
    • Cellは隠れしまうと、入ってたデータが消滅してしまう
    • ユーザーが入力したデータはスグにモデルに入れておく必要がある
    • NSManagedObjectの場合
      • 直接して変更してしまうと、キャンセルしたつもりでも他の画面の保存に巻き込まれる
      • 変更を破棄する必要がある == Undoする必要がる
        • Undoをするのは結構大変
      • または別のContextを用意して保存
        • 現実的に大変
  • 独自のデザインパターン – Wrapper Object
    • 編集画面でのViewの即座に反映させるパターン
    • ManagedObjectと同じプロパティを持つ
    • コンテキストを増やさずに変更可能ManagedObjectと振る舞う
    • 実際に書き込むタイミングになったら、ManagedObjectにコピーする
    • Wrapper Object
      • 同じプロパティ
      • 変更したいNSManagedObject
      • それぞれ持つオブジェクト
      • 反映させたい時は、別のメソッドを呼ぶ
      • Diaryがnilなら新規作成するため
    • WrapperObjectは自動的に作るの?
      • 短い場合は手動で作成する
      • 多い場合は自動で作ったりする

       


未来の自分を楽にするテクニック – 和田

  • 2年後の自分に自慢できるものはありますか?
  • Tips1 : Developer Documentを読む
    • やはり一番の基礎
    • ノイズの除去
  • Tips2: ⌘ + Shift + f(Xcode)
    • コーディング規約のすべては検索のため
    • いかに自分の書いたことを2ステップで目的地に辿り着ける
    • 検索にヒットしないツールは使わない
  • Tips3: 次期OS対策
    • β版は静観する
    • 不具合が多いので調べる情報を適宜収集
    • リリースされそうな空気を感じてからやる
  • Tips4: OSSの利用法
    • UIに関連するものは使用しない
    • 中身を参考にして知識のカテにする
    • iOSで変わってしまうので、UIにOSSをまぜると大変になる
  • Tips5: 実機検証は必要不可欠
    • 画面サイズはSimulatorで十分
    • アーキテクチャの違い(arm64, armv7s)を押さえる 
    • アーキテクチャの違いによる検証は必要
  • Tips6: iOSを知る
    • iOS癖を知る
      • 何か別の利用法があることに気づける
    • MacOSを知る
      • iOSの派生元になってる
    • 中身を知る
  • Tips7: 使えるモノは使う
    • Private APIも使い方によっては使える
    • UIWebViewのページのタイトルを取得するケース
  • 基本的な技術について
    • 全てのツールを使う必要はない
      • 基本的な知識を知ってると色々できる
    • 言語の理解
    • 必要最低限のツールをマスターする
    • 必要最低限のライブラリを使いこなす
      • Appleのクラスは大体統一されてる
  • JAHISお薬手帳データ・フォーマットについて
    • CSV形式のデータ
      • 必要最低限のデータしか入ってない
      • 人間のサポートが必要なレベル
    • QRコード読み取りライブラリが必要
      • ZXing仕様上の注意
      • 2枚以上のQR読み込み処理が必要
      • 読み取り性能の改善も必要
      • CoreImageを使っての読み込みは2枚以上の対応が難しい

会場を貸していただいたエス・エム・エスさん、参加してくださった皆さん本日はお疲れ様でした。