ごんれのラボ

iOS、Android、Adobe系ソフトの自動化スクリプトのことを書き連ねています。

DTPerのスクリプトもくもく会の主催を引き継ぎました

お知らせ

DTPerのスクリプトもくもく会の主催を id:uske-s さん(以下、ゆーすけさん)に引き継いで、私は当面補佐として運営に関わることになります。
引き継ぎ後、スムーズに回りだしたタイミングで運営から退いて、一参加者としてもくもく会に関わることにします。

なぜ引き継ぐのか

理由はいくつかあるのですが、大きなものは下記の2つです。

  • 当会を立ち上げたときから一年程度で他の方に引き継ぐつもりだった
  • 私がAdobe製品を主に扱う職業ではないため、最新の情勢に疎い部分がどうしてもできてしまった

この2つの理由は私の中でリンクしているのですが、「中途半端な自分が会を主導する立場にいて良いのか」という葛藤が必ず発生することは、私の性格上わかっていたので、ひとまず一年がんばろうと期限を決めて踏ん張ることにしたのです。
幸い、2回目からゆーすけさんを共同開催者に迎え、7回目からの主催を引き継いでいただけました。
尊敬・信頼できる方に引き継げて一安心です。

参加者の方々へ

急なご連絡で申し訳ございません。
会自体は今後も継続されますので、ご安心ください。
未熟な主催を支えてくださり、ありがとうございました。

今後について

お知らせに書いたように、当面は補佐として運営に関わります。
次回も参加しているので、よかったら声をかけてください。

最後に

これからもDTPerのスクリプトもくもく会をよろしくお願いいたします。

dtpmkmk.connpass.com

DTPerのスクリプトもくもく会 #6 を開催しました

前説

DTPerのスクリプトもくもく会 #6 を開催しました。
dtpmkmk.connpass.com

参加者は私を含め10名でした。
年度末ということも影響したのか、最少人数での開催となりました。

お詫び

会場をお借りしているYUIDEAさんへ

今回参加者のうち、訪問者カードを記載していただいていない方がいらっしゃったことに解散直前に気づくという不備がありました。
主催側で手渡しで記入していただくなどの手順を踏むべきでした。
ご迷惑をおかけして申し訳ございません。

参加者の方へ

主催である私が中途半端な状態で発表を放棄するという事態になり、不快な思いをさせてしまいました。
私の発表内容がうまくまとめられていなかったことが原因の一端であると反省しております。
しかしながら、発表(登壇)している人に対して、その行為を妨害したり、発表するための時間を横取りしたりするというのは、ルール違反です。
私はそのような行為はどのような場においても行われてはいけないという考えです。
そのため、当もくもく会では自由に発表していただく場所と時間を提供していたのです。
ですが、今回その考えを覆される事態が発生してしまいました。
不幸中の幸いだったことは、発表者が主催の私であったことでしょうか。

このようなことが二度と起こらないように、今後の対策としてconnpassグループのページに注意文を追加させていただきました。

以下のような迷惑行為の一切を禁止とします。
参加される皆さまには最低限のマナーを守っていただくよう、ご協力お願いします。
以下に該当する行為に当たると主催者が判断した場合、今後当会への参加をお断りする場合があります。
1. 他の参加者に対する迷惑行為
2. 会の運営を妨害する行為
3. 主催者の指示に従わない行為
4. 会場の機材に対して、意図的に破損・汚損すること
5. 会場の機材を勝手に持ち帰ること
6. その他、主催者が著しいマナー違反にあたると判断した行為

ごくごく当たり前のルールではございますが、ご協力をお願いいたします。

Tweetのまとめ

Togetter でまとめ用意中です。
完成次第、公開します。

今後の予定

次回は5/19(土)に開催予定です。
会場が変わりまして、コワーキングスペースになります。
dtpmkmk.connpass.com

Watchアプリのラベルに表示した文字列を横スクロールさせる

概要

Qiitaにあげた記事の転記。(一部変更点あり)

qiita.com

Watchアプリのラベルに表示した文字列を横スクロールさせたいという要望があったので、WKInterfaceLabelのExtensionで実現した。

サンプル動画

仕様など

  • スクロールは、一定時間おきに文字列の左端を削除することで実現
  • エンドレス版は、削除した文字列を末尾に追加し、電子掲示板のような動きにした

