The Pragmatic Ball boy

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

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"]

iPadのマルチタスク(SlideOver, SplitView)対応

iOS9からiPadでmultitaskingが使えるようになります。 SlideOverはiPad Air以降、SplitViewはiPad Air2以降で利用できます。

SlideOver, SplitViewに対応したアプリの作成方法

新規で作成する場合

Xcode7で新しいプロジェクトを作ると特に何もしなくてもよいです

既存のアプリを対応させる場合

下記の条件をすべて満たすことが必要

  • Xcode7(iOS9SDK)でビルド
  • launch screen storyboardを使う
  • iPadですべての回転方向をサポートする

ドキュメント

Adopting Multitasking Enhancements on iPad: Slide Over and Split View Quick Start

Swift2でStringを指定した文字で分割

beta5でsplitの仕様がちょっと変わり、以下のようにすることで文字列を分割できます。

let string1 = "hoge"
let string2 = string1.characters.split("o").map{ String($0) }  // [h, ge]

Swift2からStringはCollectionTypeではなくなり、Stringの保持するcharactorsがCollectionTypeになっています

Strings in Swift 2 - Swift Blog - Apple Developer

Swift1系

struct String {
    init()
}

extension String : CollectionType {

Swift2

extension String {
    /// A `String`'s collection of `Character`s ([extended grapheme
    /// clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster))
    /// elements.
    public struct CharacterView {
        /// Create a view of the `Character`s in `text`.
        public init(_ text: String)
    }
    /// A collection of `Characters` representing the `String`'s
    /// [extended grapheme
    /// clusters](http://www.unicode.org/glossary/#extended_grapheme_cluster).
    public var characters: String.CharacterView { get }

...

extension String.CharacterView : CollectionType, Indexable, SequenceType {
    /// A character position.

splitはCollectionTypeのメソッドで仕様は以下のようになっています。

CollectionType Protocol Reference

charactors.split('separator') だけだと結果はCharacterViewのArrayとなってしまうので、map{ String($0) } でStringに変換しています。

SwiftでArray内のOptionalをunwrapする

なにを言っているのかよくわからないタイトルになってますが、 Array<T?>をArrayにする方法です。(Swift1.2以降)

いろいろやり方はありますが、一番手短にかける方法は このようにflatMapにかけるだけ

let array1: [String?] = ["1", nil, "2"]
let array2 = array1.flatMap{ $0 }

というのも、mapとflatMapはinterfaceにも微妙な違いがあるため

func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
func flatMap<T>(@noescape transform: (Self.Generator.Element) -> T?) -> [T]

iOS9 ATSの設定方法

β4現在

Xcode7でビルドするとiOS9以降でApp Transport Security (ATS) が利用可能となります

App Transport Security Technote: App Transport Security Technote

デフォルトでhttps通信が必須となるので、この挙動を変更するには上の仕様に書いてあるようにInfo.plistに設定を記載する必要があります。

ATS設定パターン

1. 全ての通信にApp Transport Securityを適用

設定不要

2. 全ての通信にApp Transport Securityを適用しない

以下のように設定することでhttpsでなくても通信可能となります

f:id:yanamura:20150725122127p:plain

3. 指定したドメインのみ適用する

指定したドメインのみhttps通信をmustにしたい場合は以下のように設定します サブドメインも含めたい場合は、 NSIncludesSubdomainsをYESにします。

f:id:yanamura:20150725123159p:plain

この例では、yahoo.co.jpとそのサブドメインに対して通信する場合はhttpsが必須となり、 その他のサイトでは任意となります。

4. 指定したドメイン以外に適用する

指定したドメインのみhttps通信をmustにしたく”ない”場合は以下のように設定します

f:id:yanamura:20150725140625p:plain

この例では、yahoo.co.jpのみhttpで通信可能となり、それ以外はhttpsが必須となります。

これ以外の細かい設定はAppTransportSecurityTechnoteに色々とパラメータが用意されているので、それらを参照ください

Xcode7 betaでiOS8.4端末にインストールできない場合

Xcode7 beta3で、iOS8.4の端末をつなげても"ineligible devices"となり、端末が選択できず実機にインストールできない。。 (追記:beta4ではiOS8.4に対応してます)

調べてみると8.4が入ってないです

$ ls /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/
6.0/            7.0/            8.1/            8.3/
6.1/            7.1/            8.2/            9.0 (13A4254u)/

解決方法としては、Xcode6.4から8.4をコピーしてくればとりあえず動きました

$ cp -r /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/8.4\ \(12H141\) /Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/

XcodeのSwiftのFile Templateを変更する

Xcodeでデフォルトで生成される以下のようなtemplateを変更したい場合は

//
//  File.swift
//  Project_name
//
//  Created by xxxxxxxxxxx on xxxx/xx/xx.
//  Copyright © 2015年 xxxxxxxxx xxxxxxx. All rights reserved.
//

import Foundation

このようにユーザーのHomeディレクトリ配下にTemplateファイルを配置してやり、

$ mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates/User\ Templates

$ cd ~/Library/Developer/Xcode/Templates/File\ Templates/User\ Templates

$ cp -r /Applications/Xcode.app/Contents/Developer/Library/Xcode/Templates/File\ Templates/Source/Swift\ File.xctemplate .

FILEBASENAME_.swiftを変更すれば、Xcodeでファイルを作る際に、このようにUser Templateが現れるので、ここのSwift Fileを選択すれば独自のtemplateを使うことができます。 f:id:yanamura:20150705011200p:plain

AppCodeだったらこんな面倒なことしなくてもtemplate変更できるんですが・・

Swift2でのbitmask

Swift2からRawOptionSetTypeがOptionSetType (OptionSetType Protocol Reference) に変わったため、

例えばUIViewAutoresizingの場合、 これまでは

view?.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight

だったのが

self.view?.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]

になります。

OptionSetTypeのprotocol extensionでいくつかメソッドが用意されているので、これらを使って、and, or, exclusive orなどのbit演算を行ったり、containsを使ってbitが立っているかチェックできます

extension OptionSetType {

    /// Returns the set of elements contained in `self`, in `other`, or in
    /// both `self` and `other`.
    func union(other: Self) -> Self

    /// Returns the set of elements contained in both `self` and `other`.
    func intersect(other: Self) -> Self

    /// Returns the set of elements contained in `self` or in `other`,
    /// but not in both `self` and `other`.
    func exclusiveOr(other: Self) -> Self
}

extension OptionSetType where Element == Self {

    /// Returns `true` if `self` contains `member`.
    ///
    /// - Equivalent to `self.intersect([member]) == [member]`
    func contains(member: Self) -> Bool

    /// If `member` is not already contained in `self`, insert it.
    ///
    /// - Equivalent to `self.unionInPlace([member])`
    /// - Postcondition: `self.contains(member)`
    mutating func insert(member: Self)

    /// If `member` is contained in `self`, remove and return it.
    /// Otherwise, return `nil`.
    ///
    /// - Postcondition: `self.intersect([member]).isEmpty`
    mutating func remove(member: Self) -> Self?
}