cockscomblog?

cockscomb on hatena blog

技術書クラウドファンディングプロジェクト「iOS 11 Programming」

以前に妻のいとこに会ったとき、当時大学生だったそのいとこは、大学のレポートをスマートフォンで書いている、と教えてくれた。現代ではそういうものだという話は聞いたことがあったが、実際の事例であることがそのときわかった。ふつうはパソコンを使うでしょう、と考える人もいるかもしれないが、大学生がパソコンを使えるようになったのだって、ほんの最近のことなのかもしれない。

先日iOS 11が発表された。iPadドラッグ&ドロップができたりするらしい。どれどれと思って、いろいろと新しいAPIを見てみると、これは確かによくできている。iPadは新しいiOS 11で、「パーソナルコンピュータ」になるのかもしれない。

ここでいうパーソナルコンピュータというのは、人間ひとりひとりを拡張する道具、というような意味である。Windowsデスクトップだけを指すのではないし、ましてMacだけがそうであるはずもない。新しいiOS 11は、ドラッグ&ドロップによる直感的なデータの受け渡しや、あるいはファイルを中心とした新しいアプリ間の連携によって、生産的な活動に耐えうるコンピュータになっていくのだろう。

先日僕にも子供が生まれた。僕の子もまた、成長するにつれてパソコンを必要とするだろう。そのとき僕が買い与えるのは、Macなのか、iPadなのか、それとも未だ見ぬ未来の何かなのか、まだ知るすべはない。ただ、その日のために備えておきたいと思う。

さて、縁あって「iOS 11 Programming」という本の、まさにドラッグ&ドロップやファイルを中心とした新しい機能について、章を書かせてもらえることになった。新たなパーソナルコンピュータたるiOS 11について、他では読めないようなものを書きたいと思う。とはいっても、この本はいま流行りのクラウドファンディングであって、これが成立しない限りは世に出ることもない。これを読んでいるみなさんひとりひとりに、未来のパーソナルコンピュータを支えるため、クラウドファンディングに協力する責任がここに発生した。ぜひご協力頂けると幸いです。

ところでこの記事も全て新しいiPad ProとSmart Keyboardで書きました。

子供が生まれました

本日午前11時に、僕と妻の子供が生まれました。予定日を10日ほど過ぎた、3,700グラムの男の子です。日付が変わった頃から陣痛がきて、それからあれよあれよという間に産まれていました。今はただ妻と息子の無事をありがたく思います。

結婚してからおよそ1年半になりますが、新しい家族の存在は新鮮で、とても嬉しく、そして自分に息子がいるという事実がまだ不思議です。自覚に欠けているようにも感じますが、子供が僕と妻を少しずつ親にしてくれるのだろうと思っています。

家事にも育児にも仕事にも、ますますがんばって参りたいと思います。

今後ともよろしくおねがいいたします。

年末年始に飲み過ぎないために

諸君は酒が好きだろうか。筆者の場合は少しくらい、好きである。酒を飲んでもいいことなどない。僅かに楽しいような気分になるばかりで、だんだんと頭がボーッとしてきて身体もダルくなり、ひどいときには周囲の人間にまで迷惑をかける。しかし、よく冷えたビールの一口目の、あのなんとも言い難い美味さよ。あのたった一口分の美味さが脳神経に刻みつけられ、繰り返しくりかえし酒を飲んでしまう。なんと愚かなことだろう。

さて本稿では、種々の問題をはらむ飲酒について、特に二つの問題を取り上げ実践的なアドバイスを諸君らに授けたい。筆者のアドバイスは主に Swift によって記述される。

ビール1パイント問題

アイリッシュパブなどに行ってビールを頼むと、「中ジョッキ」のような曖昧な単位でなく、「パイント」という単位で量を指定しなければならない。諸君らも安易に1パイントで頼んでしまって思ったより多くて苦労する、といった経験をしたことがあるかもしれない。

パイントはヤード・ポンド法における体積の単位であり、イギリスでは1パイントがおよそ0.568リットルに相当する。アメリカでは0.473リットルであるが、ビールにおいてはイギリス式のパイントが用いられる。

Swift と Foundation.framework でこれを示すと、以下のようになる。

let onePint = Measurement(value: 1, unit: UnitVolume.imperialPints) // 1.0 pt
onePint.converted(to: .liters) // 0.568261 L

