The Pragmatic Ball boy

iOSを中心にやってる万年球拾いの老害エンジニアメモ

コード行数の測定

Swiftでコード行数を測ろうとツールを探していたらclocというのがよさげでした

インストール

インストール方法は思ったよりたくさん用意されていてlinux系だけでなく、node.jsやhomebrew, macports用のも用意されていて、WindowsMac環境でも楽にインストールできます

npm install -g cloc                    # https://www.npmjs.com/package/cloc
sudo apt-get install cloc              # Debian, Ubuntu
sudo yum install cloc                  # Red Hat, Fedora
sudo pacman -S cloc                    # Arch
sudo pkg install cloc                  # FreeBSD
sudo port install cloc                 # Mac OS X with MacPorts
brew install cloc                      # Mac OS X with Homebrew

使い方

基本的にはclocのあとに対象のディレクトリを指定すれば、そのディレクトリ以下を含めてコードを解析してくれます

実行

$ cloc Hoge

実行結果

    1572 text files.
    1417 unique files.
     684 files ignored.

http://cloc.sourceforge.net v 1.64  T=19.50 s (46.0 files/s, 9700.3 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C                               92           5068           5097          39838
Bourne Shell                    15           5644           7198          34171
Swift                          365           4776           4414          24868
m4                               8           1074            133           9969
C/C++ Header                   253           4803          14036           8319
Objective C                     43           1790            721           7418
make                            20            845            547           6923
YAML                            17             11              0            410
JSON                            14              0              0            360
D                               64              0              0            242
Python                           2             42             32            164
HTML                             1              0              0             94
Ruby                             1              6              0             60
Go                               1             10             15             20
-------------------------------------------------------------------------------
SUM:                           896          24069          32193         132856
-------------------------------------------------------------------------------

実行結果を見ると、SwiftやGoなどいろんな言語にも対応しているので、clocだけで、いろんなプロジェクトに適用できそうです。

また、いろいろなオプションも用意されており、 例えば以下のようにオプションを指定することにより除外するディレクトリを指定したりできます。

$ cloc Hoge --exclude-dir=vendor

詳しくはドキュメントを御覧ください

WebP.frameworkの作成方法

libwebpをclone

> git clone https://chromium.googlesource.com/webm/libwebp

最新のをcheckout

> git checkout 0.4.4

iosbuild.shを実行

> iosbuild.sh

iosbuild.shでautomakeがないとか怒られた場合は以下を入れる

Command line tools

> xcode-select --install

automake, autoconfとか

> brew install libtool
> brew install automake
> brew install autoconf

成功するとiosbuild.shを実行したディレクトリにWebP.frameworkが生成されます。

Swiftでコマンドラインでカバレッジを取る方法

Objective-Cのときはgcov使ってUnitTestのカバレッジを取っていましたが、 Xcode7から(?)llvm-covが使えるようになったのでこれを使ってみます。

ドキュメントを見ると使い方としては結構単純で、 レポートを出力するには、以下のようにPROFILEとBINを与えてやればレポートが出力されます。

llvm-cov report [options] -instr-profile PROFILE BIN [SOURCES]

PROFILEとBINは、コードカバレッジをONにしてテストを実行するとお馴染みのDerived Data以下に出力されます。

以上のことを踏まえてコマンドラインで実行すると以下のようにすることでカバレッジのレポートを出力させることができます。

注:{}内はプロジェクトによって変更してください。

xcodebuild test -scheme {SchemeName} -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 6' -derivedDataPath build -enableCodeCoverage YES
xcrun llvm-cov report -instr-profile build/Build/Intermediates/CodeCoverage/{SchemeName}/Coverage.profdata build/Build/Intermediates/CodeCoverage/{SchemeName}/Products/Debug-iphonesimulator/{ProductName}.app/{ProductName}

結果はこんな感じで出力されます。

