xyk blog

最近は iOS 開発の記事が多めです。

SwiftでDEBUG Macroを使う

環境: swift2.2 Xcode7.3.1

Swiftで以下のようなDEBUG Macroを使いたい。

        #if DEBUG
            print("DEBUG")
        #elseif STAGING
            print("STAGING")
        #else
            print("ELSE")
        #endif

Objective-C の場合はPreprocessor Macrosに設定していたが Swift ではOther Swift Flagsの方に設定する必要がある。
PROJECT or TARGETS -> Build Settings -> Swift Compiler - Custom Flags -> Other Swift Flags から対象の Configuration に-D DEBUGを追加することでフラグとして利用できるようになる。

f:id:xyk:20160624100723p:plain

後は実行する TARGET の Scheme の Build Configuration が合っていればOK。

f:id:xyk:20160624100736p:plain

以上。

ついでに Configuration の追加だが PROJECT -> Info -> Configurations から行う。

f:id:xyk:20160624101047p:plain

SwiftでオブジェクトをNSUserDefaultsに保存する

環境:swift2.1

NSUserDefaultsにオブジェクトのまま保存したかったが、保存できるオブジェクトはNSArray, NSDictionary, NSString, NSNumber, NSDate ,NSDataに限られていた。

調べたところ、オブジェクトをNSDataに変換できることがわかった。
NSDataにできればNSUserDefaultsにも保存できる。

実装方法

  • 対象のオブジェクトにNSCodingプロトコルのデコードメソッドinit?(coder aDecoder: NSCoder)エンコードメソッドencodeWithCoder(aCoder: NSCoder)を実装する
  • 保存時はNSKeyedArchiver.archivedDataWithRootObject:でオブジェクトをNSDataに変換
  • 読み込み時はNSKeyedUnarchiver.unarchiveObjectWithData:NSDataからオブジェクトに変換

以下サンプルコード

struct UserDefaultsKey {
    
    static let User = "user"
}

struct SerializedKey {
    
    static let UserId   = "userId"
    static let Uuid     = "uuid"
    static let NickName = "nickName"
}

class User: NSObject, NSCoding {
    
    var userId: Int
    var uuid: String
    var nickName: String?
    
    init(userId: Int, uuid: String, nickName: String? = nil) {
        self.userId = userId
        self.uuid = uuid
        self.nickName = nickName
    }

    required init?(coder aDecoder: NSCoder) {
        
        self.userId   = aDecoder.decodeObjectForKey(SerializedKey.UserId)   as? Int    ?? 0
        self.uuid     = aDecoder.decodeObjectForKey(SerializedKey.Uuid)     as? String ?? ""
        self.nickName = aDecoder.decodeObjectForKey(SerializedKey.NickName) as? String
    }
    
    func encodeWithCoder(aCoder: NSCoder) {
        
        aCoder.encodeObject(self.userId,   forKey: SerializedKey.UserId)
        aCoder.encodeObject(self.uuid,     forKey: SerializedKey.Uuid)
        aCoder.encodeObject(self.nickName, forKey: SerializedKey.NickName)
    }
}

class UserService {
    
    static let sharedInstance = UserService()
    
    private let userDefaults = NSUserDefaults.standardUserDefaults()
    
    func register(userId userId: Int, uuid: String, nickName: String? = nil) {
        
        let archivedObject = NSKeyedArchiver.archivedDataWithRootObject(User(userId: userId, uuid: uuid, nickName: nickName))
        self.userDefaults.setObject(archivedObject, forKey: UserDefaultsKey.User)
        self.userDefaults.synchronize()
    }

    var registeredUser: User? {
        
        guard let unarchivedObject = self.userDefaults.objectForKey(UserDefaultsKey.User) as? NSData,
            let user = NSKeyedUnarchiver.unarchiveObjectWithData(unarchivedObject) as? User else {

            return nil
        }
        
        return user
    }
}

使用時

// 保存
UserService.sharedInstance.register(userId: 1, uuid: "xxxxx")

// 取得
let user = UserService.sharedInstance.registeredUser

この方法でNSDataにすればシリアライズ・デシリアライズできるのでNSUserDefaultsではなくファイルとしても保存できる。

参考:

