iOSアプリケーションを開発しているマシンを変える

毎回はまるので書いておく。

■キーチェーンアクセス

「アプリケーション > ユーティリティ > キーチェーンアクセス > 証明書アシスタント > 認証局」で証明書を要求する。入力内容は以下のとおり。

  • メールアドレス
  • 通称

「ディスクに保存をする」を選択しデスクトップに保存する。

■Provisioning Portal

Provisioning Portal > Certificates」を選択し、フォームから保存した証明書をアップロードする。

  1. Distribution Provisioning Profilesを作りなおす
  2. 作りなおしたProvisioning Profilesをダウンロードしダブルクリックでインストールする
  3. XCodeの「Build Setting > Code Signing > Release」で新しいProvisioning Profileを選択する

Wildcard Appとプロビジョニングプロファイル(おまけ)

Provisioning Portal >
App IDs > create
」で以下のように入力するとwild card appが作れる。

description Wildcard App
Bundle Identifier *

Provisioning Portal > Provisioning > Development > NEW Profile」で以下のように入力する。

Profile Name Team Provisioning Profile
App ID Wildcard App

上述により全てのアプリの開発で有効なprovisioning profileが作成できるので、1アプリごとに作る手間が省ける。

androidアプリは(パスワードと)keystoreがあればマシンを変えてもそのまま開発できる。iOSアプリは開発マシンの認証(のようなものが)ある。iPhoneは手間がかかるが管理画面に入れれば新しいマシンで開発を進められる。androidはkeystoreが無くなるとアプリの更新ができなくなる。

UITableViewCellの中にUIButtonを配置する

btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
[btn setTag:BTN];
[btn setFrame:CGRectMake(200, 5, 50, 50)];
[btn setTitle:@"t" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(onCellButtonPushed:event:) forControlEvents:UIControlEventTouchUpInside];
[cell.contentView addSubview:btn];

以下のようにしてどのcellのボタンが押されたか判定する。

- (void)onCellButtonPushed:(UIButton *)button event:(id)event {
    NSSet *touches = [event allTouches];
    UITouch *touch = [touches anyObject];
    CGPoint currentTouchPosition = [touch locationInView:self.tableView];
    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];
    if (indexPath != nil) {
    }
}

UINavigationControllerを使う

新しい事をやりすぎて古いことを忘れそうなのでメモしておく。

■実装

早速実装する。

AppDelegate.h

#import <UIKit/UIKit.h>
#import "MainViewController.h"

@class MainViewController;

@interface SampleNavigationAppDelegate : NSObject <UIApplicationDelegate> {
@private
    UINavigationController *nav;
    MainViewController *mainViewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) UINavigationController *nav;
@property (nonatomic, retain) MainViewController *mainViewController;
@end

AppDelegate.m

#import "SampleNavigationAppDelegate.h"

@implementation SampleNavigationAppDelegate
@synthesize nav;
@synthesize mainViewController;

@synthesize window=_window;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Override point for customization after application launch.
    mainViewController = [[MainViewController alloc] init];
    nav = [[UINavigationController alloc] initWithRootViewController:mainViewController];
    nav.view.frame = [UIScreen mainScreen].applicationFrame;
    [self.window addSubview:nav.view];
    [self.window makeKeyAndVisible];
    return YES;
}

MainViewController.h

#import <UIKit/UIKit.h>
#import "SecondViewController.h"

@class SecondViewController;

@interface MainViewController : UIViewController {
    SecondViewController *secondViewController;
}
@property (nonatomic, retain) SecondViewController *secondViewController;
@end

MainViewController.m

@implementation MainViewController
@synthesize secondViewController;

-(void)onclick:(UIButton*)sender{
    secondViewController = [[SecondViewController alloc] init];
    [self.navigationController pushViewController:secondViewController animated:YES];
    
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self.view setFrame:CGRectMake(0, 0, 320, 480)];
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
    [label setText:@"first view"];
    [self.view addSubview:label];
    [label release];
    
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [btn setFrame:CGRectMake(250, 0, 50, 30)];
    [btn addTarget:self action:@selector(onclick:) forControlEvents:UIControlEventTouchDown];
    [self.view addSubview:btn];
}

画面は凄い適当だけど・・・

Push Notificationを使ってみる

