noki雑記

iOS、ときどきAndroid

ReactNative をちょっと触ってみました

ReactNativeがいい感じ、というのを聞いたので触ってみました。 内容はちょっと古いですが、Javascriptもあまり触ったこと無いので、それも含めてほとんどメモです。

環境構築

公式ドキュメントに沿えばOK.

実行してみる

下記コマンドを実行することでアプリを動かすことができます。

$ react-native run-ios
$ react-native run-android

ただ、Androidエミュレータを立ち上げた状態から始めないといけませんでした。エミュレータAndroid Studio から起動するのが推奨されてるっぽいけれど、コマンドでやってみました。

$ $ANDROID_HOME/tools/emulator -avd エミュレータの名前

iOSもたまにシミュレータが起動しないことがあるので、その場合はシミュレータを起動した状態でコマンドを実行すればOK。
(ビルドは成功するが No devices are booted. というエラーが表示されることがある。)

import, export

既存のライブラリや新規に作成したコンポーネントなどを読み込みたい場合は以下のようにする。

import React from 'react';
import Component1 from './src/component1';

import したファイル内に定義したあるクラスを呼びたい場合は方法が2つ。
export default を使って定義することで、import時に自動で呼ばれるようになります。

export default class Component1 extends React.Component {
  render() {
    return (
      <Text>Component 1</Text>
      );
  }
}

export defaultを使用すると、1つのファイルに複数の、外から使用したいクラスを定義できなくなります。常にdefault指定したクラスが呼ばれるため。 なので、使用したいクラスにexportを付け、import時にクラスを指定する必要がある。

export class Component1 extends React.Component {
  ...
}
export class Component2 extends React.Component {
  ...
}
import {Component1, Component2} from './src/components';

Props and State

PropsStateもどちらもコンポーネントを制御するためのデータ。
Propsは固定された値を指し、変化する値はStateとして保存します。

// text is Props
<SampleView text="this is sample text." />
// showText is State
constructor(props) {
  super(props);
  this.state = { showText: true };
  setIntervar(() => {
    this.setState({ showText: !this.state.showText });
  }, 1000);
}

Strictモード

React Native の機能ではなくJavascriptの機能だが、Strictモード(厳格モード)というのがあります。 このモードでは的確なエラーチェックが行われるため、曖昧な実装がエラーとなります。 Strictモードにする場合はファイルに以下を追加します。

'use strict'

ES6 (ES2015)

ECMAScript とは言語仕様のこと。95年に公開されたJavascriptは、多くのブラウザが独自に拡張していったことで互換性が低くなったため、Ecmaインターナショナルが中心となって言語仕様を標準化。それがECMAScriptJavascriptECMAScriptに基いた言語の1つであり、ActionScriptなどもその1つ。

Lifecycle

ネットワーク関連の実装をした時に、react native can only update a mounted or mounting componentというエラー(Warning)が出ました。通信開始時にIndicatorを表示し、通信完了時にIndicatorを非表示にするような実装でしたが、どうもIndicatorの準備が整う前にIndicatorを表示させようとしてエラーになっていたらしい。
ではコンポーネントの準備が整ったタイミングがいつなのかというと、componentDidMount()が呼ばれたときでした。他にもcomponentWillUnmount()などがよく使われるらしいです。各コンポーネントcomponentDidMount()を実装したところWarningは消え、期待通りに動作しました。

参考サイト

Navigation

iOSのNavigationBarを表示してPush遷移をしたい場合は、公式ドキュメントで react-navigation を使うことがまず載っています。しかし、v1.0.0-beta.9時点の react-navigationを使用するとBackAndroid is deprecated. Please use BAckHandler instead.という警告が表示されます。ということで現時点では、React Nativeに古くから存在する Navigator を使おうとも思いましたが、Navigator の公式ドキュメントが見当たらない。。。なのでググりながら実装するしかないです。

react-navigation

公式ドキュメントのやり方でいけました。 上述した警告が出るのでバージョンアップに期待。

