ホームページ開発ツール>Xojo / Real Studio Trial and Error・CocoaのDeclareで通知センターを利用する・Delegate設定編

 Xojo / Real Studio Trial and Error

CocoaのDeclareで通知センターを利用する・Delegate設定編

目次
 はじめに

 前回のケースでは、通知を発行したアプリ(自分自身)が最前面にあると、通知が表示されない、という問題がありました。
 この問題は、Delegateを設定することで回避できることまでは分かっていたのですが、Real Studio(Xojo)での対処法が不明で、長らくペンディングとなっていましたが、目処が立ちましたので纏めておきました。

 なお検証には、Xojo 2014 Release 3.1を用いています。(Mac mini + OS X 10.10.2 Yosemite)


 Delegate設定の仕組み

 情報の宝庫であるmacoslibを眺めていたところ、「objc_XXXX」というのが目に留まりました。
 調べてみたらこれは、Objective-CのランタイムAPIを用いて、ダイナミック(動的)にクラスを生成する仕組みであることが分かりました。

 参考サイト(1):Safx: NSObjectを継承しないクラスをランタイムAPIで作成する方法について

 どうもmacoslibは、この仕組みを使ってDelegateを実現させているようでした。
 ということで、ランタイムAPIの調査も行いつつmacoslibを勉強させて頂いたところ、その手順は、
  1. クラスをダイナミックに新規生成する
  2. 1.のクラスをランタイムに登録する
  3. 1.のクラスに「Delegateの対象としたいメソッドとその受け口」を追加する
  4. 1.のクラスを実体(インスタンス)化させる
  5. Delegateを設定したいインスタンスに、4.のインスタンスを指定(SetDelegate)する
のようでした。これをコードで示すと、
// 文字列を指定してクラスオブジェクト/セレクタを取得する。最初に一回宣言しておけばよい。
Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr
  
// Declare宣言
Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr
Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr)
Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean

// クラス名をmyNSUserNotificationCenterDelegate(名前は任意。少なくとも今回のケースでは参照されない。)、メタクラス名をNSObjectにして、生成
Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myNSUserNotificationCenterDelegate", 0)
// ランタイムに登録(参照を可能とするため)
objc_registerClassPair newClassId
// Delegateの対象となるメソッドを追加(userNotificationCenter:shouldPresentNotification:をXojo側で用意したShouldPresentNotificationメソッドで受け取る。)
if not class_addMethod (newClassId, NSSelectorFromString("userNotificationCenter:shouldPresentNotification:"), AddressOf ShouldPresentNotification, "B@:@@") then
    msgBox "error."
    return
end if

// 上記で生成したクラスのインスタンスを作成
Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
Dim delegateId As Ptr = init(alloc(newClassId))

…

// Delegateを設定したいインスタンスに、上記で作成したインスタンスをセット
Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Ptr, id As Ptr)
setDelegate center, delegateId  // centerはDelegateを設定したいインスタンスで、別途取得しておく
注)class_addMethodの最後の引数("B@:@@")は「type encodings」で、例えば以下を参照。(参考サイト(1)にも解説/リンクあり。)
 参考サイト(2):ダイナミックObjective-C (20) メソッドとは何か(3) - メソッドの型を読み解く | マイナビニュース

 あとは別途、Delegateの「受け口」を、Xojo側のメソッドとして実装しておくと、実行時にそのメソッドが呼び出される、という訳です。


 Xojoでの実装

 以下は、前回のコードにDelegate関連の処理を追加したものです。
メソッド名: SendNotification(注:名前は任意)

Declare Function NSClassFromString Lib "Cocoa" (aClassName As CFStringRef) As Ptr
Declare Function NSSelectorFromString Lib "Cocoa" (aSelName As CFStringRef) As Ptr

// Delegateを設定するための仕込み ----------
Declare Function objc_allocateClassPair Lib "Cocoa" (superclass As Ptr, name As CString, extraBytes As Integer) as Ptr
Declare Sub objc_registerClassPair Lib "Cocoa" (cls As Ptr)
Declare Function class_addMethod Lib "Cocoa" (cls As Ptr, name As Ptr, imp As Ptr, types As CString) As Boolean

Dim newClassId As Ptr = objc_allocateClassPair(NSClassFromString("NSObject"), "myNSUserNotificationCenterDelegate", 0)
objc_registerClassPair newClassId
if not class_addMethod (newClassId, NSSelectorFromString("userNotificationCenter:shouldPresentNotification:"), AddressOf ShouldPresentNotification, "B@:@@") then
    msgBox "error."
    return
