The Pragmatic Ball boy

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

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は個人でなにかリリースしたいとおもいます

良いお年を!

コード行数の測定

Swiftでコード行数を測ろうとツールを探していたらclocというのがよさげでした

インストール

インストール方法は思ったよりたくさん用意されていてlinux系だけでなく、node.jsやhomebrew, macports用のも用意されていて、WindowsMac環境でも楽にインストールできます

npm install -g cloc                    # https://www.npmjs.com/package/cloc
sudo apt-get install cloc              # Debian, Ubuntu
sudo yum install cloc                  # Red Hat, Fedora
sudo pacman -S cloc                    # Arch
sudo pkg install cloc                  # FreeBSD
sudo port install cloc                 # Mac OS X with MacPorts
brew install cloc                      # Mac OS X with Homebrew

使い方

基本的にはclocのあとに対象のディレクトリを指定すれば、そのディレクトリ以下を含めてコードを解析してくれます

実行

$ cloc Hoge

実行結果

    1572 text files.
    1417 unique files.
     684 files ignored.

http://cloc.sourceforge.net v 1.64  T=19.50 s (46.0 files/s, 9700.3 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C                               92           5068           5097          39838
Bourne Shell                    15           5644           7198          34171
Swift                          365           4776           4414          24868
m4                               8           1074            133           9969
C/C++ Header                   253           4803          14036           8319
Objective C                     43           1790            721           7418
make                            20            845            547           6923
YAML                            17             11              0            410
JSON                            14              0              0            360
D                               64              0              0            242
Python                           2             42             32            164
HTML                             1              0              0             94
Ruby                             1              6              0             60
Go                               1             10             15             20
-------------------------------------------------------------------------------
SUM:                           896          24069          32193         132856
-------------------------------------------------------------------------------

実行結果を見ると、SwiftやGoなどいろんな言語にも対応しているので、clocだけで、いろんなプロジェクトに適用できそうです。

また、いろいろなオプションも用意されており、 例えば以下のようにオプションを指定することにより除外するディレクトリを指定したりできます。

$ cloc Hoge --exclude-dir=vendor

詳しくはドキュメントを御覧ください

WebP.frameworkの作成方法

libwebpをclone

> git clone https://chromium.googlesource.com/webm/libwebp

最新のをcheckout

> git checkout 0.4.4

iosbuild.shを実行

> iosbuild.sh

iosbuild.shでautomakeがないとか怒られた場合は以下を入れる

Command line tools

> xcode-select --install

automake, autoconfとか

> brew install libtool
> brew install automake
> brew install autoconf

成功するとiosbuild.shを実行したディレクトリにWebP.frameworkが生成されます。

Swiftでコマンドラインでカバレッジを取る方法

Objective-Cのときはgcov使ってUnitTestのカバレッジを取っていましたが、 Xcode7から(?)llvm-covが使えるようになったのでこれを使ってみます。

ドキュメントを見ると使い方としては結構単純で、 レポートを出力するには、以下のようにPROFILEとBINを与えてやればレポートが出力されます。

llvm-cov report [options] -instr-profile PROFILE BIN [SOURCES]

PROFILEとBINは、コードカバレッジをONにしてテストを実行するとお馴染みのDerived Data以下に出力されます。

以上のことを踏まえてコマンドラインで実行すると以下のようにすることでカバレッジのレポートを出力させることができます。

注:{}内はプロジェクトによって変更してください。

xcodebuild test -scheme {SchemeName} -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 6' -derivedDataPath build -enableCodeCoverage YES
xcrun llvm-cov report -instr-profile build/Build/Intermediates/CodeCoverage/{SchemeName}/Coverage.profdata build/Build/Intermediates/CodeCoverage/{SchemeName}/Products/Debug-iphonesimulator/{ProductName}.app/{ProductName}

結果はこんな感じで出力されます。

