The Pragmatic Ball boy

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

Xcode8でテストが実行できない Could not determine bundle identifier for TEST_HOST

Xcode8でxcodebuildでtestを走らせると以下のようなエラーがでてテストが実行できなくなりました

xcodebuild: error: Failed to build workspace XXX with scheme XXX.
    Reason: Could not determine bundle identifier for XXXTests's TEST_HOST: 

どうもTEST_HOST、つまりテスト対象のビルドバイナリがないと怒られてるみたいで、 testの前にbuildを追加したら通りました。

$ xcodebuild -workspace XXX -scheme XXX -configuration Debug clean build test -destination 'platform=iOS Simulator,OS=10.0,name=iPhone 6'

ちなみに、xcodebuildじゃなくて、Xcodeでテスト実行する際も、ビルドする前にいきなりテストを実行すると同じエラーが発生します。 これについては、先にビルドしておけば解決します。

動画のフォトアルバムへの保存

動画のフォトアルバムへの保存

iOS8まで

import AssetsLibrary

...

ALAssetsLibrary().writeVideoAtPathToSavedPhotosAlbum(filePathURL) { _ in
    // 完了後の処理
}

iOS9以降

import Photos

...

PHPhotoLibrary.sharedPhotoLibrary().performChanges(
    {
        PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(filePathURL)
    },
    completionHandler: { _ in
        // 完了後の処理
    }
)

Android Studioをインストールしてプロジェクトをビルドするときに躓いたところ

Androidの既存のプロジェクトをビルドしようとしてAndroid Studioのインストールからはじめると 毎回同じようなところで躓いている気がするのでメモ

エラーで Unsupported major.minor version 52.0 が出た時

原因

これはAndroidStudioが使っているJDKが古いと出る

対策

  1. まずはJDK8をインストールする
  2. AndroidStudioを起動して、File -> Other Settings -> Default Project Structure を選択
  3. JDK locationを変更してJDK8のPathに変更する

エラーで Error:failed to find Build Tools revision 23.0.0 rc3 が出た時

原因

これは対応するAndroid SDK Build Toolsがインストールされていないのが原因

対策

  1. AndroidStudioを起動、Tools -> Android -> SDK ManagerでSDK Managerを起動する。
  2. SDK Toolsのタブを選択し、下の方にある"Show Package Details"のチェックボックスにチェックを入れる
    • f:id:yanamura:20160902193059p:plain
  3. そうするとAndroid SDK Build Toolsの全てのバージョンが表示されるので、必要な物にチェックを入れてインストールする

既存のプロジェクトでSwift2.3のままXcode8でビルドを通す

Xcode8でビルドするとデフォルトではSwift3でコンパイルされてしまいます。 とりあえずXcode8にあげるけどSwift2.3のままにしておいて、Swift3対応は後でという場合の対処方法です。

Build Settings

Build Settingsの"Use Legacy Swift Language Version"を"Yes"にする

  • f:id:yanamura:20160830183604p:plain

Cocoapodsの変更

Podfileの一番最後に以下を追加

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['SWIFT_VERSION'] = '2.3'
    end
  end
end

ライブラリをXcode8&Swift2.3対応のブランチを指定するようにする

Swift2.3とSwift3ではバイナリ互換がないので、ライブラリも含めてSwift2.3でコンパイルする必要があります

pod 'Alamofire', :git => 'https://github.com/Alamofire/Alamofire', :branch => 'swift2.3'
pod 'Realm', :git => 'https://github.com/realm/realm-cocoa.git', :branch => 'master', :submodules => true
pod 'RealmSwift', :git => 'https://github.com/realm/realm-cocoa.git', :branch => 'master', :submodules => true

AnsibleでUbuntu16.04使った時にでたエラー

Ansibleで使うUbuntuを16.04にしてみたところいくつかエラーが発生した

config.vm.box = "ubuntu/xenial64"

エラーその1

vagrant upしてTASK [setup]で止まる

TASK [setup] *******************************************************************
fatal: [default]: FAILED! => {"changed": false, "failed": true, "module_stderr": "", "module_stdout": "/bin/sh: 1: /usr/bin/python: not found\r\n", "msg": "MODULE FAILURE", "parsed": false}