Navigator

やろうと思ったら、Navigator が deprecated になっていました。react-native-deprecated-custom-componentsというのをインストールしてimportすればいけるらしいが、名前からして入れたくないので却下。 結局、最新バージョンを使用した場合は、Navigation に関しては正常に動作するものはなかったです。バージョンアップに期待するか、いい感じに動作する下位バージョンを探すしかないようです。

// npm でインストールしたライブラリの確認
$ npm ls
$ npm ls react-navigation

react-native-navigation

公式ドキュメントには使い方は載っていないものの、紹介されているライブラリがあったので、使ってみました。警告もでないので、今のところこれが一番いいかもしれない。
ドキュメント
他と違って、上記ドキュメントに書いてあるとおり、iOSAndroidそれぞれのプロジェクトに追加修正しなければなりません。
react-navigationとくらべて、リリースタグを見てみると、かなり頻繁に更新されているようなので、いまのところは良さそう。

setState() のエラー

Stateは、リアクティブ的に動作するので便利ではあるのですが、画面を遷移しているとCan only update a mounted or mounting component.という警告が出ます。state が解放されていないのか、Component が開放されていないのか、よくわからないがエラーとなってしまう。とりあえずの回避策として、良くない例ではあるが、フラグ管理することにしました。

constructor(props) {
  super(props);
  this.state = { ... };
  this.isMounted = false;
}
componentDidMount() {
  this.isMounted = true;
}
componentWillUnmounted() {
  this.isMounted = false;
}

デバッグ関連

コンソールログ出力

以下実行すると各プラットフォーム毎にログが出力されます。

$ react-native log-ios
$ react-native log-android

Chrome Developer Tools

公式ドキュメント 上記だけでは画面遷移時にどんな値が渡っているかなどの情報が見れなかったが、Chrome Developer Tools を使うと出力されました。 iOSなら、シミュレータ上で Cmd + d を押してデバッグメニューを表示し、Remote JS Debugging を選択。すると http://localhost:8081/debugger-ui が開くので、そこで Developer Tool を開き、シミュレータ上で Cmd + r でリロードすればOK。

自動更新

ソースコードの保存時にアプリを更新してくれる機能が2つあります。Live ReloadHot ReloadingLive Reload は保存時に自動で再起動してくれるし、Hot Reloadingは変更箇所(変更画面)のみ変わります。例えば、複数回遷移した後に表示される画面の修正では、Live Reloadだと最初の画面まで戻ってしまいます(再起動)が、Hot Reloadingだと更新箇所だけ変わるのでデバッグしやすいと思います。

イベントハンドラ内でthisが使えない

以下のコードだと、動きそうで動きませんでした。onPressメソッド内で参照しているthisundefinedになってしまいます。

<TouchableHighlight
  onPress = { this.onPress }>
  ...
</TouchableHighlight>
...
onPress() {
  console.log(this.message);
}

こちらの記事 に書いているが、イベントハンドラにはthisは自動でバインドされないようで、さらにjsのバージョンによっても違うらしい。解決策はいろいろあるようだが、とりあえず以下で動いた。

AutoLayout を使って苦戦した箇所

AutoLayout を使う機会があったので、よく分かる Auto Layoutを読みながら開発を行いました。 普段からStoryBoard等は使わないで開発を行っているため、SnapKitというライブラリを使用しました。

苦戦した箇所はいろいろあるのですが、覚えている範囲でいくつかまとめます。

トルツメ

UIStackViewを使うとトルツメも簡単に実現できます。いくつかのViewをUIStackViewに追加していき、必要なときに非表示にしたいViewのisHiddentrueにするだけでトルツメされます。
レイアウトを組む上で、特に問題なければ(対応OSバージョンが9以上で良いなど)、UIStackViewを使うと楽ですね。

[label1, label2, label3].forEach(stackView.addArrangedSubview)

stackView.axis = .horizontal
stackView.spacing = 10
stackView.distribution = .fillEqually
stackView.snp.makeConstraints { (make) in
  // add constraints
}