Measurement は Foundation に iOS 10 の世代から追加された、Double 型の値と Unit 型の単位を持った、単位付きの値を表す struct である。体積の単位を表す UnitVolume は抽象クラスである Dimension のサブクラスで、また DimensionUnit のサブクラスである。Unit は単に単位を表し、Dimension はそれに加えて単位変換を行うために baseUnit()UnitConverter が関連づけられている。

これらの仕組みにより、converted(to:) メソッドで単位変換を行うことができる。英国法定標準のパイントである UnitVolume.imperialPints から SI 併用単位の UnitVolume.liters へ変換できる。(ところで litre じゃなく liter なのはアメリカっぽいと思います。)

let halfPint = Measurement(value: 0.5, unit: UnitVolume.imperialPints) // 0.5 pt
halfPint.converted(to: .liters) // 0.2841305 L

(halfPint * 3).converted(to: .liters) // 0.8523915 L

Swift の Foundation インターフェースでは演算子が定義されているので、ハーフパイントのビールを3杯くらい飲めばおおよそいい感じになる、ということがわかる。

結論:ハーフパイントくらいでいい

さらに Measurement では、MeasurementFormatter を使って読みやすい文字列を得られる。

let onePint = Measurement(value: 1, unit: UnitVolume.imperialPints) // 1.0 pt

let formatter = MeasurementFormatter()
formatter.locale = Locale(identifier: "ja-JP")
formatter.unitStyle = .long

formatter.string(from: onePint) // "0.568リットル"

日本酒1合問題

日本酒のラインナップが充実した居酒屋でお気に入りの銘柄を1合注文したとき、後に続くおちょこ追加、おちょこ人数分によって、僅かに舐めるばかりの量しか残らない。諸君らも、このような事例で涙したことは一度や二度では済まないだろう。

1合がいかほどの量か、ビールのように調べてみたいが、生憎 Foundation の UnitVolume には「合」という単位は存在していない。しかし幸いなことに、自分で単位を追加することができる。

extension UnitVolume {
    static var: UnitVolume {
        return UnitVolume(symbol: "合", converter: UnitConverterLinear(coefficient: 0.18039))
    }
}

このように UnitConverterLinear を利用して新しい UnitVolume を作ることができる。UnitConverterLinearUnitConverter 抽象クラスのサブクラスであり、coefficient と constant によって単純な線形の変換を行うことができる。UnitVolumebaseUnit().liters であるから、尺貫法における1合が約0.18039リットルであることにより、このように書けるのである。

let oneGo = Measurement(value: 1, unit: UnitVolume.合) // 1.0 合
oneGo.converted(to: .liters) // 0.18039 L

これによって Measurement において合という単位が利用できるようになり、人数に応じて適切な量について考えられるようになった。

UnitConverter は他にも実装することができ、例えば逆数への変換などが存在し得るだろう。

結論:みんなで飲むなら多めにたのむ

総括

本論により、Foundation の Measurement 関連 API の利用方法を示した。その中で我々は、以下の結論を得た。

  • ハーフパイントくらいでいい
  • みんなで飲むなら多めにたのむ
  • 飲み過ぎないように気をつける

それでは皆さんよいお年を。


本稿の内容は関西モバイルアプリ研究会 #21において行われた筆者の研究発表から、その内容を再録したものである。

これは飲酒を勧めるものではなく、京都市清酒の普及の促進に関する条例とも関係しない。筆者の居住地である日本国では、未成年者飲酒禁止法により満20歳未満の者の飲酒は禁止されている。また酒気帯び及び酒酔い運転は犯罪である。飲酒は健康な成年に限って、自己の自由意志によってのみ行われるべきであり、何人も飲み過ぎてはいけない。

Swift 3 の private と fileprivate

おはようございます。バットマンです。PlayStation VR でバットマン:アーカム VRを遊んだところ、現実とゲームの区別がつかなくなりました。ヘッドセットを外した後も、手がオブジェクトにめり込むんじゃないかと錯覚するようになった。

ところで Swift ピープルの皆さんも、最近はプロジェクトを Swift 3 にマイグレーションしたりして暮らしていらっしゃることかと思います。御多分に洩れず、僕もマイグレーションして暮らしています。いろいろコツも掴めてきたところではありますが、それはともかく、privatefileprivate について割り切れないものを感じている昨今です。