原因はUbuntu 16.04ではデフォルトでPython3系が入っているためで、これを2系に変えないとAnsibleは動かない

ということでVagrantfileに以下を追加して対応

  config.vm.provision "shell", inline: <<-SHELL
    sudo apt-get update
    sudo apt-get install -y python-simplejson
  SHELL

エラーその2

apt: upgrade=fullでエラーがでる

TASK [common : upgrade a server] ***********************************************
fatal: [default]: FAILED! => {"changed": false, "failed": true, "msg": "Could not find aptitude. Please ensure it is installed."}

apt: upgradeの前に以下を追加

- name: install aptitude
          apt: pkg=aptitude

RxSwiftでTableView その3

RxTableViewSectionedReloadDataSourceでは複数Sectionに対応できましたが、 Section単位でreloadData()がされてしまっていました。

今回はRxTableViewSectionedAnimatedDataSourceを使ってみます

RxTableViewSectionedAnimatedDataSource

ViewControllerはRxTableViewSectionedReloadDataSourceとほぼ一緒で、dataSourceの型がRxTableViewSectionedAnimatedDataSourceになっただけです。

ViewController

import RxSwift
import RxCocoa
import RxDataSources

class MultiListViewController: UIViewController {
  
  @IBOutlet weak var tableView: UITableView!
  @IBOutlet weak var button1: UIButton!

  private let viewModel = MultiListViewModel()

  private let dataSource = RxTableViewSectionedAnimatedDataSource<MySectionModel>()
  private let disposeBag = DisposeBag()

  override func viewDidLoad() {
    dataSource.configureCell = { (_, tableView, indexPath, element) in
      let cell = tableView.dequeueReusableCellWithIdentifier("Cell1")!
      cell.textLabel?.text = "\(element) @ row \(indexPath.row)"
      return cell
    }

    viewModel.items.asObservable()
      .bindTo(tableView.rx_itemsWithDataSource(dataSource))
      .addDisposableTo(disposeBag)

    button1.rx_tap
      .bindNext { [weak self] in
      self?.viewModel.change()
    }
    .addDisposableTo(disposeBag)
  }
}

ViewModel

dataSourceとbindingさせる配列はAnimatableSectionModelType protocolに対応している必要があります。

更にAnimatableSectionModelTypeのitemはIdentificableType protocolに対応している必要があります。これはitemが変化したかどうか判定するために使われます。

struct MySectionModel {
  var header: String
  var items: [Item]
}

extension MySectionModel : AnimatableSectionModelType {
  typealias Item = String

  var identity: String {
    return header
  }

  init(original: MySectionModel, items: [Item]) {
    self = original
    self.items = items
  }
}

extension String: IdentifiableType {
  public typealias Identity = String

  public var identity : Identity {
    return self
  }
}

class MultiListViewModel {

  let items = Variable<[MySectionModel]>([])

  var section1 = MySectionModel(header: "section1", items: ["a", "b", "c"])
  var section2 = MySectionModel(header: "section2", items: ["d", "e", "f"])

  init () {
    items.value.append(section1)
    items.value.append(section2)
  }

  func change() {
    items.value[0] = MySectionModel(header: "section1", items: ["a", "b", "c", "z"])
  }
}

変更があった場合、内部ではtableViewのperformBatchUpdatesが実行されるので変化のあったcellだけ変更がかかります。

アニメーションを変更したい場合はAnimationContigurationを変更してやればよいです。

    dataSource.animationConfiguration = AnimationConfiguration(insertAnimation: .Right, reloadAnimation: .Right, deleteAnimation: .Right)

RxTableViewSectionedAnimatedDataSourceの使い所

  • cellの挿入や削除などでアニメーションを入れたい場合
  • 変化があった場合にSectionやTableView全体をreloadDataしたくない場合

ただ気になるところが、内部的な変化の判定するために、変更前後の配列の比較を行っているので効率的とはいえません。 利用する側にとっては何も考えずに配列ごと上書きしてしまえばよいのですが、すごい大きい配列になった場合にパフォーマンスとか大丈夫なのかなと懸念がありそうな気がします。

