The Pragmatic Ball boy

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

Swift3.0.1で若干変わったIntなどの数値型⇔AnyObjectのcast

Xcode8(Swift3.0.0)だと↓のテストは通るんですが、

import XCTest
@testable import TypeTest

class TypeTests: XCTestCase {
    
    func testType_WhenIntToAnyObject() {
        let intValue = ["hoge": Int(1) as AnyObject]

        XCTAssertTrue(intValue["hoge"] is Int)
        XCTAssertTrue(intValue["hoge"] is Float)
        XCTAssertTrue(intValue["hoge"] is Double)
        XCTAssertTrue(intValue["hoge"] is CGFloat)
    }

    func testType_WhenCGFloatToAnyObject() {
        let cgfloatValue = ["hoge": CGFloat(1.0) as AnyObject]

        XCTAssertTrue(cgfloatValue["hoge"] is Float)
        XCTAssertTrue(cgfloatalue["hoge"] is Double)
        XCTAssertTrue(cgfloatValue["hoge"] is CGFloat)
    }
}

Xcode8.1(Swift3.0.1)だと↓のようにAnyObjectにcastする前の型のみ正しく判定されるようになってました f:id:yanamura:20161109100647p:plain

SE-0139によるところかと思われます。 あんまりちゃんと読んでないですが、以下のようなこと書いてあったし。

Bridged NSNumber and NSValue objects must be castable back to their original Swift value types.

雑にAnyObjectをcast backしてる場合はSwfit3.0.1にするとcastに失敗するようになるので注意が必要かもです

CGRectの新旧書き方対応表

CGRectGetWidth( rect ) rect.width
CGRectGetHeight( rect ) rect.height
CGRectGetMinX( rect ) rect.minX
CGRectGetMidX( rect ) rect.midX
CGRectGetMaxX( rect ) rect.maxX
CGRectGetMinY( rect ) rect.minY
CGRectGetMidY( rect ) rect.midY
CGRectGetMaxY( rect ) rect.maxY
CGRectIsNull( rect ) rect.isNull
CGRectIsEmpty( rect ) rect.isEmpty
CGRectIsInfinite( rect ) rect.isInfinite
CGRectStandardize( rect ) rect.standardize
CGRectIntegral( rect ) rect.integral
CGRectInset(rect, 1.0, -2.0) rect.insetBy(dx: 1.0, dy: -2.0)
CGRectOffset(rect, -1.0, 2.0) rect.offsetBy(dx: -1.0, dy: 2.0)
CGRectUnion(rect1, rect2) rect1.union(rect2)
CGRectIntersection( rect1 ,rect2) rect1.intersect(rect2)
CGRectContainsRect( rect1,rect2) rect1.contains(rect2)
CGRectContainsPoint(rect ,point) rect.contains(point)
CGRectIntersectsRect( rect1,rect2 ) rect1.intersects(rect2)

Xcode8にしたときのプッシュ通知対応

Project SettingsのCapabilitiesのタブを開き、PushNotificationsをONにします

f:id:yanamura:20160916183405p:plain

production.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>production</string>
</dict>
</plist>

development.entitlements

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>aps-environment</key>
    <string>development</string>
    <key>keychain-access-groups</key>
    <array>
        <string>$(AppIdentifierPrefix)share</string>
    </array>
</dict>
</plist>

Build SettingsのCode Signing Entitlementsで、Configurationによって使うentitlementsを変えるようにします

f:id:yanamura:20160916183853p:plain

最後にこれは必須ではないですが、

        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.currentNotificationCenter().requestAuthorizationWithOptions([.Alert, .Sound, .Badge]) { granted, _ in
                if granted {
                    UIApplication.sharedApplication().registerForRemoteNotifications()
                }
            }
        } else {
            let settings = UIUserNotificationSettings(forTypes: [.Alert, .Sound, .Badge], categories: nil)
            UIApplication.sharedApplication().registerUserNotificationSettings(settings)
            UIApplication.sharedApplication().registerForRemoteNotifications()
        }

Xcode8のxcodebuildでarchive時にsignining周りでエラーがでたときの対処法

Xcode8でxcodebuildを使ってコマンドラインでarchiveしようとすると以下のようなエラーがでて失敗するようになりました。

Check dependencies
XXX has conflicting provisioning settings.XXX is automatically signed, but provisioning profile XXX_Adhoc has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor, or switch to manual signing in the project editor.
Code signing is required for product type 'Application' in SDK 'iOS 10.0'

使っていたコマンドは以下のような感じで、通常の設定では実機で試しやすいようにCode Signing IdentityをiOS Developerにしていて、 コマンドラインで実行するときにDistributionに変更するようにしていました。

xcodebuild \
  -workspace XXX \
  -scheme XXX \
  -configuration XXX \
  -archivePath XXX \
  CODE_SIGN_IDENTITY="iPhone Distribution: XXX (XXXX)" \
  archive

がXcode8ではこれでは通りません

対処法

Xcode8からは自動でsigningを管理する機能がついたため、これをxcodebuildで変更しようとするとコンフリクトしてると怒られるみたいです、エラーを見る限り。。

自分のケースだとコマンドラインでのビルド時にCode Signingを変更したいので、自動管理を諦めます。

よって、まずは、Project SettingのGeneralタブのSigningにある"Automatically manage signingをOFFにします

f:id:yanamura:20160916115640p:plain

これだけでは解決せず、Xcode8から追加されたPROVISIONING_PROFILE_SPECIFIERを設定してやると解決しました。 PROVISIONING_PROFILE_SPECIFIERにはprovisioning profileの名前をいれればOKです。

xcodebuild \
  -workspace XXX \
  -scheme XXX \
  -configuration XXX \
  -archivePath XXX \
  CODE_SIGN_IDENTITY="iPhone Distribution: XXX (XXXX)" \
  PROVISIONING_PROFILE_SPECIFIER=XXX\
  archive

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でもいいんじゃないかなとか色々怪しい気がするんですが、もうちょっと勉強したいと思います