2015年4月17日金曜日

仕事で使うiOSアプリ開発

はじめに

趣味の開発だと見過ごしがちな注意点が、仕事の開発だと多く存在します。
特に、ネットで検索した情報だとその場しのぎのプログラムが書かれていることが多いのも原因の一つだと思われます。

今回は、私がiOSのアプリを開発して気づいた点・ノウハウ化されるべき点をまとめてみました。ここで書かれている以外にもまだまだ書くことは多いです。少しずつ蓄積していこうと思います。

User Interactionの制御

UIに関して、Webとスマホアプリとの一番大きな違いは
・同時に複数のオブジェクトを操作できる点
です。

もう少し細かく説明しましょう。
Webであればカーソルは1つしかないので、同時に同じボタンをクリックすることはあり得ません。
しかしスマホの場合、複数の指を用いた操作を提供しているため、ある指でボタンを押しながら、他の指で別のボタンを押したりドラッグする操作を許容してしまいます。

スマホ開発において、ボタンに限らずUIオブジェクトを作成する時は、他のオブジェクトとの同時操作可能性を常に意識しなければなりません。
この可能性を無視してアプリを作ると、アプリがクラッシュ・操作不能になるような致命的なバグが大量に埋め込まれてしまいます。以下、代表的な事例とその回避策をあげてみます。

UIButton
let button = UIButton()
button.exclusiveTouch = true

exclusiveTouchをtrueにすることで、他のボタンとの同時操作をブロックします。
デフォルトではfalseになっています。もちろん、複数の指で操作するUIを組み立てるときはこの限りではありません。ただ、指一本で操作するボタンの方が多いと思われるため、UIButtonを使用する時はexclusiveTouch属性をtrueにするのを癖にするぐらいでちょうど良いと思います。

Gesture Recognizer
let view = UIView()
let gesture = UIGestureRecognizer()
view.addGestureRecognizer(gesture)
view.exclusiveTouch = true

UIButtonと同様です。UIViewにGestureを付与する時は同時操作の可能性が生まれます。
exclusiveTouch属性を忘れないようにしましょう。

Animation

Animation中はuserInteractionEnabledをfalseにしUI操作をブロックしましょう。

データの保存

アプリのデータベースの持ち方は大きく2つに分かれます。
1. サーバーへの保存(リモートのサーバー)
2. ローカルへの保存(iPhone端末内)

1. サーバへの保存

サーバーにデータを保存する際は、個人情報の取り扱いについて明記し・ユーザに許可を求める必要があります。
当たり前のように思われるかもしれませんが、iOSアプリ開発ではこのようなプライバシーポリシーに特に留意しなければなりません。その理由はiOSアプリ審査という手続きに起因します。iPhone端末にインストールされるアプリケーションがユーザに害を及ぼさないことを保証するために、Appleにより厳しい審査が施されます。
ユーザへの許可なく個人情報をサーバーに送るアプリなどは悪質なアプリとみなされApp Storeへの公開を差し押さえられるばかりか、最悪、開発アカウントを凍結される恐れがあります。

ローカルへの保存

とくにスマホゲームアプリの場合、ローカルにユーザデータを保存することがチートの可能性を生み出すことがあります。最近のゲームはソーシャル機能を持ったものが非常に多く、個人のチートがユーザ全体・サービス全体に影響を与えるため致命傷になりかねません。

オフラインの可能性

ネットワークを利用するアプリの場合は、オフラインの可能性を考えましょう。
オフライン時使用不可と割り切るのも一つの手法ですが、ネットワーク接続時にキャッシュとしてローカルにデータを保存し、オフラインコンテンツを提供することでユーザの満足度が上がります。

サーバー連携

サーバーへのデータ送信に失敗した場合の処理、再送信処理などを考慮しましょう。

画面規格対応

見積もりを出す際に、どの画面規格に対応するかを必ず確認しましょう。
iPadなどは勿論ですが、iPhone5 ~ iPhone6Plusでもアスペクト比が異なるため、
特定の端末依存で画面の開発をしていると、違う端末でスタイルが崩れることがあります。
コードベースで画面の組み立てを行っている場合は特に注意です。

確認方法

Xcode 6.0からPreview機能が追加されました。Xcode上で全ての画面規格でのレイアウトを確認することができます。ただし、コードにより動的にaddSubviewした部品は確認できません。

メモリリーク

知らない人にメモリリークとは何かを簡単に説明すると、
・オブジェクト(例:UIView、画面を追加する)を作成するとメモリを確保する必要がある
・不要になったオブジェクトは解放されなければならない

A. 解放したメモリに誤ってアクセスするとエラーによりアプリが強制終了する(クラッシュ)
B. 不要になったメモリを解放せずに放っておくと、確保しているメモリの容量が増大していき、一定容量を超えた時にOSによりアプリが強制終了される

詳しく勉強したい人は、ARCやガベージコレクションについて検索すると良いと思います。

iOS開発でよくあるメモリリークと、その防ぐ方法について説明します。

循環参照

オブジェクトAがオブジェクトBを持つ場合、オブジェクトAからオブジェクトBへの強い参照が生まれます。自分自身を指す強い参照がなくなった時に、オブジェクトは自動的に解放されます。つまり、画面遷移などでオブジェクトAが不要になり解放された時にその参照先であるオブジェクトBも自動的に参照されます。

循環参照とは、2つのオブジェクトが相互に強い参照を持った時に、どちらのオブジェクトも解放されなくなることを言います。

例1. オブジェクトBがオブジェクトAを持つ場合

// 強い参照
class ObjectB {
var objA: ObjectA?
// 弱い参照
class ObjectB {
weak var objA: ObjectA?

のように、weak参照子をつけることにより、強い参照を回避できます。(ObjectAから見た時にObjectBからの参照は無視される)

例2. クロージャの場合
複数のclass(オブジェクト)間で情報をやり取りする時にクロージャを使用することがあります。クロージャは強い参照を生み出します。

// 強い参照
closure = {
    ...
}
// 弱い参照
closure = {
    [weak self] in
    ...
}

のように修飾子をつけることで強い参照を回避できます。
修飾子はweak, unownedの2種類存在します。
使い分けは、
・selfがnilの可能性がある時はweak
・selfが常にnilではない時はunowned
となります。
unownedを使用してselfがnilだった場合はアプリがクラッシュするため、
迷う場合はweakを使っていればOKです。

クロージャの循環参照については、この記事が大変参考になります。(日本語)
http://qiita.com/susieyy/items/f57f9362b8e0a89023cb

メモリリークの発見

Add exception break pointにより、例外発生時にブレークできるためデバッグしやすくなります。

画面遷移の作り方

pushした画面をpopする、不要になった画面を解放するなどの処理を怠ると、メモリリークすることがあります。注意しましょう。

アプリ起動処理の実行時間制限

以下の条件を満たした時に、アプリが強制終了します。
・AppDelegateのアプリ起動 〜 最初のUIViewControllerの画面が表示されるまでに一定時間経過する(20秒〜30秒)
・Releaseモードでビルドした場合

つまり、アプリ起動時の初期化処理などをAppDelegateの中に書いてはいけません。
初期化処理用のUIViewControllerを作成し、
viewDidAppear -> 初期化処理 -> アプリのコンテンツ画面に遷移
とする必要があります。

厄介なことに、Macから実機に直接接続しDebugモードで実験している時には確認することができません。要注意です。

こちらの記事も参考になります。(日本語)
http://cocoadays.blogspot.jp/2011/02/ios.html

容量

アプリの容量が100MBを超える場合、Wifi接続時のみしかApp Storeからのアプリインストールができません。

アップデート

DBマイグレーション

かならずDBのバージョン管理を行い、カラムが追加・削除される可能性を考慮しましょう。

ログ

NSLog, printlnの違い

swiftの場合、NSLog, printlnの2種類のログ出力方法があります。
printlnの方が高速かつ手軽であり、ネットで検索した際もprintlnを使用しているコードが多い印象がありますが、製品開発の場合はNSLogを使用するほうをおすすめします。
理由は

  1. printlnはスレッドセーフでない
    printlnの場合、非同期処理の際に、出力が混ざることがあります。

