ごんれのラボ

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

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")
        }
    }
}

使い方

ソースコード参照。

まとめ

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

DTPer のスクリプトもくもく会への想いとか他の地域での開催とか

概要

DTPer のスクリプトもくもく会への想いとか他の地域での開催とかを Twitter ではよく話しているのですが、ブログにもまとめておいたほうがいいだろうなーと思ったので、書きます。
ポエムです。

もくもく会への想い

みんなでスクリプトに関することを話せる場ってほんとに少なくて、自分自身、勉強会の懇親会などで有識者の人を捕まえて話して気持ちを盛り上げるということをしてきました。
でも、懇親会では細かいところまで話せませんし、教える立場になってもその短時間で相手のわからないところを理解して回答するのは至難の業でした。
でも、もくもく会なら、スクリプトのことを話せるんですよ。
「このコードのここがよくわからない」「こういうのが書きたいけど、ここで詰まっている」
そういう会話がもくもく会ではたくさん聞こえてきますし、私自身参加者とお話して、積極的にコミュニケーションを取るようにしています。
せっかくスクリプトについて興味のある人、知識がある人たちが集まっているのだから、その時間を有効に使っていただけるとうれしいですね。

ゆっくり静かなところでもくもく作業をしたいという方には参加しづらい会になってしまうかもしれないけど、そういうもくもく会を希望されるのであれば、思いきって開催されてみてはいかがでしょうか。
おそらく参加者は集まると思いますし、誰かに教えることができる人だけが会を開催できるわけではなくて、不安であれば知識のある方と一緒に開催するなど、方法はあると思っています。
また、教える側にはデメリットしかないと思う方もいるかもしれませんが、人に教えられないことは実は自分でもほとんど理解していないってことなんですよね。
人に理解してもらうためにどう説明するか、どう噛み砕くか。
その作業をすることによって、表面的に理解していた気持ちになっていたことが、自分の身になり、自分の言葉で相手に伝えることができるようになります。
私自身、もくもく会を主催し、勉強会に登壇し、成長できたと自負しています。

まあ、もくもく会については id:uske-s さんに負担をかけまくっていますが…。
一人ですべてを管理するのはしんどいので、お手伝いをお願いして本当に良かったし、感謝しています。
いつもありがとうございます。

そんなわけで、もくもく会兼わくわく会が根づいていったらいいなーと思っています。
主催も私である必要性って一切ないんですよね、実は。

他の地域での開催

ありがたいことにいろんな地域の方から「出張版はないのー?」と要望をいただいていますが、主催である我々が特別なにかをしているわけではないので、各地域で必要だと思われた方が開催されるほうが地域に根づいていいんじゃないかなーと思っていたりします。
10人程度入れる場所さえあれば、なんとかなるもんです。
いきなり始めるのは気後れしてしまうということであれば、交通費などかかってしまいますが、一度我々のもくもく会にご参加いただいて、どういう雰囲気、段取りで進めているのか、見学されてはいかがでしょうか。
ほんと、特別なことはしていませんので。
東京で開催しているのは私が東京住まいだからという理由でしかなくて、将来的に私がどこかに引っ越しても、需要があればその地域でもくもく会っぽいことはやりたいと思っています。

とはいったものの、交通費をいただければ、継続的な開催に向けて最初の数回だけお手伝いというのは可能だと思っています。
ただ、結局は参加者がいてこそのもくもく会なので、継続して参加してもらうための施策などは、ご検討いただく必要があるかもしれませんね。

継続開催が難しければ「温泉で合宿しようぜ!」みたいなお祭り的な会でも面白そうですね。
そういうのは参加したいです!

単純に参加したい!という方は、Slack で随時情報共有が行われていますし(私があまり見れていませんが…)、もくもく会開催中はハッシュタグも用意していますので、それを利用していただくと楽しんでいただけるかと思います。
もくもく会関係なく、なにか聞きたいことがある場合などは、私などにメンションを飛ばしてもらえれば、お答えできることもあると思います。
もしくは、答えられそうな人に投げますw

最後に

偉そうなことを書きましたが、もくもく会は参加者があってこその会です。
いつもご参加ありがとうございます。
これからもDTPer のスクリプトもくもく会をよろしくお願いします!

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

前説

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

参加者は私を含め16名でした。
相変わらずたくさんの方にご参加いただけて嬉しいです。

当日の雰囲気

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

Tweetのまとめ

Togetter でまとめました。
togetter.com

発表について

私と @MD5500 さんが AppleScript について、@kickbase さんから VSCode で ExtendScript を書きやすくする方法について発表を行いました。

私の発表資料です。

資料中のソースコードはこちら。

-- アラート的なやつ
display dialog "Hello world!"