...

// if necessary
label2.isHidden = true

UIStackViewを使わないトルツメの方法もいくつかありますが、自分が苦戦したのは優先度を変更する方法でした。

まず優先度についてですが、AutoLayoutの優先度は1 ~ 1000までの値を設定できます。何も設定しない場合はUILayoutPriorityRequired(1000)が設定されます。 制約が重複していた場合は、優先度が高い順に満たされていきます。 このUILayoutPriorityRequiredが設定された制約は、別の優先度に変更することができません。NSInternalInconsistencyExceptionの例外が投げられます。 優先度を変更する必要がある場合は999以下の値を使用するか、UILayoutPriorityDefaultHighUILayoutPriorityDefaultLowを使用することで解決できます。

トルツメの話に戻りますが、下記のように優先度を指定したConstraintを保持しておき、必要な箇所で優先度を変更することでトルツメが実現できます。 3つのUILabelを表示中に、1つのLabelを非表示にした時にトルツメします。具体的には、label6の左端を決める制約を2つ用意し、それぞれ異なる優先度にしておきます。 優先度の高い制約が優先されるので、必要に応じて優先度を切り替えることでレイアウトを変えています。この優先度に1000を設定していたため、どのように実装しても優先度を変更することができず、はまりました。AutoLayoutをそのまま使っている場合は例外が投げられてクラッシュしてしまうので気づきやすいのですが、SnapKitを使っていると優先度の値が変わらないだけだったので、余計にはまってしまいました。

[label4, label5, label6].forEach(view.addSubview)

label4.snp.makeConstraints { (make) in
    // add constraints
}

label5.snp.makeConstraints { (make) in
    // add constraints
}

label6.snp.makeConstraints { (make) in
    // add constraints

    label6Constraint1 = make.left.equalTo(label5.snp.right).offset(10).priority(UILayoutPriorityDefaultHigh).constraint
    label6Constraint2 = make.left.equalTo(label4.snp.right).offset(10).priority(UILayoutPriorityDefaultLow).constraint
}

// if necessary
label5.isHidden = true
label6Constraint1?.update(priority: label5.isHidden ? UILayoutPriorityDefaultLow : UILayoutPriorityDefaultHigh)
label6Constraint2?.update(priority: label5.isHidden ? UILayoutPriorityDefaultHigh : UILayoutPriorityDefaultLow)

複数オブジェクトの中央揃え

複数のViewをまとめたカスタムビューを画面中央に配置しつつ、外枠をつけたり背景色をつけたりするときにちょっと悩みました。 イメージとしては、UIButtonに画像とテキストを配置した状態はどのように実装するのか、といった感じです。 中央揃え自体は、centerXcenterYを使用することでなんとかなるのですが、カスタムビュー自体のサイズが0になってしまっていました。なかなか苦戦しました。

実装した際は、intrinsicContentSizeを使うことで解決しました。これは固有のサイズを返すもので、Viewのサイズを計算して返すことでそのView自体のサイズを知ることが出来ます。例えばUILabelのテキストの長さに応じて可変になるViewであれば、そのViewのintrinsicContentSizeでUILabelのintrinsicContentSizeを使った値を返してやることでUILabelが収まるサイズを知ることが出来ます。

実際にはmarginや固定のwidthを指定している箇所もあるため、それらをきちんと管理しないと面倒なことになりそうです。このあたりなんとかしたい。

private let view = UIView()
private let label = UILabel()
private var fixWidth: CGFloat = 0.0
private var fixHeight: CGFloat = 0.0

override var intrinsicContentSize: CGSize {
    return CGSize(width: fixWidth + label.intrinsicContentSize.width, height: fixHeight)
}