Filename                    Regions    Miss   Cover Functions  Executed
-----------------------------------------------------------------------
.../Headers/NSException.h         0       0    nan%         0      nan%
...eaders/NSObjCRuntime.h         0       0    nan%         0      nan%
...usr/include/MacTypes.h         0       0    nan%         0      nan%
...sdk/usr/include/math.h         0       0    nan%         0      nan%
...sr/include/objc/objc.h         0       0    nan%         0      nan%
...r/include/sys/_types.h         0       0    nan%         0      nan%
..._iOS/AppDelegate.swift        26      21  19.23%        14    21.43%

ただこれファイル名が長いと...省略されて見えなかったりするのがちょっと微妙。。

あと、CIと連動させて表示したいとかだとこのままだと厳しいですね

3D Touch Peak, Popの使い方

1. 3D Touchの発火元となるviewの登録

UIViewControllerのregisterForPreviewingWithDelegateというメソッドを使って、3D Touchに反応するViewと、3D Touchが発生した際にハンドリングするdelegateを登録します。

    override func viewDidLoad() {
        super.viewDidLoad()
  
        imageView.userInteractionEnabled = true // imageViewの場合これが必要

        registerForPreviewingWithDelegate(self, sourceView: imageView)
    }

2. UIViewControllerPreviewingDelegateを継承

3D Touchに反応するUIViewControllerにUIViewControllerPreviewingDelegateを継承させます

class ViewController: UIViewController, UIViewControllerPreviewingDelegate {

3. UIViewControllerPreviewingDelegateのメソッドの実装

UIViewControllerPreviewingDelegateの2つのrequiredのメソッドを実装します

previewingContext:viewControllerForLocation: の実装

こちらはpeak(プレビュー画面を表示)に対応します。

この中では、peakで表示するViewControllerをreturnします。

    func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
        print("peak")

        let sb = UIStoryboard(name: "Storyboard2", bundle: nil)
        let vc = sb.instantiateInitialViewController()
        vc?.preferredContentSize = CGSize(width: 0, height: 200) // 表示サイズ

        print(previewingContext.sourceRect)

        return vc
    }

peakで表示するサイズはViewControllerのpreferredContentSizeで指定することができます。

previewwingContext.sourceRectは3D Touchした際にblurしない範囲(浮き出る部分)を示しています。 sourceViewがimageViewなどでやる場合は特に指定する必要はないですが、tableviewなどの場合、タッチされたcellの位置が浮き出る感じに表示されたほうがよいので、こういうときはpreviewwingContext.sourceRect = cell.frameという風に設定するのがよいと思われます。

previewingContext:commitViewController: の実装

こちらはpop(プレビュー画面で更に強く押して全画面表示)する際に呼ばれるメソッドです。 このメソッド内でpresentViewControllerやnavigationcontroller.pushViewControllerなどを使って次の画面に遷移させます。 引数のcommitViewControllerはpreviewingContext:viewControllerForLocation: でreturnしたViewControllerです。

このメソッドでなにもしないとpeak画面で更に強く押しても何も起こりません。(消える)

    func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
        print("pop")

        presentViewController(viewControllerToCommit, animated: true) {}
    }

4. プレビューアクションの設定

プレビュー画面で下にスクロールしたら出てくるアクションの設定方法です。

プレビュー表示に使うViewController(previewingContext:viewControllerForLocation:でreturnするViewController)でpreviewActionItems() -> [UIPreviewActionItem]をオーバーライドします。

    override func previewActionItems() -> [UIPreviewActionItem] {
        let action1 = UIPreviewAction(title: "action 1", style: .Default) { (previewAction, viewController) in
            print("action 1")
        }
        let action2 = UIPreviewAction(title: "action 1", style: .Destructive) { (previewAction, viewController) in
            print("action 2")
        }

        let subAction1 = UIPreviewAction(title: "subaction 1", style: .Default) { (previewAction, viewController) in
            print("sub action 1")
        }
        let subAction2 = UIPreviewAction(title: "subaction 2", style: .Default) { (previewAction, viewController) in
            print("sub action 2")
        }
        let groupedActions = UIPreviewActionGroup(title: "Sub Actions…", style: .Default, actions: [subAction1, subAction2] )

        return [action1, action2, groupedActions]
    }

iOS9のクイックアクション対応

ホーム画面のアプリアイコンを3D Touchすると実行されるクイックアクションの実装方法です。

