2015年06月17日

テストフレームワークXCTest

iOS担当のエモトです.私事ではありますが,ラブライブ関係者様,劇場版の興行収益ランキング1位おめでとうございます.私も微力ながら貢献させていただきました.

さて先日のWWDCにて, Xcodeのアップデートが発表されました.ネイティヴアプリに変わったwatchOS2やオープンソースになるSwift2.0などと比べると話題性はあまり大きくないですが,他の新機能にはなかなか興味深いものがあり,Xcode7の正式リリースが楽しみです.

私が気になるものの1つは,User Interface Testing / Recording です.XcodeはテストフレームワークXCTestを用いたテストが可能ですが,UIテストは非対応でした.UIテスト用のサードライブラリはこれまでもありましたが,近年iOS端末の解像感が多様化してきたので使用を控えていました.標準でサポートされるのであれば,是非使っていきたいですね.

そのUI Testing / Recordingの紹介といきたいですが,Xcode7はいまbeta(2015/06/17 現在)ということもあり控えるとして,今回はテストフレームワークXCTestの簡単な使い方です(UI Testing / Recordingは一般公開されてからですね).

XCTestを使ってみる

例として,二つの実数を加算するメソッドをもつクラス(Calculator)を作成して,このメソッドをテストしてみます.実際は単純な足し算ですが,あとで非同期でのテストを試すため,わざと遅延処理をいれた完了ブロックを追加しています.

Calculator.h
#import <Foundation/Foundation.h>

extern NSString *const kNotificationCalculatorFinished;
extern NSString *const kNotificationCalculatorFailed;
typedef void (^CalculatorBlock) (float answer);

@interface Calculator : NSObject

@property (nonatomic) float answer;
@property (nonatomic, retain) NSString *answerString;

-(float)add:(float)v1
         to:(float)v2
 completion:(CalculatorBlock)completion;

@end
Calculator.m
#import "Calculator.h"

NSString *const kNotificationCalculatorFinished = @"kNotificationCalculatorFinished";
NSString *const kNotificationCalculatorFailed = @"kNotificationCalculatorFailed";
NSTimeInterval const kDelayInterval = 0.5;

@implementation Calculator

-(float)add:(float)v1 to:(float)v2 completion:(CalculatorBlock)completion
{
    self.answer = v1+v2;
    self.answerString = [NSString stringWithFormat:@"%@", @(self.answer)];
    
    if (completion) {
        dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW,
                                             kDelayInterval * (NSTimeInterval)NSEC_PER_SEC);
        dispatch_after(when, dispatch_get_main_queue(), ^{
            completion(self.answer);
        });
    }
    
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc postNotificationName:kNotificationCalculatorFinished
                      object:nil];
    
    return self.answer;
}
@end

テストをしてみた

プロジェクト作成時に特に設定を変更していなければ「“プロジェクト名”Tests」というフォルダーがプロジェクトに作成されていると思います(ない場合はターゲット追加からテストを追加してください).その中でmファイルにテストコードを書いていきます.例として,加算メソッドのテストを書いてみます.
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "Calculator.h"

@interface SampleTestAppTests : XCTestCase
{
    Calculator *calculator_;
}
@end

@implementation SampleTestAppTests

- (void)setUp {
    [super setUp];
    calculator_ = [[Calculator alloc] init];
}

- (void)tearDown {
    [super tearDown];
}

- (void)testAdd
{
    float v1 = 1;
    float v2 = 2;
    float v3 = [calculator_ add:v1 to:v2 completion:nil];
    XCTAssertEqual(v3, 3);
}
@end
ここで決まりとして,テストコードは頭文字に必ずtestをつけます.この頭文字がないとテストコードと認識してくれません.テストコードを書き終えたら,左側のツリー状からコードを選択するか,ソース部の行番号の左あたりをクリックしてテストを実行します.なにも問題がなければテストは成功です.

xctest_run2.png

非同期でもテストがしたい

テストは非同期にも対応しています.幾つかの方法があり,以下のようにしてテストコードを書くことができます.
- (void)testAddAsync
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"add async"];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:^(float answer) {
        XCTAssertEqual(answer, 3);
        [expectation fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}

- (void)testAddNotification
{
    XCTestExpectation *exp = [self expectationForNotification:kNotificationCalculatorFinished
                                                       object:nil
                                                      handler:^BOOL(NSNotification * __nonnull notification)
                              {
                                  NSLog(@"%s, notification %@", __func__, notification);
                                  [exp fulfill];
                                  return true;
                              }];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:nil];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}

- (void)testAddKVO1
{
    [self keyValueObservingExpectationForObject:calculator_
                                        keyPath:@"answerString"
                                  expectedValue:@"3"];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:nil];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}

