xyk blog

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

いつも設定するAppearanceのメモ

環境:
Swift2.2
iOS8以降対象

いつも設定している Appearance のコピペ用メモ。
以下を AppDelegate で呼ぶ。
mainColor は extension で独自に設定したもの。

private func setupAppearance() {

    // アプリケーション全体のtintColor設定
    self.window?.tintColor = UIColor.mainColor

    // ステータスバーの文字色を白に。
    // プラス`Info.plist`に`View controller-based status bar appearance = NO`を追加
    UIApplication.sharedApplication().statusBarStyle = .LightContent

    // ナビゲーションバーの色
    UINavigationBar.appearance().barTintColor = UIColor.mainColor
    // ナビゲーションバーボタンの色を白に。
    UINavigationBar.appearance().tintColor = UIColor.whiteColor()
    // ナビゲーションバーのタイトル文字色を白に。
    UINavigationBar.appearance().titleTextAttributes = [
        NSForegroundColorAttributeName: UIColor.whiteColor()
    ]
}

こんな感じに。

f:id:xyk:20160813124200p:plain

SwiftでTupleとCaseを組み合わせて使う

環境: Swift2.2

タプルとcaseを組み合わせて使うと便利だったのでメモ。

Switch(case)文で使う

オプショナル型な複数の値の組み合わせで場合分けしたい場合に使う。
また値はアンラップして取り出して変数にバインドする。
ポイントはcase部分で「?」をつけること。これでオプショナル型でnilでない場合にマッチする。

let num: Int? = ...
let str: String? = ...

switch(num, str) {

case let (num?, str?): // num, strともオプショナル型でnil以外にマッチ
// case let (.Some(num), .Some(str)): // これでもよい
    print("num: \(num), str: \(str)")

case (let num?, nil): // numはオプショナル型でnil以外、strはnilにマッチ
// case (.Some(let num), nil): // これでもよい
    print("num: \(num)")

case case (nil, _?): // numはnil、strはオプショナル型でnil以外にマッチ
// case (nil, .Some(_)): // これでもよい
    break

default: // num、strともにnilにマッチ
    break
}

if letの代わりにif case letに使う

if case letでパターンマッチが利用できる。
オプショナルをアンラップして変数にバインド。
これもポイントはcase部分で「?」をつけること。

// a は Optional
if case let x? = a {
}

// 以下と同じ
if case .Some(let x) = a {
}

そしてタプルを組み合わせて使うと、複数の値を一気にバインドできる。
if letよりスッキリ書ける気がする。

if let

// a, b, c は Optional
if let x = a,
  let y = b,
  let z = c {

}

if case let

if case let (x?, y?, z?) = (a, b, c) {

}

参考:

Swift2のパターンマッチ構文集(ほぼ翻訳) - Qiita
http://qiita.com/mono0926/items/f2875a9eacef53e88122

MySQLに街区レベル位置参照情報のCSVデータをインポートする

環境
Mac
MySQL Server version: 5.7.13

位置参照情報ダウンロードサービス
http://nlftp.mlit.go.jp/isj/

今回はこちらから東京都の大字・町丁目レベルのデータをダウンロードする。
13000-09.0b.zipというファイルがダウンロードされる。
これを解凍すると13_2015.csvというCSVファイルがあるのでこのデータをMySQLにインポートする。

まず先に文字コードShift_JISからUTF-8に変換しておく。
nkfコマンドを使おうと思ったがMacには入っていなかったのでhomebrewでインストールした。

$ brew install nkf

UTF-8に変換

$ nkf -w 13_2015.csv > 13_2015_utf8.csv

中身を確認してみる。