Info.plistの変更

UIApplicationShortcutItemsを追加します。

Info.plist

    <key>UIApplicationShortcutItems</key>
    <array>
        <dict>
            <key>UIApplicationShortcutItemIconType</key>
            <string>UIApplicationShortcutIconTypeShare</string>
            <key>UIApplicationShortcutItemSubtitle</key>
            <string>shortcutSubtitle</string>
            <key>UIApplicationShortcutItemTitle</key>
            <string>shortcutTitle</string>
            <key>UIApplicationShortcutItemType</key>
            <string>$(PRODUCT_BUNDLE_IDENTIFIER).Second</string>
            <key>UIApplicationShortcutItemUserInfo</key>
            <dict>
                <key>secondShortcutKey1</key>
                <string>secondShortcutValue1</string>
            </dict>
        </dict>
    </array>

仕様

詳しい仕様はこちらのUIApplicationShortcutItemsを参照 https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/iPhoneOSKeys.html#//apple_ref/doc/uid/TP40009252-SW36

必須事項

必須なのは、クイックアクションの種類を表すStringであるUIApplicationShortcutItemType と 表示するクイックアクション名を表すUIApplicationShortcutItemTitle です。

アイコンの設定

システムの用意しているアイコンを利用する場合

UIApplicationShortcutItemIconTypeをkeyにして、UIApplicationShortcutItonTypeを指定します UIApplicationShortcutIcon Class Reference

  • UIApplicationShortcutIconType
enum UIApplicationShortcutIconType : Int {
    case Compose
    case Play
    case Pause
    case Add
    case Location
    case Search
    case Share
    case Prohibit
    case Contact
    case Home
    case MarkLocation
    case Favorite
    case Love
    case Cloud
    case Invitation
    case Confirmation
    case Mail
    case Message
    case Date
    case Time
    case CapturePhoto
    case CaptureVideo
    case Task
    case TaskCompleted
    case Alarm
    case Bookmark
    case Shuffle
    case Audio
    case Update
}
            <key>UIApplicationShortcutItemIconType</key>
            <string>UIApplicationShortcutIconTypeShare</string>

独自アイコンを使う場合

UIApplicationShortcutItemIconFileをkeyにして、ファイル名を指定

  • 画像の条件
    • square, single color, and 35x35 point

AppDelegateの変更

クイックアクションが実行されるとAppDelegateのperformActionForShortcutItemが呼ばれます。 shortcutItemを参照すれば、どのクイックアクションが実行されたか判別できるので、それによって対応する処理を実装すればよいです。

    func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {

}

動的にクイックアクションを追加する方法

Info.plistに書く静的にクイックアクションを追加する方法とは別に、アプリ起動後にアプリのプログラムから動的にクイックアクションを追加することもできます

やることとしては、UIApplicationShortCutItemのインスタンスを生成して、UIApplicationのshortcutItemsにセットすればよいです。

        if let shortcutItems = application.shortcutItems where shortcutItems.isEmpty {
            // Construct the items.
            let shortcut3 = UIMutableApplicationShortcutItem(type: "hoge", localizedTitle: "Play", localizedSubtitle: "Will Play an item", icon: UIApplicationShortcutIcon(type: .Play), userInfo: [
                "": UIApplicationShortcutIconType.Play.rawValue
                ]
            )

            application.shortcutItems = [shortcut3]
        }

Xcode7のCore AnimationのProfilerはiOS9には対応していない

訂正:Xcode7.1では治ってました

Xcode7を使って、InstrumentsのCore Animationを使って実機でパフォーマンスを測ろうとしても下の図のように実機を選択することができません f:id:yanamura:20151122161325p:plain

iOS8.4の端末を使って試してみたところ選択することができたので、iOS9にはまだ対応していないと思われます

SwiftでUnitTest時に環境変数で分岐させる

テスト対象を@testable import XXXXを使ってimportした場合に、テスト対象のコードをテスト時だけ分岐させて特定の処理を行わないようにしたりしたい場合の対処方法です。

方法としては、環境変数がセットされているかどうかでテストかどうかを判定します。

環境変数の追加