iOS でオブジェクトをシリアライズしてファイルに保存する方法 - A Day In The Life
http://glassonion.hatenablog.com/entry/20110904/1315145330

UIImage と UILabel を合成する

環境:Swift2.1

UIImage と UILabel を合成する方法について。

  1. UIImage をセットした UIImageView を作成、そしてそれに UILabel を addSubview する。

  2. UIImageView(UIView) が持つ CALayer プロパティのrenderInContextメソッドでグラフィックコンテキストに描画する。

  3. グラフィックコンテキストから描画した画像を取得する。

以下、サンプルコード。UIImage は前回記事colorImageメソッドを使って作成している。

let yellowImage = UIImage.colorImage(color: UIColor.yellowColor(), size: CGSize(width: 100, height: 100))
let yellowImageView = UIImageView(image: yellowImage)

let myLabel = UILabel(frame: CGRect(x: 10, y: 10, width: 80, height: 17))
myLabel.text = "Hello!"
myLabel.backgroundColor = UIColor.greenColor()
myLabel.textAlignment = .Center
yellowImageView.addSubview(myLabel)

UIGraphicsBeginImageContextWithOptions(yellowImageView.frame.size, false, UIScreen.mainScreen().scale)

if let context = UIGraphicsGetCurrentContext() {
    
    yellowImageView.layer.renderInContext(context)
    let newYellowImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    let newYellowImageView = UIImageView(image: newYellowImage)
}

Playground での実行結果

f:id:xyk:20151114182513p:plain

単色で塗りつぶした UIImage を生成する

指定した UIColor の単色で塗りつぶした UIImage を生成したい。

※ 追記済み

Swift5

iOS10.0 から追加された UIGraphicsImageRenderer を使用する。

A. UIColor の Extension に追加

extension UIColor {
    func image(size: CGSize) -> UIImage {
        return UIGraphicsImageRenderer(size: size).image { rendererContext in
            self.setFill() // 色を指定
            rendererContext.fill(.init(origin: .zero, size: size)) // 塗りつぶす
        }
    }
}

使用時

let redImage = UIColor.red.image(size: .init(width: 50, height: 50))

B. UIImage の Extension に追加

extension UIImage {
    convenience init?(color: UIColor, size: CGSize) {
        guard let cgImage = UIGraphicsImageRenderer(size: size).image(actions: { rendererContext in
            rendererContext.cgContext.setFillColor(color.cgColor) // 色を指定
            rendererContext.fill(.init(origin: .zero, size: size)) // 塗りつぶす
        }).cgImage else {
            return nil
        }
        self.init(cgImage: cgImage)
    }
}

使用時

// Optional
let blueImage = UIImage(color: .blue, size: .init(width: 50, height: 50))

Swift3

UIImage の Extension に追加。

extension UIImage {
    
    static func image(color: UIColor, size: CGSize) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
        let context = UIGraphicsGetCurrentContext()!
        context.setFillColor(color.cgColor)
        context.fill(CGRect(origin: .zero, size: size))
        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        return image
    }
}

使用時

let greenImage = UIImage.image(color: .green, size: CGSize(width: 50, height: 50))

Swift2

UIImage の Extension に追加。

import UIKit

extension UIImage {
    
    static func image(color color: UIColor, size: CGSize) -> UIImage {
        UIGraphicsBeginImageContext(size)
        let context = UIGraphicsGetCurrentContext()
        CGContextSetFillColorWithColor(context, color.CGColor)
        CGContextFillRect(context, CGRect(origin: CGPointZero, size: size))
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return image
    }
}

使用時

let greenImage = UIImage.image(color: UIColor.greenColor(), size: CGSize(width: 50, height: 50))

UIView が持つ描画・レイアウト更新系のメソッドメモ

setNeedsLayout()

現在の子Viewの配置を無効にし、次の更新サイクルで配置し直すようにする(メインスレッドから呼ぶこと)。
このメソッドは即時の更新を強制するものではなく、次の更新サイクルを待つので、更新要求を書き留めたらすぐに処理を戻す。
これを利用して複数のViewの配置を無効にできる。

layoutIfNeeded()

子Viewを即時に配置する。描画前に子Viewを強制的に配置するために使う。
このメソッドを呼んだViewをルートとし、その子孫View全ての配置を行う。

