Category Archives: Ios

最近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. 返り値の
    LupinusHTTP

    responseJSON

    responseString

    などどういう風にレスポンスを受け取るかを決める。

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枚以上の対応が難しい

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

この記事はXcode6β4の時点でのHealthKitについて扱っています。 

HealthKit + PromiseKit事始め 基本的なデータの読み書き | Technology-Gym では、HealthKitを使った基本的な読み書きについて紹介しました。

単純に読み書きするだけでも様々なデータの型が用意されたり、データ共有もできるの使う意味はありますが、
さらにHealthKitにはHKStatisticsという統計(集計という方が近い?)処理するクラスが用意されています。 

HKStatisticsとHKStatisticsCollectionというクラスが統計処理に関係するものです、
それぞれ取得するのにHKStatisticsQueryとHKStatisticsCollectionQueryというのものを使います。 

以下の記事を先に読んでおくと理解しやすいでしょう。

このHKStatisticsとHKStatisticsQueryの対応関係は、前回の読み込みでやった
HKSampleとHKSampleQueryと全く同じです。 

HKQuantitySample

HKSampleQueryを叩いて、HKSampleの配列が結果として取得できるのと同じです。

 

ただし、SampleQueryと少し違い、HKStatisticsQueryの結果返ってくるのはひとつのHKStatisticsオブジェクトです。

HKStatisticsQueryを見てみるとコールバックのresultが配列じゃない事が分かります。

@interface HKStatisticsQuery : HKQuery

- (instancetype)initWithQuantityType:(HKQuantityType *)quantityType
             quantitySamplePredicate:(NSPredicate *)quantitySamplePredicate
                             options:(HKStatisticsOptions)options
                   completionHandler:(void(^)(HKStatisticsQuery *query, HKStatistics *result, NSError *error))handler;

@end

そのため、図にすると以下のような形ですね。

 

HKStatisticsQuery

HKStatisticsには最小値や最大値や平均値といった統計データが含まれているので、配列である必要がないのは当然といえば当然ですね。

HKStatisticsCollection

HKStatisticsについてはHealthKit 入門 2 – I’m Sei.を読むと大体使い方はわかると思います。
また、同時に平均値などは自分でHKSampleQueryを叩いた結果を使えば求められるし、そこまでメリットが見えないかもしれません。

HKStatisticsを活用するためにあるのがHKStatisticsCollectionです。

HKStatisticsCollectionは名前の通り、HKStatisticsを集めたものです(配列もあるが、グルーピングするというのが近い)

まずは実際にHKStatisticsCollectionを取得してみましょう。

サンプルはazu/StatisticsHealthKitにおいてあります。

azu/StatisticsHealthKit 

このサンプルではランダムな気温を追加して、それを折れ線グラフで表示する機能が入っています。
このグラフ作成にHKStatisticsCollectionが役に立つと思います。 

// return HKStatisticsCollection
- (PMKPromise *)collection:(HKQuantityType *) quantityType predicate:(NSPredicate *) predicate options:(HKStatisticsOptions) options anchorDate:(NSDate *) date components:(NSDateComponents *) components {
    return [PMKPromise new:^(PMKPromiseFulfiller fulfiller, PMKPromiseRejecter rejecter) {
        HKStatisticsCollectionQuery *collectionQuery = [[HKStatisticsCollectionQuery alloc] initWithQuantityType:quantityType quantitySamplePredicate:predicate options:options anchorDate:date intervalComponents:components];
        collectionQuery.initialResultsHandler = ^(HKStatisticsCollectionQuery *query, HKStatisticsCollection *result, NSError *error) {
            if (!error) {
                fulfiller(result);
            } else {
                rejecter(error);
            }
        };
        [self.healthStore executeQuery:collectionQuery];
    }];
}

HKStatisticsCollectionはstartとendの日付を指定して、その間をどういう間隔で区切っていくかをNSDateComponentで指定します。(HKStatisticsOptionsはどの統計データを取得するのかのオプション) 