RxSwiftでTableView その2

rx_itemsWithCellIdentifierではSection1つしかダメでした。

複数Sectionを使う方法としていくつかあるっぽいのですが、 まずRXTableViewSectionedReloadDataSourceを使ってみます。

RXTableViewSectionedReloadDataSource

これはRxSwiftには含まれておらず別途RxDataSourcesを導入する必要があります。

ViewContorller

import RxSwift
import RxCocoa
import RxDataSources

class MultiListViewController: UIViewController {
  
  @IBOutlet weak var tableView: UITableView!
  @IBOutlet weak var button1: UIButton!

  private let viewModel = MultiListViewModel()

  private let dataSource = RxTableViewSectionedReloadDataSource<MySectionModel>()
  private let disposeBag = DisposeBag()

  override func viewDidLoad() {
    dataSource.configureCell = { (_, tableView, indexPath, element) in
      let cell = tableView.dequeueReusableCellWithIdentifier("Cell1")!
      cell.textLabel?.text = "\(element) @ row \(indexPath.row)"
      return cell
    }

    viewModel.items.asObservable()
      .bindTo(tableView.rx_itemsWithDataSource(dataSource))
      .addDisposableTo(disposeBag)

    button1.rx_tap
      .bindNext { [weak self] in
      self?.viewModel.change()
    }
    .addDisposableTo(disposeBag)
  }
}

ViewModel

RXTableViewSectionedReloadDataSourceをbindせる配列はSectionModelType protocolに対応している必要があります。

import RxSwift
import RxCocoa
import RxDataSources

struct MySectionModel {
  var rows: [String]

  init(rows: [String]) {
    self.rows = rows
  }
}

extension MySectionModel: SectionModelType {
  var items: [String] {
    return rows
  }

  init(original: MySectionModel, items: [String]) {
    self = original
    self.rows = items
  }
}

class MultiListViewModel {

  let items = Variable<[MySectionModel]>([])
  private var section1 = MySectionModel(rows: ["a", "b", "c"])
  private var section2 = MySectionModel(rows: ["A", "B", "C"])

  init () {
    items.value.append(section1)
    items.value.append(section2)
  }

  func change() {
    items.value[0] = MySectionModel(rows: ["x", "y", "z"])
  }
}

RXTableViewSectionedReloadDataSourceはその名の通り、Section単位でreloadData()をして表示内容を更新します。 Sectionの中身を変えたいときは上のfunc change()のようにSection毎データを更新してやらないとreloadData()が発火しないようです。

RXTableViewSectionedReloadDataSourceの使い所

  • 複数Sectionが扱いたい場合
  • Sectionを更新するときはそのSectionまるごとデータを差し替えて支障がないとき

補足

ここではSectionModelTypeプロトコルに対応させたMySectionModelを自分で定義しましたが、 RxSwiftでSectionModelというジェネリクスのstructが用意されているのでそれを使っても実装できます

public struct SectionModel<Section, ItemType>

RxSwiftでTableView その1

RxSwiftでTableViewとdatasourceをbindingさせる方法はいくつかあるようなので1つずつ見ていきます。 今回はrx_itemsWithCellIdentifilerを使ってみます。

rx_itemsWithCellIdentifier

単にSectionが1つのリストをTableViewに表示するだけであればrx_itemsWithCellIdentifierを使えば非常にシンプルに実装できます。

ViewController

class ListViewController: UIViewController {

  @IBOutlet weak var tableView: UITableView!
  @IBOutlet weak var button: UIButton!

  private let viewModel = ListViewModel()
  private let disposeBag = DisposeBag()

  override func viewDidLoad() {
    viewModel.friends.asObservable()
      .bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) { (row, element, cell) in
        cell.textLabel!.text = element
      }
      .addDisposableTo(disposeBag)

    tableView.rx_itemSelected
      .subscribeNext { [weak self] indexPath in
        self?.viewModel.selectAtIndex(indexPath.row)
        self?.tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
    .addDisposableTo(disposeBag)

    button.rx_tap
      .bindNext {
        self.viewModel.add()
      }
      .addDisposableTo(disposeBag)
  }
}