留意点

いくつか留意すべき点がある。

表示する文字列がラベルに収まる長さかどうかの判定ができない

UILabelでは、自身のframeのwidthと表示する文字列のwidthを比較して、ラベルに収まるかどうかの判定を行うことが可能。

参照:UILabelの高さ計算時に気をつけること

しかし、WKInterfaceLabelは下記の理由から、UILabelのように判定を行うことができない。

  • ラベルに表示している文字列を取得するAPIがない
  • ラベルに適用されているUIFontを取得するAPIがない
  • ラベルのframeを取得するAPIがない

回避策として、enumなどを用いて各値を保持しておくという案を思いついたが、ラベルごとに異なるであろう設定をもれなく抜き出し、かつ改修があったときに追随していくことは苦痛に思えたので、「常にスクロールする」仕様とすることが無難だろう。

処理が重く、CPUの使用率に影響がある

当然のことだが、Xcodeでデバッグした限り、スクロールさせるとCPUの使用率があがる。
試しに15個程度のラベルをスクロールさせてみたが、CPU使用率が50%以上という結果になった。
画面外のラベルはスクロールさせないなど、可能な限りスクロールさせないための処理は必須。

WKInterfaceLabelはサブクラスが機能しない

スクロール処理とは関係ない話ではあるが、WKInterfaceLabelはサブクラスが機能しない。

Do not subclass or create instances of this class yourself. Instead, define outlets in your interface controller class and connect them to the corresponding objects in your storyboard file. For example, to refer to a label object in your interface, define a property with the following syntax in your interface controller class:

公式ドキュメントから抜粋。

Xcode上ではなんのサジェストも行われず、上記のドキュメントの記載に気づかずに実装したときの状態を表にした。

やったこと 可否
サブクラス化
Storyboard上のカスタムクラスの設定
ビルド
サブクラス内のメソッドの呼び出し ×

一見サブクラス化できるように思えるが、実際は動かないので、かなしい。

ソースコード&使い方

サンプル動画のInterfaceControllerの実装もあわせて記載。

iOS App同様に、Watch Appがバックグラウンドに移行したときにスクロールを止める処理をお忘れなく。

まとめ

Watch Appはいろいろと制限があって、難しい。
あと情報が少ない上に古いものが多くて、つらい。
みなさんもお持ちの情報で公開できるものがあれば、どんなものでも公開していただけるとお互いに幸せになれそう。

DTPerのスクリプトもくもく会 #6 を開催します

申し込みページ

dtpmkmk.connpass.com