Filename                    Regions    Miss   Cover Functions  Executed
-----------------------------------------------------------------------
.../Headers/NSException.h         0       0    nan%         0      nan%
...eaders/NSObjCRuntime.h         0       0    nan%         0      nan%
...usr/include/MacTypes.h         0       0    nan%         0      nan%
...sdk/usr/include/math.h         0       0    nan%         0      nan%
...sr/include/objc/objc.h         0       0    nan%         0      nan%
...r/include/sys/_types.h         0       0    nan%         0      nan%
..._iOS/AppDelegate.swift        26      21  19.23%        14    21.43%

ただこれファイル名が長いと...省略されて見えなかったりするのがちょっと微妙。。

あと、CIと連動させて表示したいとかだとこのままだと厳しいですね

3D Touch Peak, Popの使い方

1. 3D Touchの発火元となるviewの登録

UIViewControllerのregisterForPreviewingWithDelegateというメソッドを使って、3D Touchに反応するViewと、3D Touchが発生した際にハンドリングするdelegateを登録します。

    override func viewDidLoad() {
        super.viewDidLoad()
  
        imageView.userInteractionEnabled = true // imageViewの場合これが必要

        registerForPreviewingWithDelegate(self, sourceView: imageView)
    }

2. UIViewControllerPreviewingDelegateを継承

3D Touchに反応するUIViewControllerにUIViewControllerPreviewingDelegateを継承させます

class ViewController: UIViewController, UIViewControllerPreviewingDelegate {

3. UIViewControllerPreviewingDelegateのメソッドの実装

UIViewControllerPreviewingDelegateの2つのrequiredのメソッドを実装します

previewingContext:viewControllerForLocation: の実装

こちらはpeak(プレビュー画面を表示)に対応します。

この中では、peakで表示するViewControllerをreturnします。