-- 文字列代入
set cat to "レノくん"

-- 数値代入
set one to 1

-- 配列(リスト)
set array to {0, 1, 2}
display dialog first item of array

--連想配列(レコード)
set hash to {name:"レノくん", age:"7"}
display dialog name of hash

-- 文字列結合
set leno to cat & "、かわいい"
display dialog leno

-- 条件式
if cat is "レノくん" then
    display dialog "OK"
else
    display dialog "NG"
end if

--繰り返し
--よくある for  的なやつ
repeat with i from 1 to 3
    display dialog i
end repeat
--for in 的なやつ
repeat with i in {0, 1, 2}
    display dialog i
end repeat

--メソッド(ハンドラ)
my showDialog("テスト")
on showDialog(message)
    display dialog message
end showDialog

内容としては「AppleScript って聞いたことがあるけど、どんなものかよく知らない」人向けにざっくりまとめたものになります。
資料にはありませんが、現役時代に使っていた AppleScript のデモを行いました。
(先日の登壇の際に発表しようと思って、テンパって忘れてたやーつ…)
シェル経由でなんでもできる AppleScript の素晴らしさを伝えられたかと思います。

今後の予定

今年の開催は終わったので、次回は来年1月に開催予定です。
業界問わず忙しい時期だとは思いますが、みなさんのご参加をお待ちしております。

Objective-C で OS バージョン判定を簡潔に行う方法

Objective-C で OS バージョン判定を簡潔に行う方法

概要

Qiita に投稿した下記記事と同じ内容です。
qiita.com

OS のバージョンを調べる方法が、Objective-C だと面倒くさい方法しかないのかと思ってて、うんざりしてたら、そんなことなかったので紹介。

環境

  • MacOS 10.12.6
  • Xcode 9.0.1

従来の方法

記述量が多くて面倒だった。

if ([UIDevice currentDevice].systemVersion.floatValue >= 11.0) {
    NSLog(@"iOS11以上だよ");
} else {
    NSLog(@"iOS11未満だよ");
}

簡潔な方法

記述量が少なくて良い。

if (@available(iOS 11.0, *)) {
    NSLog(@"iOS11以上だよ");
} else {
    NSLog(@"iOS11未満だよ");
}

注意点

@available(〜) と他の条件を一緒に記載すると warning が出る。
ネストすれば大丈夫。

NG

Bool isClose = YES;
if (@available(iOS 11.0, *) && isClose) {
    NSLog(@"iOS11以上だよ");
}

OK

Bool isClose = YES;
if (@available(iOS 11.0, *)) {
    if (isClose) {
        NSLog(@"iOS11以上だよ");
    }
}

ネストせずに書けたらいいなぁ。

参考:Negating Objective-C's @available keyword

まとめ

新しいやり方がないか、定期的に確認しないとダメだね。

Unity で Xcode のプロジェクトを生成後に必要な設定を Unity の Script で自動化する方法

概要

Qiita に投稿した下記記事と同じ内容です。
qiita.com

iOS 用の Cocoa Touch Static Library を作ったものの、Unity でビルドが通らず、Xcode の Build Settings を変更する必要があった。
毎回手作業で変更するには項目数が多すぎたのと、自分以外の環境でビルドできないと意味がないので、自動化する方法がないか調べてみた。

Cocoa Touch Static Library を作る方法

下記を参考にしました。
UnityでAndroid、iOS両対応のC++プラグインの実装
Unity & iOSプラグイン連携
[Unity] C#とObjective-Cの連携まとめ

自動化する方法

下記を参考にしました。
UnityでXcodeの設定を自動化する方法まとめ

コピペですが、一応、ソースコード。

XcodeSettingsPostProcesser.cs

using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.iOS.Xcode;
using UnityEditor.Callbacks;
using System.Collections;

public class XcodeSettingsPostProcesser
{
    [PostProcessBuildAttribute(0)]
    public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject)
    {
        // iOS以外のプラットフォームは処理を行わない
        if (buildTarget != BuildTarget.iOS)
        {
            return; 
        }

        // PBXProjectの初期化
        var projectPath = pathToBuiltProject + "/Unity-iPhone.xcodeproj/project.pbxproj";
        PBXProject pbxProject = new PBXProject();
        pbxProject.ReadFromFile(projectPath);
        string targetGuid = pbxProject.TargetGuidByName("Unity-iPhone");

        // ここに自動化の処理を記述する
        pbxProject.AddBuildProperty(targetGuid, "CLANG_ENABLE_MODULES", "NO");
        pbxProject.AddBuildProperty(targetGuid, "CLANG_ENABLE_MODULE_DEBUGGING", "YES");
        pbxProject.AddBuildProperty(targetGuid, "CLANG_ENABLE_OBJC_ARC", "NO");

        // 設定を反映
        File.WriteAllText(projectPath, pbxProject.WriteToString());
    }
}