■プロビジョニングポータル

App IDs > Configure > Enable for Apple Push Notification serviceにチェックを入れ、「Development Push SSL Certificate」のConfigureをクリックする。

キーチェーンアクセスを起動させ、キーチェーンアクセス > 証明書アシスタント > 認証局に証明書を要求

以下の項目を入力する。

  1. ユーザーのメールアドレス
  2. 通称(コモンネーム)

ディスクに保存を選択するとCertificateSigningRequest.certSigningRequestというファイルが保存される。「続ける」を押す。

continueを押し保存したファイルをアップロードすると、「Development Push SSL Certificate」のStatusがEnableになるので、Downloadしてダブルクリックしキーチェーンアクセスに保存する。

キーチェーンアクセス > 自分の証明書

  1. Apple Development Push Servicesの証明書を選択し、ファイルから書き出しを実行してcert.p12で保存
  2. Apple Development Push Servicesの秘密鍵を選択し、ファイルから書き出しを実行してkey.p12で保存

保存したディレクトリで以下のコマンドを実行する。

openssl pkcs12 -clcerts -nokeys -out cert.pem -in cert.p12
openssl pkcs12 -nocerts -out key.pem -in key.p12
openssl rsa -in key.pem -out key-noenc.pem
cat cert.pem key-noenc.pem > dev.pem

ここで生成されたdev.pemはサーバー側で使用する。

■クライアント側の実装

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | 
                                                                           UIRemoteNotificationTypeBadge |
                                                                           UIRemoteNotificationTypeSound)];
    return YES;
}

以下のメソッドはdevice tokenを受け取った時に実行される。binaryを変換して(文字列を取り出して)サーバー側の仕様にそった形式にして送信する。

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
    NSString *str = [NSString stringWithFormat:@"%@",deviceToken];
    NSString *newString = [str stringByReplacingOccurrencesOfString:@" " withString:@""];
    newString = [newString stringByReplacingOccurrencesOfString:@"<" withString:@""];
    newString = [newString stringByReplacingOccurrencesOfString:@">" withString:@""];
    
    NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
    [ud setObject:newString forKey:KEY_DEVICE_TOKEN];
    [ud synchronize];
}
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {
    NSLog(@"error:%@", error);
}

リモート通知はシミュレータではサポートされていない。また、「エンタイトルメント文字列が見つかりません」と表示されたら、有効な開発用のプロビジョニングプロファイルがインストールできていないという事である。

以下のメソッドはpush notificationを受け取った時に実行される。

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
    UILocalNotification *notification = [[UILocalNotification alloc] init];
    [notification setFireDate:[NSDate date]];
    [notification setTimeZone:[NSTimeZone defaultTimeZone]];
    [notification setAlertBody:[[userInfo objectForKey:@"aps"] objectForKey:@"alert"]];
    [notification setUserInfo:userInfo];
    [[UIApplication sharedApplication] presentLocalNotificationNow:notification];
}

■サーバー側の実装

Socket通信ができれば言語は特に限定しないが今回はゆとり言語PHPとゆとりライブラリでやる。

cd vendors/
wget http://apns-php.googlecode.com/files/ApnsPHP-r100.zip
unzip ApnsPHP-r100.zip

Cakeとの親和性が悪いので以下のようにして読み込む。

App::import('Vendor', 'ApnsPHP/Log/Interface');
App::import('Vendor', 'ApnsPHP/Log/Embedded');
App::import('Vendor', 'ApnsPHP/Abstract');
App::import('Vendor', 'ApnsPHP/Exception');
App::import('Vendor', 'ApnsPHP/Push');
App::import('Vendor', 'ApnsPHP/Message');

$push = new ApnsPHP_Push(
    ApnsPHP_Abstract::ENVIRONMENT_SANDBOX,
    '/path-to-cert/dev.pem'
);
$push->connect();
$message = new ApnsPHP_Message('123456789db0b2478f123456789f8067e3c61234567891df1d04851234567898');//device token
$message->setText('こんにちはこんばんわ');
$message->setExpiry(30);
$push->add($message);
$push->send();
$push->disconnect();

参考

UITextViewをEditableにしてplaceholderを実装する

UITextFieldでは以下のようにすることでプレースホルダーを使用することができる。