end if

Declare Function alloc Lib "Cocoa" selector "alloc" (class_id As Ptr) As Ptr
Declare Function init Lib "Cocoa" selector "init" (obj_id As Ptr) As Ptr
Dim delegateId As Ptr = init(alloc(newClassId))
// ----------

// 以下は(追加分と書かれた行を除いて)前回と同じ
Dim notification As Ptr = NSClassFromString("NSUserNotification")
Declare Function notificationAlloc Lib "Cocoa" Selector "alloc" (receiver As Ptr) As Ptr
notification = notificationAlloc(notification)
Declare Function notificationInit Lib "Cocoa" Selector "init" (receiver As Ptr) As Ptr
notification = notificationInit(notification)

Declare Sub setTitle Lib "Cocoa" Selector "setTitle:" (receiver As Ptr, id As CFStringRef)
setTitle(notification, "Hello World")

Declare Sub setInformativeText Lib "Cocoa" Selector "setInformativeText:" (receiver As Ptr, id As CFStringRef)
setInformativeText(notification, "my message")

Dim date1 As Ptr = NSClassFromString("NSDate")
Declare Function date Lib "Cocoa" Selector "date" (receiver As Ptr) As Ptr
date1 = date(date1)
Dim date2 As Ptr = NSClassFromString("NSDate")
Declare Function dateWithTimeInterval Lib "Cocoa" Selector "dateWithTimeInterval:sinceDate:" (receiver As Ptr, id1 As Double, id2 As Ptr) As Ptr
date2 = dateWithTimeInterval(date2,5,date1)
Declare Sub setDeliveryDate Lib "Cocoa" Selector "setDeliveryDate:" (receiver As Ptr, id As Ptr)
setDeliveryDate(notification, date2)

Declare Sub setSoundName Lib "Cocoa" Selector "setSoundName:" (receiver As Ptr, id As CFStringRef)
setSoundName(notification, "DefaultSoundName")

Dim center As Ptr = NSClassFromString("NSUserNotificationCenter")
Declare Function defaultUserNotificationCenter Lib "Cocoa" Selector "defaultUserNotificationCenter" (receiver As Ptr) As Ptr
center = defaultUserNotificationCenter(center)

Declare Sub setDelegate Lib "Cocoa" Selector "setDelegate:" (receiver As Ptr, id As Ptr)  // 追加分
setDelegate center, delegateId  // 追加分

Declare Sub scheduleNotification Lib "Cocoa" Selector "scheduleNotification:" (receiver As Ptr, id As Ptr)
scheduleNotification(center, notification)
 上記メソッドを、(例えば)Windowに置いたPushButtonのActionイベントから呼び出します。

 また、class_addMethodで指定した名前を持つメソッドを(例えば)Windowに追加します。
メソッド名: ShouldPresentNotification
引数: center As Ptr, notification As Ptr(注意事項あり。詳細は追記参照)
戻り値型: Boolean

return true  // trueを返すことで、自身が最前面にあってもバナー表示が可能に
追記1:動的クラス生成は、共有メソッド(Shared Methods)で行った方がよさそうです。詳細はこちらを参照してください。
追記2:引数ですが、メソッド本来の引数の前に、Ptr型引数(共有メソッドの場合は2個。メソッドの場合は1個になるようだが詳細は未調査)が必要のようです。ただし、引数の参照をしないのであれば、無くてもとくに問題はなさそう?(この件も、詳細はこちらを参照してください。)
 実行してみたところ、自身が最前面にあっても、バナー表示されることを確認しました。


 おわりに

 今回は仕組みを理解するために、極力シンプルな書き方を心懸けました。
(例えば、delegateIdは然るべきタイミングでReleaseするべきと思われますが、そのためにはコードを工夫する必要があります。)
 実用目的であれば、macoslibのように汎用化する(というか、そのまま利用させて頂く)ことを考えた方がいいでしょう。


 お世話になったサイト

 貴重な情報をご提供頂いている皆様に、お礼申し上げます。(以下、順不同)

 参考サイト(1):Safx: NSObjectを継承しないクラスをランタイムAPIで作成する方法について
 参考サイト(2):ダイナミックObjective-C (20) メソッドとは何か(3) - メソッドの型を読み解く | マイナビニュース


 更新履歴

 2015.09.30 Xojoでの実装の記述に、追記を追加
 2015.02.27 新規作成


[Home]  [MacSoft]  [Donation]  [History]  [Privacy Policy]  [Affiliate Policy]