210 Views
October 24, 14
スライド概要
http://connpass.com/event/8629/
2023年10月からSpeaker Deckに移行しました。最新情報はこちらをご覧ください。 https://speakerdeck.com/lycorptech_jp
既存アプリの iOS8対応 Mao Nishi
今日話すこと • 今回のiOS8対応範囲 • ヤフオク!アプリで起きた問題 • Extension Today対応 • 掛かった工数 • ユーザの反響
ヤフオク!アプリについて
ヤフオク!アプリについて • • iPhone版 • 2010年10月リリース(当時はiOS4.1) • コード上でUI部品を生成している箇所多数 iPad版 • 2013年12月リリース • xib、storyboardは当然活用
今回のiOS8対応範囲について
ヤフオク!アプリの iOS8対応の範囲 iOS8での正常動 作を目指す iPhone/iPad 対応済み! iOS8独自機能 iPhone6/iPhone6 (Extentionなどを Plus向けにレイア ウトする 搭載) 対応済み! これから
ヤフオク!アプリの iOS8対応の範囲 iOS8での正常動 作を目指す iPhone/iPad 対応済み! iOS8独自機能 iPhone6/iPhone6 (Extentionなどを Plus向けにレイア ウトする 搭載) 対応済み! これから
iOS8対応時に出会った事象・ 不具合等を紹介します
これからiOS8対応にあたられる 方の参考になればと思います
CASE 1 回転時にレイアウトが崩れる
とりあえずビルドして 動かしてみた
期待する動き
予期しない動き
回せば回すほど.. レイアウトが崩れていく事態に
原因 [[UIScreen mainScreen] applicationFrame];
原因 CGRect appFrame = [[UIScreen mainScreen] applicationFrame]; /* 以下はiOS8からは端末の向きによって返却される値が変わるようになった*/ CGFloat height = appFrame.size.height; CGFloat width = appFrame.size.width;
iOS7でのheightとwidth height width width height 長い方がheightという 前提でも成り立つ
iOS8でのheightとwidth height height width width 長い方がheightという 前提でコードを 書いてしまっていた
端末の向きにってheight、width に変化があるメソッド • • • [[UIScreen mainScreen] applicationFrame]; [[UIScreen mainScreen] bounds]; [[UIApplication sharedApplication] statusBarFrame]; これらを使っている箇所は見直しましょう
回転検知時に呼ばれる処理も変更 - (void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientatio n duration:(NSTimeInterval)duration;
回転検知時に呼ばれる処理も変更 - (void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientatio n duration:(NSTimeInterval)duration; - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coord inator; 回転検知ではなく、サイズが変更されたと考える
実際の対応内容 //iOS7以前の画面回転開始時の処理 - (void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation duration: (NSTimeInterval)duration { //端末の向き取得 BOOL isLandscape = UIInterfaceOrientationIsLandscape(toInterfaceOrientation); //以降width、heightを取得して回転後の座標位置変更処理を行う }
実際の対応内容
//iOS7以前の画面回転開始時の処理
- (void)willRotateToInterfaceOrientation:
(UIInterfaceOrientation)toInterfaceOrientation duration:
(NSTimeInterval)duration
{
//端末の向き取得
BOOL isLandscape =
UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
//以降width、heightを取得して回転後の座標位置変更処理を行う
}
!
//iOS8以降のサイズ変更時(回転時)の処理
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator
{
//端末の向き取得
BOOL isLandscape = (size.height <= size.width);
//以降width、heightを取得して回転後の座標位置変更処理を行う
}
実際の対応内容
//iOS7以前の画面回転開始時の処理
- (void)willRotateToInterfaceOrientation:
(UIInterfaceOrientation)toInterfaceOrientation duration:
(NSTimeInterval)duration
{
//端末の向き取得
BOOL isLandscape =
UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
//以降width、heightを取得して回転後の座標位置変更処理を行う
}
!
//iOS8以降のサイズ変更時(回転時)の処理
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator
{
//端末の向き取得
BOOL isLandscape = (size.height <= size.width);
//以降width、heightを取得して回転後の座標位置変更処理を行う
}
CASE 2 罫線の左が切れる
罫線の左側が切れる問題
iOS7対応の時に行った処理 [UITableViewCell appearance].separatorInset = UIEdgeInsetsZero;
iOS8の新しいプロパティlayoutMargins によりマージンが設定されている (lldb) p (UIEdgeInsets)[self.tableView layoutMargins] (UIEdgeInsets) $1 = (top = 0, left = 16, bottom = 0, right = 16)
コンテンツのマージン 設定をオフにする -(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.tableView.layoutMargins = UIEdgeInsetsZero; }
コンテンツのマージン 設定をオフにする -(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.tableView.layoutMargins = UIEdgeInsetsZero; }
コンテンツのマージン 設定をオフにする -(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; self.tableView.layoutMargins = UIEdgeInsetsZero; }
CASE 3 デバイストークンが 取得できない
デバイストークン取得処理変更 [[UIApplication sharedApplication] registerForRemoteNotificationTypes: (UIRemoteNotificationTypeBadge| UIRemoteNotificationTypeSound| UIRemoteNotificationTypeAlert)];
デバイストークン取得処理変更 [[UIApplication sharedApplication] registerForRemoteNotificationTypes: (UIRemoteNotificationTypeBadge| UIRemoteNotificationTypeSound| UIRemoteNotificationTypeAlert)];
デバイストークン取得処理変更 //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; ! UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; ! [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; ! //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications]; iOSバージョン毎に処理を分岐する必要がある
デバイストークン取得処理変更 //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; ! UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; ! [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; ! //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications];
デバイストークン取得処理変更 //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; ! UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; ! [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; ! //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications];
デバイストークン取得処理変更 //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; ! UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; ! [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; ! //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications];
デバイストークン取得処理変更 //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; ! UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; ! [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; ! //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications];
デバイストークン取得処理変更 //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; ! UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil]; ! [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; ! //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications]; iOSバージョン毎に処理を分岐する必要がある
OSバージョンで分岐させてます + (void)registerNotification { //iOS8とそれ以外で設定を変更する必要がある } if ([YJUtil isIOS8]){ [AucNotificationConfigure registerNotificationAfteriOS8]; } else { [AucNotificationConfigure registerNotificationBeforeiOS7]; }
InteractiveなPushにも 対応しています
iOS8以後の処理 + (void)registerNotificationAfteriOS8 { UIMutableUserNotificationAction *bidAction = [[UIMutableUserNotificationAction alloc] init]; bidAction.identifier = XXXXXXX; bidAction.title = @"入札する"; bidAction.activationMode = UIUserNotificationActivationModeForeground; bidAction.destructive = NO; bidAction.authenticationRequired = NO; UIMutableUserNotificationCategory *inviteCategory = [[UIMutableUserNotificationCategory alloc] init]; inviteCategory.identifier = XXXXXXX; [inviteCategory setActions:@[bidAction] forContext:UIUserNotificationActionContextMinimal]; //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; NSSet *categories = [NSSet setWithObject:inviteCategory]; UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:categories]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications]; }
iOS8以後の処理 + (void)registerNotificationAfteriOS8 { UIMutableUserNotificationAction *bidAction = [[UIMutableUserNotificationAction alloc] init]; bidAction.identifier = XXXXXXX; bidAction.title = @"入札する"; bidAction.activationMode = UIUserNotificationActivationModeForeground; bidAction.destructive = NO; bidAction.authenticationRequired = NO; UIMutableUserNotificationCategory *inviteCategory = [[UIMutableUserNotificationCategory alloc] init]; inviteCategory.identifier = XXXXXXX; [inviteCategory setActions:@[bidAction] forContext:UIUserNotificationActionContextMinimal]; //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; NSSet *categories = [NSSet setWithObject:inviteCategory]; UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:categories]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications]; }
iOS8以後の処理 + (void)registerNotificationAfteriOS8 { UIMutableUserNotificationAction *bidAction = [[UIMutableUserNotificationAction alloc] init]; bidAction.identifier = XXXXXXX; bidAction.title = @"入札する"; bidAction.activationMode = UIUserNotificationActivationModeForeground; bidAction.destructive = NO; bidAction.authenticationRequired = NO; UIMutableUserNotificationCategory *inviteCategory = [[UIMutableUserNotificationCategory alloc] init]; inviteCategory.identifier = XXXXXXX; [inviteCategory setActions:@[bidAction] forContext:UIUserNotificationActionContextMinimal]; //通知タイプの設定 UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeSound | UIUserNotificationTypeAlert; NSSet *categories = [NSSet setWithObject:inviteCategory]; UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:categories]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; //Push通知の利用許可をとる [[UIApplication sharedApplication] registerForRemoteNotifications]; }
CASE 4 iPadでカメラが反応しない
iPadでカメラ撮影する時 UIImagePickerController * picker = [[UIImagePickerController alloc] init]; picker.delegate = self; picker.sourceType = UIImagePickerControllerSourceTypeCamera; ! [self presentViewController:picker animated:YES completion:nil];
iPadでカメラ撮影する時 UIImagePickerController * picker = [[UIImagePickerController alloc] init]; picker.delegate = self; picker.sourceType = UIImagePickerControllerSourceTypeCamera; ! [self presentViewController:picker animated:YES completion:nil];
iPadでカメラ撮影する時
UIImagePickerController * picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
!
dispatch_async(dispatch_get_main_queue(), ^ {
[self presentViewController:picker animated:YES completion:nil];
});
非同期で起動しないと固まってしまう
iPadでカメラ撮影する時
UIImagePickerController * picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
!
dispatch_async(dispatch_get_main_queue(), ^ {
[self presentViewController:picker animated:YES completion:nil];
});
非同期で起動しないと固まってしまう
iPadでカメラ撮影する時
UIImagePickerController * picker = [[UIImagePickerController alloc] init];
picker.delegate = self;
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
dispatch_async(dispatch_get_main_queue(), ^ {
[self presentViewController:picker animated:YES completion:nil];
});
} else {
[self presentViewController:picker animated:YES completion:nil];
}
非同期で起動しないと固まってしまう
iPadでアルバムから写真を選 択する際も同様
iPadでアルバムから写真を選択する際も同様
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc]init];
imagePickerController.delegate = self;
imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
!
self.popover = [[UIPopoverController alloc] initWithContentViewController:
imagePickerController];
self.popover.delegate = self;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
dispatch_async(dispatch_get_main_queue(), ^ {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
});
} else {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
非同期で起動しないと固まってしまう
iPadでアルバムから写真を選択する際も同様
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc]init];
imagePickerController.delegate = self;
imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
!
self.popover = [[UIPopoverController alloc] initWithContentViewController:
imagePickerController];
self.popover.delegate = self;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
dispatch_async(dispatch_get_main_queue(), ^ {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
});
} else {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
非同期で起動しないと固まってしまう
iPadでアルバムから写真を選択する際も同様
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc]init];
imagePickerController.delegate = self;
imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
!
self.popover = [[UIPopoverController alloc] initWithContentViewController:
imagePickerController];
self.popover.delegate = self;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
dispatch_async(dispatch_get_main_queue(), ^ {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
});
} else {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
非同期で起動しないと固まってしまう
iPadでアルバムから写真を選択する際も同様
UIImagePickerController *imagePickerController = [[UIImagePickerController alloc]init];
imagePickerController.delegate = self;
imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
!
self.popover = [[UIPopoverController alloc] initWithContentViewController:
imagePickerController];
self.popover.delegate = self;
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")) {
dispatch_async(dispatch_get_main_queue(), ^ {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
});
} else {
[self.popover presentPopoverFromRect:cell.frame inView:self.view
permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
非同期で起動しないと固まってしまう
CASE 5 タブ画像が表示されない
タブ画像が表示されない問題
- (void)setFinishedSelectedImage:(UIImage *)selectedImage withFinishedUnselectedImage:(UIImage *)unselectedImage;
setFinishedSelectedImageはDepricated - (void)setFinishedSelectedImage:(UIImage *)selectedImage withFinishedUnselectedImage:(UIImage *)unselectedImage;
setFinishedSelectedImageはDepricated UIImage *m1 = [[UIImage imageNamed:@"m1.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UIImage *m2 = [[UIImage imageNamed:@"m2.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UITabBarItem *tab = [[UITabBarItem alloc] initWithTitle:@"" image:m1 selectedImage:m2]; ! !
setFinishedSelectedImageはDepricated UIImage *m1 = [[UIImage imageNamed:@"m1.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UIImage *m2 = [[UIImage imageNamed:@"m2.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UITabBarItem *tab = [[UITabBarItem alloc] initWithTitle:@"" image:m1 selectedImage:m2]; ! !
setFinishedSelectedImageはDepricated UIImage *m1 = [[UIImage imageNamed:@"m1.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UIImage *m2 = [[UIImage imageNamed:@"m2.png"] imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; UITabBarItem *tab = [[UITabBarItem alloc] initWithTitle:@"" image:m1 selectedImage:m2]; UIImageRenderingModeAlwaysOriginalと共に生成する ! !
CASE 6 Extentionの共通ロジックどうする 問題
アプリ本体とExtentionで 利用する共通部品クラスにおいて [UIApplication sharedApplication] が使われているメソッドがある 色々な事情で共通部品クラスに 大きな修正を加えることができませんでした
+ (UIApplication *)sharedApplication NS_EXTENSION_UNAVAILABLE_IOS("Use view controller based solutions where appropriate instead."); NS_EXTENSION_UNAVAILABLE_IOS のメソッドはExtention内では利用できない
どうするべきか
!
!
// 特定のアプリを起動する
void launchXXXXX(NSString* message)
{
NSString* url = [NSString stringWithFormat:
@"%@://XXXXX/?message=%@", kXXXXSchemes, message];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
}
!
Preprocessor Macroを使う方法
#ifndef AUC_WIDGET
!
// 特定のアプリを起動する
void launchXXXXX(NSString* message)
{
NSString* url = [NSString stringWithFormat:
@"%@://XXXXX/?message=%@", kXXXXSchemes, message];
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:url]];
}
#endif
Preprocessor Macroを使う方法
#ifndef AUC_WIDGET
!
// 特定のアプリを起動する
void launchXXXXX(NSString* message)
{
NSString* url = [NSString stringWithFormat:
@"%@://XXXXX/?message=%@", kXXXXSchemes, message];
[[UIApplication sharedApplication] openURL:[NSURL
URLWithString:url]];
}
#endif
できるだけ共通部品から取り除くべきですが、
一手段として参考にしてください
ExtentionのPreprocessor Macroの設定例
Extention Today対応
Extention Todayについて • ガイドライン上、スクロールできるUIはユーザに とって好ましくないとの記述がある • ヤフオク!では入札中の商品を一覧できるExtention Todayを作成したかった • 一覧から入札できればなお良い(でもウィジェッ トではキーボードは利用できない)
iOS8対応に 掛かった工数
iOS8対応に掛かった工数(iPhone) iOS8での不具合 修正 ウィジェット 作成 合計 制作 ー 3人日 3人日 開発 4人日 4人日 8人日
iOS8対応に掛かった工数(iPad) iOS8での不具合 修正 ウィジェット 作成 合計 制作 ー 0.5人日 0.5人日 開発 3人日 1人日 4人日
開発工数 iOS7対応>>>>>iOS8対応>iOS6対応
リリース後の反響
最後に
http://topic.auctions.yahoo.co.jp/promo/hr/p/
http://topic.auctions.yahoo.co.jp/promo/hr/p/