privatefileprivate

SE-0025 Scoped Access Level で Swift 3 に導入された privatefileprivate は、Swift 2 までの private はファイル内で可視であるというのを fileprivate とした上で、定義内で可視であるという private を新たに導入したものである。これによって private の意味が他の言語などと揃い、一方で Swift の extension の仕組みと親和的な fileprivate が残された、ということになる。

Swift 2 から Swift 3 へ自動マイグレータを適用すると、Swift 2 の private はすべて Swift 3 の fileprivate へ書き換えられる。これらは全く同じなので正しい。しかし fileprivate は長ったらしいので、可能なら private へ置き換えていきたい。ひとまずすべて private にしてみて、コンパイルが通るように fileprivate へ戻す、といった方法で可能な限り private にしていく。extension で何らかの protocol に準拠していて、そのために stored property を fileprivate にしなければならない、というようなことがある。

そういうことをしていると、いまこの privatefileprivate の差が、いったい何の意味を持っているのかだんだん分からなくなる。このコードを読むとき、privatefileprivate の違いをどういう風に解釈したらよいのか。

同一ファイル上で extension によって実装を分けている場合、privatefileprivate を明確に分離できない。同一ファイル上の別な classstruct であれば、privatefileprivate の違いが役に立つかもしれない。しかしそれは、そもそも別なファイルに分けておけばよかったのかもしれない。

ということで、privatefileprivate の分離自体は正しいのだけど、実際にはうまく使い分けることが難しく、一筋縄にはいかない。

他方で internalpublicopen は明々白々に使い分けられて便利。

あなたの知らない UIKit の世界 — UITableView に UITextView を置きたい

f:id:cockscomb:20160906232707p:plain:h540:right

UITableView の見た目で様々な要素を表示しつつ、その一部として UITextView を使いたい、という需要があると思う。最も身近な例は標準のメールアプリである。

これは UIKit の API を駆使することで実現可能である。本記事ではこれを実現する方法を通して、あまり知られていないであろう UIKit の機能について紹介する。

UITextView の intrinsic content size

Auto Layout において、UIView は自身が表示されるためにちょうどよいサイズを知っている。具体的には var intrinsicContentSize: CGSize である(Swift 3.0 では property で、Swift 2.3 まではメソッドである)。このサイズは他の NSLayoutConstraint と関連して、デフォルトではプライオリティ 750 で縮みにくく、プライオリティ 250 で広がりにくい。

例えば UIImageView では画像のサイズがこれに相当するし、UILabel では文字列を描画するのに最低限必要なサイズと等しくなるはずである。

ここで UITextView の intrinsic content size はどうかというと、そのままでは widthheight-1CGSize が返る。UITextViewUIScrollView を継承しているので、表示する内容に関わらず、ちょうどよいサイズというものを持たないのである。しかしここで、UIScrollView 由来の var isScrollEnabled: Bool property に false を設定すると、こんどはちょうどよいサイズが返るようになる。

let textView = UITextView()
textView.text = "Think different"

textView.intrinsicContentSize() // {w -1 h -1}

textView.scrollEnabled = false

textView.intrinsicContentSize() // {w 86 h 30}

UITextView の intrinsic content size (Swift 2.3)

この挙動を利用して UITableViewCell の Auto Layout を設定することができる。

ところで UITextViewUIScrollView を継承しているのは正当で、内容となる文字列が非常に長い場合に、表示されていない部分のレイアウトを遅延させることができる。実際に、長大な内容の UITextViewvar contentSize: CGSize を取得すると、場合によっては本来のサイズが返ってこないはずだ。逆に言えばスクロールを無効にした UITextView では、長大な内容を持つ場合に明確なパフォーマンスの低下がみられることに留意するべきである。具体的な仕組みに関しては NSLayoutManager Class Reference の “Noncontiguous Layout” を参照すること。

UITableView の Auto Layout

UITableView の self-sizing cell について、詳細は “Working with Self-Sizing Table View Cells” を参照。

UITextView を置いた UITableViewCell に適切に NSLayoutConstraint を設定する。このとき最小の高さを設定しておく方が良いかもしれない。あとは UITableViewAutomaticDimension を高さとして返す。