  2. printlnはDevicesからのログに出力されない
    XcodeのコンソールではNSLog, printlnどちらともログ出力されますが、Devicesから確認できるコンソールではNSLogでしかログ出力されません。

例えば、TestFlightで配布したアプリで動作試験をしていたらクラッシュした場合、Deviceからログを確認することになりますが、printlnで出力したログは確認できません。

アーキテクチャ

32bit, 64bit

iPhone5cまでは32bitですが、iphone5s以降は64bitになっています。
バイナリをいじる処理が含まれる場合は双方で十分に試験を行いましょう。
Simulatorで試験をする場合はSimulatorのarchitectureを確認したうえで試験しましょう。

スレッド

サーバー通信やDB操作などはメインスレッドとは別のスレッドで行いましょう。
サーバー通信をメインスレッドで行ってしまうと、処理が重い時に画面がフリーズしてしまいます。

iOSバージョン別対応

iOSバージョンで使用するメソッドが異なることがあるので注意しましょう。

検証端末の取得

iOSのバージョンをダウングレードすることはできません。
最新バージョン以外のiOSに対応する場合は、検証端末をバージョンごとに調達する必要があります。開発会社であれば古いバージョンのiPhone端末をアップグレードしないまま保持しておくことが多いです。
Simulatorを使用することもできますが、カメラなどSimulatorでは再現できない機能もあり、様々な面で完全な試験のためには実機を用意する他ありません。

UILocalNotificationの上限

アラート機能のために使用するUILocalNotificationですが、実は最大64個までしか同時に登録することができません。古いアラートが無効化された時に備えて再登録処理を考慮しましょう。
iOS8でのバグにより64個を超えて登録できることもあるようですが、公式サイトには確かに64個と明記されています。
https://developer.apple.com/library/ios/documentation/iPhone/Reference/UILocalNotification_Class/

WatchKitで画面遷移する

はじめに

WatchKitで画面遷移を作成する方法を説明します。

対象

iOSアプリを少しでも作成したことのある人
言語はSwiftを使用しますが、objective-Cでもやり方に大きな違いはありません

つくるもの

・2枚の画面からなる画面遷移
・ボタンを押して、新しい画面に遷移する
・遷移後、戻るボタンにより元の画面に戻る

・遷移方法がmodal, pushの2種類あるのでそれぞれを試します
iOSアプリを開発したことがある方はご存知の通り、modal遷移は詳細表示のための一時的な画面遷移、push遷移は階層構造を持った画面を行き来するための画面遷移です。

作成手順

1. iPhoneアプリを作成する

File -> new -> ProjectからiOSアプリを作成します。
TemplateはiOS -> Application -> Single View Applicationとします。

Product NameはWatchKitTransitionとします。
言語はSwiftを選択します。Core Dataは使用しません。

2. iPhoneアプリと連携するWatchKitアプリを作成する

File -> New -> TargetからApple Watch Targetを追加します。
TemplateはiOS -> Apple Watch -> WatchKit Appとします。
言語はSwiftです。
Include Notification Sceneにチェックが入った状態で、Finishします。
SchemeはActiveさせましょう。

3. 遷移のトリガーとなるボタンを配置する

interface.storyboard上でInterfaceController上にボタンを2つ配置します。
1つにはmodal遷移を、もう1つにはpush遷移を設定していきます。

ボタンを配置したら、ソースコード上へ参照を引っ張りましょう。
storyboardファイルとInterfaceControllerファイルを同時に開き、ctrl + ドラッグでSent Actionsからの参照を張ります。これで、ボタンを押した時の処理を記述できるようになりました。modal遷移用のボタンに対してはpresentButtonTapped、push遷移用のボタンに対してはpushButtonTappedという名前をつけます。

4. 遷移先画面を作成する

遷移した先の画面を作成しましょう。
modal遷移、push遷移それぞれで遷移先の画面を作成するので、
interface.storyboard上に新しいInterface Controllerを2つ配置します。
modal遷移用のInterface Controllerに対してはIdentifierを”PresentNewView”として設定します。
push遷移用のInterface Controllerに対してはIdentifierを”PushNewView”として設定します。

5. 画面遷移を作成する

InterfaceControllerのソースコードを編集していきます

presentButtonTappedメソッドを以下のように編集します

@IBAction func presentButtonTapped() {
    presentControllerWithName("PresentNewView", context: nil)
}

pushButtonTappedメソッドを以下のように編集します

@IBAction func pushButtonTapped() {
    pushControllerWithName("PushNewView", context: nil)
}

これで完成です。

起動

Xcodeシミュレータで実際に動かしてみましょう。

TargetをWatchKitTransition WatchKit Appに変更します。
ちなみにCommand + control + [でTargetの切り替えが出来ます。
同様にCommand + Rで起動しましょう。

iPhoneのシミュレータとApple Watchのシミュレータが同時に起動します。
もしApple Watchのシミュレータが起動しなければ、
メニューのHardware -> External Displays -> Apple Watchを選択するとシミュレータ画面が追加されます。

デモ

ボタンを押すことで画面遷移します。
遷移先の画面には自動で戻るボタンが設置されているため、自由に行き来できます。

ソースコード

こちらからどうぞ
https://github.com/takanori-matsumoto-mulodo/WatchKitTransition

2015年4月3日金曜日

iOS Extensionでデータを共有する

はじめに

これまで、Apple Watch、Action ExtensionとiOS Extension機能を実装する方法を説明してきました。今回もiOS Extensionに関する話です。

iOSアプリでデータを保存する際、基本的には1つのアプリの中だけでデータを共有することが出来ます。複数のアプリ間でデータを共有するためにはApp Groupsなどを利用しなければなりません。

Extensionはベースのアプリの拡張機能として提供されるにもかかわらず、ベースのアプリから独立した領域にストレージが割り当てられます。
ベースアプリとExtension間でデータを共有するためには複数のアプリのときと同様にApp Groupsを利用する必要があります。

今回は、NSUserDefautlsとplistファイルでデータを保存する場合を例にして、ベースのアプリとExtension間でデータを共有する方法を確認していきます。

対象

前回の記事(iOS Extensionの作り方講座(Action Extension編))を読んだ方向け
iOS開発アカウントを持っている人(App Groupsを利用するために必要です)
Xcode 6.2

作業内容

Extensionの例として、Action Extensionを利用します。
実際にExtensionの機能の実装はしません。
ベースのアプリで保存したデータが、Extensionからアクセスできることを確認します。

手順

作成手順を示します。
1. iPhoneアプリを作成する
2. Action Extensionを追加する
3. App Groupsを設定する
4. App Groupsを通じて、ベースアプリとExtension間でデータを共有できることを確認する

1. iPhoneアプリを作成する

File -> new -> ProjectからiOSアプリを作成します。
TemplateはiOS -> Application -> Single View Applicationとします。
Product NameはSDSampleとします。
言語はSwiftを選択します。

2. Action Extensionを追加する

File -> New -> TargetからTargetを追加します。
TemplateはiOS -> Application Extension -> Action Extensionとします。
名前はSDActionExtensionとします。
言語はSwiftです。
Action Typeはデフォルト値(Presents User Interface)のままです。
SchemeもActivateします。

3. App Groupsを設定する

Apple developer member centerのidentifiers設定にApp Groupsの項目があります。
ここからApp GroupsのIDを設定します。ID名はgroupから始まる必要があります。
名前を”group.com.hoge”にしたとして以下説明します。

SDSampleのTargetを選択してCapabilitiesタブからApp GroupsをONにします。
このとき、iOSの開発アカウントを尋ねられます。App Groupsを設定した開発アカウントを選択します。もしここでつまづくようであれば、Xcode -> Preferences -> Accounts項目に正しくアカウントが設定されているかどうかを確認しましょう。

作成したApp GroupsのIDが一覧に表示される筈なのでチェックを入れます。

次にSDActionExtensionのTargetについても同様にApp GroupsをONにしgroupのIDのチェックを入れます。

4. App Groupsを通じて、ベースアプリとExtension間でデータを共有できることを確認する
NSUserDefaultsのデータを共有する

SDSample/ViewController.swiftのviewDidLoad内に以下を追記します。

let groupName = "group.com.hoge"
let commonUserDefaults = NSUserDefaults(suiteName: groupName)!
// save
commonUserDefaults.setValue(1, forKey: "Sample")
// load
println(commonUserDefaults.valueForKey("Sample"))
println(commonUserDefaults.valueForKey("Extension"))

次に、SDActionExtension/ActionViewController.swiftのviewDidLoad内に以下を追記します。
既にviewDidLoad内にテンプレートコードが書かれていますが、Extensionとしての機能は今回利用しないので削除してしまって問題ありません。

let groupName = "group.com.hoge"
let commonUserDefaults = NSUserDefaults(suiteName: groupName)!
// save
commonUserDefaults.setValue(1, forKey: "Extension")
// load
println(commonUserDefaults.valueForKey("Sample"))
println(commonUserDefaults.valueForKey("Extension"))
}

SDSampleとSDActionExtensionは異なるストレージを保持するので、NSUserDefaults.standardUserDefaults()を使用した際には、互いに保存したデータにアクセスすることは出来ませんが、App Groupsを経由することで、データの共有が可能になります。

まずはSDSampleをTargetに設定しビルド&実行します。
次にSDActionExtensionをTargetに設定しビルド&実行します。
SDSampleを実行した際に”Sample”という名前のKeyに値を保存し、
SDActionExtensionの実行時に”Sample”のKeyが取得できることを確認しています。
ビルドする順番を逆にした場合も同様です。

plistファイルのデータを共有する

SDSample/ViewController.swiftのviewDidLoad内に以下を追記します。
(NSUserDefaultsに関するコードは消してしまいましょう)

let groupName = "group.com.hoge"
let fileManager = NSFileManager.defaultManager()
let samplePlistData = ["id": 1, "name": "Sample"] as NSDictionary

let groupPath = fileManager.containerURLForSecurityApplicationGroupIdentifier(groupName)!.path!
let samplePlistPath = groupPath.stringByAppendingPathComponent("sample.plist")
let extensionPlistPath = groupPath.stringByAppendingPathComponent("extension.plist")
// save
let saveData = NSKeyedArchiver.archivedDataWithRootObject(samplePlistData)
fileManager.createFileAtPath(samplePlistPath, contents: saveData, attributes: nil)

// load
if fileManager.fileExistsAtPath(samplePlistPath) {
    println("Exist: \(samplePlistPath)")
    let loadData = NSData(contentsOfFile: samplePlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}
if fileManager.fileExistsAtPath(extensionPlistPath) {
    println("Exist: \(extensionPlistPath)")
    let loadData = NSData(contentsOfFile: extensionPlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}

次に、SDActionExtension/ActionViewController.swiftのviewDidLoad内に以下を追記します。

let groupName = "group.com.hoge"
let fileManager = NSFileManager.defaultManager()
let extensionPlistData = ["id": 1, "name": "Extension"] as NSDictionary

let groupPath = fileManager.containerURLForSecurityApplicationGroupIdentifier(groupName)!.path!
let samplePlistPath = groupPath.stringByAppendingPathComponent("sample.plist")
let extensionPlistPath = groupPath.stringByAppendingPathComponent("extension.plist")
// save
let saveData = NSKeyedArchiver.archivedDataWithRootObject(extensionPlistData)
fileManager.createFileAtPath(extensionPlistPath, contents: saveData, attributes: nil)

// load
if fileManager.fileExistsAtPath(samplePlistPath) {
    println("Exist: \(samplePlistPath)")
    let loadData = NSData(contentsOfFile: samplePlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}
if fileManager.fileExistsAtPath(extensionPlistPath) {
    println("Exist: \(extensionPlistPath)")
    let loadData = NSData(contentsOfFile: extensionPlistPath)!
    let dic = NSKeyedUnarchiver.unarchiveObjectWithData(loadData) as NSDictionary
    println(dic)
}

NSUserDefaultsの場合と同様にビルドして確認してみましょう。
お互いのplistファイルにアクセスできる筈です。

2015年4月2日木曜日

iOS Extensionの作り方講座(Action Extension編)

はじめに

Extensionというのは、1つのアプリの範囲を超えた機能を実装するための仕組みです。iOS8以降から利用できるようになった機能の一つです。

例えば、Extensionを使って以下のような機能が実装できるようになります。
・Safariで見つけた画像を、独自の画像コレクションアプリに取り込む
・カスタムキーボードアプリを作成して、他のアプリの入力UIとして使用する
・iPhone通知センターのTodayビューに独自のウィジェットを追加する

以上のように、Extensionは他のアプリと連携する機能の実装や、iPhoneの標準機能の拡張を実現するための仕組みです。Extensionを利用することでアプリ同士の密な連携が取れるようになるので、iPhoneそのもののカスタマイズ性が上がりアプリユーザにとっても嬉しい仕組みです。
Extensionを開発するためには、Extensionのベースとなるアプリが必ず必要になります。ベースとなるiOSアプリをApp StoreからインストールしたときにExtensionも一緒にインストールされます。

前回の記事を読んだ方は気づいたかもしれませんが、
以前紹介したApple Watch向けアプリも実はExtensionの一種です。

これからExtensionを利用した開発が増えていくことが予想されます。
今回は、Extensonに慣れるための簡単なサンプルアプリの作成チュートリアルを行います。

対象

iOSアプリを少しでも作成したことのある人
Extensionを全く利用したことがない人
開発言語はSwift
Xcode 6.2

作成するもの

今回は以下のExtensionを作成します。
・Safariブラウザを閲覧しているときに、閲覧しているページと同じページを独自アプリで開くExtension

使用するExtension

今回はAction Extensionを利用します。
Extensionは特定の拡張機能を実装するのに特化した仕組みなため、あらかじめ利用できるExtensionの種類が定められています。

Action Extensionというのはホストアプリ(今回の場合はSafari)のコンテンツを操作・表示するためのExtensionです。Extensionの中でも特に汎用性の高い機能です。

今回のExtensionではSafariブラウザで開いているページのURLをExtension経由でゲストアプリ(つまり独自アプリ)に教えてあげることで、同じページを開かせます。

作成手順

概要

作成手順を示します。
1. iPhoneアプリを作成する(Extensionのベースとなるアプリです。)
2. Action Extensionを追加する
3. Web Viewを追加する
4. SafariブラウザからURLを受け取りWeb Viewに表示する

1. iPhoneアプリを作成する

iOSアプリを作成する通常の方法と何も変わりません。

File -> new -> ProjectからiOSアプリを作成します。

TemplateはiOS -> Application -> Single View Applicationとします。
Product NameはAETSampleとします。
言語はSwiftを選択します。Core Dataは使用しません。

2. Action Extensionを追加する

File -> New -> TargetからTargetを追加します。
TemplateはiOS -> Application Extension -> Action Extensionとします。
名前はAETExtensionとします。
言語はSwiftです。
Action Typeはデフォルト値(Presents User Interface)のままです。

Activate Scheme?に対してはActivateと答えましょう。
ExtensionをビルドするためのSchemeが追加されます。

3. WebViewを追加する

AETExtension/MainInterface.storyboardを開きます。
初期状態でUIImageViewのUIが用意されていますが、今回は使用しないためUIImageViewは削除します。
“Done”ボタンを含むNavigation Barは再利用できるのでそのままでOKです。

UIImageViewと同じ場所に置き換えるようにして、UIWebViewを追加しましょう。

さらに、ActionViewControllerファイルにIBOutlet参照を張ります。

4. SafariブラウザからURLを受け取りWeb Viewに表示する

次はActionViewControllerを編集していきます

viewDidLoad内に既にコードが書かれていると思います。
このコードはstoryboardにデフォルトで配置されていたUIImageViewに対するコードです。ホストアプリ(Safariブラウザ)から値を受け取る処理は共通部分が多いので、既存のコードを編集する形で実装していきます。

viewDidLoadを以下のように編集してください。

// ActionViewController
    override func viewDidLoad() {
        super.viewDidLoad()

        // Get the item[s] we're handling from the extension context.

        // For example, look for an image and place it into an image view.
        // Replace this with something appropriate for the type[s] your extension supports.
        for item: AnyObject in self.extensionContext!.inputItems {
            let inputItem = item as NSExtensionItem
            for provider: AnyObject in inputItem.attachments! {
                let itemProvider = provider as NSItemProvider
                if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as NSString) {
                    itemProvider.loadItemForTypeIdentifier(kUTTypeURL as NSString, options: nil, completionHandler: {
                        [weak self] (data, error) in
                        if error != nil {
                            println(error)
                        }
                        if let url = data as NSURL? {
                            println("url: \(url)")
                            self?.webView.loadRequest(NSURLRequest(URL: url))
                        }
                    })
                }
            }
        }
    }
ポイント1
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as NSString) {
    itemProvider.loadItemForTypeIdentifier(kUTTypeURL as NSString, options: nil, completionHandler: {...})
}

Action Extensionによりホストアプリから受け取る値は様々です。
更にホストアプリから受け取る値は複数存在することもあり、どのような値が来るかは分からないため、受け取った値の種類を判別する必要があります。
既存のコードではUIImageを受け取るためにkUTTypeImageが指定されていました。
今回はURLを受け取るのでkUTTypeURLを指定します。

ポイント2
self?.webView.loadRequest(NSURLRequest(URL: url))

URLタイプの値を受け取ったときにwebViewからURLリクエストを行いwebページを読み込みます。

実装はこれで終了です。

デモ

Xcodeシミュレータで実際に動かしてみましょう。

TargetをAETExtensionに変更します。
Command + control + [でTargetの切り替えが出来ます。
Command + Rで起動しましょう。

起動する際にExtentionを起動するホストアプリを選択する必要があります。
Safariを選択しましょう。

Safariの画面下部のツールバーのアクティビティボタン(真ん中)を選択します。
表示されるバーの中にAETExtensionというボタンがある筈です。
無い場合は下のバーのその他を選択し、AETExtension項目にチェックを入れましょう。

AETExtensionを選択すると、AETSampleアプリが起動し、Safariと同じWebページが表示されます。

補足

できあがったアプリはこちらからダウンロードできます。
https://github.com/takanori-matsumoto-mulodo/AETSample

2015年3月24日火曜日

Apple Watch向けWatchKitアプリの作り方講座

はじめに

Xcode6.2よりWatchKitのAPIが一般向けに公開され、シミュレータ上で誰でもApple Watch向けアプリが開発できるようになりました。

開発するための情報が未だ少ないですが、既にアプリ開発に取りかかっている開発者も多いようです。
今回は、WatchKitアプリを全く開発したことが無いiOS開発者向けに、WatchKitアプリ作成のチュートリアルを行います。

対象

iOSアプリを少しでも作成したことのある人
言語はSwiftを使用しますが、objective-Cでもやり方に大きな違いはありません

つくるもの

GitHub上に、WatchKitのサンプルアプリが公開されています。
今回はこちらのアプリを参考に、WatchKitアプリの作成方法を順番に説明していきたいと思います。
https://github.com/kostiakoval/WatchKit-Apps

リポジトリ内には、シチュエーションに合わせた機能ごとにサンプルが用意されています。今回作成していくサンプルアプリは3. AppsCommunicationです。

ペアリングされたApple WatchとiPhone間で、AppleWatch上で押したボタンに応じてiPhoneにアクションが送れる、というアプリです。

作成手順

概要

作成手順を示します。
1. iPhoneアプリを作成する
2. iPhoneアプリと連携するWatchKitアプリをTargetとして追加する
3. [iPhone側実装] Apple Watchでボタンを押したときの処理を設定する
4. [iPhone側実装] Apple Watchからのアクションを受け取る処理を設定する
5. [WatchKit側実装] ボタンのUIを設定する
6. [WatchKit側実装] ボタンを押したときのiPhoneへのアクションの送信処理を設定する

1. iPhoneアプリを作成する

WatchKitアプリはApple Watch上で単独で起動するアプリではありません。
iOSアプリの拡張機能として提供されます。開発方法も、Xcode上でiOSアプリにTargetを追加する形で実装していきます。
作成したWatchKitアプリは、App StoreからiOSアプリをインストールした際に、ペアリングしたApple Watchにインストールされます。

File -> new -> ProjectからiOSアプリを作成します。

TemplateはiOS -> Application -> Single View Applicationとします。
Product NameはMyAppsCommunicationとします。
言語はSwiftを選択します。Core Dataは使用しません。

2. iPhoneアプリと連携するWatchKitアプリを作成する

WatchKitのTargetを追加することで、既存のiOSアプリをApple Watch向けに拡張することが出来ます。
File -> New -> TargetからTargetを追加します。
TemplateはiOS -> Apple Watch -> WatchKit Appとします。
言語はSwiftです。
Include Notification Sceneにチェックが入った状態で、Finishします。
Include Notification Sceneについては、Apple Watch Programming Guideに書かれている通り、Notification機能を使用するつもりが無くてもチェックを入れることが推奨されています。

引用:

If you plan to implement a glance or custom notification interface, select the appropriate checkboxes.
For notification interfaces, it is recommended that you select the Include Notification Scene checkbox, even if you do not plan on implementing that interface right away. Selecting that checkbox adds an additional file to your project for debugging your notification interfaces. If you do not select that option, later you must create the file manually.

Activate “MyAppsCommunication WatchKit App” scheme?のダイアログに対しては、Activateします。

3. [iPhone側] アクションを受け取ったときのViewの更新を設定する

Apple Watch上でボタンを押したときに、iPhone画面上のラベルの文字が変わる、という機能を想定します。

Templateとして作成されているViewControllerに、
・ラベル
・ラベルの文字を変えるメソッド
をそれぞれ実装しましょう。

Main.storyboardとViewController.swiftを2つ開きます。
ファイル名をダブルクリックするなどして、同時に2つのウィンドウが見えるようにしましょう。

Main.storyboard上でViewControllerの画面中央にLabelを配置します。
右下のウィンドウからLabelをドラッグすることで配置できます。Label領域は広めに取りましょう。

storbyboard上で配置したLabelを選択し、ViewControllerのソースコードにOutletの参照を作成します。参照の名前はlabelとします。

ViewController上にlabelの文字を変更するメソッドを追加します。

// ViewController
    @IBOutlet weak var label: UILabel!
    func changeLabel(text: String) {
        label.text = text
    }
4. [iPhone側] Apple Watchからのアクションレシーバーを設定する

AppDelegate.swiftを編集します。
以下の処理を追加します。

// AppDelegate.swift
    func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {

        if let info = userInfo as? [String: String] {
            let VC = window?.rootViewController as ViewController
            VC.changeLabel(info["label"]!)
            reply(["response" : "success"])
        } else {
            reply(["response" : "failed"])
        }

    }

WatchKitから受け取った処理を設定するためのデリゲートメソッドはhandleWatchKitExtensionRequestで設定できます。

ポイント1
if let info = userInfo as? [String: String] {
} else {
}

この書き方はswiftでよく使う定型句です。
optional型の値をlet定数に入れてif分で評価することで、nil値の評価を行っています。userInfoの値がnilで無かったときはif ~ elseでinfo変数として使用できます。

ポイント2

userInfo、つまりWatchKitから受け取った値がnilでなかった時は、ViewControllerのchangeLabelメソッドを呼び出し、画面上の文字を変えます。画面上に表示する文字は、WatchKitから受け取ります。

ポイント3

replyはWatchKitからのcallback関数です。
reply関数が呼ばれなかった場合、WatchKit側でwarningが出るため、呼ぶようにしましょう。
引数を空データにしたければ、

reply([NSObject : AnyObject]())

とすればOKです。
今回は、処理の成功/失敗を引数に与えています。

5. [WatchKit側] ボタンのUIを設定する

ソースコードの構成を見てみましょう。
WatchKitのTargetを追加することで、以下の2つのグループが追加されました。
・MyAppsCommunication WatchKit Extension
・MyAppsCommunication WatchKit App

WatchKitのアプリの構造は大きく2つに分かれます。

WatchKit Extension
WatchKitの動的な処理を行います。iOSアプリと同じようにコントローラを実装します。
余談ですが、一見Apple Watch上で全ての処理を行っているように思われがちですが、WatchKit ExtensionはiPhone側にインストールされます。
WatchKit App
Apple WatchにインストールされるUIです。ボタンの配置・画面遷移などを構築していきます。
UIの構築はstoryboard上で行っていきます。
iOSアプリではinterface builder(GUI)でもaddSubviewメソッド(CUI)でもどちらでもUIの構築を行えました。
しかしWatchKitではaddSubviewのようなCUIベースでのUI構築は行えません(2015年3月現在)。

さて、UIを構築していきます。
まずInterface.storyboardを開きます。
ボタンを3つ追加します。やりかたはiOSアプリのときと同じです。

Mainという矢印のついたビューに対して、Buttonを3つ追加します。

6. [WatchKit側] ボタンを押したときのiPhoneへの送信処理を設定する

3つのボタンについて、Interface.storyboardからInterfaceController.swiftに対しアクションの参照を追加します。
アクションの名前はbutton1Tapped, button2Tapped, button3Tappedとしました。

さらに、WKInterfaceController.openParentApplicationメソッドでiOSアプリにアクションの通知を送ります。

Main.storyboardとViewController.swiftのときと同じ具合です。

// InterfaceController
    @IBAction func butotn1Tapped() {
        buttonTapped("button1")
    }

    @IBAction func button2Tapped() {
        buttonTapped("button2")
    }

    @IBAction func button3Tapped() {
        buttonTapped("button3")
    }

    func buttonTapped(label: String) {
        var userInfo = ["label": label]
        WKInterfaceController.openParentApplication(userInfo, reply: {
            (data, error) in
            if let error = error {
                println(error)
            }
            if let data = data {
                println(data)
            }
        })
    }

実装はこれで終了です。

起動

準備

Xcodeシミュレータで実際に動かしてみましょう。

TargetをMyAppsCommunication WatchKit Appに変更します。
ちなみにCommand + control + [でTargetの切り替えが出来ます。
同様にCommand + Rで起動しましょう。

iPhoneのシミュレータとApple Watchのシミュレータが同時に起動します。
もしApple Watchのシミュレータが起動しなければ、
メニューのHardware -> External Displays -> Apple Watchを選択するとシミュレータ画面が追加されます。

起動直後は
iPhoneは、ホーム画面にMyAppsCommunicationアプリがインストールされた状態
Apple Watchはボタンが3つ表示された状態
になるはずです。

iPhone側で、MyAppsCommunicationを選択してアプリを起動します。

デモ

Apple Watch上のボタンを好きに押してみましょう。
iPhone側でのLabelの文字が切り替わる筈です。

補足:デバッグについて

TargetをiOSアプリにすると、iOSアプリのみのデバッグ、
TargetをWatch Appにすると、WatchKitアプリのみのデバッグになります。

iOS, WatchKitの2つを同時にデバッグする方法が以下に紹介されています。
https://mkswap.net/m/blog/How+to+debug+an+iOS+app+while+the+WatchKit+app+is+currently+running+in+the+simulator
やり方は次の通りです。

  1. TargetをWatch Appにして、インストールする
  2. iPhone側でアプリを起動する
  3. アクティブウィンドウをXcodeに戻す
  4. Debug -> Attach to ProcessからMyAppsCommunicationアプリのプロセスを選択する

これで同時にデバッグすることができます。どちらのソースコードについても、ブレークポイントを貼ることができるようになります。

できあがったアプリはこちら
https://github.com/takanori-matsumoto-mulodo/MyAppsCommunication

2015年3月16日月曜日

知らない人向け DI(Dependency Injection)の説明

DIについて調べたので簡単に説明します

DIとは

  • クラス同士の依存関係を設定する仕組みを作ること
具体的に説明すると
  • クラス内のメンバ変数(インスタンス)をセットするための注入関数を用意して、注入関数をクラス外部から呼び出すこと
です。

DIを利用するメリット

  1. 柔軟性が生まれる
  2. テストしやすくなる(テストコードが書きやすい)

実例を見せましょう。
Javaのソースコードで具体例を示します。説明のために細かいところは省きます。

DI が無いコード

public class Factory {
  public void run() {
    System.out.println("Factory run");
  }
}

public class Manager {
  private Factory factory;

  public Manager() {
    factory = new Factory();
  }
  public void runFactory() {
    factory.run();
  }
}

public class DITest {
  public static void main(String[] args) {
    Manager manager = new Manager();
    manager.runFactory();
  }
}
ManagerクラスがFactoryクラスのインスタンス変数をメンバとして保持しているので、ManagerとFactoryは依存関係にあることが分かります。
Factoryクラスのインスタンス変数はManagerクラス内部で生成されています。

DI があるコード

public interface Factory {
  void run();
}

public class PencilFactory implements Factory {
  public void run() {
    System.out.println("Pencil Factory run");
  }
}

public class Manager {
  private Factory factory;
  public void setFactory(Factory f) {
    factory = f;
  }
  public Manager() {
  }
  public void runFactory() {
    factory.run();
  }
}

public class DITest {
  public static void main(String[] args) {
    Manager manager = new Manager();
    manager.setFactory(new PencilFactory());
    manager.runFactory();
  }
}
インターフェースが利用され、変数をセットするためのsetFactory関数が実装されています。
Factoryのインスタンス変数は、Managerクラスの外部(DITest)からsetFactory関数を通じて設定されています。
これが依存性の注入(DI、Dependency Injection)です。


ここで、DIのメリットについてもう一度確認してみましょう。

DIを利用するメリット

  1. 柔軟性が生まれる
  2. テストしやすくなる(テストコードが書きやすい)

1については、インターフェースを利用することにより抽象的な実装が出来るようになりました。
クラス内変数を定義する際にインターフェースで定義することで、複数のクラスを受け入れる余裕が生まれました。

2については、インスタンスのモックアップを外部から設定することが出来るようになりました。
様々な条件に対するテストケースを作成するときに、インスタンス、つまり条件を外部から設定する機構が生まれました。

DIはデザインパターンにおける重要な概念です。
エンジニア初心者の方は、ぜひ覚えておきましょう。

2015年3月5日木曜日

Gitのcommitを美しくするtips

  • git commit --amend
    直前のcommitに混ぜて一緒にcommitすることができます
    このときに書いたコミットメッセージで直前のコミットメッセージを上書きすることが出来ます
    以下のようなシチュエーションで利用できます
    • コミットメッセージを打ち間違えたから書き換えたい
    • コミットした後、ソースコードにデバッグコードが混じっていることに気づいた。直前のコミットにデバッグコードの削除分も含めたい
  • git rebase -i head~{n}
    git commit --amendの拡張版です。直前のcommitだけでなく、過去全てのcommit logに対して
    • コミットメッセージを書き換える
    • 複数のコミットを1つのコミットに結合する
    ことができます。
    nにはheadから遡るcommitの数を指定します。
    例えば、
    git rebase -i head~4
    のようになります。 以下のようなシチュエーションで利用できます
    • ローカルでの作業のために細かくcommitはしたいけど、リモートブランチにpushするcommitはまとめたい、といったシチュエーションで利用できます。
  • git add -p
    unstageなソースコードをcommitするためにaddコマンドを実行しますが、git addではファイル単位でしか追加することが出来ません。
    git add -pを使用することでファイル内のセクションごとに追加するかどうかを分けることが出来ます。
    以下のようなシチュエーションで利用できます
    • 試行錯誤して大量の修正を施した後まとめてaddした。同じファイル内に複数の機能の修正が混ざっているので、別々にaddしてcommitしたい。

gitを使い倒して、上手なソースコード管理を目指しましょう!

2015年2月26日木曜日

Capistrano 3を使ってデプロイ作業を自動化する

テストの自動化


デプロイ作業は手動でも行えますが、

  • デプロイ手順のマニュアルを作成する必要がある
  • 手動で行うため、手順を間違えないように集中して作業しなければならない
  • デプロイ作業に時間がかかる

といったデメリットがあります。

その結果

  • デプロイ手順を間違える
  • デプロイしている暇がないから、リモートサーバー上で直接ファイルを編集する

といった問題が発生しがちです。

デプロイ作業を自動化することにより

  • 作業ミスが無くなる
  • デプロイ作業にかかる時間・労力が限りなく減る

といった恩恵を得ることが出来ます。

特に納期前などの繁忙期にデプロイが自動で行えるかどうかの違いは非常に大きいです。
時間をかけて学ぶだけの価値は間違いなくあります。

Capistrano


Capistranoとはデプロイ作業を自動化するためのフレームワークです。
Capistrano自体はrubyのフレームワークですが、デプロイ対象のプログラム言語は問いません。
phpのプログラムなどでもCapistranoによりデプロイ作業を自動化することが出来ます。
しかし非常に便利なフレームワークである一方、学習コストが高いため手を付けられずにいる方も多いのではないでしょうか?

今回はCapistranoを使用したことが無い人を対象として、デプロイ自動化手順を少しずつ説明していきたいと思います。量が多いのでいくつかに分割して少しずつ説明していきます。

Capistranoの最新バージョンは3ですが、2と3とでは設定方法が大きく異なるので使用するバージョンに注意してください。今回はCapistrano 3を使用していきます。

お題


今回はデプロイ自動化の一番の基本となる

  • リモートサーバーにソースファイルをアップロードする作業

をCapistrano 3を使って自動化することにします。

概要


  • ローカルマシン上にcapistranoをインストールする
  • ローカルマシン上のソースコードをGitHubのリモートリポジト* リ上にアップロードする
  • ローカルマシン上でデプロイコマンドを実行する
  • GitHubリポジトリ上のソースコードがリモートサーバーにアップロードされる
  • デプロイコマンドはローカルのリポジトリ上で実行します。

前提条件


  • Gitを使用したことがある
  • リモートサーバーがある
  • リモートサーバーにSSHでログインできる

作業手順


ローカルマシン

1. ローカルマシン上にプロジェクトファイルを作成します。

mkdir cap_test
cd cap_test

2. アップロードしたいソースファイルを作成します。

touch test_file

3. GitHubでリモートリポジトリを作成しpushします

git remote add origin git@github.com:{your_account}/cap_test.git
git add .
git cm
git push origin master

リモートリポジトリの名前はcap_testとしました。
{yout_account}にはGitHubのアカウント名が入ります。

4. capistranoをインストールします

bundle経由でインストールします

bundle init # Gemfileが作成される

add line Gemfile

gem 'capistrano', '~> 3.0.1'

bundle install # capistranoがインストールされる

5. capistranoの初期化

cap install # capistrano用のディレクトリ・ファイルが作成されます

6. デプロイ設定ファイルの編集

デプロイの設定のために編集するファイルは2種類あります。

  • グローバルな設定ファイル
    config/deploy.rb # デプロイ全体の設定を行います。
  • 環境ごとの設定ファイル
    config/deploy/*.rb # 環境ごとの設定を行います。

今回はstaging環境を前提としたデプロイを行います。
編集するファイルは以下の2つです。

  • config/deploy.rb
  • config/deploy/staging.rb

    deploy.rbファイルを編集

    lock '3.3.5'
    set :application, 'cap_test'
    set :repo_url, 'git@github.com:{your_account}/cap_test.git'

    deploy/staging.rbを編集する

    リモートサーバーの情報に合わせて以下を設定します

    • ホストIPアドレス
    • ホストユーザ名
    • ローカルマシン上に保存したSSHの秘密鍵

    server 'xx.xx.xx.xx', user: 'remote-server-user', roles: %w{web}
    set :ssh_options, {
    keys: %w(~/.ssh/key.pem),
    }

7. ssh-agentの設定

デプロイ時にリモートサーバー上でgit cloneが実行されますが、
このとき秘密鍵が必要になります。
リモートサーバー上に秘密鍵を設定することでもgit cloneできますが、
秘密鍵の転送・設定は厄介です。特にリモートサーバーが複数ある場合その都度設定するのは面倒です。
ssh-agentを使用することにより、ローカルマシンの秘密鍵を使用してリモートサーバー上でgit cloneすることができます。

以下のコマンドによりgit cloneに用いる鍵をssh-agentに登録します(Macの場合、OSにより方法が異なります)

ssh-add ~/key.pem

8. ローカルマシン上での設定は以上で終了です。

GitHubに最新ファイルをコミットします。

git add .
git commit
git push origin master

リモートサーバー

次にリモートサーバー上での設定を行います。
リモートサーバーにログインします。

1. リモートサーバ上にアップロード用のディレクトリを作成する

デフォルトの設定では/var/www以下にアップロードされます
もしなければ作成します。

mkdir /var/www

デプロイ時にwww以下にさらにディレクトリが作成されるので、所有者も変更しておきます。

chown remote-server-user /var/www

2. gitのインストール

デプロイ時に、リモートサーバー上でgit cloneが実行されます。
もしgitがインストールされていなければインストールしておきます。

yum -y install git

デプロイ

デプロイの準備が整いました。
ではデプロイしてみましょう。
ローカルマシンのプロジェクトディレクトリ上でデプロイコマンドを実行します。

cap staging deploy

さくっとデプロイできましたか?

2015年2月18日水曜日

ブラインドSQLインジェクション

Webシステムを攻撃する手法としてSQLインジェクションが有名です。 今回は実際に、SQLインジェクションによりパスワードを盗む方法を説明します。

ログインフォームから送信したIDとパスワードが、ソースコード内で以下のような式でクエリされるとします。 また有効なIDが1つ判明しているものとします。ユーザから送信されたIDとパスワードがDB上の値と一致していなければ、クエリの返り値は0件なのでログインすることは出来ません。

  select * from user where id='$id' AND pass='$pass'

  1. 必ずログインする
  2. パスワードに以下の文字列を入力します。

    ' OR 1 OR '
    これは以下のように展開され実行されます。
      select * from user where id='$id' AND pass='' OR 1 OR ''
    
    OR 1が挿入されることにより無条件でuserがselectされてしまいます。 クエリ結果の最初のユーザをログインユーザとする、と処理されるシステムの場合、この方法によりパスワードを知らなくても必ずログインすることができます。

  3. パスワードを見つける
  4. 上記の方法によりログインそのものは成功しますが、パスワードはまだ判明していません。
    挿入する文字列を工夫することで更にパスワードまで判明させることが出来ます。

    ' OR 1 OR '
    としていたのを
    ' OR pass like '%' OR '
    と書き換えます。 これは以下のように展開されます。
      select * from user where id='$id' AND pass='' OR pass like '%' OR ''  
    
    %は0文字以上の任意の文字列にヒットします。 この状態でもやはりログインに成功します。 次は以下の文字列をパスワードとして送信します。
    ' OR pass like 'a%' OR '
    これはpassの最初の1文字目が'a'であるもののみにヒットします。
    この結果はログイン成功 / ログイン失敗に分かれます。
    もし指定したIDのuserのpassの1文字目がaであった場合は、ログインに成功します。
    もし指定したIDのuserのpassの1文字目がaでなかった場合は、ログインに失敗します。
    これをa ~ Z, 0 ~ 9の全ての文字に対して行うことで確実にpassの最初の1文字を特定することが出来ます。
    1文字目を特定することが出来れば同じ原理で2文字目以降も特定可能です。
    もし全ての文字に対してログインが失敗した場合、次の文字はありません。

    このようにして、パスワードを盗むことができます。

2015年2月16日月曜日

Mysqlの接続について【localhost】と【127.0.0.1】の違い


ソケット(socket)ファイルとは
 ソケットファイルとはUNIXOSのプロセス間通信で使用されるファイルである。

特徴:
・ソケットファイルを用いることでサーバー/クライアント間のプロセス間通信をファイルを通じて行うことができる。
・ファイルを使用する為、サーバー/クライアントが同一ホストの場合に限られる。
・ソケットファイルを用いるとTCP/IP接続と比べ、高速な通信が可能となる。

【localhost】と【127.0.0.1】の違いについて
 localhost】はソケットファイルを使用した接続が行われるが、【127.0.0.1】では、TCP/IPを利用した接続が行われます。


では、Mysqlの接続について確認してみましょう

コマンドラインでmysqlに接続
下記のコマンドラインで設定を確認してみます
$php --ri mysqli

mysqli

MysqlI Support => enabled
Client API library version => mysqlnd 5.0.11-dev - 20120503 - $Id: f373ea5dd5538761406a8022a4b8a374418b240e $
Active Persistent Links => 0
Inactive Persistent Links => 0
Active Links => 0

Directive => Local Value => Master Value
mysqli.max_links => Unlimited => Unlimited
mysqli.max_persistent => Unlimited => Unlimited
mysqli.allow_persistent => On => On
mysqli.rollback_on_cached_plink => Off => Off
mysqli.default_host => no value => no value
mysqli.default_user => no value => no value
mysqli.default_pw => no value => no value
mysqli.default_port => 3306 => 3306
mysqli.default_socket => /tmp/mysql.sock => /tmp/mysql.sock
mysqli.reconnect => Off => Off
mysqli.allow_local_infile => On => On

上記により、デフォルトは/tmp/mysql.sockのソケットファイルを使用することが分かりました。

実際にサンプルを作成して確認してみます。サンプル 名はmysqli.phpにする。
<?php
 //定義
 $host = 'localhost';
 $username = 'fuel_dev';
 $password = 'tammulodo';
 $database = 'fuel_dev';
 $port = '3306';
 $socket = '';

 //mysqliクラスのオブジェクトを作成
 $mysqli = new mysqli($host, $username, $password, $database, $port, $socket);
 //エラーが発生したら
 if ($mysqli->connect_error){
   print("接続失敗:" . $mysqli->connect_error);
   exit();
 }
 print("接続に成功しました\n");

 //切断
 $mysqli->close();

コマンドラインで実行
$ php -f mysqli.php
接続に成功しました

上記のサンプルにより、ホスト名は【localhost】を指定し、ソケットファイルを使用してmysqlに接続することが確認出来ました。

Webブラウザからmysqlに接続
上記のサンプルをFuelPHPの実行フォルダにコーピーし、実行すると下記のエラーが発生しました。

Webサーバーからmysqlに接続する時、TCP/IP経由で接続する必要がある為、ホスト名が明示に【127.0.0.1】を指定しなければなりません。
では、上記のサンプルにホスト名を”localhost”から"127.0.0.1"に変更して、再度実行してみると、正常に接続することが出来ました。

まとめ
コマンドラインでmysqlに接続する時、ソケットファイルを使用しても、TCP/IP経由してもmysqlに接続することが出来ますが、Webブラウザからmysqlに接続する時、TCP/IP経由で接続する必要があるので、ホスト名が明示に指定しなければなりません。

2015年2月13日金曜日

BloggerでcodeのSyntax Highlighting

Bloggerに限りませんが、Blog PostでcodeをSyntax Highlightingする一つの方法は、google-code-prettifyを使用することです。

https://code.google.com/p/google-code-prettify/

他の方法としてはGithub GISTをembedすれば簡単ですが、一つのPageで多用するとPageが重くなるのでそちらを使いたい場合は適度に自重してください。

以下、Bloggerでのgoogle-code-prettifyの導入方法です。

スタイルの選択

好みのスタイルをhttp://google-code-prettify.googlecode.com/svn/trunk/styles/index.html から選択します。本ブログではSunburstを採用します。異論は認めません。 URLをコピーします。Sunburstの場合は、https://code.google.com/p/google-code-prettify/source/browse/trunk/styles/sunburst.css です。以下の手順は、Sunburstと仮定して書いています。

HTMLエディタ

Blogger Admin PanelにあるSide Menuの、Template -> Edit HTMLでHTMLエディタ画面を開きます。

スタイルの貼り付け

head tag内の末尾に以下のコードを貼り付けます。

<link rel="stylesheet" type="text/css" href="https://google-code-prettify.googlecode.com/svn/trunk/styles/sunburst.css">

templateがxmlである場合、末尾に / が必要です。

<link rel="stylesheet" type="text/css" href="https://google-code-prettify.googlecode.com/svn/trunk/styles/sunburst.css" />

スクリプトの貼付け

HTMLエディタ内のbody tagの末尾に以下のコードを貼り付けます。

<script type="text/javascript" src="https://google-code-prettify.googlecode.com/svn/trunk/src/prettify.js"></script> <script type="text/javascript">prettyPrint();</script>

Save templateボタンを押下します。これで設定は完了です。


動作確認

preタグ

pre tagのclassにprettyprintを指定すると、syntax highlightされます。

    var logging = context.services.logging;
    logging.debug('Got request ${request.uri} .');

AndroidのListViewでSwipeRefreshLayoutを使ってみた

こんにちは。岡田です。

今回は前回に続いて、ListViewについての記事を書く。
ListViewのRefreshするUIついて、色々な方法があると思うが、
今回はListViewを下にSwipeすることでRefreshができるSwipeRefreshLayoutを使ってみた。
SwipeRefreshLayoutは、Support Library revision 19.1.0から追加された。

Dart appengineでloggingする方法

Dart appengineで、print()やlogging libraryを使用してもterminalにlogが吐かれませんでした。logが吐かれないとまともに開発することは難しいため、調査しました。

appengine packageのソースコードをチラ見する

appengine.dartをのぞいてみると、

export 'api/logging.dart';

とあり、import ‘package:appengine/appengine.dart’;したsource codeではloggingが使用可能であることが分かります。

api/logging.dartではLoggingというabstract classが定義されています。そこでは、functionとしてcritical(), error(), warning(), debug()が定義されています。明らかにLog Levelを意味しています。 LogLevel classでは、各log levelが

static const LogLevel CRITICAL = const LogLevel._('Critical', 4);

などと定義されています。


実体を探すと、appengine/src/api_impl/logging_impl.dartで、appengineのlogging機構を使用するコードになっているようです。(なにやらIssueやらTODOやら見えますね。)

つまり、Dartのappengine packageにはappengine用の独自のlogging libraryが含まれていますので、標準libraryのlogging等をimportする必要はありませんし、そして(少なくとも現在は)使うこともできないようです。

サンプルコードを検索する

そして、おもむろにサンプルコードを検索し、loggingをしている箇所を発見しました。 https://github.com/dart-lang/appengine_samples/blob/master/cloud_datastore/bin/server.dart#L97

var logging = context.services.logging;
logging.debug('Got request ${request.uri} .');

確かにappengineのloggingを利用しています。

しかし、このサンプルコードがあるサンプルプロジェクトを動かしてみても、logが吐かれません。print()デバッグもできません。

Github IssuesやGoogle Groupを検索する

関連するGithub IssuesやGoogle Groupを検索したところ、以下のスレッドを発見。

https://groups.google.com/a/dartlang.org/forum/#!searchin/cloud/log/cloud/Eyi-BfujX0U/Y8QW2kHycm8J

現在はまだ、–verbosity debug フラグを付けないとlogが表示されないようで、将来はこのフラグ無しでlogがterminalに表示されるようになるそうです。

そこで、

$ gcloud preview app run app.yaml

ではなく、

$ gcloud --verbosity debug preview app run app.yaml

として起動したところ、無事loggingとprint()の内容がterminalに表示されました。

冗長な表示内容

しかし、–verbosity debugでは他の冗長な内容が表示されすぎて辛い。なにか他の起動optionはないかと

$ gcloud preview app run --help

してみると、–log-level というoptionを発見。

しかし、–log-level=“debug” (または–log-level debug)を付加しても、自分が試した限りでは期待する動作をしてくれませんでした。

というわけで、

現状ではまだ、–verbosity debugにするしかないのかも。まあまだベータ版ですし、これからのアップデートに期待。

DartEditorなどのdebuggerとの統合ももう少し先になるようです。 http://stackoverflow.com/questions/26807828/how-can-i-debug-a-dart-appengine-app

Google App Engine Managed VM Dartを使ってみる 後編

この記事は、前回のGoogle App Engine Managed VM Dartを使ってみる の続きです。

gcloud SDKのインストール

Google cloud SDKをインストールします。 手順については、https://cloud.google.com/sdk/#Quick_Start を参照してください。 忘れずに $ gcloud auth login しましょう。

Google Cloudにsample projectを作る

WebブラウザのGoogle Developers Consoleなどから、Dart GAEの動作確認用のsample projectを作ります。 作成したら、そのsample projectのproject-idをセットします。 $ gcloud config set project

gcloudを最新版にする。

$ gcloud components update app

Sample project でhello world

ここから、Dart app engineのprojectをlocalに作成していきます。

$ mkdir helloworld && cd helloworld

app.yamlの作成

Appengineアプリケーションにはapp.yamlが必要です。以下の内容でapp.yamlファイルを作成します。

version: helloworld
runtime: custom
vm: true
api_version: 1

Dockerfileの作成

以下の内容でDockerfileファイルを作成します。

FROM google/dart-runtime

pubspec.yamlの作成

Dartアプリケーションなので、pubspec.yamlが必要です。以下の内容でpubspec.yamlファイルを作成します。

name: helloworld
version: 0.1.0
author: <your name>
dependencies:
  appengine: '>=0.2.1 <0.3.0'

dependencies sectionでappengine packageを指定しています。 忘れずに pub getしてpackageをinstallしておきましょう。

hello world コードを書く

Dart appengineは必ずbin/server.dartから起動します。 bin/ directoryを作成します。

$ mkdir bin

bin/server.dartを作成し、以下のコードを書きます。

import 'dart:io';

import 'package:appengine/appengine.dart';

main() {
  runAppEngine((HttpRequest request) {
    request.response..write('Hello, world!')
                    ..close();
  });
}

Docker imagesの取得

起動する前に、docker imagesを取得しておきます。

$ docker pull google/dart-runtime

起動

local環境でappengineを起動します。

$gcloud preview app run app.yaml

ブラウザから、http://localhost:8080にアクセスし、Hello, world!’と表示されていることを確認します。

Deployment

$ gcloud preview app deploy app.yaml
  • Dart GAEはGoogle Container Engineを使用しているため、クレジットカードを登録して課金を有効にする必要があります。 Google developers consoleから、デプロイしたurlにアクセスし、動作を確認して下さい。

まとめ

詳細な解説は、https://www.dartlang.org/server/google-cloud-platform/app-engine/run.html を参照してください。 コードサンプルは、https://github.com/dart-lang/appengine_samples にまとめられています。コードサンプルではgcloudのAPIとしてcloud_datastore、cloud_sql, memcache, modulesなどのサンプルが確認できます。

Dart app engineはベータ版であるため、シリアスなプロジェクトでの採用にはまだ慎重になるべきです。ドキュメントも未整備の部分が多いのですが、ライブラリののコードを読んで楽しめる人は挑戦してみると面白いかもしれません。AppEngineのサービスに乗ってDartでサーバーを構築できるのは大きな魅力なので、折にふれて動向を追っていければと思います。

2015年2月9日月曜日

CollectionViewでPagingする方法

iOSアプリで左右にスクロール(Paging)する画面を作る際は、PageViewControllerを使用することが多いと思います。今回はPageViewControllerの代わりにUICollectionViewを使用してPagingする方法を説明します。 PageViewControllerを使用した場合、 各Pageにindexのプロパティを持たせて、スクロールのdelegateメソッド内で次のpageのindexを求めて、左右のスクロールでそれぞれ設定してあげて・・・、と何かと面倒です。 単純なPagingだけならもっと簡単に出来ます。

  1. StoryboardでCollection View Controllerを追加します。

    Pagingのスタイルの設定はStoryboardで行います。
    OutlineパネルでCollection Viewを選択します。
    Utilityパネルの右から3番目の設定を開きます。
    • LayoutをFlowに設定します。
    • Scroll DirectionをHorizontalに設定します。
    • ScrollingをScrolling Enabledに設定します。
    • Paging Enabledにチェックを入れます。
    Storyboardでの設定は以上で終了です。 次は、表示するViewの設定をソースコード上で行います。
  2. UICollectionViewControllerを継承したCocoa Touch Classファイルを作成します。

  3. 説明のため名前はPagingViewControllerとしました。
    設定するポイントは以下です。
    pagesプロパティ
    UIViewControllerの配列です。  viewDidLoadした際に、UIViewControllerのインスタンスを追加します。 numberOfItemsInSectionの返り値として、pagesのサイズを返します。
    UICollectionViewDelegateFlowLayout
    追加したクラスのdelegateとして設定します。
    viewの表示
    cell.contentViewにpagesのviewをaddSubviewします。
    view, cellのサイズ
    self.view.frame
    collectionView.frame
    cell.contentView.frame
    がそれぞれ等しくなるようにします。

    できあがったソースコードはこちら
    git@github.com:takanori-matsumoto-mulodo/EasyPager.git

    
    import UIKit
    
    let reuseIdentifier = "Cell"
    
    class PagingViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
    
        var pages = [UIViewController]()
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // Uncomment the following line to preserve selection between presentations
            // self.clearsSelectionOnViewWillAppear = false
    
            // Register cell classes
            self.collectionView!.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
    
            // Do any additional setup after loading the view.
            let vc1 = UIViewController()
            vc1.view.backgroundColor = UIColor.redColor()
            pages.append(vc1)
            let vc2 = UIViewController()
            vc2.view.backgroundColor = UIColor.blueColor()
            pages.append(vc2)
        }
    
        override func didReceiveMemoryWarning() {
            super.didReceiveMemoryWarning()
            // Dispose of any resources that can be recreated.
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            pages[0].view.frame = view.bounds
            pages[1].view.frame = view.bounds
        }
    
        // MARK: UICollectionViewDataSource
        override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
            //#warning Incomplete method implementation -- Return the number of sections
            return 1
        }
    
    
        override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            //#warning Incomplete method implementation -- Return the number of items in the section
            return pages.count
        }
    
        override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as UICollectionViewCell
        
            // Configure the cell
            let row = indexPath.row
            let vc = pages[row].view
            cell.contentView.addSubview(vc)
        
            return cell
        }
        
        // MARK: UICollectionViewDelegateFlowLayout
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
            return collectionView.frame.size
        }
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat {
            return 0
        }
        func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat {
            return 0
        }
    }
    
    

Google App Engine Managed VM Dartを使ってみる

こんにちは、naoto@qword.meです。QwordのFounder MemberでProduct Managerをしています。

  • Qwordは現在Private Beta版です。

QwordではServerの一部及びAdmin ConsoleにDartを採用しています。Dartはmodernでsimpleな言語仕様でSDKが充実しており、Browser側とServer側を同一の言語で開発、保守できる優れた言語です。

Server環境では将来有力な選択肢になりそうなGoogle App Engine Managed VMのDartバージョンを使ってみましょう。 ここではセットアップの手順を紹介します。

前提

  • 環境はMac。WindowsとLinuxユーザーは公式サイトにGo
  • Homebrew、Google Cloud Platform、Docker、Dartについての基本的な知識がある、または調べて理解できること。

Dartのインストール

まだDartを入れていなければこれを機会に入れましょう。Homebrewを使うと簡単です。DartEditorをインストールする方法でもいいです。

$ brew tap dart-lang/dart
$ brew install dart dartium

PATHを通します。.bashrcなどに以下を追加します。実際にインストールされたdirectoryは$ which dart などで調べてください。

$ export PATH=$PATH:<installation directory>/dart/dart-sdk/bin

Dockerと関連ツールのインストール

Boot2Docker 1.3.1を以下のリンクからダウンロードし、インストールします。versionは必ず1.3.1にしてください。それ以外のversionでは動きません(2015/2/8/現在)。 https://github.com/boot2docker/osx-installer/releases

これで、boot2docker、docker、VirtualBoxがインストールされます。

Dockerのconfigulation

$ mkdir ~/.boot2docker
$ echo 'ISOURL = "https://github.com/boot2docker/boot2docker/releases/download/v1.3.0/boot2docker.iso"' > ~/.boot2docker/profile
$ boot2docker init

boot2dockerの起動

$ boot2docker up

これで、VirtualBoxにてboot2dockerが起動していることが確認できます。

Docker imageの入手

$ $(boot2docker shellinit)
$ docker pull google/docker-registry

ここでエラーが発生した場合は、boot2dockerのversionを確認すること。

Docker imageの確認

$ docker images

DartVM on dockerのversionを確認。

$ docker run google/dart /usr/bin/dart --version

次回はGoogle App Engineにdeployして動作確認をします。

2015年2月5日木曜日

AndroidのListViewでNetworkから取得した画像をResize後にCacheする

こんにちは。岡田です。

今回は、AndroidのListViewでNetworkから取得した画像をResize後にCacheする方法について書いていく。
AndroidのListViewでImage ResourceをNetworkから取得する場合に
ViewがRefreshされる度に、画像を読み込みを行うとUsabilityが下がる。
このため一度取得したDataは内部でCacheし、再度取得に行かないように実装する。
また、取得した画像の横幅が、端末の横幅よりも広い場合は、画像をResizeする。

2015年2月2日月曜日

UIVisualEffectViewを試してみた(iOS8)

iOS7以前までは、画像に対してBlur効果(磨りガラス効果)を適用するためには外部のライブラリを使用する必要がありました。
しかしiOS8になり、ようやくBlur Effectが既存のAPIとして提供されることになったので試してみました。

  1. Blur効果を適用する

            
                let imageView = UIImageView()
                imageView.frame = view.frame
                imageView.image = UIImage(named: "bg_image.png")!
                view.addSubview(imageView)
    
                let darkBlurEffect = UIBlurEffect(style: UIBlurEffectStyle.Dark)
                let darkBlurView = UIVisualEffectView(effect: darkBlurEffect)
                darkBlurView.frame = CGRectMake(0, 0, 100, 100)
                view.addSubview(darkBlurView)
            
          
    bg_image.pngには好きな画像を選んでください。
    bg_image.pngにBlurがかかったように表示されます。
  2. Blur効果のオプション

    • UIBlurEffectStyle
      1. Dark
      2. Light
      3. ExtraLight
      の3種類が用意されています。
    • ぼかし半径
      変更不可能です。
    • 透明度
      変更可能ですが、alpha値を1未満にするとBlur効果自体が薄れてしまうので、alpha値 = 1で使用することが推奨されています。
    • 色の変更
      UIVisualEffectViewのbackgroundColorを変更しても色は変更されません。Blurの色を変更するためには新しいViewをUIVisualEffectViewの裏側に挿入する必要があります。
    • アニメーションへのBlur
      背景のImageViewやUIVisualEffectViewが動いても、Blur効果は動的に適用されます。
  3. Blur効果の確認テスト

    Blur効果のテスト用コードを書きました。

    ソースはこちら https://github.com/takanori-matsumoto-mulodo/BlurView

              
                git clone git@github.com:takanori-matsumoto-mulodo/BlurView.git
              
             
    で試してみてください

    UIVisualEffectViewをドラッグで動かせるようにして、背景にどのようにBlurがかかるかを確認できます。

2015年1月30日金曜日

WebComponentsを始めよう

こんにちは。naoto@qword.meです。私の記事では、Qwordで採用している要素技術を紹介していきます。まずはWebComponentsです。

WebComponentsとはなにか

ざっくりいうと、カスタムUIコンポーネントを作成、共有するためのWeb標準です。以下の要素で構成されています。

  • Template
  • Custom Elements
  • Shadow DOM
  • HTML Imports

http://webcomponents.org/

Polyfill

残念ながらこれを実装しているブラウザはまだ多くないため JavaScript Polyfillを利用する必要があります。

Polyfillはbowerで管理するのがよいでしょう。(bowerの説明は省略。)

$ bower install webcomponentsjs

htmlの内に以下のコードを書いてwebcomponentsのpolyfillをloadします。Browserがひとつでも要素技術を実装していない場合には、polyfillをloadするようにしています。

<script>
  if ('registerElement' in document
    && 'createShadowRoot' in HTMLElement.prototype
    && 'import' in document.createElement('link')
    && 'content' in document.createElement('template')) {
    // We're using a browser with native WC support!
  } else {
    document.write('<script src="bower_components/webcomponentsjs/webcomponents.min.js"><\/script>');
  }
</script>

Polymer

WebComponents自体は低いレベルの要素技術であり、実際の開発ではその上に構築されたJavaScriptのライブラリを採用することになることが多いでしょう。現時点での代表的なものはPolymerとX-TAGです。QwordではPolymerを採用しています。

次回以降は、必要に応じてWebComponentsの解説を加えつつ、Polymerの解説をしていきます。

2015年1月29日木曜日

Rocketeerを使ってみた

こんにちは。岡田です。
今回は、PHPのデプロイツールであるRocketeerを使ってみたので記事にする。
GitHubにおいてあるアプリケーションを、環境 Aから、リモートの環境 Bにデプロイしようと思う。

FuelPHPをインストールする手順と初期設定のメモ

FuelPHPの初心者です。
FuelPHPをインストールする手順と初期設定についてのメモです。

Mac OS でのFuelPHPのインストール手順
1. oilコマンドのインストール
$ curl get.fuelphp.com/oil | sh

インストールが正常になりましたら、下記のコマンドで確認
$ which oil
/usr/bin/oil

2. FuelPHPのインストール
- 作業フォルダを作成
*)例:~/work/fuelphpを想定する場合
$ cd
$ mkdir work
$ cd work

- FuelPHPのインストール
下記のコマンドで実行すれば、GitHubからFuelPHPの最新の正式リリース版をダウンロードします。
$ oil create fuelphp

3. apacheの設定
- Mac os のデフォルトでapacheをインストールしていますので、それを使えば便利です。
- デフォルトの作業場所は下記になります。
/Library/WebServer/Documents
- apacheからインストールしたfuelphpにアクセス出来るようにシンボリックリンクを設定します。
$ ln -s ~/work/fuelphp/public fuelphp

4. FuelPHPの動作確認
- ブラウザでhttp://localhost/fuelphp/にアクセスしてみます。
FuelPHPWelcomeパージが表示されれば、インストールは完了です。


しかし、下記のようなエラーが発生していますので、最初にTimezoneの設定が必要です。

・エラー情報:
-----------
Fuel\Core\PhpErrorException [ Error ]:
date_default_timezone_get(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected the timezone 'UTC' for now, but please set date.timezone to select your timezone.
-----------

・Timezoneの設定:
下記のファイルを開いて
/work/fuelphp/fuel/app/config/config.php

Line 97辺りに、デフォルトのTimezoneを設定します。
例:
'default_timezone'   => 'Asia/Tokyo',

上記の設定を保存して、再度ブラウザでhttp://localhost/fuelphp/にアクセスし、FuelPHPWelcomeパージが正常に表示するようになりました。

2015年1月27日火曜日

Swift + Realm DB をCocoaPodsで導入する手順

MacにCocoaPodsをインストール

  1. gemを使ってCocoaPodsをインストール

    sudo gem install cocoapods
    

    プレリリースのベータ版が必要になることがあるので、念のため併せてインストール

    gem install cocoapods --pre
    
  2. terminalを再起動する

    忘れがちです。
    再起動後にバージョンを確認しインストールされていることを確認します

    pod -v
    
  3. Set Up

    pod setup
    

XcodeのProjectを作成

Xcodeでの詳しいProject作成方法は省略します
使用する言語はSwiftを選択します
説明のため名前をProjectの名前をSampleとして以下説明します

Xcode ProjectにCocoaPodsを導入する

  1. Xcodeで作成したプロジェクトのディレクトリに移動

    cd project root directory
    ls -la
        Sample
        Sample.xcodeproj
        SampleTests
    
  2. CocoaPodsを初期化

    pod init
    

    Podfileが作成されます

  3. Podfileの編集

    target 'Sample', exclusive: true do
        pod 'Realm'
    end
    
    target 'SampleTests', exclusive: true do
        pod 'Realm/Headers'
    end
    

    ブロックを用いて、ターゲットごとにインストールするpodを分けることが出来ます。
    exclusive: trueとすると、ブロック内のpodのみしかインストールしないようになります。

  4. pod initした同じディレクトリで以下のコマンドを実行します。

    
    pod install
    
  5. Xcodeのプロジェクトを一旦閉じて、Sample.xcworkspaceという名前のプロジェクトを開きます。

    この時点で、Realm DBの導入自体は終了です。

SwiftでのRealm DBの利用

CocoaPodsでインストールしたRealm DBはObjective-Cで書かれているので、
Swiftのプロジェクトでも利用するためにBridging Headerファイルを追加します。
以下のヘッダファイルをプロジェクトに追加します。
Sample-Bridging-Header.h

  1. 以下の行を追記

    #import <Realm/Realm.h>
    
  2. プロジェクトのBuild Settings -> swift compiler

    Objecctive-C Bridging headerの項目に

    Sample/Sample-Bridging-Header.h
    

    を追加する。パスを間違えやすいので注意してください。環境によっては別の階層かもしれません。

  3. 確認
    swiftのファイルに

    RLMObject()
    

    等と書いてエラーにならなければ正しく導入できています。