ViewModel

final class ListViewModel {
  let friends = Variable<[String]>([])

  init () {
    friends.value.append("1")
    friends.value.append("2")
    friends.value.append("3")
  }

  func selectAtIndex(index: Int) {
    print(friends.value[index])
  }

  func add() {
    friends.value.append("10")
  }
}

このように、単に表示するだけであればrx_itemsWithCellIdentifierとdatasouceをbindingするだけなのですごく楽です。

ただRxSwift内部のソースを見てみると、以下のようにdatasourceに変化があった場合reloadData()を使っているので全体に再描画が発生します。

/// RxTableViewReactiveArrayDataSource
class RxTableViewReactiveArrayDataSource<Element>
    : _RxTableViewReactiveArrayDataSource
    , SectionedViewDataSourceType {
    typealias CellFactory = (UITableView, Int, Element) -> UITableViewCell
    
    ...
    
    func tableView(tableView: UITableView, observedElements: [Element]) {
        self.itemModels = observedElements
        
        tableView.reloadData()
    }
}

rx_itemsWithCellIdentifierの使い所

以上のことから使い所を考えると

  • Sectionが1つしかない
  • datasourceに変化があったときに毎回reloadData()が走っても問題ないような場合
  • cellの挿入にアニメーションが不要

といった感じかなと

SwiftLint

Swiftでコーディング規約に沿っているかチェックするツールにSwiftLintというのがあるのでつかってみました。

SwiftLintではコーディング規約としてGithubのSwift style guildeを使っています。

使い方

  1. $ brew install swiftlint
  2. XcodeでSwiftLintをかけるプロジェクトを開く
  3. ProjectSettingのBuildPhasesを開く
  4. 左上の+ボタンを選択して、New Run Script Phaseを選択 f:id:yanamura:20160618211623p:plain
  5. Run Script内に以下を記載
if which swiftlint >/dev/null; then
  swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

f:id:yanamura:20160618211853p:plain

こうするとビルドするとコーディング規約のチェックが走るようになります。

例外

ただし、このままだと全てのファイルが対象となってしまい、3rd Partyのライブラリなども含まれてしまいます。

ディレクトリを対象からはずす

以下のようにして対象外のディレクトリを指定します

  • .swiftlint.yml というファイルをプロジェクトのトップディレクトリに作成します。
  • .swiftlint.yml内には以下のようにexcluded: 以下に、対象外とするディレクトリを列挙すればよいです
excluded:
      - Pods

ルールを変更する

このルールは適用したくないとか、errorではなくwarningにしたい場合は、ルールを.swift.ymlに書きます

# 除外するルール
disabled_rules:
    - line_length
    - trailing_whitespace
    - trailing_newline
    - function_parameter_count
    - force_try
    - type_name
    - function_body_length

# ルールの変更
force_cast: warning
variable_name:
    min_length: 2

ソースコードの行単位でルールを除外する

コメントで // swiftlint:disable:this <ルール名> で行単位で除外できます。

let cell = tableView.dequeueReusableCellWithIdentifier("cell1", forIndexPath: indexPath) as! Cell1 // swiftlint:disable:this force_cast

細かいところはSwiftLintのREADMEを読むとよいです。

RxSwiftでログイン画面のサンプル実装

ユーザー名とパスワードを規定の文字数以上入力するとログインボタンが有効になるViewControllerをRxSwiftを使って実装してみました。

SwiftBondしか使ったことなく初めてRxSwiftを使ってみた感想としては、RxSwiftのほうが似たようなものがたくさんあり学習コストは高いなと思いました。 Observableについて見てみると、RxSwiftのほうはReactiveXのObservableであるためonNext,onCompleted,onErrorがありますが、SwiftBondのほう単に変化を見るだけでいわばonNextしかないようなものなので、その辺で差がメリット・デメリットにあらわれているのかなと。

ViewController

import UIKit
import RxSwift
import RxCocoa

