読者です 読者をやめる 読者になる 読者になる

SHIBUYA 3%

(元在宅が)渋谷で働くエンジニアの備忘録的memo & 雑記 - ココロはいつもSHIBUYAに在り

UIがないAction Extensionを試す

事前に適当にSingle View Applicationでプロジェクトを生成

f:id:fukurou31:20160223011406p:plain

CapabilitiesでApp GroupsONに切り替える(未使用だけどメモ)

f:id:fukurou31:20160223011528p:plain

  • アカウントが聞かれます
  • 設定済みであれば選択。未設定の場合は新規に設定

  • 今回事前にAppleのdevサイトでApp Groupsを登録してあります。 f:id:fukurou31:20160223011932p:plain group.が強制的に先頭についてイラっときますが仕方無さそうなのでそのままで。

App Groups

同一ディベロッパーがリリースしたアプリ間でデータのストレージを共有できる機能。 NSUserDefautsを使って本体アプリとApp Extensionで共通のデータを使いたい場合には、initWithSuiteNameで指定する必要あり。 App Groups専用のストレージに保存になるので、既存のsuiteNameを指定していないものは使えません。

本体のアプリにApp Extensionを追加

f:id:fukurou31:20160223012437p:plain

No User Interfaceを選択 f:id:fukurou31:20160223012639p:plain

ディレクトリに専用のグループが作成され、以下のファイルが自動で生成されます。 - 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

追加は出来た。(Macsafariを起動して、開発タブからシミュレータを指定すると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に止まってくれました。