- (void)testAddKVO2
{
    [self keyValueObservingExpectationForObject:calculator_
                                        keyPath:@"answerString"
                                        handler:^BOOL(id  __nonnull observedObject, NSDictionary * __nonnull change)
     {
         NSLog(@"%s, observedObject %@, change %@", __func__, observedObject, change);
         
         NSString *correct = @"3";
         
         //        Calculator *tempCalc = (Calculator*)observedObject;
         NSString *newStr = change[@"new"];
         return [newStr isEqualToString:correct];
     }];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:nil];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}


XCTestに関して簡単に紹介しました.より詳細な情報はアップルが公式ドキュメントを公開しているので,それを参照してもらえると幸いです.
posted by Seesaa京都スタッフ at 17:16| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする

2015年06月16日

Apple Watch対応のアプリを作ってみました

iOS担当の田中です。
Apple Watchが発売され結構な日が経ちましたが、開発するにあたりどんな感じか確かめるために簡単なアプリを開発してみました。

機能


・地図の表示
・コメント入力
・情報保存

Apple Watch側はこの3機能のみです。
iPhone側は保存データをリストに表示したり編集したりと実装してみましたが、今回はApple Watch側の部分にのみ触れていきたいと思います。

動作


地図表示


アプリ起動時は現在地を表示しています。

map1.png

地図を表示するまでのコードはこんな感じになっています。
#import <CoreLocation/CoreLocation.h>

@interface InterfaceController()
<
  CLLocationManagerDelegate
>

@property (weak, nonatomic) IBOutlet WKInterfaceMap *map;
@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic) CLLocationManager *locationManager;

@end

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];
    
    self.locationManager = [CLLocationManager new];
    self.locationManager.delegate = self;

    [self.locationManager startUpdatingLocation];
}

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
    CLLocation *nowLocation = locations.lastObject;
    double lat = nowLocation.coordinate.latitude;
    double lng = nowLocation.coordinate.longitude;
    
    MKCoordinateSpan currentSpan = MKCoordinateSpanMake(0.01f, 0.01f);
    CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(lat, lng);
    MKCoordinateRegion region = MKCoordinateRegionMake(coordinate, currentSpan);
    
    self.coordinate = coordinate;
    
    MKMapPoint newCenterPoint = MKMapPointForCoordinate(coordinate);
    [self.map setVisibleMapRect:MKMapRectMake(newCenterPoint.x, newCenterPoint.y, currentSpan.latitudeDelta, currentSpan.longitudeDelta)];
    [self.map setRegion:region];

    [self.map addAnnotation:coordinate
               withPinColor:WKInterfaceMapPinColorPurple];
}

iPhone側で位置情報取得の許可を取っていない場合は位置情報が取得できないため、先に確認処理を行う必要があります。

文字入力


位置情報の保存に合わせてコメントを残せるようにしました。
Apple Watchではキーボードが使えないため、自前で作成するか絵文字か音声で文字を入力することになります。

map2.png map3.png

絵文字や音声入力のviewの表示は以下のコードで使用することができます。

[self presentTextInputControllerWithSuggestions:nil
                               allowedInputMode:WKTextInputModeAllowAnimatedEmoji
                                     completion:
 ^(NSArray *results) {
    if (results.count) {
        id result = results.firstObject;
    }
 }];
 

入力後はresultsの中に入力結果が入っていますので取得後適切な形で処理をします。

保存


最後に取得したデータをiPhone側に保存します。

[WKInterfaceController openParentApplication:dict
                                       reply:
 ^(NSDictionary *replyInfo, NSError *error) {
 }];

このメソッドを使用することによりiPhone側にデータが送られます。
dictの部分に保存したいデータをDictionaryにしています。

あとはiPhone側のAppDelegateに以下のメソッドを追加しデータを取得し保存処理を行います。

- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply
{
}

以上の流れになります。

今の仕様だと大分iPhone側の処理に依存してしまい制限が大きいですが、Watch OS 2.0からネイティブの開発が可能になるはずなので自由度は大きく広がりそうです。
posted by Seesaa京都スタッフ at 18:02| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする

2015年06月08日

もうWebView実装は不要!?「Chrome Custom Tabs」を一足先に試してみた

はじめに

渋谷オフィスから 今中 がお届けします。
ご無沙汰しております。

Google I/O 2015が開催されました。
私は六本木ヒルズ近くの映画館でサテライト上映されていたものを鑑賞するイベントに参加して、キーノートを拝見しておりました。
映画館のシアターをひとつ、一晩借り切るという非日常が楽しかったです。

さて、そんなGoogle I/Oで発表された話題から、「Chrome Custom Tabs」についてお届けしたいと思います。

続きを読む
posted by Seesaa京都スタッフ at 21:10| Comment(0) | Android | このブログの読者になる | 更新情報をチェックする