Information

  • 前回と同じく、株式会社YUIDEA さんの会議室をお借りしています
  • 大型モニタをお借りしているので、画面を使って質問したいこと、発表したいことがあればハッシュタグ(#dtpscriptmkmk6)に流してください
    • モニタはDサブとHDMIに対応しています。Thunderbolt2→HDMIの変換ケーブルを1本ご用意します。
  • Git について、なにか質問があれば当日お聞きします

最後に

ブログでの告知が遅れてすみません。
ご参加お待ちしております!

DTPerのスクリプトもくもく会 #5 を開催しました

前説

DTPerのスクリプトもくもく会 #5 を開催しました。
dtpmkmk.connpass.com

参加者は私を含め15名でした。
今回は初心者枠の申込みが多く、6名に大幅増員しました。

当日の雰囲気

今回は初心者枠を増やしたこともあり、参加者同士で教えあったいただく機会が多かったように思います。
第一回から初心者枠で参加していただいている方からなかなか高度な質問があり、成長の早さに驚かされるばかりです。

共同開催者の id:uske-s さんがまとめを書かれておりますので、そちらもご参照ください。 uske-s.hatenablog.com

Tweetのまとめ

Togetter でまとめました。
落ち着いてきたのか、View数が少ないですね…。 togetter.com

発表について

id:uske-s さんが Extend Script Tool Kit について発表を行いました。
いつもふわっと使っていた Extend Script Tool Kit について細かめに説明してくださっていました。
資料は下記をご参照ください。

www.slideshare.net

今後の予定

次回は3月に開催予定です。

Macにインストール済の特定のアプリケーションを一覧表示して、任意のバージョンを起動できるLauncherアプリケーションを作る その1

概要

Qiitaにあげた記事の転記。 qiita.com

ユーザーが複数のバージョンを同一マシンにインストール可能なアプリケーション(ex. Adobe Illuctrator CCなど)用に、インストール済のバージョンを一覧表示して、任意のバージョンを起動できるLauncherアプリケーションを作る。

参考までに、Illustratorで現在インストールできるバージョン一覧がこちら。

f:id:macneko-ayu:20171208172855p:plain
インストール可能なバージョン一覧

作ろうと思った経緯

Adobe Illuctrator CCの最新バージョン(CC 2018、公式にはv22.0?)に致命的なバグがあって、そのバージョンを使って印刷事故を起こす人を減らしたいと思って、書いてみることにした。
どんなバグかは、下記フォーラムを参照。かなり致命的なバグ。

forums.adobe.com

なお、すでにものかのさんが同じ趣旨、かつもっと素晴らしいものを公開されている。
Glee Ai 1.0.0

私は少し違うアプローチのアプリにする予定。
ものかのさんのには似たようなアプリを作ることを了承いただいている。

当面の目標

他にも追加したい機能はあるものの、形にしないと先に進まないので、下記のようなアプリケーションを当面の目標とする。

f:id:macneko-ayu:20171208172852p:plain
Launcherアプリケーションの画面サンプル

  • Activated(起動中)とNot activated(未起動)のリストを表示
  • 未起動リストのセルをタップすると、そのバージョンが起動して起動中リストのセルに移動

この記事を読んでできること

  • 最低限必要な情報を有したModelと、そのModelをメンバ変数に持つViewModelを作成

この記事を読んでできないこと

  • UI との繋ぎ込み(画面表示については一行も書けず…)
  • アプリケーションの起動処理

どのように実現するか

インストール済のアプリケーションを取得する

真っ先に思いつくのはApplicationディレクトリの中をアプリケーション名でゴリゴリ検索するという手段だが、さすがにイケてなさすぎる。
調べたところ、BundleIdentifierを渡すと該当するアプリケーションのファイルURLの配列を返してくれる関数があることがわかった。

LSCopyApplicationURLsForBundleIdentifier(_:_:) - Core Services | Apple Developer Documentation

// BundleIdentifierにマッチするApplicationのファイルUrlを取得
// 同じBundleIdentifierのApplicationがインストールされている場合もあり得るので配列となる
let bundleIdentifier = "com.apple.dt.xcode"
guard let applicationUrls = LSCopyApplicationURLsForBundleIdentifier(bundleIdentifier as CFString, nil)?.takeUnretainedValue() as? [URL] else {
    return
}

/*
(lldb) po applicationUrls
▿ 2 elements
  ▿ 0 : file:///Applications/Xcode.app/
    - _url : file:///Applications/Xcode.app/
▿ 1 : file:///Applications/Xcode8.3.3.app/
    - _url : file:///Applications/Xcode8.3.3.app/
 */

アプリケーションの基本的な情報を取得する

現状、必要と思われる情報は下記。

  • アプリケーションの名前
  • アプリケーションのバージョン
  • アイコン画像
  • 起動しているか否かを判断するフラグ
  • Bundle Identifier
  • アプリケーションのファイルパス

ほとんどの情報がinfo.plistを参照すれば取得できるので、info.plistをNSDictionaryとして展開して取得することにした。

let fileUrl = URL(fileURLWithPath: "/Applications/Xcode.app")

// エンコーディングされているのでデコード
guard let filePath = fileUrl.path.removingPercentEncoding else {
    return
}
// 情報を取得するためにplistを取得
let plistPath = filePath + "/Contents/Info.plist"
if !FileManager.default.fileExists(atPath: plistPath) {
    return
}
guard let infoDictionary = NSDictionary(contentsOfFile: plistPath) else {
    return
}

/*
(lldb) po infoDictionary
▿ 37 elements
  ▿ 0 : 2 elements
    - key : NSAppleScriptEnabled
    - value : 1
  ▿ 1 : 2 elements
    - key : CFBundleInfoDictionaryVersion
    - value : 6.0
  ▿ 2 : 2 elements
    - key : DTPlatformVersion
    - value : GM
(以下略)
*/

上述した情報の一部を展開したNSDictionaryから取得する方法は下記の通り。

// アプリケーションのバージョン 
let bundleVersion = infoDictionary["CFBundleVersion"] as? String ?? ""
// Bundle Identifier
let bundleIdentifier = infoDictionary["CFBundleIdentifier"] as? String ?? ""

アプリケーションの名前はinfo.plistから取得すると、Finder上の名称と異なってしまうので、FileManagerから取得した。

let fileUrl = URL(fileURLWithPath: "/Applications/Xcode.app")
guard let filePath = fileUrl.path.removingPercentEncoding else {
    return
}
let displayName = FileManager.default.displayName(atPath: filePath)

アイコン画像を生成する

アイコン画像は /Applications/hoge.app/Contents/Resources/ の中に格納されている拡張子が「.icns」のファイルから作成する。 まず、アイコンファイル名を取得。

let iconFileName = infoDictionary["CFBundleIconFile"] as? String

続いて、ファイル名と格納されているディレクトリのパスをつなげればファイルパスがわかるので、そのファイルパスを元にNSImageを生成する。

var icon: NSImage = nil
// Applicationのiconファイル名を取得
if let iconFilePath = infoDictionary["CFBundleIconFile"] as? String {
    // ファイルパスからNSImageを作成
    icon = generateIconImage(filePath: filePath + "/Contents/Resources/" + iconFilePath, iconSize: iconSize)
}

/// iconを作成する
///
/// - Parameters:
///   - filePath: iconのファイルパス
///   - iconSize: 作成するiconのサイズ
/// - Returns: 作成に成功したらicon画像、失敗したらnilを返す
private func generateIconImage(filePath: String) -> NSImage? {
    let iconSize = NSSize(width: 120, height: 120)
    // ファイル名の末尾が.icnsでなければ拡張子を追加する(アプリケーションによってアイコンファイル名に拡張子がない場合もある)
    var imagePath = filePath
    if !imagePath.hasSuffix(".icns") {
        imagePath += ".icns"
    }
    let iconImage = NSImage(contentsOfFile: imagePath)
    iconImage?.size = iconSize
    iconImage?.lockFocus()
    NSGraphicsContext.current?.imageInterpolation = .high
    iconImage?.draw(in: CGRect(x: 0, y: 0, width: iconSize.width, height: iconSize.height))
    iconImage?.unlockFocus()
    return iconImage
}

起動中か否かのフラグをもたせる

未実装!(ごめんなさい)
NSWorkSpace クラスに起動中のアプリケーションを取得する関数があるので、それと比較させれば初期化時は判断できると考えている。
ただ、随時起動状態を監視しなければいけないので、一旦先送りとした。

今回作成したもの

プロジェクトをGitHubにアップした。 LaunchAppFromBundleIdentifier

Model、ViewModel、ユニットテストのみ、転記しておく。

まず、ViewModelと付随するテスト。

AppListViewModel.swift

import Cocoa

struct AppListViewModel {
    // icon画像のサイズをstaticで保持
    static let iconSize = NSSize(width: 120, height: 120)
    var items = [AppModel]()
    
    init?(bundleIdentifier: String) {
        // BundleIdentifierにマッチするApplicationのファイルUrlを取得
        // 同じBundleIdentifierのApplicationがインストールされている場合もあり得るので配列となる
        guard let applicationUrls = LSCopyApplicationURLsForBundleIdentifier(bundleIdentifier as CFString, nil)?.takeUnretainedValue() as? [URL] else {
            return nil
        }
        // モデルを作成
        for applicationUrl in applicationUrls {
            guard let model = AppModel(fileUrl: applicationUrl, iconSize: AppListViewModel.iconSize) else {
                break
            }
            items.append(model)
        }
    }
}

AppListViewModelTests.swift

import XCTest
@testable import LaunchAppFromBundleIdentifier

class AppListViewModelTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }

    func testInitializeFromValidBundleIdentifier() {
        let viewModel = AppListViewModel(bundleIdentifier: "com.apple.dt.xcode")
        XCTAssertTrue(viewModel!.items.count >= 0)
        let model = viewModel!.items.first
        XCTAssertEqual(model!.displayName, "Xcode.app")
        XCTAssertEqual(model!.bundleIdentifier, "com.apple.dt.Xcode")
        XCTAssertEqual(model!.bundleVersion, "13532")
        XCTAssertNotNil(model!.icon)
        XCTAssertEqual(model!.filePath, "/Applications/Xcode.app")
        XCTAssertEqual(model!.isActive, false)
    }
    
    func testInitializeFromInValidBundleIdentifier() {
        let viewModel = AppListViewModel(bundleIdentifier: "com.hoge.fuga")
        XCTAssertNil(viewModel)
    }
}