UITextField *field = [[UITextField alloc] init];
[field setFrame:CGRectMake(10, 120, 190, 35)];
[field setPlaceholder:@"入力してください"];

複数行のテキスト入力欄が必要な場合はUITextViewをEditableにして対応するが、上述のような手法でプレースホルダーを用いることはできない。

■実装

UIPlaceHolderTextView.h

#import "UIPlaceHolderTextView.h"

@implementation UIPlaceHolderTextView
@synthesize placeholder;
@synthesize placeholderColor;
@synthesize placeholderLabel;

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [placeholderLabel release]; placeholderLabel = nil;
    [placeholderColor release]; placeholderColor = nil;
    [placeholder release]; placeholder = nil;
    [super dealloc];
}

- (void)awakeFromNib {
    [super awakeFromNib];
    [self setPlaceholder:@""];
    [self setPlaceholderColor:[UIColor lightGrayColor]];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
}

- (id)initWithFrame:(CGRect)frame {
    if((self = [super initWithFrame:frame])) {
        [self setPlaceholder:@""];
        [self setPlaceholderColor:[UIColor lightGrayColor]];
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textChanged:) name:UITextViewTextDidChangeNotification object:nil];
    }
    return self;
}

// 表示テキストに変更があった時
- (void)textChanged:(NSNotification *)notification {
    if([[self placeholder] length] == 0) {
        return;
    }
    
    if([[self text] length] == 0) {
        [[self viewWithTag:999] setAlpha:1];
    }
    else {
        [[self viewWithTag:999] setAlpha:0];
    }
}

- (void)setText:(NSString *)text {
    [super setText:text];
    [self textChanged:nil];
}

// drawRect時にplaceholderのUILabelを準備してViewに追加する
- (void)drawRect:(CGRect)rect {
    if([[self placeholder] length] > 0) {
        if (placeholderLabel == nil) {
            placeholderLabel = [[UILabel alloc] initWithFrame:CGRectMake(8,8,self.bounds.size.width - 16,0)];
            placeholderLabel.lineBreakMode = UILineBreakModeWordWrap;
            placeholderLabel.numberOfLines = 0;
            placeholderLabel.font = self.font;
            placeholderLabel.backgroundColor = [UIColor clearColor];
            placeholderLabel.textColor = self.placeholderColor;
            placeholderLabel.alpha = 0;
            placeholderLabel.tag = 999;
            [self addSubview:placeholderLabel];
        }
        
        placeholderLabel.text = self.placeholder;
        [placeholderLabel sizeToFit];
        [self sendSubviewToBack:placeholderLabel];
    }
    
    if([[self text] length] == 0 && [[self placeholder] length] > 0 ) {
        [[self viewWithTag:999] setAlpha:1];
    }
    
    [super drawRect:rect];
}
@end

クライアントコード

以下のようにすることで使用することができる。

UIPlaceHolderTextView *view = [[UIPlaceHolderTextView alloc] init];
[view setPlaceholder:@"(^o^)"];

■参考

In App Purchaseをつかってみる

■フレームワーク

StoreKitを追加する。

#import <StoreKit/StoreKit.h>

■実装

以下のようにしてSKProductsRequestDelegateとSKPaymentTransactionObserverを実装する。/p>

HogeViewController.h

@interface HogeViewController : UIViewController<SKProductsRequestDelegate, SKPaymentTransactionObserver> {
    bool isLoading;
    SKProductsRequest       *skProductsRequest;
    UIActivityIndicatorView *spinner;
    UIView                  *loaderBg;
    UILabel                 *loaderTitle;
}
@property bool isLoading;
- (IBAction)purchaseButtonPushed:(id)sender;
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response;
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransaction:(NSArray *)transactions;
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0);

HogeViewController.m