override init(frame: CGRect) {
    super.init(frame: frame)

    [view, label].forEach(addSubview)

    view.backgroundColor = .black
    view.snp.makeConstraints { (make) in
        make.size.equalTo(20)
        make.centerY.equalToSuperview()
        make.left.equalToSuperview().inset(10)
    }
    fixWidth += 20 + 10  // view.width + padding
    fixHeight += 20 // view.height

    label.text = "Label"
    label.snp.makeConstraints { (make) in
        make.left.equalTo(view.snp.right).offset(10)
        make.centerY.equalToSuperview()
    }
    fixWidth += 10  // margin

    fixWidth += 10  // padding
}

ただ、後から考えてみるとこんなことをする必要はありませんでした。上記コードだと、labelに右端の制約を付けてやることで、intrinsicContentSizeは必要なくなります。かなりコードがすっきりします。StoryBoard等でAutoLayoutを使用していれば気づいたかもしれないですね。
まあ、ここでのintrinsicContentSizeの知識が後述のUICollectionViewで活きたような気がするのですが、これももっと考えればうまく実装できそうです。

label.snp.makeConstraints { (make) in
    make.left.equalTo(view.snp.right).offset(10)
    make.right.equalToSuperview().inset(10)
    make.centerY.equalToSuperview()
}

CollectionView の Self-Sizing Cells

UITableView の Self-Sizing Cells は非常に便利です。自動でセルの高さを調整してくれる機能ですね。 これをUICollectionViewで使おうとしたのですが、iOS10以上でないと使えないとのことでした。

結局これも、intrinsicContentSizeでサイズを計算してやり、それをCollectionViewに返してやるようにして対処しました。このあたりももう少しスマートに書けたらなと思いますね。

幅に合わせてマージンを詰める

デザインをあてはめたレイアウトを組んでいき、後から「iPhone5とかで表示させると幅が狭くなる」ことに気づき、微妙なマージンの調整などをすることがありました。 例えば、デザイン通りでれば10ptのマージンが、iPhone5の場合は5ptくらいにしたい、など。
下記のような感じで、less than or equalを指定することで解決できました。

space.snp.makeConstraints { (make) in
    // add constraints
    make.width.equalTo(10).priority(UILayoutPriorityDefaultHigh)
    make.width.lessThanOrEqualTo(10).priority(UILayoutPriorityDefaultLow)
    ...
}

まとめ

AutoLayoutは非常に便利だと感じました。AutoLayoutを使う以前は、initviewDidLoadで初期化などを行いlayoutSubviews等でレイアウトをごにょごにょしていましたが、AutoLayoutを使うようになってからは初期化と同じタイミングで制約をつけるだけなので、コードが見やすくなった気がします。ライブラリのおかげもあると思いますが。

iPhone Xも発表されましたし、UIStackViewなど、今後もAutoLayoutを使う上で便利なコンポーネントや機能が増えていくのかなと思います。使いこなしていきたいです。

ちなみに、業務でもたまにAndroidを実装することがあるのですが、AndroidConstraintLayoutも気になっています。時間があるときに勉強してみようと思います。

デザインパターンの基本をやってみて

普段作っているアプリを、変更に強く柔軟な作りにしたいなと思い、「Java言語で学ぶデザインパターン入門」を読んでみました。恥ずかしながら、デザインパターンを意識したことがあまりなかったので、実践例がいまいちイメージできないものもありましたが、読んでみた感想をまとめてみます。

抽象化(透過的に)すべきか考える

本書では抽象化をうまく使い、変化に強そうなコードに仕上げています。そういったコードを読んでいると、「こういうコード書いてみたいなぁ」と思う箇所がいくつもありました。そのせいか、複数のクラスに何かしら共通部分が多かったり、if文やswitch文の分岐が多くなってきた場合など、抽象化すべきかを考えるようになりました。仕様が漠然としていたり、共通部分が曖昧な場合は無理に抽象化すべきではありませんが、それでも一考するようにしています。

分割・移譲

オブジェクト指向でよく目にする「単一責任」と似たことですが、クラスを分割しておくことで目的が明確にし、修正範囲を限定することができます。また、自分が出来ない(役割ではない)部分に関しては移譲を使うことで、責任範囲を明らかにする手もあります。実際には単一責任のように細分する必要が無いケースもあると思いますが、複雑になりそうな(なってきた)場合は一考してみる必要がありそうです。