    func previewingContext(previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? {
        print("peak")

        let sb = UIStoryboard(name: "Storyboard2", bundle: nil)
        let vc = sb.instantiateInitialViewController()
        vc?.preferredContentSize = CGSize(width: 0, height: 200) // 表示サイズ

        print(previewingContext.sourceRect)

        return vc
    }

peakで表示するサイズはViewControllerのpreferredContentSizeで指定することができます。

previewwingContext.sourceRectは3D Touchした際にblurしない範囲(浮き出る部分)を示しています。 sourceViewがimageViewなどでやる場合は特に指定する必要はないですが、tableviewなどの場合、タッチされたcellの位置が浮き出る感じに表示されたほうがよいので、こういうときはpreviewwingContext.sourceRect = cell.frameという風に設定するのがよいと思われます。

previewingContext:commitViewController: の実装

こちらはpop(プレビュー画面で更に強く押して全画面表示)する際に呼ばれるメソッドです。 このメソッド内でpresentViewControllerやnavigationcontroller.pushViewControllerなどを使って次の画面に遷移させます。 引数のcommitViewControllerはpreviewingContext:viewControllerForLocation: でreturnしたViewControllerです。

このメソッドでなにもしないとpeak画面で更に強く押しても何も起こりません。(消える)

    func previewingContext(previewingContext: UIViewControllerPreviewing, commitViewController viewControllerToCommit: UIViewController) {
        print("pop")

        presentViewController(viewControllerToCommit, animated: true) {}
    }

4. プレビューアクションの設定

プレビュー画面で下にスクロールしたら出てくるアクションの設定方法です。

プレビュー表示に使うViewController(previewingContext:viewControllerForLocation:でreturnするViewController)でpreviewActionItems() -> [UIPreviewActionItem]をオーバーライドします。

    override func previewActionItems() -> [UIPreviewActionItem] {
        let action1 = UIPreviewAction(title: "action 1", style: .Default) { (previewAction, viewController) in
            print("action 1")
        }
        let action2 = UIPreviewAction(title: "action 1", style: .Destructive) { (previewAction, viewController) in
            print("action 2")
        }

        let subAction1 = UIPreviewAction(title: "subaction 1", style: .Default) { (previewAction, viewController) in
            print("sub action 1")
        }
        let subAction2 = UIPreviewAction(title: "subaction 2", style: .Default) { (previewAction, viewController) in
            print("sub action 2")
        }
        let groupedActions = UIPreviewActionGroup(title: "Sub Actions…", style: .Default, actions: [subAction1, subAction2] )

        return [action1, action2, groupedActions]
    }

iOS9のクイックアクション対応

ホーム画面のアプリアイコンを3D Touchすると実行されるクイックアクションの実装方法です。

Info.plistの変更

UIApplicationShortcutItemsを追加します。

Info.plist

    <key>UIApplicationShortcutItems</key>
    <array>
        <dict>
            <key>UIApplicationShortcutItemIconType</key>
            <string>UIApplicationShortcutIconTypeShare</string>
            <key>UIApplicationShortcutItemSubtitle</key>
            <string>shortcutSubtitle</string>
            <key>UIApplicationShortcutItemTitle</key>
            <string>shortcutTitle</string>
            <key>UIApplicationShortcutItemType</key>
            <string>$(PRODUCT_BUNDLE_IDENTIFIER).Second</string>
            <key>UIApplicationShortcutItemUserInfo</key>
            <dict>
                <key>secondShortcutKey1</key>
                <string>secondShortcutValue1</string>
            </dict>
        </dict>
    </array>

仕様

詳しい仕様はこちらのUIApplicationShortcutItemsを参照 https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/iPhoneOSKeys.html#//apple_ref/doc/uid/TP40009252-SW36

必須事項

必須なのは、クイックアクションの種類を表すStringであるUIApplicationShortcutItemType と 表示するクイックアクション名を表すUIApplicationShortcutItemTitle です。

アイコンの設定

システムの用意しているアイコンを利用する場合

UIApplicationShortcutItemIconTypeをkeyにして、UIApplicationShortcutItonTypeを指定します UIApplicationShortcutIcon Class Reference

  • UIApplicationShortcutIconType
enum UIApplicationShortcutIconType : Int {
    case Compose
    case Play
    case Pause
    case Add
    case Location
    case Search
    case Share
    case Prohibit
    case Contact
    case Home
    case MarkLocation
    case Favorite
    case Love
    case Cloud
    case Invitation
    case Confirmation
    case Mail
    case Message
    case Date
    case Time
    case CapturePhoto
    case CaptureVideo
    case Task
    case TaskCompleted
    case Alarm
    case Bookmark
    case Shuffle
    case Audio
    case Update
}
            <key>UIApplicationShortcutItemIconType</key>
            <string>UIApplicationShortcutIconTypeShare</string>

独自アイコンを使う場合

UIApplicationShortcutItemIconFileをkeyにして、ファイル名を指定

  • 画像の条件
    • square, single color, and 35x35 point

AppDelegateの変更

クイックアクションが実行されるとAppDelegateのperformActionForShortcutItemが呼ばれます。 shortcutItemを参照すれば、どのクイックアクションが実行されたか判別できるので、それによって対応する処理を実装すればよいです。

    func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: Bool -> Void) {

}

動的にクイックアクションを追加する方法

Info.plistに書く静的にクイックアクションを追加する方法とは別に、アプリ起動後にアプリのプログラムから動的にクイックアクションを追加することもできます

やることとしては、UIApplicationShortCutItemのインスタンスを生成して、UIApplicationのshortcutItemsにセットすればよいです。

        if let shortcutItems = application.shortcutItems where shortcutItems.isEmpty {
            // Construct the items.
            let shortcut3 = UIMutableApplicationShortcutItem(type: "hoge", localizedTitle: "Play", localizedSubtitle: "Will Play an item", icon: UIApplicationShortcutIcon(type: .Play), userInfo: [
                "": UIApplicationShortcutIconType.Play.rawValue
                ]
            )

            application.shortcutItems = [shortcut3]
        }