The Pragmatic Ball boy

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

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

FuelPHPのインストーラの中身

FuelPHPのインストール時のコマンドでなにをやっているのか調査

$ curl get.fuelphp.com/oil | sh

$ oil create fuelphp

$ curl get.fuelphp.com/oil | sh では、/usr/bin/oilにhttp://get.fuelphp.com/installer.shを書き込んでる

#!/bin/bash

PREFIX="/usr/bin/"

install_oil() {

    if [ `which sudo` ]; then
        sudo sh -c "curl --silent http://get.fuelphp.com/installer.sh > ${PREFIX}oil"
        sudo chmod +x ${PREFIX}oil
    else
        sh -c "curl --silent http://get.fuelphp.com/installer.sh > ${PREFIX}oil"
        chmod +x ${PREFIX}oil
    fi
}

#
# Handle execution
#
main() {

  # Start installation
  install_oil
  exit 0
}

main

http://get.fuelphp.com/installer.shの中身はこのようになっていて oil createが実行されたときにFuelPHPのgitリポジトリからcloneしてきてsubmodule updateしてcomposerをアップデートして、refine installでパーミッションを変更するといういたって単純なことをしてる。

#!/bin/bash

# if we have the oil script in the current directory, start that
if [ -f "./oil" ]; then
        php oil "$@"