class LoginViewController: UIViewController {

  @IBOutlet weak var inputTextField: UITextField!
  @IBOutlet weak var loginButton: UIButton!
  @IBOutlet weak var passwordTextField: UITextField!

  private let viewModel = LoginViewModel()
  private let disposeBag = DisposeBag()

  override func viewDidLoad() {
    super.viewDidLoad()

    addBindings()
  }

  private func addBindings() {
    // username
    // UITextField -> ViewModel
    inputTextField.rx_text
      .bindTo(viewModel.userName)
      .addDisposableTo(disposeBag)
    // ViewModel -> UITextField
    viewModel.userName.asObservable()
      .observeOn(MainScheduler.instance)
      .bindTo(inputTextField.rx_text)
      .addDisposableTo(disposeBag)

    // password
    // UITextField -> ViewModel
    passwordTextField.rx_text
      .bindTo(viewModel.password)
      .addDisposableTo(disposeBag)
    // ViewModel -> UITextField
    viewModel.password.asObservable()
      .observeOn(MainScheduler.instance)
      .bindTo(passwordTextField.rx_text)
      .addDisposableTo(disposeBag)

    // UIButtonのタップイベント
    loginButton.rx_tap
      .subscribeNext { [weak self] in
        self?.viewModel.login()
      }
      .addDisposableTo(disposeBag)
    // ViewModel -> UIButtonのenabled
    viewModel.enableLoginButton
      .bindTo(loginButton.rx_enabled)
      .addDisposableTo(disposeBag)
  }
}

ViewModel

import Foundation
import RxSwift
import RxCocoa

class LoginViewModel {
  let userName = Variable("")
  let password = Variable("")
  let enableLoginButton: Observable<Bool>

  init() {
    // 3文字以上だったらボタンをenbaleにする
    enableLoginButton = Observable.combineLatest(userName.asObservable(), password.asObservable()) {
      $0.0.characters.count >= 3 && $0.1.characters.count >= 3
    }
  }

  func login() {
    print("user=", userName.value)
    print("password=", password.value)
    userName.value = ""
    password.value = ""
  }
}

VariableよりDriverとかのほうがよかったのかとか、VariableをsubscribeするならsubscribeNextじゃなくてsubscribeでもいいんじゃないかなとか色々怪しい気がするんですが、もうちょっと勉強したいと思います

iOSでSlackのWebSocket疎通まで

やること

iOSからSlackのWebSocketを使ってメッセージのやり取りをできる状態にする

事前準備

redirect urlを用意せずにモバイルアプリ単体でアクセストークンを取得するのは無理っぽいのでとりあえずテスト用のを使う

手順

  1. Web APIrtm.startを使ってWebSocket Message ServerのURLを取得する(30秒しか有効期間がないので毎回取得が必要)
  2. 取得したURLを使ってWebSocketのコネクションをはる

超適当なサンプル

  1. 適当にXcodeでプロジェクトを作成
  2. Podfileに以下を記載(cocoapods 1.0以上)
target 'プロジェクト名' do
  use_frameworks!

  pod 'Alamofire', '~> 3.4'
  pod 'Starscream', '~> 1.1.3'
end
  1. $ pod install
  2. ViewControllerを以下のように書く(accessTokenのところにaccessTokenを入れる)
import Alamofire
import Starscream

class ViewController: UIViewController, WebSocketDelegate {
  var socket: WebSocket?
  let accessToken = "xxx"

  override func viewDidLoad() {
    super.viewDidLoad()

    let URL = "https://slack.com/api/rtm.start?token=\(accessToken)"

    Alamofire.request(.POST, URL, encoding: .JSON)
      .responseJSON { [weak self] response in
        print(response.request)  // original URL request
        print(response.response) // URL response
        print(response.data)     // server data
        print(response.result)   // result of response serialization

        if let JSON = response.result.value {
          print("JSON: \(JSON)")
          print("url:", JSON["url"])

          if let urlString = JSON["url"] as? String {
            self?.socket = WebSocket(url: NSURL(string: urlString)!)
            self?.socket?.delegate = self
            self?.socket?.connect()
          }
        }
    }
  }