続いて、Modelと付随するテスト。

AppModel.swift

import Cocoa

struct AppModel {
    let displayName: String
    let bundleIdentifier: String
    let bundleVersion: String
    var icon: NSImage? = nil
    let filePath: String
    var isActive: Bool = false
    
    init?(fileUrl: URL, iconSize: NSSize) {
        // エンコーディングされているのでデコード
        guard let filePath = fileUrl.path.removingPercentEncoding else {
            return nil
        }
        // 情報を取得するためにplistを取得
        let plistPath = filePath + "/Contents/Info.plist"
        if !FileManager.default.fileExists(atPath: plistPath) {
            return nil
        }
        guard let infoDictionary = NSDictionary(contentsOfFile: plistPath) else {
            return nil
        }
        // Applicationの名称はdisplayNameじゃないとFinder上の名称と異なることがある
        self.displayName = FileManager.default.displayName(atPath: filePath)
        self.bundleIdentifier = infoDictionary["CFBundleIdentifier"] as? String ?? ""
        self.bundleVersion = infoDictionary["CFBundleVersion"] as? String ?? ""
        self.filePath = filePath
        // Applicationのiconファイル名を取得
        if let iconFilePath = infoDictionary["CFBundleIconFile"] as? String {
            // ファイルパスからNSImageを作成
            self.icon = generateIconImage(filePath: filePath + "/Contents/Resources/" + iconFilePath, iconSize: iconSize)
        }
    }
    
    
    /// iconを作成する
    ///
    /// - Parameters:
    ///   - filePath: iconのファイルパス
    ///   - iconSize: 作成するiconのサイズ
    /// - Returns: 作成に成功したらicon画像、失敗したらnilを返す
    private func generateIconImage(filePath: String, iconSize: NSSize) -> NSImage? {
        var imagePath = filePath
        if !imagePath.hasSuffix(".icns") {
            imagePath += ".icns"
        }
        let iconImage = NSImage(contentsOfFile: imagePath)
        iconImage?.size = iconSize
        iconImage?.lockFocus()
        NSGraphicsContext.current?.imageInterpolation = .high
        iconImage?.draw(in: CGRect(x: 0, y: 0, width: iconSize.width, height: iconSize.height))
        iconImage?.unlockFocus()
        return iconImage
    }
}