else

                # check for bash commandline options
        if [ "$1" == "create" ]; then

                                # make sure git is installed
                if [ ! `which git` ]; then
                    echo "For this installer to work you'll need to install Git."
                    echo '        http://git-scm.com/'
                fi

                                # clone the repository, and make sure the latest master is active
                git clone --recursive git://github.com/fuel/fuel.git "./$2"
                cd ./$2
                branch=`git branch -a | grep -v "remote" | grep "master" | tail -1`
                branch=${branch#* }
                git checkout $branch
                git submodule foreach git checkout $branch

                # run composer
                php composer.phar self-update
                php composer.phar update

                # fix potential rights issues
                cd ..
                php "./$2/oil" refine install
        else
                echo 'This is not a valid Fuel installation so Oil is a bit lost.'
                echo '        http://fuelphp.com/docs/installation/instructions.html'

        fi
fi

ただgitリポジトリの最新のmasterブランチをとってくるようになっているので、 バージョンを指定したいとかは使えないですが、常に最新使うんだったら特に問題なさげ

Ruby on Railsの開発環境構築

Using Vagrant for Rails Development - GoRails のを参考に実施

VagrantVirtualBoxをインストール

Install Vagrant

Downloads – Oracle VM VirtualBox

$ vagrant plugin install vagrant-vbguest
$ vagrant plugin install vagrant-librarian-chef
$ cd MY_RAILS_PROJECT # Change this to your Rails project directory
$ vagrant init
$ touch Cheffile
site "http://community.opscode.com/api/v1"

cookbook 'apt'
cookbook 'build-essential'
cookbook 'mysql'
cookbook 'ruby_build'
cookbook 'nodejs', git: 'https://github.com/mdxp/nodejs-cookbook'
cookbook 'rbenv', git: 'https://github.com/fnichol/chef-rbenv'
cookbook 'vim'
# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  # Use Ubuntu 14.04 Trusty Tahr 64-bit as our operating system
  config.vm.box = "ubuntu/trusty64"

  # Configurate the virtual machine to use 2GB of RAM
  config.vm.provider :virtualbox do |vb|
    vb.customize ["modifyvm", :id, "--memory", "2048"]
  end

  # Forward the Rails server default port to the host
  config.vm.network :forwarded_port, guest: 3000, host: 3000

  # Use Chef Solo to provision our virtual machine
  config.vm.provision :chef_solo do |chef|
    chef.cookbooks_path = ["cookbooks", "site-cookbooks"]

    chef.add_recipe "apt"
    chef.add_recipe "nodejs"
    chef.add_recipe "ruby_build"
    chef.add_recipe "rbenv::user"
    chef.add_recipe "rbenv::vagrant"
    chef.add_recipe "vim"
    chef.add_recipe "mysql::server"
    chef.add_recipe "mysql::client"

    # Install Ruby 2.1.2 and Bundler
    # Set an empty root password for MySQL to make things simple
    chef.json = {
      rbenv: {
        user_installs: [{
          user: 'vagrant',
          rubies: ["2.1.2"],
          global: "2.1.2",
          gems: {
            "2.1.2" => [
              { name: "bundler" },
              { name: "rails", version: "4.1.6" }
            ]
          }
        }]
      },
      mysql: {
        server_root_password: ''
      }
    }
  end
end

以下エラー対応

==> default: could not find recipe server for cookbook mysql
==> default:
==> default: [2015-04-04T10:18:45+00:00] ERROR: Running exception handlers
==> default: [2015-04-04T10:18:45+00:00] ERROR: Exception handlers complete
==> default: [2015-04-04T10:18:45+00:00] FATAL: Stacktrace dumped to /var/chef/cache/chef-stacktrace.out
==> default: [2015-04-04T10:18:45+00:00] ERROR: could not find recipe server for cookbook mysql
==> default: [2015-04-04T10:18:46+00:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)
Chef never successfully completed! Any errors should be visible in the
output above. Please fix your recipes so that they properly complete.

Cheffileを以下のように修正

cookbook 'mysql','5.6'
$ vagrant provision
==> default: [2015-04-04T10:48:42+00:00] INFO: Running queued delayed notifications before re-raising exception
==> default: [2015-04-04T10:48:42+00:00] ERROR: Running exception handlers
==> default: [2015-04-04T10:48:42+00:00] ERROR: Exception handlers complete
==> default: [2015-04-04T10:48:42+00:00] FATAL: Stacktrace dumped to /var/chef/cache/chef-stacktrace.out
==> default: [2015-04-04T10:48:42+00:00] ERROR: rbenv_gem[2.1.2::bundler (vagrant)] (vagrant) (rbenv::user line 63) had an error: NoMethodError: undefined method `rbenv_rehash' for #<Chef::Provider::Package::RbenvRubygems:0x00000003e77ec8>
==> default: [2015-04-04T10:48:43+00:00] FATAL: Chef::Exceptions::ChildConvergeError: Chef run process exited unsuccessfully (exit code 1)
Chef never successfully completed! Any errors should be visible in the
output above. Please fix your recipes so that they properly complete.

cookbooks/rbenv/libraries/chef_provider_package_rbenvrubygems.rbを

def rehash
  rbenv_rehash new_resource do
    root_path rbenv_root
    user rbenv_user if rbenv_user
    action :nothing
  end.run_action(:run)
end

以下に編集

def rehash
  e = ::Chef::Resource::RbenvRehash.new(new_resource, @run_context)
  e.root_path rbenv_root
  e.user rbenv_user if rbenv_user
  e.action :nothing
  e.run_action(:run)
end
$ vagrant provision

以上でできたわけですが、chef-rbenvのリポジトリは一年以上放置されていてるようだったので、forkして修正を取り込みました

https://github.com/yanamura3/chef-rbenv

cocos2d-js 3.xでJS_CallFunctionでcrash

cocos2d-jsを2系から3系に変更した際にjavascriptのコールバックを呼ぶとexec bad accessでcrashするようになりました。

解決方法はこちらで JS_CallFunctionName crashed on iAP finished callback - Cocos2d-x Forum

JSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJCET

をJS_CallFunctionする前に呼べばよいです。

all asynchronous function call have this problem

と書いてあったのでC++から非同期でjavascriptをたたく場合にはJSB_AUTOCOMPARTMENT_WITH_GLOBAL_OBJECTをつかう必要がありそう

Objective-Cでnil結合演算子的なことをするには

Swiftだとnil結合演算子として"??"が用意されてますが、 Objective-C(というかC)でも似たようなことができます。

ここ(Conditionals - Using the GNU Compiler Collection (GCC))に書いてあるように3項演算子の真ん中を省略すると、条件式が非0の場合に条件式の値になるので

x ? x : y

x ?: y

は同じになります。(厳密にはxの評価される回数が異なりますが)

なのでObjective-C的にnilだったらデフォルト値をつっこむみたいな処理を書くと

NSString* outputString = (inputString != nil) ? inputString : @"default";

NSString* outputString = inputString ?: @"default";

このように短く書くことができます