  // MARK: WebSocket Delegate
  func websocketDidConnect(socket: WebSocket) {
    print("did connect")
  }

  func websocketDidDisconnect(socket: WebSocket, error: NSError?) {

  }

  func websocketDidReceiveMessage(socket: WebSocket, text: String) {
    print("message:", text)
  }

  func websocketDidReceiveData(socket: WebSocket, data: NSData) {
    print("data:", data)
  }
}

動かして以下が表示されればOK

did connect
message: {"type":"hello"}

久しぶりにcocoapodsを使ってはまったところ

cocoapods 1.0.1

The dependency xxx is not used in any concrete target.

Podfileの書き方がcocoapods 1.0系になって変わったらしく

Podfileを

$ pod init

で生成すると

# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'

target 'SampleProj' do
  # Comment this line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for SampleProj
  pod 'Bond', '~> 4.0'

  target 'SampleProjTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

このようにプロジェクト名でテンプレが生成されるので、Pods for xxx以下にpod xxxを書けば良い

Unable to find a specification for 'xxxxx'

$ pod setup

してから

$ pod install

で解決

Crashlyticsにクラッシュレポートが送信されない

普通はFabricで手順通りにやってればクラッシュレポートは送信されるはずなんですが、なぜか送信されないってことがありました。

answersとかは動いているのでFabric自体はちゃんと取り込まれてるのになぜだ・・と思って調べたら

troubleshootingに書いてありました https://docs.fabric.io/ios/crashlytics/crashlytics.html#troubleshooting

Make sure our SDK line is after all other 3rd-party SDK lines that install an exception handler. (We need to be last one called in your appDidFinishLaunchingWithOptions method.)

他に例外ハンドラを使うようなライブラリ使ってる場合は、appDidFinishLaunchingWithOptionsの一番最後でFabric.with([Crashlytics()])呼べってことでした。

なぜかとういうと、クラッシュレポートを送信するライブラリはたぶんNSSetUncaughtExceptionHandlerを使って例外をキャッチしてクラッシュチェックをしているのですが、この関数は一つしかハンドラをセットできないので、複数クラッシュレポートを送るようなライブラリを使っていた場合、最後にセットしたものが動くってことだと思われます。 https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Miscellaneous/Foundation_Functions/#//apple_ref/c/func/NSSetUncaughtExceptionHandler

おまけ

どうしてもFabricを使いつつ他の例外ハンドラを動かしたい場合は、こんな感じでCrashlyticsのセットした例外ハンドラを保存しておいて、NSSetUncaughtExceptionHandlerで独自のコールバックをセットしてその中で、Crashlyticsのハンドラの実行と独自の処理をやればいいのではないかと思います(試してないけど

  static var exceptionHandler: ((NSException) -> Void)?

  func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

    Fabric.with([Crashlytics()])

    AppDelegate.exceptionHandler = NSGetUncaughtExceptionHandler();
    let callback: @convention(c) (NSException) -> Void = {
      // do somithing

      AppDelegate.exceptionHandler?($0)
    }
    NSSetUncaughtExceptionHandler(callback)

    return true
  }

2015振り返り

運動

いつも3ヶ月くらいで飽きてましたが、今年は週1走ることを目標にやってみてだいたい達成できました。

f:id:yanamura:20151231180637p:plain

ブログ

ここ2年くらいさぼてったので、今年は月1くらいで継続的に書くことはできました

2015-01-01から1年間の記事一覧 - Pragmatic ball boy

会社のエンジニアブログでも初めて投稿し、100近くはてぶ獲得できました

64bit環境におけるObjective-Cのポインタ | GREE Engineers' Blog

仕事

現職に入社以来ずっと同じ仕事ばっかりだったので、社内の新規事業開発でエンジニアを募集していたので挙手してジョインしてLIMIAというサービスをリリースしました(といっても僕はほぼ実装してないですが・・

limia.jp

まとめ

  • 飽きっぽい人でも目標があると続くのでやっぱり目標大事ですよね

2016は個人でなにかリリースしたいとおもいます

良いお年を!