酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Azure Mobile Services(モバイルサービス)の通知ハブに登録されているiOSデバイスに向けてAPNsを使ってプッシュ通知する

スマートフォンを使っていると当たり前の機能のうちに「プッシュ通知」があります。

たとえば、iPhoneやiPadでGmailアプリなどのメールアプリを使っていると、メールを受け取るとユーザーに向けて「メールを受信しました」とプッシュ通知されます。

プッシュ通知を送るための仕組みは、iOS(Apple)、Windows(Microsoft)、Android(Google)とプラットフォームごとに用意されています。当然、実装に必要のための手順がバラバラにも拘わらず、モバイルアプリは複数のプラットフォームでリリースするのも当たり前になっています。プッシュ通知を送るための実装が異なることで工数が増大して苦しんでいた方も多いと思います。

各プラットフォームでのプッシュ通知の実装をラッピングして、簡単にプッシュ通知とデバイスの管理ができるようにするサードパーティ製のサービスには「Parse」や「Amaozon SNS」というものがあります。それらのひとつに「Azure Mobile Service(モバイルサービス)」があります。*1

事前準備

Apple Push Notification Service(APNs)でプッシュ通知を送る場合の処理については、Appleからプログラミングガイドが出ていますのでこのドキュメントを読みます。

用語は異なりますが基本的には「Windows PhoneでAzure Mobile ServiceとNotification Hubを利用して2ステップでプッシュ通知機能を実装する #wpdev_jp - 酢ろぐ!」で説明しているイメージ図と同じです。APNsへプッシュ通知を送信するのにあたり、通知ハブの機能を利用するには「Azure Notification Hubs を使用して iOS アプリにプッシュ通知を送信する | Microsoft Docs」を参考にしてください。

プッシュ通知を受け取るための準備

アプリ側の実装

アプリ側では以下のように実装します。iOS 7.1以前とiOS 8以降で実装が異なります。

-(BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // ...

    // プッシュ通知を受けるためにデバイストークンの要求
    if ([application respondsToSelector:@selector(registerUserNotificationSettings:)])
    {
        // iOS 8 以降
        UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeAlert;
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [application registerUserNotificationSettings:settings];
    } else {
        // iOS 7.1 以前
        UIRemoteNotificationType types = UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge;
        [application registerForRemoteNotificationTypes:types];
    }
    
    return YES;
}

- (void)application:(UIApplication *)application 
    didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
    [application registerForRemoteNotifications];
}

- (void)application:(UIApplication*)application 
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken
{
    // サーバーにデバイストークンを設定する処理を実装する
    NSString* serviceUrl = @"https://{service_name}.azure-mobile.net/";
    NSString* appKey = @"{your application key}";
        
    MSClient* client = [MSClient clientWithApplicationURLString:serviceUrl
                                                 applicationKey:appKey];
                                                    
    // 通知ハブを使う場合は以下のように実装する
    [self.client.push registerNativeWithDeviceToken:deviceToken
        tags:nil completion:nil]; 
}

iOSデバイスに向けてプッシュ通知する

バッチ処理等でiOSデバイスに向けてプッシュ通知を送る場合の処理です。

// 接続文字列
var connnectionString = "Endpoint=sb://example-ns.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<your key>";

// ハブ名
var hubName = "<your hub name>";

// テスト送信か?
// Production(本番)にプッシュ通知を送信する場合は false
// Development(開発)にプッシュ通知を送信する場合は true
var enableTestSend = false;

// 接続文字列からNotificationHubClientオブジェクトを生成する
var hubClient = NotificationHubClient.CreateClientFromConnectionString(
    connnectionString, hubName, false);

// 通知を送信!!!!
var json = "{\"aps\":{\"alert\":\"hage\"},\"app\":{\"id\":\"kazuakix\"}}";
await client.SendAppleNativeNotificationAsync(json);

アプリで通知を受け取る

アプリでプッシュ通知を受け取った後の処理についてです。userInfoには送信されたプッシュ通知のペイロードがパースされた状態で格納されています。

-(void)application:(UIApplication *)application 
    didReceiveRemoteNotification:(NSDictionary *)userInfo 
    fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    if (application.applicationState == UIApplicationStateInactive) {
        // アプリプロセスがバックグラウンドで動作している状態
    } else if (application.applicationState == UIApplicationStateActive) {
        // アプリプロセスがフォアグランドで動作している状態
    }
    
    // pushのペイロードから読み取る
    NSDictionary *app = [userInfo objectForKey:@"app"];
    NSString *userId = [app objectForKey:@"id"];
    
    // 受け取ったよ!!!!!!
    UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"didReceiveRemoteNotification" 
        message:@"received!" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alertView show];
}