ライブラリやユーティリティに応用してみる

Swift2 系から Swift3 へのアップデート作業を行っていたときに、Swift3に対応していないライブラリがあったりして、ちょっと困ったことがありました。ライブラリは便利なので有効活用していきたいと思う一方で、活用している割合が少ないライブラリに関しては自作しても良いかなと思っています。

例えば通信まわりで有名な Alamofire ですが、単純な GET や POST だけを行うアプリであれば、URLSession で作ってしまうとか。その場合、APIの定義とうまく合わせたり、通信成功・失敗を外から扱いやすくしたりなど、考慮することは多いです。しかし、再利用性は高くなると思うのでチャレンジしてみたいです。

普段から使用していたパターンも多い

標準コンポーネントで使われているものや、考え方が似ているものを自作しているものも含め、ある程度の規模のプロダクトになるといくつかのデザインパターンが使われています。Delegate、Singleton、Prototype、Builder、Observer、Iterator パターンなどなど。ただ、それぞれ適材適所だと思っていて、例えば Singleton で作るのか Static な Class もしくは構造体で作るのか、Delegate なのか Notification なのかなど、何が適しているかは吟味したいところです。

Singleton

Singleton が必要なケースとしては、インスタンスが1つのみ必要で、複数存在してはならない場合だと思います。外から複数インスタンスを生成できてしまうと Singleton の意味が無くなってしまいます。そのため、Singleton を構造体で作ってしまうのも意味がなくなってしまいますね。構造体は値型なので、オブジェクトを複数生成してしますと参照先がばらばらになってしまいます。ユーティリティ的な役割が必要であれば、Static なメソッドを持つクラスや構造体で良いと思うし、どちらかというと構造体で作ってしまった方が分かりやすい気がしています。

Adapter と Delegate

きちんと理解できている人にとってはアホみたいな話かもしれませんが、Adapter と Delegate パターンの区別で迷ったので自分なりにまとめてみます。

Android には RecyclerView.Adapter など、Adapter という名前のついたクラスがあるので、Adapter パターンなのだろうと適当に思っていました。しかし、Delegate パターンと見比べてみると、RecyclerView.Adapter の使い方は UITableViewDataSource などと似ているなと思い始めました。

Delegate パターンの場合、既存クラスが Delegate クラス(インタフェースや抽象クラス)を利用することを前提として作られているのに対し、Adapter パターンを適用する場合は、そういった用意がされていないクラスと、処理に必要なデータなどを結びつける(変換する)機能を実装する目的があります。

そう考えてみると、Delegate パターンは UIKit などでもよく使われているので分かりやすいのですが、Adapter パターンはイメージしにくいです。特に Swift であれば、必要な箇所で private extension を使用することで Adapter パターンのようなことができてしまうと思います。stored property が必要であれば別ですが。Java で実装する場合だと、既存クラスのサブクラスを用意し、ターゲットとなるインタフェース等を implement するところですね。

Mediator

画面仕様が複雑になればなるほど、管理すべき対象が散らばっている状況は避けたくなります。Mediator パターンを使うことで、考え方がシンプルになりそうな気がしました。複数の部品が絡まっている複雑な処理を、Mediator 役がまとめるパターンですね。例としてログインフォームが出てきましたが、少なからずありそうな状況です。実際にはバリデーションに引っかかった項目を赤くするとか、通信中、データが無い時など、いろいろまとめられそうですね。

State

使ってみたいというか、実装する際に検討してみたいパターン。 UIはほぼ同じものでも、状況によって処理を分けることがあります。通信中やソフトウェアキーボードが表示されている時、アプリ全体で使いまわすデータに状態があるときなど、状態管理用の変数を用意しようとした時にちょっと考えてみようかと思います。

Strategy、Decorator

