The Pragmatic Ball boy

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

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?
}

Vagrantでjavascriptやcssのファイルを変更しても反映されない

Vagrantjavascriptcssを変更しても反映されないという問題にはまりました。

ブラウザのキャッシュの問題かと思ったのですが、Chrome Developer ToolのSettingで"Disable cache"にしても変化なし。

ならばサーバー側だということでnginxのキャッシュとかを調べていたら、nginxとかの問題ではなく、VirtualBoxのBugのようで、VirtualBox(Vagrant)とshareしているフォルダにおいてるとだめぽい。

http://docs.vagrantup.com/v2/synced-folders/virtualbox.html

ここに書いてあるようにsendfileをoffにすればなおります。

nginx

sendfile off;

apache

EnableSendFile off

Ansibleでユーザーにグループを追加してもすぐに反映されない

Ansibleでユーザーにグループを追加するのは以下のようなやり方でできます

user - Manage user accounts — Ansible Documentation

- user: name=hoge group=test append=yes

が、この後に、hogeユーザーで、testのグループパーミッションのついたファイルやディレクトリを操作しようとするとなぜかpermission deniedになってしまいます。

調べたところissueにのっていて、 どうもflush the connectionを実装してもらわないと無理ぽい Impossible to add oneself to a group to use this group permission · Issue #921 · ansible/ansible-modules-core · GitHub

なので、グループパーミッションを利用して制限かけるのは諦めて他の方法でやるしかない

AnsibleでVagrantのprovisioningする際のgitの認証を通すには

ここに書いてあるやり方でいけました。

AnsibleからVagrantにssh経由でgit cloneする時の注意点 - Qiita

が、あまり関係ないと思っていた

sudo: no

が結構重要だったりしました。

gitを実行するTaskをsudoで実行してしまうと、userが変わってしまうのでssh forward agentが効かなくなってしまうっぽいので、gitなどのtaskはsudoをnoにしておく必要があります。

http://stackoverflow.com/questions/20952689/ansible-ssh-forwarding-doesnt-seem-to-work-with-vagrant/22768453#22768453

VagrantとAnsibleでFuelPHP開発環境構築(Ubuntu 14.04, nginx)

事前準備

  • Vagrantのインストール
  • Ansibleのインストール

ここではこれらのインストール方法は割愛

環境構築

とりあえず開発環境作りたい場合は、以下を実行し、しばらくすると環境が構築されます。

$ git clone https://github.com/yanamura3/FuelEnv
$ cd FuelEnv
$ vagrant up

localhost:3334をブラウザで開いてFuelPHPの画面がでればOKです。 FuelPHPでの開発はFuelEnv以下にfuelというフォルダができているのでそれを使えばよいです。



詳細

いじったところなどのメモ

Vagrant

VagrantFile

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = "ubuntu/trusty64"

  config.vm.provider "virtualbox" do |vb|
    # Customize the amount of memory on the VM:
    vb.memory = "2048"
  end

  config.vm.network "forwarded_port", guest: 80, host: 3334

  config.vm.synced_folder "./", "/var/www"

  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "provisionings/site.yml"
    ansible.limit = "default"
    ansible.groups = {
      "vagrant" => ["default"],
    }
  end
end

config.vm.synced_folderでカレントディレクトリとVM側の/var/www以下を同期するようにしています。 これはあとでFuelPHPを/var/www以下にcloneしてきて、PHPStormで開発しやすくするためにこうしました。

  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "provisionings/site.yml"

このように書くことで簡単にVagrantからansibleのplaybookを実行することができます。(これによってinvetoryの設定など面倒なことをVagrant側がやってくれます)

    ansible.limit = "default"
    ansible.groups = {
      "vagrant" => ["default"],
    }

この部分は、VagrantからAnsibleを実行されたときのみで条件をわけるためにこうしています。host名はVagrantではdefaultになっていました。

Ansible

Ansibleというか、Fuelを使う上での設定周りではまったところで、主にnginx周りです。 Ansibleに関してはansible-example(ansible/ansible-examples · GitHub)をベースにやるとやりやすかったです。

Why Ansible

サーバー側に何もいれなくてよいので、knife-soloでやるよりはこっちでいいかなくらいのノリです。

nginx

nginx.confのほうはsites-enableのconfigファイルを参照するように変更

/etc/nginx/nginx.conf

include /etc/nginx/sites-enable/*.conf

virtualhostの設定自体はsites-available以下において、sites-enable以下にそれに対するシンボリックリンクを置くようにしました。(不要なときにシンボリックリンクだけ消せばよいので

/etc/nginx/sites-available/virtual.conf

server {
       listen 80 default_server;
       server_name localhost;

       root /var/www/fuel/public;

       location / {
                try_files $uri /index.php?$uri&$args;
        }

        location ~ \.php$ {
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                include fastcgi_params;
        }
}

rootを/var/www/fuel/publicとpublicフォルダ以下になるように設定。

とくにlocationのところが重要で、localhost:xxxxでwelcomeページは開けるけど、localhost:xxxx/helloが開けない という場合はここの記述が間違っている可能性が高いです。

fastcgiを動かすために、fastcgi_passを127.0.0.1:9000にして、fpmの設定で9000番ポートをlistenするように修正しています。

/etc/php/fpm/pool.d/www.conf

listen = 9000

また普通にnginxをインストールするとバージョンが1.4とかになるので最新のstableをとってくるように変更しました

- name: get nginx signing key
  apt_key: url=http://nginx.org/keys/nginx_signing.key

- name: update nginx sources1
  apt_repository: repo="deb http://nginx.org/packages/ubuntu/ trusty nginx"

- name: update nginx sources2
  apt_repository: repo="deb-src http://nginx.org/packages/ubuntu/ trusty nginx"

- name: install nginx
  apt: pkg=nginx state=present update_cache=yes