layoutSubviews()

子Viewを配置する。デフォルトの実装では、子Viewの大きさや位置を決めるために設定したConstraintを使う。
サブクラスでは子ViewのautoresizingとConstraintに基づく振る舞いが期待通りにならない場合にオーバーライドする。
このメソッドは直接呼ばないこと。
配置を更新したい場合は次の描画の更新前にsetNeedsLayout()メソッドを呼ぶ。
即時に更新したい場合はlayoutIfNeeded()メソッドを呼ぶ。

setNeedsDisplay()

Viewのboundsの矩形全体に再描画が必要であることを示す。
このメソッドは即時の更新を強制するものではなく、次の更新サイクルを待つので、更新要求を書き留めたらすぐに処理を戻す。
このメソッドはViewの内容や外見が変わった時にのみ使うこと。

updateConstraintsIfNeeded()

このViewとその子ViewのConstraintを更新する。
新しい配置作業がViewに呼び出されると、システムは、Viewとその子ViewのConstraintが現在のView階層とConstraintの情報で確実に更新されるようにするためにこのメソッドを呼ぶ。
このメソッドはシステムに自動的に呼ばれるが、必要な時に手動で呼んでもよい。
このメソッドはオーバーライドしないこと。

updateConstraints()

配置が実行される直前に呼ばれ、ViewのConstraintを更新する。
自身でConstraintを設定する場合にオーバーライドする。
呼ばれた時、まだViewのプロパティが変更されていない段階で、意図した全ての必須Constraintが適切に存在するかを確かめることができる。
Constraintの更新段階ではConstraintを無効にしてはいけない。
また配置や描画を呼んでもいけない。
実装の最後でSuper実装を呼ぶこと。

setNeedsUpdateConstraints()

ViewのConstraintが更新を必要とするかどうかを管理する。
プロパティの変更がConstraintに影響を与える時など、このメソッドを呼ぶことで、Constraintがどこかのタイミングで更新を必要としていることを示すことができる。
システムは通常の配置作業の一部としてupdateConstraints()メソッドを呼ぶ。
必要になる直前にすべてを一度に更新することにより、次の配置作業までの間にViewを複数の変更が会った時も、不要なConstraintの再計算をしなくてすむ。


setNeedsLayout と setNeedsDisplay の違い

setNeedsLayout はサブビューを含むレイアウトの更新で layoutSubviews() が呼ばれるのに対し、 setNeedsDisplay は自ビューの再描画で drawRect() が呼ばれ、 layoutSubviews() は呼ばれない。

UIImage と NSData の相互変換

環境: Swift2.0

UIImage -> NSData

UIImagePNGRepresentation関数、またはUIImageJPEGRepresentation関数を使う。

関数定義

// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
public func UIImagePNGRepresentation(image: UIImage) -> NSData?

// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
public func UIImageJPEGRepresentation(image: UIImage, _ compressionQuality: CGFloat) -> NSData?

PNG 形式の画像データとして取り出す。

let data: NSData? = UIImagePNGRepresentation(image)

JPEG 形式の画像データとして取り出す。

let data: NSData? = UIImageJPEGRepresentation(image, 0.8) // 圧縮率 0(most)..1(least)

NSData -> UIImage

UIImage クラスに NSData を引数に渡すコンストラクタがある。

public init?(data: NSData)

これを使ってインスタンスを作成すればOK。

let image: UIImage? = UIImage(data: data)

NSData が Optional の場合は flatMap を使うとすっきり書ける。

let image: UIImage? = data.flatMap(UIImage.init)

同じiOSバージョンのシミュレータが重複して表示されてしまう場合の解決方法

環境:
Xcode7.0.1

Beta 版と GM をインストールしたら重複して表示されるようになってしまった。

f:id:xyk:20151012172911p:plain

iOSシミュレータを削除するには Xcode > Window > Devicesから削除できる。

f:id:xyk:20151012223020p:plain

削除したいデバイスがたくさんあって個別に面倒な場合は

~/Library/Developer/CoreSimulator

のCoreSimulatorディレクトリごと削除して再起動でもよいらしい(試してない)。

iOS シミュレータ・アプリのディレクトリの場所

環境:
Xcode7.1

アプリデータのディレクトリを調べる

