iOS/iPhone/iPad/watchOS/tvOS/MacOSX/Android プログラミング, Objective-C, Cocoa, Swiftなど
先日のビューによる画面遷移では、二通りに画面サイズを取得するコードがあったと思うが、その値を見てみる事にする。
CGRect screenBounds = [[UIScreen mainScreen] applicationFrame];
DBGMSG(@"screenBounds:(%f, %f, %f, %f)",
screenBounds.origin.x,
screenBounds.origin.y,
screenBounds.size.width,
screenBounds.size.height);
CGRect windowBounds = [[UIScreen mainScreen] bounds];
DBGMSG(@"windowBounds:(%f, %f, %f, %f)",
windowBounds.origin.x,
windowBounds.origin.y,
windowBounds.size.width,
windowBounds.size.height);
後の話で出てくるが、注目すべきは、screenBoundsはステータスバー分、Y軸方向に20下がっている。この事が、nibを使った場合に、気をつけないと行けない事になるが、詳しくは次の機会で。
補足の説明だ。
frameは上位ビューの座標系の値となっている。なので、Y軸の値はステータスバー分、下がっている。boundsはローカル座標系の値なので、上位との位置関係を考慮しないので、原点(0, 0)からの値となっている。
windowのサイズは画面一杯にする。なので、ステータス分は考えない、画面のサイズを設定する。ビューは、表示される領域のサイズにしないと、その部分は隠れてしまう。サンプルでも、そのようにしている。
何事も試してみないと分からない。今回は少しおかしいと思う事もあえてやってみる。
ビューを管理するものといてビューコントローラがあるが、それをInterface Builderを使わないで作成してみよう。
新規ファイルとして、UIViewControllerのサブクラスを作成。ビューコントローラの実装が必要なメソッドにデバッグ出力を埋め込んでみた。
#import <UIKit/UIKit.h>
@interface MyViewController : UIViewController
@end
#import "MyViewController.h"
@interface MyViewController ()
- (void)_init;
@end
@implementation MyViewController
- (id)init
{
DBGMSG(@"%s", __func__);
self = [super init];
if (self) {
[self _init];
}
return self;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
DBGMSG(@"%s", __func__);
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
[self _init];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
DBGMSG(@"%s", __func__);
self = [super initWithCoder:aDecoder];
if (self) {
[self _init];
}
return self;
}
- (void)_init
{
DBGMSG(@"%s", __func__);
}
- (void)dealloc
{
DBGMSG(@"%s", __func__);
}
- (void)loadView
{
DBGMSG(@"%s", __func__);
[super loadView];
}
- (void)viewDidLoad
{
DBGMSG(@"%s", __func__);
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)viewDidUnload
{
DBGMSG(@"%s", __func__);
[super viewDidUnload];
}
- (void)viewWillAppear:(BOOL)animated
{
DBGMSG(@"%s", __func__);
[super viewWillAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated
{
DBGMSG(@"%s", __func__);
[super viewDidAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated
{
DBGMSG(@"%s", __func__);
[super viewWillDisappear:animated];
}
- (void)viewDidDisappear:(BOOL)animated
{
DBGMSG(@"%s", __func__);
[super viewDidDisappear:animated];
}
- (void)didReceiveMemoryWarning
{
DBGMSG(@"%s", __func__);
[super didReceiveMemoryWarning];
}
@end
そして、このビューコントローラのインスタンスを生成して、ビューを取り出してみる。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
....
self.myViewController = [[MyViewController alloc] init];
....
}
当たり前だが、viewDidLoad等は呼ばれない。つまり、ビューコントローラはInterface Builderで生成されるものということか?
Apple Developerサイトの文書はよく読むべきだ。
iOS4からウィンドウにrootViewControllerというプロパティが追加された。これに生成したビューコントローラを設定してみよう。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
....
self.myViewController = [[MyViewController alloc] init];
self.myViewController.view.backgroundColor = [UIColor yellowColor];
self.window.rootViewController = self.myViewController;
....
[self.window makeKeyAndVisible];
return YES;
}
viewDidLoad等が呼ばれるようになった。これで、最初のビューコントローラの管理する問題は解決した。これ以降については、次の機会に。
では、ルートビューコントローラを使って画面遷移はできないか?試してみよう。
ビューの時と同様に赤と青の二つのビューコントローラを生成し、初めは赤を表示する。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
CGRect screenBounds = [[UIScreen mainScreen] applicationFrame];
CGRect windowBounds = [[UIScreen mainScreen] bounds];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
self.myViewController1 = [[MyViewController alloc] init];
self.myViewController1.view.backgroundColor = [UIColor redColor];
self.myViewController2 = [[MyViewController alloc] init];
self.myViewController2.view.backgroundColor = [UIColor blueColor];
self.isView1 = YES;
self.window.rootViewController = self.myViewController1;
[self.window makeKeyAndVisible];
return YES;
}
そして、これもビューとのとき同様にタッチされたら切り替える。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.isView1) {
self.isView1 = NO;
self.window.rootViewController = self.myViewController2;
}
else {
self.isView1 = YES;
self.window.rootViewController = self.myViewController1;
}
}
画面は切り替わった。そして、viewDidLoad等が呼ばれるとログを出力するようにしたところ、alloc/initしたタイミングでviewDidLoadまでは呼ばれている。そして、ルートビューコントローラに設定したらviewWillAppear:等が呼ばれる。viewWillAppear:等が呼ばれるのは、タッチの際のルートビューコントローラの変更時でも同様だ。
画面遷移とは、ビューの入れ替えと考えるれば、ビューさえ扱えば画面遷移が実装できるという事になるが、ビューコントローラには画面単位でのビューの生成/削除や、画面が開店した際に対応したイベントを受け取れるので、ビューコントローラを利用しない手はないということになるが、イベントのフレームワーク内部で管理され、フレームワークで用意されているナビゲーションコントローラやタブバーコントローラを使えば、その恩恵に預かる事は出来るが、これらを使わず、かつ、nibを利用しない場合は、その恩恵に預かる事は難しい。
これに対しての解決策として、iOS5からビューコントローラに子ビューコントローラを管理するコンテナ機能を追加されたのだと思う。これの利用の方法については、以前、紹介したが、実際に使ってみて気がついて細かな点について、発表していきたいと思う。
「独自のコンテナViewController」に戻る。
サンプルコードはRetina 4inchは考慮していないので、試す場合は、以前の画面サイズで行って欲しい。画面と同じサイズのDefault.pngを用意する。その際、座標が分かりやすい図とする。そして、最初に表示するViewControllerの画面にも、Default.pngを貼付けて欲しい。そして、実行するとどうなるか?最初の画面が表示される際に、図が少し下がったように見えないか?
これは、親ビューコントローラのビューの座標の始点が、ステータスバー分だけ下がっているが、nibでの子ビューコントローラーのビューの座標が、ステータスバーを考慮して下げられているからだ。
なので、小ビューコントローラのビューの始点を原点にすれば、解決する。
@implementation CVCViewController
...
- (void)viewDidLoad
{
[super viewDidLoad];
/* 子ビューコントローラを取得 */
UIStoryboard *oneStoryboard = [UIStoryboard storyboardWithName:@"OneStoryboard" bundle:nil];
UIStoryboard *twoStoryboard = [UIStoryboard storyboardWithName:@"TwoStoryboard" bundle:nil];
OneViewController *oneViewController = [oneStoryboard instantiateInitialViewController];
TwoViewController *twoViewController = [twoStoryboard instantiateInitialViewController];
/* コンテナViewControllerの子ViewControllerに登録 */
[self addChildViewController:oneViewController];
[self addChildViewController:twoViewController];
oneViewController.cvcViewController = self;
twoViewController.cvcViewController = self;
/* 強制的に呼ぶ */
[oneViewController didMoveToParentViewController:self];
[twoViewController didMoveToParentViewController:self];
/* 最初の画面を設定 */
self.selectedViewController = [self.childViewControllers objectAtIndex:0];
CGRect frame = self.selectedViewController.view.frame;
frame.origin = CGPointMake(0.0, 0.0);
self.selectedViewController.view.frame = frame;
[self.view addSubview:self.selectedViewController.view];
}
...
@end
これで、少し悩んだ事があった。
以前のサンプルと比較しやすくするため、ビューを使った画面遷移のサンプルコードをビューコントローラのコンテナ機能を使った物に変更してみた。
TransitionViewのAppDelegate.mを以下の内容に変更。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
CGRect screenBounds = [[UIScreen mainScreen] applicationFrame];
CGRect windowBounds = [[UIScreen mainScreen] bounds];
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.backgroundColor = [UIColor whiteColor];
self.containerViewController = [[UIViewController alloc] init];
self.containerViewController.view.backgroundColor = [UIColor yellowColor];
self.myViewController1 = [[MyViewController alloc] init];
self.myViewController1.view.backgroundColor = [UIColor redColor];
self.myViewController1.title = @"one";
self.myViewController2 = [[MyViewController alloc] init];
self.myViewController2.view.backgroundColor = [UIColor blueColor];
self.myViewController2.title = @"two";
self.window.rootViewController = self.containerViewController;
/* コンテナViewControllerの子ViewControllerに登録 */
[self.containerViewController addChildViewController:self.myViewController1];
[self.containerViewController addChildViewController:self.myViewController2];
/* 強制的に呼ぶ */
[self.myViewController1 didMoveToParentViewController:self.containerViewController];
[self.myViewController2 didMoveToParentViewController:self.containerViewController];
/* 最初の画面を設定 */
self.isView1 = YES;
CGRect frame = self.myViewController1.view.frame;
frame.origin = CGPointMake(0.0, 0.0);
self.myViewController1.view.frame = frame;
self.myViewController2.view.frame = frame;
[self.containerViewController.view addSubview:self.self.myViewController1.view];
[self.window makeKeyAndVisible];
return YES;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
if (self.isView1) {
self.isView1 = NO;
[self.containerViewController transitionFromViewController:self.myViewController1
toViewController:self.myViewController2
duration:1.0
options:UIViewAnimationOptionTransitionCrossDissolve
animations:NULL
completion:NULL];
}
else {
self.isView1 = YES;
[self.containerViewController transitionFromViewController:self.myViewController2
toViewController:self.myViewController1
duration:1.0
options:UIViewAnimationOptionTransitionCrossDissolve
animations:NULL
completion:NULL];
}
}
_ 今回は、ActionScriptでiOSアプリケーションを開発するお話と、UIViewControllerのコンテナ機能、EventKitとリマインダー、OSXのRetina Displya対応の発表がありました。ちょっと試してみたい内容があって興味深かったです。
WebサービスAPIをアプリケーションで利用する場合、ライセンスについて気をつけないといけない。最近、話題になっている地図サービスもアプリケーション開発者からみるとライセンスがどうなっているのか気になっていた。そこで、現状利用しやすいと思われるMicrosoftのBing Mapsを試してみる事にした。
入手方法は、『関連情報』のURLから調べる事が出来るので、これを自分がアプリケーションに組み込んだ手順を紹介する。
テンプレート「Single View Application」でプロジェクトを生成する。
プロジェクトのフォルダに、Bing Mapsの「MapControl」フォルダーをコピーする。
コピーしたフォルダをプロジェクトに追加する。
その際のオプションは既に置かれているファイルなので、コピーの指定は不要だ。
Header Search Pathsに、追加した「MapControl」フォルダーを指定する。
リンカーのフラグに「-ObjC -all_load」を追加する。
Link Binary With Librariesにフレームワークを追加する。
Interface BuilderでViewを追加して、InteractionのMultiple Touchにチェックを入れる。
そして、追加したViewのクラスをBMMapViewに変更する。
ビューコントローラのプロトコルにBMMapViewDelegateを指定して、追加したBMMapViewを指すアウトレットを追加する。
#import <UIKit/UIKit.h>
#import "BingMaps/BingMaps.h"
@interface VirtualEarthViewController : UIViewController
@property (nonatomic, weak) IBOutlet BMMapView *mapView;
@end
ビューコントローラのviewDidLoadメソッドで、以下のコードを追加する。
- (void)viewDidLoad
{
[super viewDidLoad];
self.mapView.delegate = self;
[self.mapView setShowsUserLocation:YES];
}
Bing Maps Keyをproperty listに追加する。
Bing Maps Keyの取得方法は、関連情報を参照して欲しい。
実行。
住所から現在位置を取得するサンプルがないことを考えると、ここは検索サービスの領域でライセンスという問題がありそうだ。
現在位置を取得して、そこを表示する事には制限はないようなので、試してみる事にする。
ビューコントローラをCLLocationManagerDelegateに対応させる。
#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
#import <CoreLocation/CLLocationManagerDelegate.h>
#import "BingMaps/BingMaps.h"
@interface VirtualEarthViewController : UIViewController <BMMapViewDelegate, CLLocationManagerDelegate>
@property (nonatomic, weak) IBOutlet BMMapView *mapView;
@property (nonatomic, strong) CLLocationManager *locationManager;
@end
ビューコントローラが表示される度に、現在位置を取得し、そこを表示する。
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
[self.locationManager startUpdatingLocation];
}
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
CLLocation *location = [locations objectAtIndex:0];
BMCoordinateRegion newRegion;
newRegion.center = location.coordinate;
newRegion.span.latitudeDelta = 0.0;
newRegion.span.longitudeDelta = 0.0;
[self.mapView setRegion:newRegion animated:YES];
[self.locationManager stopUpdatingLocation];
}
指定した住所を表示する単純な地図が欲しいのだが、どうしようかな。
前回の内容を補足する。
現在位置は以下のデリゲートのメソッドで取得した。
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
CLLocation *location = [locations objectAtIndex:0];
BMCoordinateRegion newRegion;
newRegion.center = location.coordinate;
newRegion.span.latitudeDelta = 0.0;
newRegion.span.longitudeDelta = 0.0;
[self.mapView setRegion:newRegion animated:YES];
[self.locationManager stopUpdatingLocation];
}
位置情報は配列となっているが、メソッドを呼ぶまでに複数のイベントが発生した場合に複数個となり、最新の位置情報は末尾となる。なので、上記では先頭の位置情報を取得しているが、宜しくないという事になる。
また、newRegion.spanは表示範囲?どう計算すればいいのか理解できていないが、0.0は宜しくないようで。0.005に変更してみた。
_ 村上幸雄 [spanは緯度経度の度で、地球一周の長さを360度で割ると、約111km。0.005は約500mという事のようですね..]
地図アプリケーションといえば住所から地図を検索する機能を期待すると思うが、住所から位置情報を取得する機能はBing Maps iOS SDKに含まれていない。特に、Bingにこだわる必要はないが、せっかくなので、Bing Maps REST Servciesを使うことにした。
以下が検索要求を出すコードだ。
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
if ((searchBar.text == nil) || (searchBar.text.length <= 0)) return;
self.data = [[NSMutableData alloc] init];
NSString *bingMapsKey = [[NSBundle mainBundle] objectForInfoDictionaryKey: @"BingMapsKey"];
NSString *query = (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(NULL,
(CFStringRef)searchBar.text,
NULL,
(CFStringRef)@"!*%'();:@&=+-$,/?%#[]~",
kCFStringEncodingUTF8);
NSString *url = [[NSString alloc] initWithFormat:@"http://dev.virtualearth.net/REST/v1/Locations?query=%@&key=%@&c=%@-%@",
query,
bingMapsKey,
[[NSLocale currentLocale] objectForKey:NSLocaleLanguageCode],
[[NSLocale currentLocale] objectForKey:NSLocaleCountryCode]];
NSLog(@"url: %@", url);
NSURLRequest *urlRequest = nil;
urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:url]];
[NSURLConnection connectionWithRequest:urlRequest delegate:self];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}
cパラメータにja-jpを指定しないと、日本語の住所が上手く検索できない。当初、入力されたテキストの言語に対応したcパラメータの指定を考えてみたが、よい考えが思い浮かばなかったので、止めた。
応答結果から緯度経度を取り出して、その位置を表示するコードだ。
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSError *error = nil;
NSDictionary *content = [NSJSONSerialization JSONObjectWithData:self.data options:NSJSONReadingAllowFragments error:&error];
NSLog(@"content:%@", content);
if (content) {
NSArray *resourceSets = [content objectForKey:@"resourceSets"];
NSLog(@"resourceSets:%@", resourceSets);
if (resourceSets) {
NSDictionary *resource = [resourceSets objectAtIndex:0];
NSLog(@"resource:%@", resource);
if (resource) {
NSArray *resources = [resource objectForKey:@"resources"];
NSLog(@"resources:%@", resources);
if ((resources) && (0 < [resources count])) {
resource = [resources objectAtIndex:0];
NSLog(@"resource:%@", resource);
if (resource) {
NSDictionary *point = [resource objectForKey:@"point"];
NSLog(@"point:%@", point);
if (point) {
NSArray *coordinates = [point objectForKey:@"coordinates"];
NSLog(@"coordinates:%@", coordinates);
if ((coordinates) && (0 < [coordinates count])) {
NSString *latitude = [coordinates objectAtIndex:0];
NSString *longitude = [coordinates objectAtIndex:1];
NSLog(@"latitude:%@", latitude);
NSLog(@"longitude:%@", longitude);
if ((latitude) && (longitude)) {
BMCoordinateRegion newRegion;
newRegion.center.latitude = [latitude floatValue];
newRegion.center.longitude = [longitude floatValue];
newRegion.span.latitudeDelta = 0.005;
newRegion.span.longitudeDelta = 0.005;
[self.mapView setRegion:newRegion animated:YES];
}
}
}
}
}
}
}
}
if ([self.searchBar canResignFirstResponder])
[self.searchBar resignFirstResponder];
}
条件分が深くなっているのは格好が悪いが、サンプルなので勘弁して欲しい。
探偵物語の工藤探偵事務所の住所を表示してみた。
Bing Mapsの地図の種類は、BMMapModeRoadとBMMapModeAerial、BMMapModeAerialWithLabelsの3つから選択できる。意味は、通常の地図と航空写真、説明付き航空写真という事だと思う。
標準の地図アプリケーションに似せて、サンプルコードに設定用ビューコントローラを追加し、地図の種類が切り替えられるようにしてみた。以下が地図の種類を切り替えるコードだ。
- (IBAction)mapMode:(id)sender
{
UISegmentedControl *mapModeSegmentedControl = sender;
switch (mapModeSegmentedControl.selectedSegmentIndex) {
case BMMapModeRoad:
self.mapView.mapMode = BMMapModeRoad;
break;
case BMMapModeAerial:
self.mapView.mapMode = BMMapModeAerial;
break;
case BMMapModeAerialWithLabels:
self.mapView.mapMode = BMMapModeAerialWithLabels;
}
}
標準の地図アプリケーションでは、設定画面への遷移は、mapCurelという非公開のアニメーションを使っているようだが、なんとかして、似せれたらと思っている。
2012年10月24日水曜日に開催されたChrome Tech Talk Night #4について簡単にまとめる。
名称: Chrome Tech Talk Night #4
日時: 2012 年 10 月 24 日(水) 19:00 - 21:00 (受付 18:30 〜 19:30)
場所: Google 東京オフィス 六本木ヒルズ森タワー 27 階
会費: 無料
主催: Google
モバイルアプリの最新動向 - High DPI とマルチタッチ( From High-DPI to multi-touch: cutting edge mobile web )(講演者:Boris Smusさん)
スライド
モバイルは多様な環境があるため、それに対応する方法を現状と将来の方向性について発表された。
一つは、iOSでいうところのRetina。様々な画面解像度に対応するお話で、iOSの場合、UIKitのサイズと、ピクセルサイズを別の物として、UIKitでのサイズは、非RetinaとRetinaで変化がないように見せたり、画像について、ファイル名を工夫して、Retinaと非Retinaの画像をフレームワーク側で使い分けてくれる方法を用意してくれているが、Webの場合は簡単には行かず、個々の状況に合わせてベストな方法を選択するということになるようだ。
もう一つの大きな発表のテーマは、デスクトップのキーボード/マウスのイベントと、モバイルのマルチタッチをどう扱うかで、将来の話となるが、Microsoftが提案しているPointイベントが紹介された。
_ eienf [メモリ節約のためviewが実際にロードされるのはそれが表示されるときです。だからviewDidLoadもviewCo..]