これも検討してみたいパターン。実践例があまり思いつかなかったのですが、例えばECサイトなんかでカートに入れた商品の合計額を求める場合、各商品の値段の合計やクーポンでの割引、大人一般か学生か小人か、誕生日や記念日での割引、期間限定での・・・など、いくつもの条件を計算に入れなければならない場合に使えないかなと考えてみました。

計算や条件式が複雑になるケースってたまにありますよね。なんとかしたいなと思いつつ、実装している最中に仕様が変わったりするケースもあるので、なかなか設計が難しかったりする時もあります。そういった時にも何かしらパターンを見つけられればなと思っています。

まとめ

デザインパターンは設計を考える際の1つの材料ですが、特定のパターンを使わないまでも、その考え方を身に着けておくことは非常に有意義だと感じました。オブジェクト指向の技術書を読んでいても、柔軟な設計にするためにいくつかのデザインパターンを使用するケースが出てきます。しかし実際の業務では、技術書に出てくる例よりも複雑な状況は多分にありうるので、慣れるまでは複数のパターンの吟味をし続けることになるかと思います。チームで開発していく以上、自分の設計をメンバーに理解してもらう責任があるので、やはりこういった考え方は重要で、共通認識としてのパターンを知っておくことも必要だと思いました。

DMM英会話を始めてみた

DMM英会話を始めました。オンライン英会話です。近々海外に行く予定で、少しでも英語を話せるように、聞けるようになるためです。もちろん英会話だけでは英語力は上がらないので、英単語や文法等も平行して学んでいます。これから英語の勉強は毎日やっていこうと思っていますが、今日はオンライン英会話1日目がどうだったのかを書きます。

Skype で相手の声が聴こえない

DMM英会話以外のオンライン英会話は経験がありませんが、Skypeを使うのは定番っぽいですね。そのSkypeで講師からCallされたので、緊張しながら受けたのですが、相手の声が全く聞こえませんでした。
結論から言うと、ウィルス対策ソフトの影響でした。Skypeによる通信を遮断していたようです。これに気づかず、無料体験レッスンとはいえ、1回分(25分)を無駄にしてしまいました。

講師が I can hear you とメッセージを送ってきたので、早急に原因がこちらにありそうだと分かったのは良かったのですが、レッスンの25分間はずっとググってました。レッスン前にとても緊張していた自分が悲しい。

しどろもどろな初レッスン

DMM英会話はレッスンで教材を使うかフリートークをするかなど、レッスンごとにいろいろオプションがあります。初めてだったので、自分の力量を知るためにもフリートークを選んでみました。かなりの頻度でフリーズしました。

全然英語が出てこない、講師の言っていることが聞き取れないなど、基本的な部分でアウトでした。それでも一応、困ったときのための英文を用意しておいたので助かったときもありました。例えば、相手が何と言ったのか分からなかったときに I couldn't catch what you said. と言ったら丁寧に言い直してくれたばかりでなく、メッセージで英文を送ってくれました。とても助かりました。

感想

思い返してみると、知らない単語があまり出てこなかったような気がしています。聞き取れなかっただけかもしれませんが。ほとんどの単語が知っているのに聞き取れない、話せないというのはとても悔しいですね。しっかり継続して口と耳を慣らしていこうと思います。

Associated Value を 条件式で処理する

enum で Associated Value を定義した際に、switch 文でしか条件式を組めないと思っていたのですが、if-case文やfor-case文なるものが swift2 から追加されていました。
下記のような感じです。

enum GameTitle {
    case dragonQuest(Int)
    case persona(Int)
}

let gameTitle: GameTitle = .dragonQuest(1)

if case .dragonQuest(1) = gameTitle {
    print("一人で竜王に立ち向かうとかマジ勇者")
}

if case .persona(let number) = gameTitle, number == 3 {
    print("映画になった")
}

let playedTitle: [GameTitle] = [.dragonQuest(1), .dragonQuest(2), .dragonQuest(6), .persona(3), .persona(4)]

for case .dragonQuest(let number) in playedTitle where number <= 3 {
    print("ドラクエ \(number) はリメイク版でならプレイしたことある")
}

