OperationとOperationQueue
複数の非同期な処理を順に実行する方法を検討していて、OperationQueueを使うと便利そうだなと思い触ってみました。
OperationとOperationQueue
キュー(OperationQueue)の中に処理(Operation)を追加しておくと、先に追加した処理から順に実行され、実行中の処理が終了すると次の処理実行される仕組みです。終了した処理はQueueから取り除かれます。
並列で処理する数を制限したり、「処理Aが終わってから処理Bを実行したい」というような依存関係も設定できます。
非同期で実行する場合
まずはOperationを継承したクラスを用意し、実行したい処理はstart()メソッドをoverrideして中に記述します。また、isConcurrent
をtrue
に設定することで並列に実行することが可能になります。
class Operation: Foundation.Operation { override var isConcurrent: Bool { return true } override func start() { // 処理 } }
非同期処理の終了を検知する
ある処理が終わった後に次の処理を実行するためには、処理が終了したことを検知する必要があります。Operationでは、isFinished
プロパティを監視(KVO)して検知する仕組みです。例えば、適当にState
で状態を管理しつつ、state = .finished
になったらisFinished
の値が変更されたことを通知するようにしています。isFinished
プロパティがread-onlyなため、willChangeValue(forKey:)
とdidChangeValue(forKey:)
を使って通知するしかなさそうです。
class Operation: Foundation.Operation { enum State { case ready, started, finished } private var state: State = .ready { willSet { willChangeValue(forKey: #keyPath(Operation.isFinished)) } didSet { didChangeValue(forKey: #keyPath(Operation.isFinished)) } } override var isFinished: Bool { return state == .finished } }
KVOでキーを指定する場合は、#keyPath
を使うと便利です。プロパティ名を文字列で指定してしまうと、コンパイルエラーにならなかったりするので面倒です。
キャンセル処理
OperationQueueにはcancelAllOperations()
メソッドがあり、キュー内の全てのOperationのcancel()
メソッドを呼ぶことが出来ます。ただし、isCanceled
プロパティが変更されるだけで、実際にOperationが実行されなくなるわけでも、キューから取り除かれるわけでもありません。キャンセルの処理は自身で記述する必要があります。
また、まだ実行されていないOperationは、キャンセルされた後であればQueueから即座に取り除くことが可能だそうです。
class Operation: Foundation.Operation { override func cancel() { super.cancel() state = .finished } }
Queueに詰める
OperationQueueに詰めると即座に実行されていきます。
class ViewController: UIViewController { private lazy var queue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 // 同時実行処理を1つに制限 return queue }() override func viewDidLoad() { super.viewDidLoad() (0...10).forEach { (num) in let operation = Operation() let name = "\(num)" operation.name = name operation.completionBlock = { print("operation finished:", name) } queue.addOperation(operation) } } }
おまけ:KVO
#keyPath
はプロパティ変更の通知を受け取った先でも使えます。便利!
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard let keyPath = keyPath, let change = change else { return } if keyPath == #keyPath(Operation.isFinished) { guard let old = change[.oldKey] as? Bool, let new = change[.newKey] as? Bool else { return } if !old && new { print("operation is finished.") } } }
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
Props
もState
もどちらもコンポーネントを制御するためのデータ。
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インターナショナルが中心となって言語仕様を標準化。それがECMAScript。JavascriptはECMAScriptに基いた言語の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
公式ドキュメントには使い方は載っていないものの、紹介されているライブラリがあったので、使ってみました。警告もでないので、今のところこれが一番いいかもしれない。
ドキュメント
他と違って、上記ドキュメントに書いてあるとおり、iOSとAndroidそれぞれのプロジェクトに追加修正しなければなりません。
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 Reload
とHot Reloading
。
Live Reload
は保存時に自動で再起動してくれるし、Hot Reloading
は変更箇所(変更画面)のみ変わります。例えば、複数回遷移した後に表示される画面の修正では、Live Reload
だと最初の画面まで戻ってしまいます(再起動)が、Hot Reloading
だと更新箇所だけ変わるのでデバッグしやすいと思います。
イベントハンドラ内でthisが使えない
以下のコードだと、動きそうで動きませんでした。onPressメソッド内で参照しているthis
がundefined
になってしまいます。
<TouchableHighlight onPress = { this.onPress }> ... </TouchableHighlight> ... onPress() { console.log(this.message); }
こちらの記事 に書いているが、イベントハンドラにはthisは自動でバインドされないようで、さらにjsのバージョンによっても違うらしい。解決策はいろいろあるようだが、とりあえず以下で動いた。
AutoLayout を使って苦戦した箇所
AutoLayout を使う機会があったので、よく分かる Auto Layout
を読みながら開発を行いました。
普段からStoryBoard等は使わないで開発を行っているため、SnapKit
というライブラリを使用しました。
苦戦した箇所はいろいろあるのですが、覚えている範囲でいくつかまとめます。
トルツメ
UIStackViewを使うとトルツメも簡単に実現できます。いくつかのViewをUIStackViewに追加していき、必要なときに非表示にしたいViewのisHidden
をtrue
にするだけでトルツメされます。
レイアウトを組む上で、特に問題なければ(対応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
以下の値を使用するか、UILayoutPriorityDefaultHigh
やUILayoutPriorityDefaultLow
を使用することで解決できます。
トルツメの話に戻りますが、下記のように優先度を指定した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に画像とテキストを配置した状態はどのように実装するのか、といった感じです。
中央揃え自体は、centerX
やcenterY
を使用することでなんとかなるのですが、カスタムビュー自体のサイズが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を使う以前は、init
やviewDidLoad
で初期化などを行いlayoutSubviews
等でレイアウトをごにょごにょしていましたが、AutoLayoutを使うようになってからは初期化と同じタイミングで制約をつけるだけなので、コードが見やすくなった気がします。ライブラリのおかげもあると思いますが。
iPhone Xも発表されましたし、UIStackViewなど、今後もAutoLayoutを使う上で便利なコンポーネントや機能が増えていくのかなと思います。使いこなしていきたいです。
ちなみに、業務でもたまにAndroidを実装することがあるのですが、AndroidのConstraintLayout
も気になっています。時間があるときに勉強してみようと思います。
デザインパターンの基本をやってみて
普段作っているアプリを、変更に強く柔軟な作りにしたいなと思い、「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
を返す
まとめ
そもそも insertRows(at:with:)
や deleteRows(at:with:)
を使用する手もあるのですが、今回はセルと一緒にフッターも開閉する必要があり、reloadSections
をどうしても使いたかったという背景があります。が、こういう問題があるのはつらいなぁと思いました。