Edit Scheme→Test→Argumentsを選択すると以下の様な画面になります。 f:id:yanamura:20151101193605p:plain

ここのEnvironment Variablesに任意の環境変数を定義します。

分岐処理の追加

上記で追加した環境変数は、NSProcessInfo().environmentを利用して取得することができます。

例えば環境変数が設定されているときはreturnするみたいな場合は以下のようにすればよいです。

guard NSProcessInfo().environment["環境変数名"] == nil else { return true }

// テスト時は実行しないコード
...

dynamic frameworkを使ってるプロジェクトでコマンドラインでipaファイル作成

Swiftでサブプロジェクトのモジュールをdynamic frameworkで取り込む場合にコマンドラインipaファイルを作成する手順です。

環境:Xcode7.1

従来のやりかたではできなかったのでメモしておきます

発生した問題

以前Objective-Cでstatic libraryでやっていたときは以下のようなやり方で作成できていました

$ xcodebuild -sdk iphoneos -target XXX -configuration Release clean build PROVISIONING_PROFILE=xxxxx-xxxx-xxxxxx-xxxxx
$ xcrun -sdk iphoneos XXX build/Release-iphoneos/***.app -o build/***.ipa --embed ****.mobileprovision

が、xcodebuildを実行したところ以下のように、dynamic frameworkをビルドする際にcode sign errorが発生してしまいビルドがこけてしまいます。

$ xcodebuild -sdk iphoneos -target XXX -configuration Release clean build PROVISIONING_PROFILE=xxxxx-xxxx-xxxxxx-xxxxx
Build settings from command line:
PROVISIONING_PROFILE = xxxxx-xxxx-xxxxxx-xxxxx
SDKROOT = iphoneos9.1

=== CLEAN TARGET YyyYYYYY OF PROJECT YyyYYYYY WITH CONFIGURATION Release ===

Check dependencies
[BEROR]Code Sign error: Provisioning profile does not match bundle identifier: The provisioning profile specified in your build settings (“xxxxxx”) has an AppID of “jp.xxxx.xxxxx” which does not match your bundle identifier “com.example.Yyyy”.

解決方法

xcocebuild build でprovisioning profileを指定するとdynamic frameworkもそのprovisioning profileを使ってビルドしようとしてcode sign errorが発生してしまうので、xcodebuild archiveをつかって一旦xcarchiveを生成して、ipaにexportする際にprovisioning profileを指定してやることによって解決しました。

$ xcodebuild -scheme XXXX archive -archivePath release/xxx.xcarchive

$ xcodebuild -exportArchive -archivePath release/xxx.xcarchive -exportOptionsPlist yyy.plist -exportPath zzz.ipa

exportOptionsPlistは以下のような感じになります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>teamID</key>
        <string>MYTEAMID123</string>
        <key>method</key>
        <string>app-store</string>
        <key>uploadSymbols</key>
        <false/>
</dict>
</plist>

methodの値を変更することで、app store向けやadhocなどビルド方法を切り替えられます。

* app-store
* ad-hoc
* package
* enterprise
* development
* developer-id

その他のexportOptionsPlist内で設定できる内容は↓で確認できます

$ xcodebuild -h

CHANGELOGの自動生成

リリースごとにCHANGELOGをもれなく書くのは結構な手間です。 そこでcommit時にコミットログとしてCHANGELOGを記載しておき、リリース時にそれをまとめて出力することでその手間を削減します。

conventional-changelogを使ってCHANGELOG.mdを生成する方法について記載します。

事前準備

  • Node.jsのインストール(詳細はここでは割愛)

  • conventional-changelogのインストール

$ sudo npm install -g conventional-changelog

プロジェクトの設定

package.jsonというファイルをプロジェクトのトップディレクトリに配置します。 そして、以下のように、package.jsonにname, version, respsitoryを記載します。

{
  "name": "SampleApp",
  "version": "1.2.0",
  "repository": {
    "type": "git",
    "url": "https://github.com/xxxx/xxxx"
  }
}

CHANGELOG情報のコミット

commitする際にCHANGELOGにのせる場合は、指定のフォーマットでcommitログ記載します。

angular, atomなど、プロジェクトによって独自のフォーマットが使われていて好みのものを利用します。

詳細はこちらを参照してください。 https://github.com/ajoslin/conventional-changelog/tree/master/conventions

CHANGELOGの生成

package.jsonを置いたのディレクトリ上で以下のコマンドを実行します。 (eslintのところはフォーマットによって変わります)

$ conventional-changelog -p eslint -i CHANGELOG.md -w

そうするとCHANGELOG.mdが生成されます。

ワークフロー

その後は、以下のようなワークフローでCHANGELOGを生成します。

  1. 変更をcommit
  2. package.jsonのversionを更新
  3. CHANGELOGを生成
  4. package.jsonCHANGELOG.mdをcommit
  5. git tagでtagづけする(このときv1.1.1といったようなタグ名でタグ付けします)
  6. git push

注意点としてはこのツールはgitのタグを見て差分をCHANGELOGに追加していくので、タグをつけるのを忘れないように

Swift2 ドキュメントコメント

ドキュメントコメントを記載することで、Option + クリックでメソッドなどの説明を表示したりするようにすることができます。

ドキュメントコメントの指定方法は2通りあり、複数行の場合は/* ... /で囲い、一行の場合は///で始めます。

/**
 say message
 */