例えば iOSシミュレータにインストールした、あるアプリのDocumentsディレクトリは以下のようになる。

~/Library/Developer/CoreSimulator/Devices/CC8FA744-B3C2-4689-839F-33B504F6168A/data/Containers/Data/Application/3E4EBE82-0BC7-405E-B143-5F40B03EBBA8/Documents

バイスID、アプリIDの部分がランダムな文字列になっていて、パスからは何のアプリなのか判別できない。

このパスはアプリで以下コードを実行することで確認することができる。

// swift3
print(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last!)

最後に使用したシミュレータのデバイスで、最後にインストールしたアプリのDocumentsディレクトリは以下のコマンド一発でFinderを開くことができる。

$ cd ~/Library/Developer/CoreSimulator/Devices/ 
  && cd `ls -t | head -n 1`/data/Containers/Data/Application 
  && cd `ls -t | head -n 1`/Documents 
  && open

iOSシミュレータのディレクトリを調べる

Xcode でアプリ開発時に使用するiOSシミュレータ。  

f:id:xyk:20151026140215p:plain

iOSシミュレータのディレクトリは以下にある。

~/Library/Developer/CoreSimulator/Devices

各シミュレータのディレクトリは以下のようにランダムな文字列の名前となっていて、これを見ただけでは、どのデバイスと紐付いているのかわからない。

f:id:xyk:20151012223634p:plain

このディレクトリがどのデバイスと紐付いているかはXcode > Window > Devicesの Identifier で確認できる。

f:id:xyk:20151027005527p:plain

または以下コマンドからでも確認できる。

$ instruments -s devices
Known Devices:
mbp [502448C4-0A7D-5D96-8EAA-3FE2C32D9C5B]
Apple TV 1080p (9.0) [33BE56A7-BB99-4A45-88E2-3C9C482EDB45]
Apple Watch - 38mm (2.0) [1F3FB32B-2C35-4951-B1D6-C797CEDC2A65]
Apple Watch - 42mm (2.0) [D0E0AA11-7276-4C76-BBFA-2B2571EC13E3]
iPad 2 (9.1) [003458F4-881A-44C9-9E73-815F50790117]
iPad Air (9.1) [CFA403BF-2738-4FF9-A32B-0520D0D8E167]
iPad Air 2 (9.1) [C402142B-130E-4456-B579-E2C032BAFB8C]
iPad Pro (9.1) [01D9E203-1D49-4342-B642-B2FC82C54CB4]
iPad Retina (9.1) [05637EC1-2820-4AB2-BE24-5D8BF7B76DE3]
iPhone 4s (9.1) [8FB284BC-7022-4E3F-9D59-41299CFBB3AC]
iPhone 5 (9.1) [05F41E8B-0C60-4917-B706-DAB2A5C683B8]
iPhone 5s (9.1) [F46048FE-9455-4814-93DF-F5CFCFEE06EA]
iPhone 6 (9.1) [951E310E-D37C-4EF1-83EA-68FFA8233CC0]
iPhone 6 Plus (9.1) [7839119B-EE08-4E39-94C4-9991EC6C804D]
iPhone 6s (9.1) [F5B6F4BB-EEC0-404E-80EA-B200433ACC47]
iPhone 6s (9.1) + Apple Watch - 38mm (2.0) [C67B741D-279E-4DF0-840A-8EE151B75756]
iPhone 6s Plus (9.1) [63933384-7F47-444F-82EE-A3BA10D6A769]
iPhone 6s Plus (9.1) + Apple Watch - 42mm (2.0) [D275AF10-7558-4BED-9B9D-75C65CD7D983]

これで紐付けはわかるが Finder でそのディレクトリまで掘っていくのが面倒くさい。
そこでディレクトリを一発で開くことができるツールを使ってみた。

SimulatorManager

シミュレータ一覧と、シミュレータ内のアプリフォルダが表示されるので選択すれば Finder でディレクトリを開くことができる。

github.com

f:id:xyk:20151022153225p:plain

バイナリのダウンロードはこちら
https://github.com/tue-savvy/SimulatorManager/releases

SimPholders2

こちらはシミュレータにインストールされたアプリ一覧が表示される。
だが、たまに上手く動かないときがある・・・

kfi-apps.com

f:id:xyk:20151012174405p:plain