$ head 13_2015_utf8.csv
"都道府県コード","都道府県名","市区町村コード","市区町村名","大字町丁目コード","大字町丁目名","緯度","経度","原典資料コード","大字・字・丁目区分コード"
"13","東京都","13101","千代田区","131010001001","一ツ橋一丁目","35.691634","139.756685","1","3"
"13","東京都","13101","千代田区","131010001002","一ツ橋二丁目","35.692947","139.757320","1","3"
"13","東京都","13101","千代田区","131010002000","一番町","35.687723","139.739668","1","1"
"13","東京都","13101","千代田区","131010003001","永田町一丁目","35.676328","139.745749","1","3"
"13","東京都","13101","千代田区","131010003002","永田町二丁目","35.675705","139.740497","1","3"
"13","東京都","13101","千代田区","131010004001","猿楽町一丁目","35.698471","139.759949","1","3"
"13","東京都","13101","千代田区","131010004002","猿楽町二丁目","35.700021","139.758377","1","3"
"13","東京都","13101","千代田区","131010005001","霞が関一丁目","35.674720","139.753419","1","3"
"13","東京都","13101","千代田区","131010005002","霞が関二丁目","35.675706","139.750734","1","3"

10列の項目があるが今回は使いたいのは名称と緯度経度のみなのでそれだけ入れるテーブルを用意する。
名称はすべて連結しnameカラムへ、緯度経度はPointにしてgeometry型のlatlonカラムに突っ込む。

-- MySQLにログイン
$ mysql -uroot

-- 適当なDBを作成
mysql> create database geo;
mysql> use geo;

-- テーブル作成
mysql> CREATE TABLE `spots` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `latlon` geometry NOT NULL,
  PRIMARY KEY (`id`),
  SPATIAL KEY `index_spots_on_latlon` (`latlon`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

※注 MySQL5.7.5以降でInnoDBでもSPATIALインデックスが使えるようになった

そして、LOAD DATA INFILEコマンドでCSVデータをインポートする。

LOAD DATA INFILE '/path/to/13_2015_utf8.csv'
REPLACE INTO TABLE
geo.spots
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\r\n' STARTING BY ''
IGNORE 1 LINES
(@dummy, @prefecture, @dummy, @city, @dummy, @area, @lat, @lon, @dummy, @dummy)
SET
name = CONCAT(@prefecture, @city, @area), 
latlon = ST_GeomFromText(CONCAT('POINT(', @lon, ' ', @lat, ')'));

Query OK, 5666 rows affected (0.20 sec)
Records: 5666  Deleted: 0  Skipped: 0  Warnings: 0

データを確認してみる。

mysql> SELECT id, name, ST_AsText(latlon) FROM spots limit 10;

+----+-----------------------------------------+-----------------------------+
| id | name                                    | ST_AsText(latlon)           |
+----+-----------------------------------------+-----------------------------+
|  1 | 東京都千代田区一ツ橋一丁目              | POINT(139.756685 35.691634) |
|  2 | 東京都千代田区一ツ橋二丁目              | POINT(139.75732 35.692947)  |
|  3 | 東京都千代田区一番町                    | POINT(139.739668 35.687723) |
|  4 | 東京都千代田区永田町一丁目              | POINT(139.745749 35.676328) |
|  5 | 東京都千代田区永田町二丁目              | POINT(139.740497 35.675705) |
|  6 | 東京都千代田区猿楽町一丁目              | POINT(139.759949 35.698471) |
|  7 | 東京都千代田区猿楽町二丁目              | POINT(139.758377 35.700021) |
|  8 | 東京都千代田区霞が関一丁目              | POINT(139.753419 35.67472)  |
|  9 | 東京都千代田区霞が関二丁目              | POINT(139.750734 35.675706) |
| 10 | 東京都千代田区霞が関三丁目              | POINT(139.748265 35.671608) |
+----+-----------------------------------------+-----------------------------+
10 rows in set (0.00 sec)

※注 STプレフィックスが付く空間用関数はMySQL5.6以降、追加されていっているもの。

CSVデータの中身は何もいじらずにサクッとできた。LOAD DATA INFILE 便利!

参考:
MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.6 LOAD DATA INFILE 構文
https://dev.mysql.com/doc/refman/5.6/ja/load-data.html

LaunchScreen.storyboard に貼り付けた画像が表示されない

シミュレータでは表示されるが、実機だと表示されない。
バグらしい、とりあえず自分の場合は iPhone 側の再起動で表示されるようになった。

Launch Storyboard not showing image when projec... | Apple Developer Forums
https://forums.developer.apple.com/message/131986

Launchscreen storyboard doesn't display image | Apple Developer Forums
https://forums.developer.apple.com/thread/17146

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)