購入処理の開始
- (IBAction)purchaseButtonPushed:(id)sender {
    if([[UIDevice currentDevice] networkAvailable] == NO){
        return;
    }
    if(self.isLoading){
        return;
    }
    if([SKPaymentQueue canMakePayments]){
        self.isLoading = true;
        
        // loader
        loaderBg = [[UIView alloc] init];
        [loaderBg setFrame:CGRectMake(100, 150, 120, 85)];
        [loaderBg setBackgroundColor:[UIColor blackColor]];
        [loaderBg.layer setCornerRadius:13.0f];
        [loaderBg setAlpha:0.7];
        [self.view addSubview:loaderBg];
        
        loaderTitle = [[UILabel alloc] init];
        [loaderTitle setTextColor:[UIColor whiteColor]];
        [loaderTitle setFrame:CGRectMake(0, 57, 120, 18)];
        [loaderTitle setText:@"Loading..."];
        [loaderTitle setTextAlignment:UITextAlignmentCenter];
        [loaderTitle setBackgroundColor:[UIColor clearColor]];
        [loaderTitle setAlpha:1.0];
        [loaderBg addSubview:loaderTitle];
        
        // spinner
        spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
        [spinner setCenter:CGPointMake(self.view.frame.size.width/2.0, self.view.frame.size.height/2.0)];
        [self.view addSubview:spinner];
        [spinner startAnimating];
        [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
        
        // 課金部分
        // identiferを元にappleサーバに問い合わせます
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
        skProductsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:@"identifer"]];
        [skProductsRequest setDelegate:self];
        [skProductsRequest start];
    }
    else{// 本体の設定でアプリ内課金をOFFにしている人向けの表示
        [self showAlert:@"cannot purchase" text:@"設定 > 一般 > 機能制限で[App内での購入]をONにしてください"];
    }
}

上述のようにアプリ内課金は本体の設定で無効にできる事を考慮する。

appleのサーバにidentiferをお問い合わせした結果の処理

identiferをセットしてリクエストしたレスポンスに商品データがあれば、その商品の購入手続きに入る。

#pragma mark -
#pragma mark SKProductsRequestDelegate
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    if (response == nil) {
        return;
    }
    for(SKProduct *product in response.products){// productを元にした購入オブジェクトをキューに入れて購入手続きに
        SKPayment *payment = [SKPayment paymentWithProduct:product];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}

後述のオブザーバーの管理を忘れないようにすること。

トランザクションの状態に変更があった時の処理

以下のように状態はswitch文で分岐する。

#pragma mark -
#pragma mark SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransaction:(NSArray *)transactions {
    BOOL isFinished = YES;
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:// 何らかのOKを押す前の処理
                break;
            case SKPaymentTransactionStatePurchased:// success : 決済手続き完了処理
                [queue finishTransaction:transaction];
                
                // もし自社サーバでユーザが購入を完了したかどうかappleサーバに確認する場合は、
                // transaction.transactionReceiptの値をbase64に変換して自社サーバに送信
                // [transaction.transactionReceipt base64EncodedString];
                
                isFinished = NO;
                break;
            case SKPaymentTransactionStateFailed://  途中でキャンセルした時
                isFinished = NO;
                [queue finishTransaction:transaction];
                break;
            case SKPaymentTransactionStateRestored:// 通常はコールされない
                isFinished = NO;
                [queue finishTransaction:transaction];
                break;
            default:
                break;
        }
    }
    if (isFinished == NO) {// トランザクションが何らかの完了をした時=>ローディングを消す
        self.isLoading = false;
        [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
        [loaderTitle removeFromSuperview];
        [loaderBg removeFromSuperview];
        [spinner stopAnimating];
    }
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_3_0){
    [self paymentQueue:queue updatedTransaction:transactions];
}
オブザーバー

dealloc部分で課金処理キューからオブザーバーをしっかり削除するようにする。

- (void)dealloc{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];// これ!!!
    [loaderBg release];
    [loaderTitle release];
    [skProductsRequest release];
    [spinner release];
    [super dealloc];
}

サーバ側

以下のようにしてユーザから送信されてきたレシート情報を自社サーバ側でappleに確認する。

$ch  = curl_init();
$url = "https://sandbox.itunes.apple.com/verifyReceipt";// サンドボックス(テスト用)
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{"receipt-data" : "' . $receipt . '"}');
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
curl_setopt($ch, CURLOPT_HEADER, FALSE);
$result = curl_exec($ch);

■テスト

xcodeからインストールしたアプリではサンドボックス経由という事が購入ダイアログで表示されます。storeからインストールしたアプリでは本番と同じダイアログになります。特にコードを変更する必要はなく自動で判定してくれるようです。