UITableViewCell の高さの更新

一度表示された UITextView の内容が変更される場合、単に Auto Layout を利用していれば func invalidateIntrinsicContentSize() の効果で全体が更新されるはずである。一方で UITableViewCell の場合は、高さの再計算が必要であることを UITableView に伝える必要がある。ただし cell をリロードするといったことは避けたい。

UITableView では、func beginUpdates()func endUpdates() を連続して呼び出すことで、 cell の高さを再計算させることができる。これはドキュメンテーションされた挙動である。UITableView Class Reference の beginUpdates() から、discussions 部分を引用する。(強調は筆者によるもの)

Call this method if you want subsequent insertions, deletion, and selection operations (for example, cellForRowAtIndexPath: and indexPathsForVisibleRows) to be animated simultaneously. You can also use this method followed by the endUpdates method to animate the change in the row heights without reloading the cell. This group of methods must conclude with an invocation of endUpdates. These method pairs can be nested. If you do not make the insertion, deletion, and selection calls inside this block, table attributes such as row count might become invalid. You should not call reloadData within the group; if you call this method within the group, you must perform any animations yourself.

このように cell のリロードなしに高さのアニメーションを行えることが明記されている。

これを利用して、var text: String? property を変更したあとや、あるいは var isEditable: Bool が true であればユーザーの入力に合わせて、beginUpdates()endUpdates() を呼び出すことで、Auto Layout と合わせて適切に cell の高さが変化する。

extension ViewController: UITextViewDelegate {

    func textViewDidChange(textView: UITextView) {
        UIView.performWithoutAnimation {
            self.tableView.beginUpdates()
            self.tableView.endUpdates()
        }
    }

}

ユーザーの入力に合わせて高さを更新する。この場合は煩わしいのでアニメーションを無効にしている。(Swift 2.3)

まとめ

UITextView のスクロールを無効にすることで intrinsic content size が変化し Auto Layout でうまく扱えることを示した。これを UITableViewCell に置いて、UITableView の self-sizing cell として利用できることを明らかにした。そして UITableViewbeginUpdates()endUpdates() のドキュメントされた挙動によって、cell の高さを再計算させられることを確認した。

これらの UIKit の挙動はあまり知られていないように思う。ひとつひとつは些細だが、組み合わせることで強力なものとなる。UIKit についての知見を深めることは、すばらしいアプリを開発するための近道である。


以上の内容は8/31の関西モバイルアプリ研究会で発表したものです。

iPhone OS 2.0 とともに UIKit が公開されて、ついには iOS 10 の正式リリースがすぐそこまで迫っている昨今ですが、長年の経験を持つ私たちでさえまだまだ知らないことが山ほどあるように思います。UIKit は非常に巧妙に設計された GUI フレームワークですから、ユーザー体験を改善するために必要な様々な仕掛けが用意されていたりします。

特に近年では、Swift というリッチな言語や、モダンパラダイムやライブラリ、あるいは種々の開発手法などと、モバイルアプリに関する話題には事欠きません。しかしながら初心に立ち返って、慣れ親しんだはずのフレームワークを子細に観察してみると、意外な発見があるかもしれません。今後もライフワークとして学び続けて、API を通して Apple に近づいていきたいと思います。

Swift の Nil Coalescing Operator でコンパイルは遅くなるか

趣味のウェブブラウジングをしていると、Swift の ?? (nil coalescing operator) がコンパイルを遅くするのではないか、といった話題*1を見かけました。この演算子は、左辺の Optional<Wrapped> 型の値が Optional.none である場合に右辺の値を返すというもので、直感的にはこれがコンパイル時間を悪化させるとは思えません。経験から言えば、このようなケースでは大抵やや複雑な型推論が発生しており、それがコンパイル時間に支配的な影響を与えています。そうであるなら、人間が少し工夫して型のコンテキストを与えてやることで、計算機はずっとよいパフォーマンスを発揮できるはずです。

ごく簡単な例で実験してみましょう。以下のコードは、let view: UIView? があるとき、座標系における view のX座標を得ようとするものです。ただし viewnil を取る場合は 0 であることにします(nil coalescing operator を用います)。このとき得られる値 x の型は CGFloat になります。

let x = view?.frame.origin.x ?? 0