ちなみに各iOSシミュレータのログディレクトリは以下にある。

~/Library/Logs/CoreSimulator

f:id:xyk:20151012223647p:plain

iOS でファイル保存、読み込み

環境: Swift2.0

アプリ内にデータを保存する場合、どこに保存するのかを調べた。
だいたい以下のディレクトリ(またはこれらの中に作成したサブディレクトリ)のどれかに保存することになるようだ。

  • Documents/
  • Library/
    • Library/Application Support/
    • Library/Caches/
  • tmp/

保存するデータの内容によって使い分ける必要がある。
Apple のガイド
https://developer.apple.com/jp/documentation/FileSystemProgrammingGuide.pdf
から引用する。

Documents/

ユーザが生成したデータを保存するために使います。ファイル共有の機能により、ユーザはこのディレクトリ以下にアクセスできます。したがって、ユーザに見せても構わないファイルのみ置いてください。 このディレクトリの内容はiTunesによってバックアップされます。

ユーザデータはDocuments/以下に置いてください。これは一般に、ユーザに積極的に見せるファイルです。ユーザが自由に作成、インポート、削除、編集する対象です。たとえば描画アプリケーションの場合、ユーザが作成するグラフィックファイルがこれに当たります。テキストエディタであればテキストファイルが該当します。動画/音声アプリケーションの場合、ユーザが後で試聴するためにダウンロードしたファイルもこれに当たります。

Documents/およびApplication Support/以下のファイルは、自動的にバックアップの対象にな ります。NSURLIsExcludedFromBackupKeyキーを指定して -[NSURL setResourceValue:forKey:error:]を実行することにより、バックアップ対象から除外できます。いつでも再生成またはダウンロードできるファイルは、この対象から外してください。大容量のメディアファイルの場合、これは特に重要です。ダウンロードした動画や音声は、バックアップ対象に含めないようにしてください。

Library/

  • Library/Application Support/
  • Library/Caches/

これは、ユーザのデータファイル以外のファイル用の最上位ディレクトリです。通常、標準的なサブディレクトリを用意し、いずれか適当な場所に保存します。iOSアプリケーションは通常、Application Support およびCachesというサブディレクトリを使いますが、独自のサブディレクトリを作成しても構いません。 ユーザに見せたくないファイルはLibraryサブディレクトリ以下に置いてください。ユーザデータのファイル保存用に使ってはなりません。 Libraryディレクトリの内容は、Cachesサブディレクトリ以下を除き、 iTunesによるバックアップの対象になります。

データキャッシュファイルはLibrary/Caches/ディレクトリ以下に置きます。キャッシュデータは、一時データよりは長期間にわたって残しておきたいけれども、補助ファイルほどではない場合に有用です。一般にキャッシュデータは、なくてもアプリケーションの動作に影響しませんが、性能改善の効果が期待できます。例として、データベースのキャッシュファイルや、一時的でいつでもダウンロード可能なデータなどがあります。なお、ディスク空間を確保するため、システムがCaches/ディレクトリを消去することがあるので、必要ならばいつでも生成し直し、あるいはダウンロードできるようになっていなければなりません。

tmp/

このディレクトリは、アプリケーションを次に起動するまで保持する必要のない一時ファイルを書き込むために使用します。不要になったファイルは削除しなければなりません。もっとも、アプリケーションが動作していないときに、システムがこのディレクトリ以下をすべて消去することがあります。 iTunesはこのディレクトリの内容をバックアップしません。

一時データはtmp/ディレクトリに置いてください。一時データとは、長期間にわたって保存しておく必要がないデータのことです。使い終わったら削除して、デバイス上の空間を消費し続けないようにしなければなりません。システムは、アプリケーションが動作していない間、定期的にここにあるファイルを消去します。したがって、一度停止した後、ファイルが残っていることを前提として処理してはなりません。

まとめると以下のようになる。

Documents/
・ユーザが作成したデータ(テキスト、写真)、再作成が不可能なデータなど重要なデータを保存するディレクトリ。
・ユーザに見せたくないファイル(設定ファイルとか)はここに置かない。
・iTunes、iCloud バックアップ対象。