NSDateComponentでの指定は

が参考になります。

以下の例だとday = 1のNSDateComponentを作って渡しているので、1日間隔で統計処理されたものを取得できます。
(hour = 8なら8時間ごとというように、 NSDateComponentが示す時間ずつずらして行く感じ)

    NSDateComponents *dayComponents = [[NSDateComponents alloc] init];
    dayComponents.day = 1;
    NSDate *startOfMonth = [[NSDate date] dateAtStartOfMonth];
    NSDate *endOfMonth = [[NSDate date] dateAtEndOfMonth];
    PMKPromise *promise = [self.storeManager collection:self.managedType predicate:nil options:HKStatisticsOptionDiscreteAverage anchorDate:startOfMonth components:dayComponents];
    return promise.then(^(HKStatisticsCollection *collection) {
        NSMutableArray *graphDataListMutable = [NSMutableArray array];
        // first day -- last day
        [collection enumerateStatisticsFromDate:startOfMonth toDate:endOfMonth withBlock:^(HKStatistics *result, BOOL *stop) {
            [graphDataListMutable addObject:result];
        }];
        self.graphDataList = graphDataListMutable;
        return graphDataListMutable;
    });

このデータ集計の間隔を決められるというのがこのクラスの大きな点です。

HKStatisticsCollection

Introducing HealthKit から引用

このようにして作成したHKStatisticsCollectionQueryを使ってHKStatisticsCollectionを取得することが出来ます。

 

HKStatisticsCollectionQuery

HKStatisticsCollectionは単純にHKStatisticsの配列を持つ一方、
もう一つHKStatisticsを扱う方法を持っています。

それが上記の例でも使っているenumerateStatisticsFromDateによる間隔ごとの列挙です。

- (void)enumerateStatisticsFromDate:(NSDate *)startDate toDate:(NSDate *)endDate withBlock:(void(^)(HKStatistics *result, BOOL *stop))block;

この列挙の大きな特徴として、データがないグループも列挙されるという点があります。 
(- (NSArray *)statistics; でHKStatisticsを取得する場合はデータがあるグループのみを取得できます。)

例えば1日間隔でデータ集計していった時に、データそのものがない日も存在すると思いますが、
列挙した場合はその日もデータがないHKStatisticsとして取得できます。 

これがサンプルのazu/StatisticsHealthKitでも使っているグラフを1日毎に描画していく時に凄く便利な仕組みとなっています。

StatisticsHealthKit

通常のCoreDataなどから取得したものから1日ごとに区切った表示をする場合、データがない日の扱いにすごく困ると思います。

そのため、手動で1日ごとの区切りのデータを作りなおしたり、何らかの工夫をいれて1日ごとの区切りに見せる必要が出てきます。

しかし、HKStatisticsCollectionは単純に1日ごとの区切りをデータを列挙することができるので、この表示用のデータを作るという仕組みが省けます。

これは工夫すれば曜日別のデータ表示などにも活用できると思います。

これで、かなり大雑把ですがHKStatisticsCollectionについての説明は終わりです。

おわりに

HealthKitは多くのクラスがあって、どこから見ていくのかが難しい感じがしますが、
基本的に種類毎のQueryやクラスがあって、取得する際にやっていることは大体同じです。

APIの設計的にも結構良くできているなーという感じがするので、データを集計して処理するような仕組みを作る場合にも参考になることが多い気がします。

また、β4でHKWorkoutというトラッキング用のクラスなどが一気に増えたり追加が多いフレームワークな気がします。
(おそらく「iOS 8 beta3」からHealthアプリがM7モーションコプロセッサによるトラッキングに対応 | Linkmanの実装関係)


明日、2014年7月25日(金)に第1回 Tech-Gym byプラスアール @SMS 【iOS勉強会、開発者向け】 : ATNDというイベントで、Healthkitについて喋ります。

詳細はTech-GymというiOS勉強会を開催しますの方を見て下さい。