このように書いたとき、view?.frame.origin.xOptional<CGFloat> 型であるから、0 という数値リテラルCGFloat 型の値であり、結果として xCGFloat 型である、というような順で型推論は解決されるでしょう。これは実際に少し複雑なので、コンテキストを与えて緩和することができます。

let x: CGFloat = view?.frame.origin.x ?? 0

一例としてはこうです。これは先ほどの例に対して xCGFloat 型であることを明示しただけです。

さて、これが本当にコンパイル速度を改善するのか試験してみましょう。Swift コンパイラに少しオプション(-Xfrontend -debug-time-function-bodies)を渡すことで、関数毎にコンパイル時間を出力することができます。元の話題の記事にあるように、Xcode プラグインの形で公開されているツールを利用することもできます。

筆者がざっくりと試験した結果では、元のような例(testA())では実際にコンパイルに時間を要しました。その他の CGFloat 型であるというコンテキストを与えた場合 if let 文などを利用した場合はほとんど変わらず、その差はありませんでした。この傾向はキャッシュをクリアした上で数度同じ試験を繰り返しても変わりませんでした。(きちんと統計的に検定すれば有意差が得られるはずですが、横着して計算していません。)

また元の話題で遅いとされていたケースも、少し書き換えることで十分早くすることができました。

// 3580.1ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) + (leftView?.bounds.width ?? 0) + 22, height: bounds.height)

// 5.5ms
return CGSize(width: size.width + (rightView?.bounds.width ?? 0) as CGFloat + (leftView?.bounds.width ?? 0) as CGFloat + 22, height: bounds.height)

現在のバージョンの Swift コンパイラでは、型推論がやや複雑になった場合に、明確なコンパイル速度の低下が見られ、その場合でもコンテキストを与えることにより型推論の範囲を限定して速度を改善できる、ということが言えるはずです。また nil coalescing operator がそのような状況を生み出しやすい可能性があります。

私見では、コンパイル時間に配慮して nil coalescing operator を使わないという判断をする必要はなく、必要に応じて型のコンテキストを与える程度で十分だと思います。それよりはコードが持つ本来の意図がより明確になるように記述するのがよいでしょう。

こちらからは以上になります。

UIButtonの画像の位置を変えたい

UIButton の画像の位置を、タイトルの左側じゃなく上側や右側にしたい。画像が左側じゃないボタンのモックを見たとき、どうやって実現するかいつも少し悩む。ということでいくつかのアプローチを検討したい。

drawRect(_:) など

UIButtonvar titleLabel: UILabel? { get }var imageView: UIImageView? { get } といった view を内包していて、描画はそういった view の側で行われるので、drawRect(_:) などの描画関連のメソッドをオーバーライドするべきではない。

独自に UIImageView を足したり CALayer を足したりして、表示される画像を追加することはできる。

setBackgroundImage(_:forState:)

画像を背景にする。UIImageresizableImageWithCapInsets(_:) と組み合わせれば、わりと好きな位置に画像を表示できるはずである。不毛な感じはする。

titleEdgeInsetsimageEdgeInsets

タイトルの画像の UIEdgeInsets でチマチマ調整する方法。もともと画像が左にあるところからがんばって計算することになって厳しい。

titleRectForContentRect(_:)imageRectForContentRect(_:)

titleRectForContentRect(_:)imageRectForContentRect(_:) といったメソッドをオーバーライドしていい感じの CGRect を返す。

似たような backgroundRectForBounds(_:) メソッドのドキュメントには

Subclasses that provide custom background adornments can override this method and return a modified bounds rectangle to prevent the button from drawing over any custom content.

ということが書いてあって、オーバーライドしてよいことがわかる。しかし contentRectForBounds(_:) を含む同種の他のメソッドには書いていない。同種の他のメソッドには対応する UIEdgeInsets があるので、そういうのを考慮しているからオーバーライドしないようにという意味なのか、温度感がわからない。

UIControl のサブクラス

完全に独自の Button を作ることもできる。UIControl を継承して何もかも自分で作ることで、自由な内容を描画できる。特に不都合はないが、面倒ではある。


titleRectForContentRect(_:)imageRectForContentRect(_:) のオーバーライドがいちばん現実的ではないか。という結論に至った。

詳解 Swift 改訂版

詳解 Swift 改訂版