Library/
・ユーザのデータファイル以外を保存するディレクトリ。自分でサブディレクトリを用意して保存する。
・ユーザに見せたくないファイルはここに置く。
・iTunes、iCloud バックアップ対象(Caches ディレクトリを除く)。

Library/Caches/
・一時的なデータを保存するディレクトリ。システムが自動削除する可能性がある。
・後から再度ダウンロードして復旧可能なデータを置くこと。
・iTunes、iCloud バックアップ対象外。

Library/Preferences/
・アプリの設定を保存するディレクトリ。NSUserDefaults のデータはここに保存される。

tmp/
・一時的なデータを保存するディレクトリ。アプリが動作してないときにシステムが自動削除する可能性がある。
・iTunes、iCloud バックアップ対象外。
・使い終わったらアプリ側で削除して容量削減に務める。

tmp/Library/Caches/の使い分けについて

どちらのディレクトリも一時ファイルの保存先として使うが、システムによるファイル削除のタイミングが違う。

ディレクト 非アクティブ中のシステムによる削除 アクティブ中のシステムによる削除 ガイド記載の使用例
tmp/ × 次のアプリの起動で保持する必要のないファイル
Caches/ ダウンロードコンテンツ

ディレクトリパスの取得方法

NSSearchPathForDirectoriesInDomains関数を使う。
またはNSHomeDirectory()でホームディレクトリを取得してそれに連結してもよい。

Documentsディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/73FBC4AB-EA27-4ECF-A1FA-9389A25DD2CD/Documents

// または
let path: String = NSHomeDirectory() + "/Documents"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]

Libraryディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.LibraryDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/7A7E5AA5-9ED9-4CE3-8559-90A946227DF8/Library

// または
let path: String = NSHomeDirectory() + "/Library"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.LibraryDirectory, inDomains: .UserDomainMask)[0]

Library/Cachesディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/3959DFC8-25A6-47C4-B246-FE8304B17864/Library/Caches

// または
let path: String = NSHomeDirectory() + "/Library/Caches"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.CachesDirectory, inDomains: .UserDomainMask)[0]

Library/Application Supportディレクト

let path: String = NSSearchPathForDirectoriesInDomains(.ApplicationSupportDirectory, .UserDomainMask, true)[0]
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/9E25C963-2F4B-4985-BA9B-B9DADF52E9EC/Library/Application Support

// または
let path: String = NSHomeDirectory() + "/Library/Application Support"

// または
let path: NSURL = NSFileManager.defaultManager().URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]

tmp/ディレクト

let path: String = NSTemporaryDirectory()
print(path)
// /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Data/Application/6572ECC7-D57A-40C0-8E0E-E52AC8F64012/tmp/
// NSHomeDirectory()と違ってパス末尾にスラッシュが含まれる。~/tmp/


ディレクトリ・ファイルの操作

NSFileManagerによる作成・削除

NSFileManagerクラスを使う。
よく使いそうな操作メソッドの定義。

// NSFileManager

// 存在チェック
public func fileExistsAtPath(path: String) -> Bool

// ディレクトリ作成
public func createDirectoryAtPath(path: String, withIntermediateDirectories createIntermediates: Bool, attributes: [String : AnyObject]?) throws

// ファイル作成
public func createFileAtPath(path: String, contents data: NSData?, attributes attr: [String : AnyObject]?) -> Bool

// 削除
public func removeItemAtPath(path: String) throws

NSDataをファイル書き込み

ファイルパス(StringまたはNSURL)を指定して NSData として書き込む。

// NSData
public func writeToFile(path: String, atomically useAuxiliaryFile: Bool) -> Bool
public func writeToFile(path: String, options writeOptionsMask: NSDataWritingOptions) throws

public func writeToURL(url: NSURL, atomically: Bool) -> Bool
public func writeToURL(url: NSURL, options writeOptionsMask: NSDataWritingOptions) throws

NSDataとしてファイル読み込み

ファイルパス(StringまたはNSURL)から NSData として読み込む。

// NSData
public init?(contentsOfFile path: String)
public init(contentsOfFile path: String, options readOptionsMask: NSDataReadingOptions) throws

public init?(contentsOfURL url: NSURL)
public init(contentsOfURL url: NSURL, options readOptionsMask: NSDataReadingOptions) throws

UIImageをファイル書き込み