AppModelTests.swift

import XCTest
@testable import LaunchAppFromBundleIdentifier

class AppModelTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }
    
    override func tearDown() {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        super.tearDown()
    }

    func testInitislizeFromValidFileUrl() {
        let fileUrl = URL(fileURLWithPath: "/Applications/Xcode.app")
        let model = AppModel(fileUrl: fileUrl, iconSize: NSSize(width: 120, height: 120))
        XCTAssertEqual(model!.displayName, "Xcode.app")
        XCTAssertEqual(model!.bundleIdentifier, "com.apple.dt.Xcode")
        XCTAssertEqual(model!.bundleVersion, "13532")
        XCTAssertNotNil(model!.icon)
        XCTAssertEqual(model!.filePath, "/Applications/Xcode.app")
        XCTAssertEqual(model!.isActive, false)
    }
    
    func testInitislizeFromInValidFileUrl() {
        let fileUrl = URL(fileURLWithPath: "/hoge/hoge.app")
        let model = AppModel(fileUrl: fileUrl, iconSize: NSSize(width: 120, height: 120))
        XCTAssertNil(model)
    }
}

次回以降の予定

  • UI(View)を作る
  • 各アプリケーションの起動状態を検知できるようにする
  • 各アプリケーションを起動できるようにする
  • たぶんRxSwiftを導入する

まとめ

今回初めてMVVMを意識して書いてみたものの、実装よりも構造に悩む時間のほうが長かったかもしれない。
今回書いたViewModelもテストするために必要だったから書いたようなものなので、どうViewに結びつけていいのかよくわかっていない。
エラー処理をまったく入れていないので、どう入れたらいいのかわからない。
初期化に失敗したらnilを返すようにしてて、そういうときにどうエラーに結びつけるのか、よくわからない。
わからないことだらけなので、気が向いた方にアドバイスをいただきたい…。

