AppStoreのレビューページのリンク

idの後ろの番号はアプリのidです。AppStoreの該当アプリのリンクを見れば確認できます。

旧(iOS10まで)
@”itms-apps://itunes.apple.com/WebObjects/MZStore.woa/wa/viewContentsUserReviews?type=Purple+Software&id=784484932″;

新(iOS11から)
@”https://itunes.apple.com/jp/app/id784484932?mt=8&action=write-review”

iOS←→watchOSの通信(watchOS2以降)

iOSとwatchOSで通信します。
watchOS2以降はwatchkit ExtensionはwatchOS側で動作します。
そのため、watchkitExtension内でiOS側の機能は使えません。iOS側で処理をやってwatchOSに送信(またはその逆)というスタイルになります。

まずは準備。

バックグラウンド処理の戻りはデリゲートメソッドに返ってきます。そのため、AppDelegateやExtensionDelegateにデリゲートを設定するのがよいです。

【AppDelegate.h、ExtensionDelegate.h】
#import <WatchConnectivity/WatchConnectivity.h>
@interface ExtensionDelegate : NSObject <WKExtensionDelegate,WCSessionDelegate>

【AppDelegate.m、ExtensionDelegate.m】
・applicationDidFinishLaunching内に以下のWCSessionの初期化処理を入れます。
    if([WCSession isSupported]){
        WCSession *session=[WCSession defaultSession];
        session.delegate=self;
        [session activateSession];
    }


★リアルタイム通信
・sendMessage、sendMessageDataメソッド
・即実行される
・replyHandlerに戻りが返ってくる
・リアルタイム通信は送信前に[[WCSession defaultSession] isReachable]を使って、通信可能かどうかを確認する。
・sendMessageはNSDictionary(文字列だけ)、sendMessageDataはNSDataで送信する。

【例:sendMessageDataメソッドを使う】
    if([[WCSession defaultSession] isReachable]){
        NSData*sendData=[NSKeyedArchiver archivedDataWithRootObject:@{@”name”:@”value”}];
        [[WCSession defaultSession] sendMessageData:sendData replyHandler:^(NSData * _Nonnull replyMessageData) {
       //ここで処理する
        NSLog(@”%@”,[reply objectForKey:@”name”]);
        } errorHandler:^(NSError * _Nonnull error) {
       //こっちでエラー処理
        }];
    }


★バックグラウンド通信
・applicationContext、transferUserInfo、transferFileメソッド。
・即実行はされない。
・一旦キューに入って、OSが勝手に(都合いい時に)通信する
・戻りはデリゲートメソッドに返ってくる。そのため、AppDelegateやExtensionDelegateにデリゲートを設定するのがよい。

バックグラウンド処理の送信
【例:transferFileメソッドを使う】
NSStirng*filePath;
//filePathを設定しておく
[[WCSession defaultSession] transferFile:[NSURL fileURLWithPath:filePath] metadata:@{@”name”:@”value”}];

バックグラウンド処理の受信
//applicationContextの受信
– (void)session:(nonnull WCSession *)session didReceiveApplicationContext:(nonnull NSDictionary<NSString *,id> *)applicationContext{
    dispatch_async(dispatch_get_main_queue(), ^{
        //ここで処理する
    });
}

//transferFileの受信
-(void)session:(WCSession *)session didReceiveFile:(WCSessionFile *)file{
    dispatch_async(dispatch_get_main_queue(), ^{
       //ここで処理する
    });
}

//transferUserInfoの受信
– (void)session:(nonnull WCSession *)session didReceiveUserInfo:(nonnull NSDictionary<NSString *,id> *)userInfo
{
    dispatch_async(dispatch_get_main_queue(), ^{
       //ここで処理する
     });
}

現在一番上にあるViewControllerを取得する。

現在一番上にあるViewControllerを取得します。
iOS9で非推奨になった「UIAlertView」を「UIAlertController」に切り替える時に使いました。

UIViewController *vCon = [UIApplication sharedApplication].keyWindow.rootViewController;

while (vCon.presentedViewController) {
      vCon = vCon.presentedViewController;
 }

iOS9以降でhttp通信をする

iOS9からはhttp通信が拒否されるようになりました。

自前のサーバーに接続するならhttps通信に切り替えれば良いのですが、外部のサイトにアクセスする場合はどうしてもhttp通信が必要になる場面があります。

その場合は、例外としてhttp通信してもよいサイトを登録します。