*1:Windowsに関しては当事者

電気通信大学の休講情報アプリについて

これは「UEC Advent Calendar 2014」の20日目の記事です。UECについては別の方が書かれていると思うのですが、UECとは東京都調布市にある電気通信大学のことです。

先に書いておくと、僕はUECの現役学生でもOBでもありません。iPhoneで使える「休講情報 for 電気通信大学」アプリを作っている人です。こんなアプリです。

https://itunes.apple.com/jp/app/xiu-jiang-qing-bao-for-dian/id635224999?mt=8&uo=4&at=10l8JW&ct=hatenablog

今日はその「休講情報 for 電気通信大学」アプリについて書きたいと思います。

UECとの出会い

UECとの出会いは、僕が休講情報アプリを作るキッカケでもあります。

2012年当時、僕はぬるぷー氏(@nullpoo)と昨今の休講情報のフォーマットの乱れについて議論を交わしていました。特に東京電機大学で採用されているUNIPAでの休講情報の配信フォーマットについては統一性がなく、ぬるぷー氏もセッションで述べているように問題がありました。

僕はこの頃、ウェブスクレイピングが楽しくて、特に自分がお世話になっている近所の大学のを中心に、色んな大学の休講情報をスクレイピングして遊んでいました。

表記自体バラツキがあり、統一したインターフェイスで休講情報を見ることができれば、学生にとってどんなに楽なのかと考えていました*1

だいたい同じ頃、ぬま氏(@numa08)と出会います。ぬま氏とはTwitterでの絡みも含めてリアルでも色々と話をしたような気がするのですが、でじこちゃんのこと以外覚えていません。でじこちゃんとは、ゲーマーズのマスコットキャラクターの「デ・ジ・キャラット」です。

デ・ジ・キャラット by くーの on pixiv

本題とはあまり関係ありませんが、高校生時代に僕もでじこちゃんにハマってしまい、挙句の果てにトレカ交換オフ会に参加することとなります。多くのバイト代がトレカに注ぎ込まれました*2。初めてインターネットの人と会うキッカケでもありますが本題と関係ないので割愛します。重要なことなので2回書きました。

話の道筋が逸れてしまいましたが、そのぬま氏がUEC生であることと知り、電気通信大学の休講情報をみることができるアプリの開発を着手します。

「休講情報 for 電気通信大学」アプリを支えるインフラ

最初、休講情報アプリを支えるインフラは下記のようになっていました。自宅にちょうどファイルを置いておくためのサーバーがあったので、自宅サーバー上で動き始めました。

f:id:ch3cooh393:20141219231454p:plain

この構成でしばらく問題なかったのですが、住んでいる家が欠陥住宅で水道の水圧があまりにも低すぎてキッチンで洗い物をしているとシャワーがでなくなる、洗濯機を回している時間には水道が一切使えない(トイレも水が溜まらないので連続使用ができない)という問題を抱えていて引っ越すにあたりサーバーを停止しなければいけませんでした。

サーバーが停止されると当然データが更新されなくなり休講情報アプリを使っている方が困るのではないかと心配になり、Amazon Web ServicesやMicrosoft Azureなどクラウド上の仮想マシンが流行り始めていたのもあり移行を決意しました。移行直後はAWS上の、次にAzure上の仮想マシン(Windows Server)を使います。インフラの構成は下記の通りです。

f:id:ch3cooh393:20141219231514p:plain

これが現在運用している形となります。Azure to AWSで無駄に転送量がかかっているのですがこれ以上の改修が面倒くさい工数をかけることができずにそのままになっています。

電気通信大学の休講情報ページをHtml Agility Packを使ってスクレイピングする

先ほどの図の①の電気通信大学の休講情報ページから休講情報の取得処理ではスクレイピングしているのですが、そのスクレイピングをするために使っているのは以下のツールとライブラリ(?)です。

  • Visual Studio 2013
  • ライブラリ: Html Agility Pack
  • ライブラリ: AWS SDK for .NET
  • ライブラリ: Json.NET

12月に入ってからこのブログで「Html Agility Pack」の使い方を紹介する記事をいくつか書いているので読んでもらえると嬉しいです。Html Agility Packを使えば電気通信大学の休講情報を簡単に取得することができます。また、Xamarin.iOSを使えばiPhone上でも同じことができます。