少しでも楽に設定を C# のソースコードに反映する方法

  1. Library を作るときの Git の履歴から、project.pbxproj ファイルの変更行をコピーして、テキストエディタにコピペ
  2. テキストエディタの検索置換で正規表現を使って整形
  3. C# のソースコードにコピペ

検索パターン

\t\t\t\t"?(.+?)"? = "?(.+?)"?;$

置換パターン

pbxProject.AddBuildProperty(targetGuid, "\1", "\2");

置換前のテキスト

             CLANG_ENABLE_MODULES = NO;
                CLANG_ENABLE_MODULE_DEBUGGING = YES;

置換後のテキスト

pbxProject.AddBuildProperty(targetGuid, "CLANG_ENABLE_MODULES", "NO");
pbxProject.AddBuildProperty(targetGuid, "CLANG_ENABLE_MODULE_DEBUGGING", "YES");

環境によって検索側のパターンは変える必要があると思う。
あと、AddBuildProperty は設定を追加するので、Unity がビルド時に設定を追加していると、「Unity が書き換えた設定 + Script で追加した設定」というおかしな状態になるので、必要に応じて関数を変えないとダメ。

感想

毎回手作業で40個ぐらい設定を変える必要がなくなったので、その分猫社員を撫でようと思った。

DTPの勉強会(東京) 第26回に登壇させていただきました

f:id:macneko-ayu:20171024185646j:plain

ご報告とお礼

先週の10/21(土)にDTPの勉強会(東京) 第26回に登壇させていただき、バージョン管理システム(Git)のお話をしてきました。
憧れの大間知さん、若手のホープと勝手に呼んでいる三枝さんと一緒に登壇できて、緊張するとともにとても楽しかったです。
登壇する機会をくださった主催者様、たくさんのフォローをいただいたスタッフ様、ありがとうございました。
お足元の悪い中、お越しくださった参加者の皆様、ありがとうございました。
登壇資料を前振りなく共有してレビューをお願いしたにも関わらず、快く引き受けてくださり、よりよい登壇資料するために忌憚ない意見をくださった友人たち、ありがとうございました。

誤り訂正

ターミナルのデモで使用した cd コマンドですが、正しくは チェンジディレクトリ でした。
すみません。
恥ずかしい…。

フォローアップについて

アンケートでいただいたご質問の回答などは、少々お待ちください。 主催者様より送信済です。

資料について

今回の登壇資料はすべて Git でバージョン管理をしながら作成しました。

f:id:macneko-ayu:20171025014044p:plain
コミット履歴
ブランチを切らずに master にコミットし続けるという楽な方法を選びました。
自宅と会社で原稿を書くために、ホスティングサービスの Visual Studio Team Services を利用しました。
www.visualstudio.com

どうしてこのサービスを選択したのかは後日記事にする予定です。

DTPerのスクリプトもくもく会について

懇親会でDTPerのスクリプトもくもく会について、ご質問をいただいたので、その際の回答をここにも記しておきます。

Q. 参加の条件はあるの?

もくもく会なので、漠然と「この作業をスクリプトで効率化したい」という程度でかまいませんので、なにか作りたいと思うものがある方に参加していただきたいです。
初心者の方は、ご自分で書籍など持参していただいて独学である程度進められる気概のある方という、わりとハードルを高めに設定させていただいております。
理由としては、往々にして「なにがわからないかわからない人にはなにもアドバイスができない」という問題がありますので、例えば「書籍のこの部分がわからない」「このサンプルのここを改変したいけど、よくわからない」というようにわからない場所を伝えていただきたいからです。
いわゆる教えてちゃんにリソースを割くのはもったいないので…。
本を持参してその内容を理解する、写経するというのも、家だとなかなかできないので、もくもく会を活用いただけるとうれしいです。

Q. パソコン持参でもいいですか?

パソコンがないともくもくできないので、持参してくださいw
会場にもよるのですが、最低限電源は用意するようにしています。

Q. 参加者の方のレベルが高すぎて気後れします

みんな最初は初心者だったので、気後れする必要はありません。
私自体、参加者の方のレベルよりも数段下にいますが、主催できています!
同じ趣味をもった仲間だと思って、気軽にご参加ください。

Q. 猫はいますか?

おかげさまで参加人数が増加中ですので、猫社員のいる弊社ではキャパオーバーです、すみません。

togetter まとめ

すごい分量でした。
また、tweet でたくさんのフォローをいただき、ありがとうございました。 togetter.com

資料と動画

主催者間のご厚意により公開許可をいただきました。

資料

動画