1.hogehoge.plist内にApp Transport Security Settings:Dictionaryを作る
2.App Transport Security Settings内にException Domains:Dictonaryを作る
3.Exception Domains内にwww.hoge.jp(許可するサイト):Booleanを作り、YESにする

アクセスするサイト分だけこれを登録しておきます。

その部分をテキストエディタで見るとこうなります。

<key>NSAppTransportSecurity</key>
 <dict>
 <key>NSExceptionDomains</key>
  <dict>
  <key>www.hoge.jp</key>
  <dict>
   <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
   <true/>
  </dict>
 </dict>
</dict>

Xcode8でコンパイルしたバイナリについて

フォトライブラリー・ブルートゥース・カレンダー・カメラへアクセスする場合は以下のものがinfo.plist内に必要。

あらかじめ許可が取れてる場合(旧バージョンで許可をとってる場合)はXcodeから直接実機にインストールすれば動くけど、アップロードした時にInvalid binaryで弾かれる。

NSPhotoLibraryUsageDescription
NSBluetoothPeripheralUsageDescription
NSCalendarsUsageDescription
NSContactsUsageDescription
NSCameraUsageDescription

使う時に(ロケーションのように)許可を取りに行く必要はないみたい。

NSContactsUsageDescriptionなしで連絡先に、NSCalendarsUsageDescriptionなしでカレンダーにアクセスするXcodeから直接起動した場合でもアプリが落ちる。

iOSのアプリ内のファイル保存のURLについて

アプリ内では以下の様にしてDocumentsディレクトリを取得する。

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];

具体的には
/var/mobile/Containers/Data/Application/B9B66EA6-051F-450C-81BC-BDA96675290E/Documents/photos/20160901_221957.mov

などとなるのだけれど、B9B66EA6…の部分は起動毎に変わる。
なので、絶対パスを保持して使おうとすると失敗する。

UIActivityControllerをiPadで使う場合

全体の流れ
1.通常通りUIActivityViewControllerをセットする
2.iPadの場合はsourceViewと表示する位置(barButtonItemやsourceRect)をセットする
3.表示する

1.
NSString *text = @”これを送信します”;
NSArray* actItems = [NSArray arrayWithObjects:text, nil];
self.actCon=[[UIActivityViewController alloc]initWithActivityItems:actItems applicationActivities:nil];

2.
self.actCon.popoverPresentationController.sourceView = self.view;
self.actCon.popoverPresentationController.barButtonItem = self.navigationItem.rightBarButtonItem;

3.
[self presentViewController:self.actCon animated:YES completion:^{    
    }];


これはUIAlertControllerをiPadで使う場合も同じ。

PDFを作る/印刷する

全体の流れ
1.PDFデータを格納する変数またはファイルを用意する
2.ページのスタート
3.内容を書く
4.プリンタに送る

※今回はPDFをデータにしたけど、ファイルにして、それをUIDocumentInteractionControllerに送っても良い。

//PDFデータを格納する変数またはファイルを用意する(今回はデータ)
NSMutableData*pdfData=[[NSMutableData alloc]initWithCapacity:0];
UIGraphicsBeginPDFContextToData(pdfData, CGRectZero, nil);

//1ページ目スタート(A4サイズ)
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0,0,612,792),nil);

//文字列書き込み
[@”書き込む文字列” drawInRect:CGRectMake(0,0,100,30)];
//イメージ書き込み
[[UIImage imageNamed:@”書き込むイメージファイル”] drawInRect:CGRectMake(0,0,100,100)];

//書き込み終了
UIGraphicsEndPDFContext();

//プリンタに送る
UIPrintInfo*printInfo=[UIPrintInfo printInfo];
printInfo.jobName=@”printjob”;
printInfo.orientation=UIPrintInfoOrientationPortrait;
printInfo.duplex=UIPrintInfoDuplexLongEdge;
printInfo.outputType=UIPrintInfoOutputGeneral;
UIPrintInteractionController*pCon=[UIPrintInteractionController sharedPrintController];
pCon.printInfo=printInfo;
pCon.printingItem=pdfData;
pCon.showsPageRange=NO;
pCon.delegate=self;

void (^completionHandler)(UIPrintInteractionController *, BOOL, NSError *) = ^(UIPrintInteractionController *printDocController, BOOL completed, NSError *error) {
        NSLog(@”%d”,completed);
        if (!completed && error) {
            NSLog(@”PrintError %@”, error);
        }
};
[pCon presentAnimated:YES completionHandler:completionHandler];