UIImageUIImagePNGRepresentationでNSDataに変換してから書き込み。

UIImagePNGRepresentation(image)?.writeToFile(imagePath, atomically: true)

UIImagePNGRepresentation(image)?.writeToURL(imageURL, atomically: true)

UIImageとしてファイル読み込み

ファイルパスから UIImage として読み込む。

// UIImage
public init?(contentsOfFile path: String)

NSURLを使ってローカルファイルを読み書きする場合の注意点

文字列のファイルパスをNSURLに変換して扱う場合、fileURLWithPath:を使うこと。

let url = NSURL(fileURLWithPath: NSTemporaryDirectory() + "dummy.txt")
// 以下は正しくない
// let url = NSURL(string: NSTemporaryDirectory() + "dummy.txt")

Carthage を使ってライブラリを管理する

環境: Xcode 7.0.1
Swift2.0

Carthage(カーセージって読むみたい)というライブラリ管理ツールを使ってみた。
CocoaPods と比べると
・CocoaPods では Static Library のlibPods.aを静的リンクしていたが 、Carthage はxcodebuildコマンドを使ってビルドした Dynamic Framework (iOS8以降)を実行時に動的にリンクする
・CocoaPods のように.xcworkspaceのようなスキームは生成されない、ライブラリは自分でプロジェクトに追加しなければならないが.xcodeprojのまま扱える
・ビルドが早い
などの違いがあるらしい。

github.com

realm.io

インストール

homebrew でインストールできる。

$ brew update
$ brew install carthage

確認

$ carthage version
0.8.0

Cartfile 作成

Cartfileというファイルを作成して、使用したいライブラリを記述していく。
GitHub レポジトリversion を指定する。

$ cat Cartfile
github "Alamofire/Alamofire" >= 3.0.0

carthage update コマンド実行

依存関係があればそれも含めてダウンロードし、そしてビルド& framework を行う。

$ carthage update
*** Fetching Alamofire
*** Checking out Alamofire at "3.0.0"
*** xcodebuild output can be found in /var/folders/ml/2hwc26zx5b30jf3k4tp_kzj00000gn/T/carthage-xcodebuild.h6xNwA.log
*** Building scheme "Alamofire watchOS" in Alamofire.xcworkspace
*** Building scheme "Alamofire OSX" in Alamofire.xcworkspace
*** Building scheme "Alamofire iOS" in Alamofire.xcworkspace

watchOSOSXiOS各プラットホーム分作成された。
iOS のみでよければ--platformで指定すればよい。

$ carthage update --platform iOS

コマンド実行後は以下のようなディレクトリ構成となった。

f:id:xyk:20151011104005p:plain

Xcode に framework を追加

作成された framework を XcodeTarget > General > Linked Frameworks and Binariesに追加する。
+ ボタンから追加するか Finder からドラッグ&ドロップでも追加できる。

f:id:xyk:20151011104033p:plain

ライブラリを使う

import Alamofire

早速 import して実行してみたところ、ビルドは成功するがシミュレータ起動直後に以下のエラーが発生した。

dyld: Library not loaded: @rpath/Alamofire.framework/Alamofire
  Referenced from: /Users/xyk/Library/Developer/CoreSimulator/Devices/B9D56604-82C8-4752-A4D8-51292D8F625A/data/Containers/Bundle/Application/304560B9-432C-4A5D-BCEF-B2328D84DED1/AlamofireSample.app/AlamofireSample
  Reason: image not found

ググってみるとTarget > General > Embedded Binariesにも追加するらしい。

f:id:xyk:20151011111502p:plain

これでエラーは解消された。

むしろはじめからEmbedded Binariesの方に追加するほうがよいかも。Linked Frameworks and Binariesにも同時に追加されるし。

ライブラリを使う分にはここまでの手順で OK だが、アプリを AppStore にサブミットするためには、さらに以下のワークアラウンドを行う必要があるらしい。

Script の追加

Build Phasesの + ボタンをクリックしNew Run Script Phaseを選択し

/usr/local/bin/carthage copy-frameworks

を入力。またInput Files

$(SRCROOT)/Carthage/Build/iOS/Alamofire.framework

を入力。

f:id:xyk:20151011114007p:plain

f:id:xyk:20151011114317p:plain