UIAlertController の UIAlertAction 実行時に、UIAlertView っぽくデリゲートメソッドを呼び出す方法

概要

UIAlertView を UIAlertController に置き換えることが決まって(いまさらとか言わないで…)、いろいろと検討した結果、UIAlertView のデリゲートメソッド内の処理をそのままか、ほぼ手を入れずにいきたいという要望があったため、UIAlertView っぽく UIAlertController の UIAlertAction 実行時にデリゲートメソッドを呼び出す実装を考えてみた。

※Qiitaに投稿した内容を転記して、一部はてなブログ用に改変 qiita.com

環境

  • MacOS 10.12.6
  • Xcode 9.1

想定

UIAlertView にタグがついてて、デリゲートメソッド内でどのボタンが押されたかを判別して処理を切り分けているよくある感じのコードの UIAlertController への置き換えを想定。

仕様

  • UIAlertView に設定していた tag を UIAlertController に付け替えできるように、UIAlertController に tag を保持できるようにしている
  • UIAlertView に設定していた delegate を UIAlertController に付け替えできるように、UIAlertController に delegate を保持できるようにしている
    • delegate 用に protocol を定義
  • UIAlertView のどのボタンがクリックされたという情報を付け替えるために UIAlertAction を追加する際に index を渡すようにして、UIAlertAction 実行時に index を取得できるようにしている

ソースコード

Gist にアップしました。
https://gist.github.com/macneko-ayu/3bcd1694c47c150ea0e36de4b8a18da4

Swift と Objective-C が混在しているという要件があるので、両方から使えるようにした。 Swift のみでの使用なら、tag と delegate は Computed Property で問題なし。

UIAlertController の Extension

UIAlertControllerExtension.swift

import UIKit

@objc protocol AlertControllerDelegate: class {
    func alertController(_ alert: UIAlertController, tappedIndex: Int) -> Void
}

private var DelegateKey: UInt8 = 0
private var TagKey: UInt8 = 0

extension UIAlertController {
    func setDelegate(_ delegate: AlertControllerDelegate?) {
        objc_setAssociatedObject(self, &DelegateKey, delegate, .OBJC_ASSOCIATION_RETAIN)
    }
    
    func getDelegate() -> AlertControllerDelegate? {
        guard let object = objc_getAssociatedObject(self, &DelegateKey) as? AlertControllerDelegate else {
            return nil
        }
        return object
    }
    
    func setTag(_ tag: Int) {
        objc_setAssociatedObject(self, &TagKey, tag, .OBJC_ASSOCIATION_RETAIN)
    }
    
    func getTag() -> Int {
        guard let object = objc_getAssociatedObject(self, &TagKey) as? Int else {
            return 0
        }
        return object
    }
    
    func addAction(title: String, style: UIAlertActionStyle = .default, index: Int) {
        addAction(UIAlertAction(title: title, style: style, handler: { (action) in
            self.getDelegate()?.alertController(self, tappedIndex: index)
            self.setDelegate(nil)
        }))
    }
}

Objective-C から使う場合

ViewController.m

@interface ViewController () <AlertControllerDelegate> 
@end

@implementation ViewController 
    
- (void)viewDidLoad {
    [super viewDidLoad];
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];
    [alert setDelegate:self];
    [alert setTag:3];
    [alert addActionWithTitle:@"OK" style:UIAlertActionStyleDefault index:5];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil];
}

- (void)alertController:(UIAlertController * _Nonnull)alert tappedIndex:(NSInteger)tappedIndex {
    if ([alert getTag] == 3 && tappedIndex == 5) {
        NSLog(@"fuga");
    }
}

@end

Swift から使う場合

ViewController.swift

class ViewController: UIViewController, AlertControllerDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        let alert = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
        alert.setDelegate(self)
        alert.setTag(5)
        alert.addAction(title: "OK", index: 3)
        present(alert, animated: true, completion: nil)
    }
    
    func alertController(_ alert: UIAlertController, tappedIndex: Int) {
        if alert.getTag() == 5 && tappedIndex == 3 {
            print("hoge")
        }
    }
}

使い方

ソースコード参照。

まとめ

黒魔術感が強すぎて、うーん…。