func say(message: String) -> String {
/// say message
func say(message: String) -> String {

関数の説明

テキストをそのままかけば関数の説明として扱われます

/// say message
func say(message: String) -> String {

引数の説明

引数一個の場合

/// say message
/// - parameter message: a message what you want to say
func say(message: String) -> String {

引数複数の場合

/// say message
/// - parameters:
///   - message: a message what you want to say
///   - language: Japanese or English
func say(message: String, language: String) -> String {

戻り値の説明

/// say message
/// - returns: a message what did say
func say(message: String) -> String {

エラーの説明

/// say message
/// - throws: ServerError
func say(message: String) throws -> String {

その他の記法

Markdown記法を使うことができます

/// # Title
///
/// ## List
/// - a
/// - b
/// - c
///
/// ## Number List
/// 1. list1
/// 2. list1
/// 3. list1
///
/// ---
///
/// ## Reference
/// [Link](http://github.com)

UnitTestでNSUserDefaultsに保存したデータを消す方法

UnitTestだとremoveObjectForKeyや

NSUserDefaults.standardUserDefaults().removeObjectForKey("key")

resetStandartUserDefaultsを呼んでもデータが消えません

NSUserDefaults.resetStandardUserDefaults()

解決方法

setObjectでnilを突っ込む

NSUserDefaults.standardUserDefaults().setObject(nil, forKey: "key")

Xcode7でシミュレーターのOSバージョンが表示されない不具合解消方法

Xcode7を使っていたところ、下の図のようにシミュレーター一覧を見ると、OSのバージョンが表示されず、どのOSなのかさっぱりわからなくなりました。。 f:id:yanamura:20150910231943p:plain

解決方法

$ rm -rf ~/Library/Developer/CoreSimulator/Devices

そしてmacを再起動します

Swift Bond v4

Swift BondがSwift2.0に対応し、更にインターフェースが大きくかわりv4としてmasterに統合されました。

主な変更点としては、

  • クラス名を刷新

    • これまではBond、Dynamicといった意味不明なクラス名がObservable, EventProducerに変わりわかりやすくなりました。
  • オペレーターの非推奨・廃止

    • ->>や<<-といったオペレータの利用が非推奨となり、bind onlyの->|が廃止されました

と言った風に、いままで個人的に微妙だなと思っていた独自オペレーターや、謎のクラス名が改善されてよくなったなと思ってます。

基本的な使い方

値の変化の監視

以下のようなusernameの値の変化を検知したい場合、

var username: String = "Steve"

以下のようにObservableを使った定義に置き換えます。

class LoginViewModel {
  let username = Observable<String?>("Steve")
}

そして値の変化のイベントを受け取る場合は、上記で定義したObservableのobserve()メソッドを使って値が変化した際に呼ばれるコールバックを登録します。

class LoginViewController: UIViewController
{
  let viewModel = LoginViewModel()

  override func viewDidLoad() {
    viewModel.username.observe { event in
      print(event.value)
    }
  }

observe()は呼ぶとすぐに、引数で渡したコールバックが呼ばれます。 これを避けたい場合はobserveNew()を使えば登録時はコールバックが呼ばれません。

値の変化の監視とバインディング

監視対象とViewの要素をバインディングして、監視対象が変更されたらViewの要素を更新するといった場合、 bindTo()メソッドを使います。

class LoginViewController: UIViewController
{
  @IBOutlet weak var usernameTextField: UITextField!
  
  let viewModel = LoginViewModel()

  override func viewDidLoad() {
    viewModel.username.bindTo(usernameTextField.bnd_text)
  }

UITextFieldのtextとバインディングする場合はbnd_textというObservableがBond側でUITextFieldのextensionとして用意されています。 この他にも基本的なViewは用意されているのでExtensions以下を見るか、bnd_で予測変換をだせば確認できます。

また、bindTo()もobserve()と同様に、登録時に即時バインディングが行われます。 これを避けたい場合はskip()を使います。

viewModel.username.skip(1).bindTo(usernameTextField.bnd_text)

skip(1)とやると最初はusernameの値がTextFieldには反映されず、次にusernameが変化したときから反映されるようになります。

双方向バインディング

bindTo()は監視対象→監視側への一方通行のバインディングでしたが、双方向にすることも可能です。

viewModel.username.bidirectionalBindTo(usernameTextField.bnd_text)

イベントのフィルタリング

特定の条件の変化があった際だけ、コールバックを受け取りたい場合は、fileterを使います。

filterはEventProducer型(Observableのsuper class)を返しますので、filterの戻り値をそのままチェーンしてやることでObservableと同じように扱えます。

  @IBOutlet weak var loginButton: UIButton!
  
  let viewModel = LoginViewModel()

  override func viewDidLoad() {
    loginButton.bnd_controlEvent
        .filter { $0 == UIControlEvents.TouchUpInside }
        .observeNew { [unowned self] event in
            self.viewModel.login()
    }
  }

バインディングの解除

Observableが破棄されると自動的にそれに関わるバインディングは解消されるようになっていますので、普通に使っているぶんには解除を気にする必要はあまりありません。

解除方法

ObservableやbindToは戻り値としてDisposableTypeを返します。 DisposableTypeのdispose()メソッドを呼べば解除できます。

let disposer = viewModel.username.bindTo(usernameTextField.bnd_text)
disposer.dispose()

Cellについて

普通はあまりバインディングの解除を気にする必要はありませんが、UITableViewCellなど再利用されるものについて使う場合は手動で解除してやる必要があります。(Cellが再利用されると解除しないと、他のバインディングが残ってしまうため)

やり方としては、cellにbindする際に、cellのbnd_bagにdisposeInしておき

      let cell = (self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as? ListCellView)!
      let viewModel = dataSource[indexPath.section][indexPath.row]
      viewModel.name.bindTo(cell.nameLabel.bnd_text).disposeIn(cell.bnd_bag)
      return cell

prepareForReuse()でbnd_bagをdispose()します

  override func prepareForReuse() {
    super.prepareForReuse()

    bnd_bag.dispose()
  }

Swift2でsubstring

Swift2(beta6以降)でadvance()が廃止され、BidirectionalIndexTypeなどにadvancedBy()が追加されたました。 書き方は以下の様に変わります

Swift1.2

var text = "123456"
text = text.substringFromIndex(advance(text.startIndex, 3))
// "456"

Swift2

var text = "123456"
text = text.substringFromIndex(text.startIndex.advancedBy(3))
// "456"

Swift2でArrayにArrayをinsertする

これまではsplice:atIndexでしたが

items.splice(insertItems, atIndex: items.endIndex)

Swift2(beta6以降)からinsertContentsOf:atに変わりました

items.insertContentsOf(insertItems, at: 1)

var items = ["a", "b", "c"]
let insertItems = ["1", "2"]
        
items.insertContentsOf(insertItems, at: 1)

// ["a", "1", "2", "b", "c"]