//UIPrintInteractionControllerのdelegate
– (UIViewController *)printInteractionControllerParentViewController:(UIPrintInteractionController *)printInteractionController{
    //UIPrintInteractionControllerの親になる(下になる)ViewControllerを返す
    return self;
}

CloudKitデータベースから通知を発行する/受け取る

全体の流れ
1.CloudKitDatabaseから通知がいくようにセットする
2.リモート通知が受け取れるようにセットする

//CloudKitデータベースから通知がいくようにセットする。どこで実行してもOK。
NSPredicate*predicate=[NSPredicate predicateWithFormat:@”TRUEPREDICATE”];
CKSubscription*subscription=[[CKSubscription alloc]initWithRecordType:@”neko” predicate:predicate options:CKSubscriptionOptionsFiresOnRecordCreation|CKSubscriptionOptionsFiresOnRecordDeletion];
subscription.notificationInfo=[[CKNotificationInfo alloc]init];
subscription.notificationInfo.alertBody=@”データベースからの通知だよ”;
subscription.notificationInfo.shouldBadge=YES;
subscription.notificationInfo.shouldSendContentAvailable=YES;
[savedSubscriptions addObject:subscription];

//リモートの通知が受け取れるようにセットする

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    UIUserNotificationSettings*settings=[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil];
    [application registerForRemoteNotifications];
    [application registerUserNotificationSettings:settings];
    [application registerForRemoteNotifications];
    …
    return YES;
}

-(void)application:(UIApplication *)application didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo fetchCompletionHandler:(nonnull void (^)(UIBackgroundFetchResult))completionHandler{
    CKNotification*notif=[CKNotification notificationFromRemoteNotificationDictionary:userInfo];
    if(notif.notificationType==CKNotificationTypeQuery){
        //ここでキャッチした時の処理を書く
    }
    completionHandler(UIBackgroundFetchResultNoData);
}

訂正:
notif.notificationType==CKNotificationTypeQueryって書くと、アクティブ・バックグラウンドの時は実行されるが、アプリ未起動時は実行されない。返ってきてる値が違うのかもしれない。要確認。
とりあえずif文を外して書いた。

CKRecordをNSUserDefaultsやファイルでキャッシュする場合

全体の流れ
1.データを格納しておくカスタムクラスを作る(NSObject<NSCoding>)
2.CKAssetはそのまま保存できないのでNSDataで保存できるようにする
2.NSCodingに必要な物をメソッドを実装する
4. 必要なデータを格納しファイルに保存したりする

//データを格納しておくカスタムクラスを作る
【DataClass.h】
#import <Foundation/Foundation.h>
@import CloudKit;
@interface DataClass : NSObject<NSCoding>{
    CKRecord*record;
    NSData*photoData;
}
@property(readwrite)CKRecord*record;
@property(readwrite)NSData*photoData;
@end

【DataClass.m】
#import “DataClass.h”
@implementation DataClass
@synthesize record;
@synthesize photoData;
-(id)initWithCoder:(NSCoder *)aDecoder{
    self.record=[aDecoder decodeObjectForKey:@”record”];
    self.photoData=[aDecoder decodeObjectForKey:@”photoData”];
    return self;
}

-(void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeObject:self.record forKey:@”record”];
    [aCoder encodeObject:self.photoData forKey:@”photoData”];
}
@end

//データを格納するクラスを準備する
DataClass*dataClass=[[DataClass alloc]init];
NSMutableArray*dataArray=[[NSMutableArray alloc]initWithCapacity:0];

//データを格納する
dataClass.record=record;
CKAsset*asset=[record valueForKey:@”photoAsset”];
dataClass.photoData=[[NSData alloc]  initWithContentsOfFile:asset.fileURL.path];
[dataArray addObject:dataClass];

//今回は日付のファイル名で保存するのでその準備
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setLocale:[NSLocale currentLocale]];
[formatter setDateFormat:@”yyyyMMdd”];

//ファイルの作成準備
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString*fileName=[[paths objectAtIndex:0] stringByAppendingFormat:@”/cacheData/%@.dat”,[formatter stringFromDate:selectedDate]];

//ファイルに保存する
[NSKeyedArchiver archiveRootObject:dataArray toFile:fileName];

//ファイルから復元する場合
[dataArray addObjectsFromArray:[NSKeyedUnarchiver unarchiveObjectWithFile:fileName]];