UIがないAction Extensionを試す
事前に適当にSingle View Application
でプロジェクトを生成
CapabilitiesでApp Groups
をON
に切り替える(未使用だけどメモ)
- アカウントが聞かれます
設定済みであれば選択。未設定の場合は新規に設定
今回事前にAppleのdevサイトで
App Groups
を登録してあります。group.
が強制的に先頭についてイラっときますが仕方無さそうなのでそのままで。
App Groups
同一ディベロッパーがリリースしたアプリ間でデータのストレージを共有できる機能。
NSUserDefauts
を使って本体アプリとApp Extensionで共通のデータを使いたい場合には、initWithSuiteName
で指定する必要あり。 App Groups専用のストレージに保存になるので、既存のsuiteNameを指定していないものは使えません。
本体のアプリにApp Extensionを追加
No User Interface
を選択
ディレクトリに専用のグループが作成され、以下のファイルが自動で生成されます。 - ActionRequestHandler.swift - Action.js - info.plist
テンプレだから当然のように動くと思っていた。。動きませんよ!
ActionRequestHandler.swift
の一部。
func beginRequestWithExtensionContext(context: NSExtensionContext) { // Do not call super in an Action extension with no user interface self.extensionContext = context var found = false // Find the item containing the results from the JavaScript preprocessing. outer: for item: AnyObject in context.inputItems { let extItem = item as! NSExtensionItem if let attachments = extItem.attachments { for itemProvider: AnyObject in attachments { if itemProvider.hasItemConformingToTypeIdentifier(String(kUTTypePropertyList)) { itemProvider.loadItemForTypeIdentifier(String(kUTTypePropertyList), options: nil, completionHandler: { (item, error) in let dictionary = item as! [String: AnyObject] NSOperationQueue.mainQueue().addOperationWithBlock { self.itemLoadCompletedWithPreprocessingResults(dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as! [NSObject: AnyObject]) } found = true }) if found { break outer } } } } } if !found { self.doneWithResults(nil) } }
悪いのは、後半4行。これ消せばとりあえず動いた(はず)。
ただいろいろ勉強がてら、この自動生成されたコードを自分なりにキレイに書いてみます
import UIKit import MobileCoreServices class ActionRequestHandler: NSObject, NSExtensionRequestHandling { var extensionContext: NSExtensionContext? func beginRequestWithExtensionContext(context: NSExtensionContext) { // Do not call super in an Action extension with no user interface self.extensionContext = context // Find the item containing the results from the JavaScript preprocessing. for item: AnyObject in context.inputItems { let extItem = item as! NSExtensionItem // early returns guard let attachments = extItem.attachments else { continue } // for-in & where for itemProvider: AnyObject in attachments where itemProvider.hasItemConformingToTypeIdentifier(String(kUTTypePropertyList)) { itemProvider.loadItemForTypeIdentifier( String(kUTTypePropertyList), options: nil, completionHandler: { [unowned self] (result: NSSecureCoding?, error: NSError!) -> Void in let dictionary = result as! [String: AnyObject] NSOperationQueue.mainQueue().addOperationWithBlock { if let dist = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? [NSObject: AnyObject] { self.itemLoadCompletedWithPreprocessingResults(dist) } } }) } } } func itemLoadCompletedWithPreprocessingResults(var javaScriptPreprocessingResults: [NSObject: AnyObject]) { // titleを加工する if let title = javaScriptPreprocessingResults["title"] as? String { javaScriptPreprocessingResults["title"] = "タイトル: 「\(title)」" } // jsに渡す -> newBackgroundColor : red javaScriptPreprocessingResults["newBackgroundColor"] = "red" self.doneWithResults(javaScriptPreprocessingResults) } func doneWithResults(resultsForJavaScriptFinalizeArg: [NSObject: AnyObject]?) { if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg { let resultsItem = NSExtensionItem() let resultsProvider = NSItemProvider( item: [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize], typeIdentifier: String(kUTTypePropertyList) ) resultsItem.attachments = [resultsProvider] self.extensionContext!.completeRequestReturningItems([resultsItem], completionHandler: nil) } else { self.extensionContext!.completeRequestReturningItems([], completionHandler: nil) } // Don't hold on to this after we finished with it. self.extensionContext = nil } }
guard
と、for-in & where
を使ってみました。guardはググればいいとして、そのメリットとして早期リターンができることが大きいのかなと。(どっかのドキュメントにもそう書いてあった)。最初の自動生成されたコードはネストが深すぎて個人的にはあまり好きじゃなかったです。。。
for-in & whereも、ループと条件を1文でかけるのでこれでネストが1個減ります。
Action.jsの疑問
arguments.completionFunction({ "currentBackgroundColor" : document.body.style.backgroundColor, "url": document.URL, "title": document.title });
少しいじりました。ActionRequestHandler
に渡すデータを辞書形式で指定しています。
今回は、今開いているページのURLとタイトルをswift側に渡しています。
↓Swiftからjsにデータを渡す処理(テンプレそのもの)
let resultsItem = NSExtensionItem() let resultsProvider = NSItemProvider( item: [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize], typeIdentifier: String(kUTTypePropertyList) ) resultsItem.attachments = [resultsProvider] // Signal that we're complete, returning our results. self.extensionContext!.completeRequestReturningItems([resultsItem], completionHandler: nil)
これでjsのfinalizeメソッドに、swift
からデータを渡してjsで制御をすることが出来るようになります。
jQuery使えないのか・・
Action.jsのrunメソッドで↓のように強引にjQueryをheaderに追加してみます
var jScript = document.createElement("script"); jScript.setAttribute("src","https://code.jquery.com/jquery-2.2.0.min.js"); if (jScript.addEventListener) { jScript.addEventListener ('DOMNodeInserted', OnNodeInserted, false); } document.head.appendChild(jScript);
参照 Using jQuery in ios 8 app extension javascript files - Stack Overflow
追加は出来た。(Macのsafariを起動して、開発
タブからシミュレータを指定するとDeveloper Toolsで要素とか見れるのそれで確認)
でも$
を参照できない。
あと、もともとjQueryが使われているページのjQuery
オブジェクトを参照できない。
Chrome Extensionと同じようなものの認識で合ってるのか・・?
存在を知らなかったDOMNodeInserted
は、非推奨らしい。
App Extensionのブレークポイントの仕方
App Extensionは、通常のアプリとはプロセスが異なるようです。(pidが違う) そして、起動が終わればプロセスは終了する。
なのでデバッグするときは、App Extensionを起動した状態(pidが存在する)で 開発者が手動でAttachする必要があります。
Debug
> Attach to Process by PID or Name
を選択し、App Extensionのプロジェクト名を選択すればうまくbreakpointに止まってくれました。
commons-lang2系と3系のStringUtils.isNumericの動作の違いメモ
以前、仕事でライブラリのアップデート調査をしてた時にすでにわかってたつもりだったけど、
今日、別プロジェクトでバグとして上がってきたのでメモ。
バグの原因になっていたのは空文字の場合
s = "" println org.apache.commons.lang.StringUtils.isNumeric(s) println org.apache.commons.lang3.StringUtils.isNumeric(s) Result: true false
GroovyConsoleで動かしてます。
結果、2系の場合はtrue
を返し、3系はfalse
を返す。
数値判定に使えるメソッドのメモ
NumberUtilsでも2系3系関係なく使えるメソッドisNumber
、isDigits
s = "3" println org.apache.commons.lang.StringUtils.isNumeric(s) println org.apache.commons.lang3.StringUtils.isNumeric(s) println org.apache.commons.lang.math.NumberUtils.isDigits(s) println org.apache.commons.lang.math.NumberUtils.isNumber(s) println org.apache.commons.lang3.math.NumberUtils.isDigits(s) println org.apache.commons.lang3.math.NumberUtils.isNumber(s) Result: true true true false true false
StringUtils.isNumeric
とNumberUtils.isDigits
は全角の数字もtrue
と判断しますが、
NumberUtils.isNumber
はfalse
です。
マイナスと10進数以外の時の挙動
s = "-100" println org.apache.commons.lang.StringUtils.isNumeric(s) println org.apache.commons.lang3.StringUtils.isNumeric(s) println org.apache.commons.lang.math.NumberUtils.isDigits(s) println org.apache.commons.lang.math.NumberUtils.isNumber(s) println org.apache.commons.lang3.math.NumberUtils.isDigits(s) println org.apache.commons.lang3.math.NumberUtils.isNumber(s) Result: false false false true false true
s = "0xff" println org.apache.commons.lang.StringUtils.isNumeric(s) println org.apache.commons.lang3.StringUtils.isNumeric(s) println org.apache.commons.lang.math.NumberUtils.isDigits(s) println org.apache.commons.lang.math.NumberUtils.isNumber(s) println org.apache.commons.lang3.math.NumberUtils.isDigits(s) println org.apache.commons.lang3.math.NumberUtils.isNumber(s) Result: false false false true false true
上でメモしたメソッドは引数にnull
を渡しても、全てfalse
を返してくれます。