実際には例外処理をかなり書いているのでこのままではありませんが、Html Agility Packを使って休講情報を取得する処理はざっくりと下記のような感じです。

var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(sr.ReadToEnd());

var record = doc.DocumentNode.Descendants("tr").Skip(1);
var query = record.Select(item =>
{
    var datas = item.Descendants("td");

    // クラス名
    var className = datas.ElementAt(0).InnerText.Trim();

    // 休講日
    var date = datas.ElementAt(1).InnerText.Trim();

    // 時限
    var period = datas.ElementAt(2).InnerText.Trim();

    // 授業名
    var subject = datas.ElementAt(3).InnerText.Trim();

    // 教員名
    var lecturer = datas.ElementAt(4).InnerText.Trim();
    if (string.IsNullOrWhiteSpace(lecturer))
    {
        lecturer = "-";
    }

    // 備考
    var note = datas.ElementAt(5).InnerText.Trim();

    // 適当に休講情報を返す
    return new { Subject = subject };
}

このようにして取得した休講情報をJson.NETでjsonフォーマットにシリアライズして、AWS SDK for .NETを使ってS3にjsonファイルをアップロードしています(ざっくりな説明)。

休講情報アプリの今後について

前述した通り、ぬまさんが喜んでくれればという想いで開発を始めた「休講情報 for 電気通信大学」ですが、開発を開始した時点でぬまさんは社会人になっていたというオチがあります。

既に休講情報アプリを開発する理由がありませんが、使い続けてくれている学生さんもいるようです。まじで???

自分が開発したアプリを使ってくれるのは嬉しく、2012年の最初のローンチから2年以上もアプリのメンテナンスを続けることができました。その他にもウェブ側の休講情報のフォーマットが変わっていないからと特にサーバーで大きな改修が必要なかったというとてもシンプルな理由もあります。

しかし、事情が変わりつつあります。iOS向けのアプリを開発している方には周知ですが、AppleはApp Store審査の担当者によって今まで問題とされていなかったことではじくことがあります。休講情報アプリでも同じことが起きました。千葉工業大学向けの休講情報アプリで致命的な不具合が発生して、それを修正させてもらえずストアから削除するということがありました。

一応、アプリ広告を掲載しているものの収益を考えるとサーバー代すらペイできていないので、複数の休講情報アプリをメンテしていくことに限界を感じ始めています。休講情報を収集するためのサーバー側のメンテナンスは仕方ないにしろ、少しでも負担を抑えるために複数あるアプリを集約してメンテナンスコストを下げようと考えいて、各大学の休講情報を統合した「休講情報アプリ(統合版)」を開発進めています。

f:id:ch3cooh393:20141220000255p:plain

現在リリースしている既存の「休講情報 for ○○大学」の今後のアップデートでは休講情報アプリ(統合版)とのデータの共通化を検討します。「休講情報アプリ(統合版)」がリリースされた後は、一定期間をおいて「休講情報 for ○○大学」については、App Storeから取り下げて休講情報も更新を停止させる予定です。

あ、統合版では今まで着手しては挫折していた、Android用のアプリ開発もします。Androidユーザーの方はしばらくお待ちいただけると嬉しいです。来春までにはなんとかします……

さいごに

UEC生でもOBでもない、共通点といえば同じ調布市にいるくらいの人間がUEC Advent Calendarを書かせていただきました。まだUECと関連ができて2年ですが調布祭も楽しませて頂いています。そのお礼(?)ではありませんが、もしiPhoneをお使いでしたら是非休講情報アプリを使ってみてください。

明日のUEC Advent Calendarの担当は、dora22sibuyaさんです。それでは!

*1:これは冷静に考えると学生は所属している大学の休講情報さえ見ることができればよいので、統一したインターフェイスにする必要がなければ楽もくそもない……

*2:基本的にバイト代は画塾とアクリル絵の具と鉛筆に消えていってた

Xamarin.iOSでMD5やSHA1のハッシュ値を計算する

MD5(Message Digest Algorithm 5)は、入力に対して128ビットのハッシュ値を出力するハッシュ関数のことで、不可逆的な一方関数を用いているところから認証などで広く使われています。他にもダウンロードしたファイルが第三者によって作者(配布者)が意図しない改竄がおこなわれていないかの調査に使用することもあります。

本記事では、Xamarin.iOSでMD5やSHA1のハッシュ値を得るためにはどうすれば良いのかご紹介します。

MD5のハッシュ値を取得する

var input = "CH3COOH";

var md5 = System.Security.Cryptography.MD5.Create();
var bytes = System.Text.UTF8Encoding.UTF8.GetBytes(input);
var hash = md5.ComputeHash(bytes);

SHA1のハッシュ値を取得する

var input = "CH3COOH";

var md5 = System.Security.Cryptography.MD5.Create();
var bytes = System.Text.UTF8Encoding.UTF8.GetBytes(input);
var hash = md5.ComputeHash(bytes);

この値が正しいかどうかは、オンラインのハッシュ変換サイト等を使って検証することができます。

関連記事

Xamarin.iOSを使ったアプリを開発中に気付いたことや調べたことをメモしています。Xamarin.iOSを使ってアプリ開発する際に逆引きとしてお使いください。

Xamarin.iOSを使ってiOSデバイスの機種名を取得するライブラリを作りました

過去に「Xamarin.iOSでiOSデバイスの機種名(モデル名)を取得する - 酢ろぐ!」でiOSデバイスの機種(モデル)名を取得する方法を紹介しました。この方法は機種名を取得するものと言うよりかはiPhone7,1といったデバイスモデルの識別子を取得する方法でした。

このデバイスモデルの識別子をiPhone 6 Plusといった人間にとってわかりやすい機種名に変換してくれるXamarin.iOS向けのライブラリを作りました。

UIDeviceHardwareExtensionsを作りました

ソースコードはUIDeviceHardwareExtensions.csの1ファイルだけなのでコピペして使ってください。

使い方

デバイスモデルの識別子を取得する。

var platform = UIDevice.CurrentDevice.GetPlatform();

機種名を取得する。

var modelName = UIDevice.CurrentDevice.GetPlatformName();

Reference

関連記事

Xamarin.iOSを使ったアプリを開発中に気付いたことや調べたことをメモしています。Xamarin.iOSを使ってアプリ開発する際に逆引きとしてお使いください。

Xamarin.iOSでHtml Agility Packを使ってHTMLをスクレイピングする

この記事は「Xamarinのカレンダー | Advent Calendar 2014 - Qiita」の10日目です。

Xamarin Advent Calendarは、Xamarinを使ったアプリ開発のTipsが1日1つずつ紹介されています。この記事はXamarin.iOSについて書いていますが、他の方はXamarin.AndroidやXamarin.Formsなどの記事を書かれており大変有益なTipsが集まっています。

この記事では、HTMLを取得して解析して特定のデータを取得する方法を考えてみましょう。

ウェブスクレイピングとは

さて、本題に入る前にスクレイピングとは何でしょうか。うーん……悩んだときにはWikipedia先生に聞いてみましょう。

ウェブスクレイピングは多くの検索エンジンによって採用されている、ボットを利用してウェブ上の情報にインデックス付けを行うウェブインデクシングと密接な関係がある。(中略)ウェブスクレイピングの用途は、オンラインでの価格比較、気象データ監視、ウェブサイトの変更検出、研究、ウェブマッシュアップやウェブデータの統合等である。

なるほど。スクレイピングとは非構造化データ(HTML)から有益な情報を取り出して利用するということですね。

他にも触れていないことも沢山ありますが、ウェブスクレイピングについてはもっと詳しく知りたい方は「http://bugrammer.hateblo.jp/entry/2014/08/29/141926」をご参照ください。

本記事で紹介すること

本記事で紹介することは以下の通りです。

  1. Xamarin Studioでサンプルアプリ用のプロジェクトを作る(前準備)
  2. Html Agility PackをXamarin.iOSで使えるようにする
  3. スクレイピング処理を実装する

さぁ、Xamarin.iOSでスクレイピング処理するアプリを作ってみましょう。

前準備

前準備ではスクレイピング処理とは関係ない部分に関してさっくりと紹介してしまいます。Xamarin Studioで新しいプロジェクトを作成する方法はこちらをご覧ください。

まずは、Xamarin Studioを起動してプロジェクトを適当に作成します。ここでは、新しいソリューションダイアログの名前(プロジェクト名)欄に「KabukaApp」と入力しました。

f:id:ch3cooh393:20141205210400p:plain

[OKボタン]をクリックするとテンプレートプロジェクトが生成されました。

ボタンとラベルを配置します

Xamarin Studioには、Xcodeを起動しなくてもStoryboardを編集することのできる「iOS Designer」が搭載されています。

左ペインにあるソリューションエクスプローラーからMainStoryboard.storyboardをダブルクリックしてiOS Designerで表示します。Single View Applicationを選択して生成したプロジェクトのRoot View Controllerは白紙の状態ですので、スクレイピング処理を開始するトリガーとなるUIButton(ボタン)スクレイピングした結果を表示するUILabel(ラベル)をそれぞれ配置しましょう。

右ペインのツールボックスからUIButtonとUILabelをドラッグアンドドロップします。適当な位置にViewを配置して、Viewのサイズを調整します。

f:id:ch3cooh393:20141205211935p:plain

ラベルに名前をつけます。右ペインにプロパティウィンドウが表示されているので、WidgetタブのName欄にViewの名前を入力します。名前を入力するとKabukaAppViewController.designer.csにプロパティが追加されます。

f:id:ch3cooh393:20141205213741p:plain

ボタンは既に追加していると思います。スクレイピング処理を開始するトリガーとなるボタンをタップした時のイベントを追加しましょう。プロパティウィンドウのEventタブを選択します。Up InsideにTappedStartという名前を入力します。

f:id:ch3cooh393:20141205213808p:plain

KabukaAppViewController.designer.csを見てみましょう。.designer.csファイルは基本的にXamarin Studioによって自動生成されたコードなので普段は見る必要のないファイルです。Objective-Cを使ってiOSアプリを開発されていた方にはご存知だと思いますが、IBOutletIBActionなどの修飾子は、Xamarin(C#コード)上では属性として定義されています。

// WARNING
//
// This file has been generated automatically by Xamarin Studio from the outlets and
// actions declared in your storyboard file.
// Manual changes to this file will not be maintained.
//
using System;
using Foundation;
using UIKit;
using System.CodeDom.Compiler;

namespace KabukaApp
{
  [Register ("KabukaAppViewController")]
  partial class KabukaAppViewController
  {
    [Outlet]
    [GeneratedCode ("iOS Designer", "1.0")]
    UILabel OutputLabel { get; set; }

    [Action ("TappedStart:")]
    [GeneratedCode ("iOS Designer", "1.0")]
    partial void TappedStart (UIButton sender);

    void ReleaseDesignerOutlets ()
    {
      if (OutputLabel != null) {
        OutputLabel.Dispose ();
        OutputLabel = null;
      }
    }
  }
}

TappedStart(UIButton)メソッドがpartialで宣言されていますので、実体を実装するためにKabukaAppViewController.csを開きます。

using System;
using System.Drawing;
using Foundation;
using UIKit;

namespace KabukaApp
{
  public partial class KabukaAppViewController : UIViewController
  {
    public KabukaAppViewController (IntPtr handle) : base (handle)
    {
    }

    // ボタンを押したらこのメソッドが呼ばれる
    partial void TappedStart (UIButton sender)
    {
      this.OutputLabel.Text = "ボタンが押されました";
    }

// .....

ここまで実装できれば一度iPhoneシミュレータで実行してプロジェクトを正常にビルドすることが可能か確認します。実行するiPhoneシミュレータのデバイスを選択します。特に理由はありませんが、ここではiPhone 4s iOS 8.1を選択します。

f:id:ch3cooh393:20141205214717p:plain

Xamarin Studioのウィンドウ左上にある▶ボタン(実行ボタン)をクリックします。少し時間はかかりますがiPhoneシミュレータが起動してアプリがデプロイされます。アプリが起動されたらボタンをタップします。

f:id:ch3cooh393:20141205214724p:plain

ラベル部分に「ボタンが押されました」と表示されると思います。はい、うまいこと行きましたね。

ウェブスクレイピングをおこなうC#ライブラリ

Html Agility Pack」という.NET Framework向けに書かれたHTMLパーサーライブラリがあります。

このライブラリはC#から利用することができます。元々はデスクトップPC用として作られていたライブラリなのですが、この記事ではDeathspikeさんによってPortable Class Library(PCL)用に移植された「HtmlAgilityPack-PCL」を利用することにします。本家の方と比べメソッドがいくつか存在しませんが機能的には十分です。

HtmlAgilityPack-PCLは、NuGet経由でインストールすることができます。しかし、残念なことに実行時にエラーが発生してしまうため僕の環境では利用できませんでした*1 *2

HtmlAgilityPack-PCLを導入する

Xamarin.iOS (Unified API)プロジェクトから利用しやすいように、iOS用のライブラリプロジェクトを作成して、HtmlAgilityPack-PCLのソースファイルを追加して みましょう。*3

GitHubからソースコード一式を任意のフォルダへダウンロードします。

f:id:ch3cooh393:20141208171540p:plain

.zipファイルを解凍します。ソースファイルは下図の通りです。.csprojファイルや.slnファイルなどのプロジェクト関係以外のファイルをそのまま利用したいと思います。

f:id:ch3cooh393:20141208171618p:plain

左ペインのKabukaAppを選択して右クリックしてコンテキストメニューを表示します。[追加]、[新しいプロジェクトを追加...]の順に選択して、新しいプロジェクトを作成します。

f:id:ch3cooh393:20141208171650p:plain

新しいプロジェクトダイアログが表示されるので、Unified APIのiOS Library Projectを選択します。プロジェクト名はなんでも良いですが、ここではHtmlAgilityPack-PCLとつけてみました。

f:id:ch3cooh393:20141208171727p:plain

先ほど解凍したソースファイルを追加します。

f:id:ch3cooh393:20141208171802p:plain

対話ウィンドウが表示され、追加の仕方を選択します。Copyを選択して[OK]ボタンをクリックします。

f:id:ch3cooh393:20141208171916p:plain

そうそう、PCLフォルダ直下のファイルもプロジェクトへ追加することを忘れないでください。

f:id:ch3cooh393:20141208171833p:plain

実際にビルドして、コンパイル時にエラーが発生しないことを確認します。

f:id:ch3cooh393:20141208171941p:plain

これでXamarin.iOS (Unified API)プロジェクトから利用するためのライブラリプロジェクトへのHtmlAgilityPack-PCLの移植は完了です。

参照の追加

アプリプロジェクトであるKabukaAppプロジェクトから、ライブラリへの参照を追加します。

f:id:ch3cooh393:20141208172026p:plain

以上で、HtmlAgilityPack-PCLの導入が完了しました。

Yahoo!ファイナンスから現在の株価を取得する

@kazuakixと株の話をしている時に欠かせないのはイオン(8267.T)です。以前Tipsを紹介したことがあるYahoo!ファイナンスの株価を表示させてみましょう。前述したTappedStartメソッドの処理を書き換えてしまいましょう。

// ボタンを押したらこのメソッドが呼ばれる
partial void TappedStart (UIButton sender)
{
    // 株価を取得したいサイトのURL
    var code = "8267.T";
    var urlstring = string.Format("http://stocks.finance.yahoo.co.jp/stocks/detail/?code={0}", code);

    // 指定したサイトのHTMLをストリームで取得する
    var doc = new HtmlAgilityPack.HtmlDocument();
    using (var client = new System.Net.WebClient())
    {
        var html = client.DownloadString(new Uri(urlstring));

        // HtmlAgilityPack.HtmlDocumentオブジェクトにHTMLを読み込ませる
        doc.LoadHtml(html);
    }

    // XPathを指定し株価部分を取得する
    var priceNode = doc.DocumentNode.Descendants("td")
        .Single(node => node.GetAttributeValue("class", "") == "stoksPrice");

    // 取得した株価がstring型なのでint型にパースする
    var price = int.Parse(priceNode.InnerText,
        NumberStyles.Integer | NumberStyles.AllowThousands, 
        NumberFormatInfo.CurrentInfo);

    this.OutputLabel.Text = string.Format("イオン(8267.T)の株価: {0}円", price);
}

上記のサンプルコードを実行します。いかがでしょうか?

f:id:ch3cooh393:20141209225840p:plain

まとめ

Xamarin.iOSでHtml Agility Packを使ってHTMLをスクレイピングする方法をご紹介しました。Objective-Cで同様のコードを書く場合には、若干骨が折れる作業になるのでそれよりかは簡単に導入できたのではないでしょうか(当社比)。

このブログでは12月に入ってからチマチマとHtml Agility Packを使ったTipsを書いています。一通りの使い方はこの記事で紹介しきってしまった感がありますが、もし「ほかのシーンではどのように使えばよいのか?」といった疑問があれば参考にしていただければと思います。

明日の「Xamarinのカレンダー | Advent Calendar 2014 - Qiita」の担当は鈴木章太郎(shosuz)さんです。鈴木さんはコミュニティ勉強会等でAzure Mobile Servicesについてお話していたのを何度か聞いたことがあります。明日はどんな記事が公開されるのか今から楽しみですね❤️

追記(2014/12/17)

PCLではなくて、本家の「Html Agility Pack」をXamarin.Androidでビルドする方法が紹介されました!こっちのが簡単!

関連記事

Xamarin.iOSを使ったアプリを開発中に気付いたことや調べたことをメモしています。Xamarin.iOSを使ってアプリ開発する際に逆引きとしてお使いください。

このブログでHtml Agility Packについて取り扱った記事をピックアップしました。

*1:dllが見つからないのか実行しようとするとFileNotFoundExceptionでアプリがクラッシュする

*2:先週に試した時には、NuGet経由でHtmlAgilityPack-PCLをプロジェクトへ導入できることを確認してたと思ったんだけど、週明けてスクリーンショットを取ってる時には使えなくなってました。Xamarin Studioをアップデートしたせいか、そもそも僕の記憶違いか……。一度、NuGetを使って導入する記事を書いてたのですが、一部削除して自前でライブラリをビルドする方法に書き直しました

*3:元々、PCL向けにビルドされているものなのですが、.slnプロジェクトを開いてもターゲットが古いのでビルドを通すのが面倒くさそうなので断念しました

Xamarin.iOSで新しいプロジェクトを作成する(Unified API)

過去にXamarin.iOSとXamarin.Macは別々のコードでメンテナンスされていて、さらに64bit対応されていませんでした。それを受けて、2014年9月に品質管理と64bit対応のため統合された「Unified API」が登場しました。過去のものは「Classic API」という位置付けになりました。詳しくはミゲル氏が書かれた下記の記事をご参照ください。

Appleは、2015年2月1日以降 iOS 8と64bit対応されていないiOSアプリの新規申請とアップデートを受け付けないと発表しており、新しく開発が始まるプロジェクトに関しては、今後のことを考えると「Unified API」を使った方が良いでしょう。ただ現時点ではいくつかのXamarin Componentsに登録されているコンポーネントがClassic APIで実装されており、Unified APIプロジェクトでは使えないこともあるので注意が必要です。

本記事では、Xamarin Studioを使ってXamarin.iOSのプロジェクトを作成する方法を紹介します。Xamarin Studioを起動します。

左側に「New Solution...」と書かれたボタンをクリックします。

f:id:ch3cooh393:20141204174906p:plain

新しいソリューションダイアログが表示されるので、左側のリストからC#iOSUnified APIiPhoneSingle View Applicationの順に選択します。任意の名前を入力して、[OK]ボタンをクリックすると新しいソリューション(とプロジェクト)が生成されます。

f:id:ch3cooh393:20141204174930p:plain

関連記事

Xamarin.iOSを使ってアプリ開発する際に逆引きとしてお使いください。

Azure Mobile ServicesからiOSアプリにプッシュ通知が送れない問題(追記あり、解決済み)

昨日1日Azure Mobile Services(以下、Mobileサービス)からiOSアプリにプッシュが送れない問題で悩んでいて解決しないので、ちょっと問題の切り分けのために現状を書いていきます。

iOSアプリに対してAzure Mobile ServicesのNotification Hubを使ってプッシュ通知を送る方法は下記の記事を参考にしてください。

MobileサービスからWindowsストアアプリにプッシュを送る方法

起動していたのがWindowsということもあって、まずは、Windowsストアアプリとの連携でテストしてみました。

  • サーバー側の準備
    • MobileサービスにパッケージIDとクライアントシークレットを登録
  • アプリ側
    • デバイスからチャンネルをモバイルサービスに送信する
  • サーバー側
    • 通知ハブのデバッグからトースト通知を送る
  • アプリ側
    • デバイスでトーストが表示される

ここまで30分くらい。TodoItemを送信したりしてテーブルにレコードが追加されるのを確認しながらだったのでプッシュ通知以外の部分で時間がかかってしまいましたが、プッシュ通知を使うのはとても簡単です。

MobileサービスからiOSアプリにプッシュを送る方法

次は本丸のiOSアプリとの連携です。

  • サーバー側の準備
    • KeyChainでcerファイルを生成する
    • iOS Dev Center*1でcerファイルをアップロードして証明書を生成する
    • 証明書をダウンロードして、p12ファイルに書き出し(これは後で使う)
    • アプリのプロビジョニングを再生成
    • Mobileサービスでp12ファイルをProductionとして登録
    • アプリをTestFlightで配信(AppStore用のプロビジョニングでビルド)
  • アプリ側
    • TestFlightからアプリをインストール
    • デバイスからデバイストークンをモバイルサービスに送信する
  • サーバー側
    • 通知ハブのデバッグからトースト通知を送る
  • アプリ側
    • プッシュ通知が飛んでこない

上記の2.のデバイストークンを送る処理は下記の通りです。iOS 8に対応させています。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   // ....

    self.client = [MSClient clientWithApplicationURLString:@"https://example.azure-mobile.net/"
                                            applicationKey:@"<Application Key>"];
    
    if ([application respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        
        // iOS 8 以降
        UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeAlert;
        UIUserNotificationSettings *settings
            = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [application registerUserNotificationSettings:settings];
        
    } else {
        
        // iOS 7.1 以前
        UIRemoteNotificationType types = UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge;
        [application registerForRemoteNotificationTypes:types];
    }
    
    return YES;
}

- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken
{
    NSLog(@"didRegisterForRemoteNotificationsWithDeviceToken %@", deviceToken);
    
    // <, >, 空白を削除
    NSString *token = [deviceToken description];
    token = [token stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
    token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];

    NSLog(@"deviceToken %@", token);
    
    MSPush* push = [self.client push];
    [push registerNativeWithDeviceToken:[token dataUsingEncoding:NSUTF8StringEncoding]
        tags:nil completion:^(NSError *error) {
            
            if (error != nil) {
                
                UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"error"
                                                                message:[error description]
                                                               delegate:nil
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
                [alert show];
                
            } else {
                
                UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"completed"
                                                                message:token
                                                               delegate:nil
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
                [alert show];
            }
    }];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    NSLog(@"didFailToRegisterForRemoteNotificationsWithError");
    
    UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"error"
                                                    message:[error description]
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
    
}
 
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
    NSLog(@"didRegisterUserNotificationSettings");
    
    [application registerForRemoteNotifications];
}

// アプリが起動中とかに通知が飛んできたら呼ばれる
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    NSLog(@"didReceiveRemoteNotification");
    
    UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"notification"
                                                    message:@"通知が来たよ"
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
}

ちなみに送信側のコードはこんな感じ。ServiceBusのSDKを使っています。

var client = NotificationHubClient.CreateClientFromConnectionString(
    "Endpoint=sb://example-ns.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<access key>",
    "<hub name>", false);

// 現在、登録されているデバイスを確認する
var allRegistrations = await client.GetAllRegistrationsAsync(0);
var registrations = allRegistrations
    .Where(d => d is AppleRegistrationDescription)
    .Cast<AppleRegistrationDescription>();

// ここで1台登録されているのを確認
Console.WriteLine("registrations {0}", registrations.Count());

// 通知を送信!!!!
var json = "{\"aps\":{\"alert\":\"hoge\"}}";
await client.SendAppleNativeNotificationAsync(json);

// 5秒待つ
Thread.Sleep(5000);

allRegistrations = await client.GetAllRegistrationsAsync(0);
registrations = allRegistrations
    .Where(d => d is AppleRegistrationDescription)
    .Cast<AppleRegistrationDescription>();

Console.WriteLine("registrations {0}", registrations.Count());

// (おそらく送信に失敗したのか)登録デバイスが0台になっている

現状(12/4 11:15)

最初に証明書関係でトラブっているのではないかと疑いました。同じ証明書(p12ファイル)を使ってAmazon SNSを試してみました。

  • Amazon SNSで試してみる
    • p12ファイルを登録
    • アラートに表示させたデバイストークンを登録
    • プッシュを送信
    • iOSデバイスでトーストが表示される

このことから証明書自体に問題ないことと言えると思います。

現状(12/4 14:15)

しばやんに教えてもらっています。

通知ハブのデバックはProductionに対しては利用できないのでは?という回答。特に明記されているわけではないけれど、デバッグ用途だし、そういうものかもしれない。

こっちに関してはNotificationHubClientを生成するときに第3引数(enableTestSend)をfalseにしているので間違いではないはず……

var client = NotificationHubClient.CreateClientFromConnectionString(
    "Endpoint=sb://example-ns.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<access key>",
    "<hub name>", false);

現状(12/4 15:00)

接続文字列等の指定ミスでそもそも通知ハブから送信できていないのでは……とも思ったが、プッシュ送信する前の登録デバイス数が1で、プッシュ送信(とおそらくAPNsからのエラー通知)後に0になることから正しく取れていると思われる。

現状(12/4 15:20)

問題解決した。デバイストークンがおかしくて不正な値を通知ハブに登録していたようだ。