UITableView の reloadSections アニメーションの不具合

ヘッダーをタップすることでセルを開閉できる、アコーディオンのようなテーブルビューを作ったときの話です。ヘッダーだけ残した状態で、セルとフッターを隠したり表示したりを切り替えたくて、 reloadSections:withRowAnimation: で開閉を実装しました。ところが、とある条件下だと開閉時のアニメーションが期待通りに動作しないことがありました。

問題

ヘッダーだけ残し、セルとフッターが隠されている状態のときに、そのセクションの1つ下のセクションの開閉アニメーションが期待通りに動作していませんでした。隠したいセクションのセルの数を numberOfRowsInSection で0を返していたのですが、ここに問題があるような気がしています。セクションのヘッダーは表示されているのにセルが0の場合は、セクション毎非表示にするのが正しいのでしょうか。
期待しない動作というのも、言葉では言いづらいのですが、アニメーション(UITableViewRowAnimation.Automatic)の開始位置がずれてしまうというものです。セルの数が0のセクションの下にセルの数が1個以上のセクションがある場合、セルが1個以上のセクションの開閉アニメーションの開始位置が上のセクションのヘッダー部分となってしまっていました。

期待する動作

とりあえず、セクションが空であるかどうかに関わらず、reloadSections 実行時のセルの開閉アニメーションの開始位置が、開閉するセクションのヘッダー部分であることとします。

対応

うまい方法が思いつかなかったので、下記の方法で対処しました。 * numberOfRowsInSection で0を返していた箇所で1を返す * cellForRow(at:) でダミーとなるセルを返す * tableView(_:heightForRowAt:) で高さを CGFloat.min を返す

まとめ

そもそも insert​Rows(at:​with:​)delete​Rows(at:​with:​)を使用する手もあるのですが、今回はセルと一緒にフッターも開閉する必要があり、reloadSections をどうしても使いたかったという背景があります。が、こういう問題があるのはつらいなぁと思いました。

iOS8 を含むSwift3対応時の不具合対応

Swift 2.1 から 3 へ移行する際に、大きな不具合が1つあったので、その対応を考えてみました。
※ ただし、多くの端末・バージョンで検証したものではありません。

問題

UITableViewDelegate の table​View(_:​height​For​Row​At:​) に渡される indexPath の値が間違っていました。具体的には、indexPath.row の値が、0, 1, 2, 3, … の順番で渡されるはずが、0, 0, 1, 2, … とずれてしまっていました。いざ表示してみると問題ないように見えるのですが、画面遷移時にレイアウトが崩れてしまいます。

不具合の内容や対応方法に関しては、こちらを参考にさせていただきました。

期待する動作

今回は、「table​View(_:​height​For​Row​At:​) で正しい値を取得する」ということでやってみます。

対応

まずはコードを貼ります。

Objective-C の Method Swizzling という、既存のメソッドを入れ替える機能を利用しました。Swift から Objective-C のコードを扱う方法は省きます。

まず参考サイトのように、問題となっている箇所を探っていくと、iOS8の UIMutableIndexPath *1get​Indexes:​range:​ で取得できる値が間違っている可能性があることがわかりました。しかし、UIMutableIndexPath は開発者側には非公開となっているので、直接書き換えることができませんでした。そのため、NSIndexPath の get​Indexes:​range:​ の値を正しい値を取得できる index​At​Position:​ で書き換えたところ、うまく動作しました。

まとめ

今回は、iOS 8 のtable​View(_:​height​For​Row​At:​) で渡される indexPath の値が間違っていることに対し、Objective-C のコードで不具合のあるメソッドを書き換え、正しい値を返す対応を考えてみました。

ただ、やはりこの不具合は怖いので、Swift3 対応をリリースする際にはなるべく iOS8 は切りたいと思っています。

*1:憶測ですが、UIMutableIndexPath はNSIndexPath を継承しているようで、iOS9 移行は使用されていないようです。