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