2016年11月17日木曜日

UITouchGestureRecognizer をやめて UILongPressGestureRecognizer や UIButton を使ってみる

適当なUIViewにUITouchGestureRecognizerを貼ってタッチアクション可能にするという実装を行うことがありますが、UITouchGestureRecognizerはUIButtonと違ってタッチして離すタイミングではなくタッチした瞬間に反応してしまうためユーザビリティ的に望ましくない場合が結構あります。

そんなときはUILongPressGestureRecognizerを代わりに使ってみましょうと言う話です。
http://stackoverflow.com/questions/12830547/how-to-implement-touch-up-inside-in-touchesbegan-touchesended

UILongPressGestureRecognizerというと、長押しして何かコンテキストメニューを表示する際に使うイメージが強いものですが、minimumPressDurationを0.01など十分に短く取ると、事実上タップとほとんど変わらないユーザビリティを得ることができます。さらにUITouchGestureRecognizerとは異なりタッチを認識した後に離すまでの間のイベントをUILongPressGestureRecognizer.stateプロパティ経由で監視することができます。その為例えば
  • タッチされている間は見た目を変える
  • タッチを話したときにアクションを実行する
といった味付けが簡単にできます。

しかしもっと簡単に透明なUIButtonを作って目的のUIViewの上に重ね、そちらにタッチをハンドリングさせたほうがより綺麗で簡単にボタンらしい挙動を再現できたので別にUILongPressGestureRecognizerを使う必要はなかったかもしれません(´・_・`) まぁこういう使い方もあるんだよという参考になれば幸いです。

ビルドが遅いのをどうにかこうにかやりくりする方法

すでに皆様痛感されていると思いますが、Swiftはビルドがとんでもなく遅いです。正確に言うと、Swiftのビルドが遅いのに加え、clang moduleのビルドが遅いため、frameworkの数が増えてくるとビルドにかかる時間がどんどん破滅的なことになってきます。

Swift & dynamic framework導入前はアーカイブ込みで10分以下だったビルド時間が、現在は20分を超えてしまっており、更に今後もSwiftコードの増加ととframeworkの増加に従ってビルド時間が伸びることが予想されます。そこでなんとかしてこの時間を短縮しようと思って調べてみました。

そもそもビルド時間が伸びる原因


  • Swiftのビルドが遅い、特に型推論が遅い
  • frameworkのビルドが遅い
  • Xcode上でcleanを実行するとプロジェクト内でビルドしているのframeworkが全てビルドし直しになり分割ビルドの恩恵が得られない

それぞれ詳しく見ていきます。

Swiftのビルドが遅い、特に型推論が遅い

以下の記事が大変参考になります。
https://thatthinginswift.com/debug-long-compile-times-swift/
Swift 3.0になって随分と高速化したため、型推論のせいで異常に時間がかかるという自体は減りましたが、それでもObjective-Cのコードと比べると10倍以上時間がかかることが多々あります。型を明示すれば高速化に寄与できますが、そうするとSwiftのせっかくの旨味が減るため悩ましいところです。

frameworkのビルドが遅い

dynamic framework = clang moduleのビルドはコードのコンパイル以上の作業を多分に含むため、分割すれば分割するほどそのオーバーヘッドが増えて無駄に時間がかかることになります。

Xcode上でcleanを実行するとプロジェクト内でビルドしているのframeworkが全てビルドし直しになり分割ビルドの恩恵が得られない

個人的に最悪なのがこれだと思います・・・せめて特定のコードに対してのみcleanできれば良いのですが、Xcodeで各種トラブルを避けるためにはcleanは必須、しかしながらcleanを実行すると全てのframeworkがビルドし直しとなって分割ビルドの恩恵が得られません。

特にCocoaPodsを使っていると全てのpod管理下のframeworkが再ビルドとなるため大変効率が悪いです。

これらを踏まえて対処法を考えてみました。と言っても大したことは出来ないのですが、

  • 定期的にビルドタイムをチェックする環境を用意する
  • CocoaPodsをやめてcarthageを使用する
  • microframeworkをやめる、どうしてもmicroframeworkを行うのであればcarthageと併用する

ビルドタイムのチェック方法は https://thatthinginswift.com/debug-long-compile-times-swift/ に記載がありますが、Other Swift Flags-Xfrontend -debug-time-function-bodiesを追加してビルドし、ビルドレポートを見ればOKです。結果はpbpaste | egrep '\.[0-9]ms' | sort -t "." -k 1 -n | tail -10などで整形すると見やすくなります。100msを超えている関数があれば手を打つ必要があるかもしれません。

CocoaPodsですが、手元で全てのframeworkをビルドしようとするためcleanした際の無駄が多いです。carthageを使えば最初にビルドを一回行って後はそれを参照して使いまわすだけ、という風にできます。ただしcarthageは最初のビルドがやたら長く、CocoaPodsを使った場合より5倍以上遅い?と言う問題があるのでその点は注意が必要です。

microframeworkを辞める論についてはおそらくいちばん賛否両論議論があるところかと思いますが、少なくとも殆どの場合、開発中はコードを使いまわす回数と時間よりXcodeをcleanしてビルドし直す回数と時間のほうが支配的であると信じています。Xcodeがちゃんとしてさえいればベストプラクティスに従ったほうが良いのでしょうが、そのようなことは伝統的にないので、ベストプラクティスにこだわらずビルドを高速化するために一つのframeworkにまとめるのも悪くはないのではと思っています。
ただしcarthageと併用できるのであれば、一度ビルドしたmicroframeworkを使いまわすメリットが享受できるので良いかもしれません。その場合、microframework側はコードベースがある程度安定している必要があります。さもなければ何度も何度もcarthage updateを走らせ直して結局時間がかかることになります。

結論

いろいろ考えてみましたが、頑張ってビルド高速化するより、新しいビルドマシンを増設したほうが結果的に良いかと思います\(^o^)/

2016/12/01 追記

Qiitaにより詳細で素晴らしいまとめがあったのでそっちを見ておいたほうが良い気がします(´・_・`)

全体的に見て型キャストとかワンライナー(とそれを誘発する演算子等)による途中の型推論が劇的に遅い原因のようですね。まぁ来年ぐらいまでにはclangが賢くなって直ってるのではないでしょうか・・・多分・・・

2016年11月14日月曜日

UNNotificationAttachment.init(identifier: String, url: URL, options: [AnyHashable : Any]? = nil)に拡張子のないURLを渡す際の注意

iOS 10で導入されたUserNotifications.frameworkは大変便利ですが、落とし穴が早速幾つか見つかっておりますのでご紹介いたします。
参考: iOS 10で画像つきのNotificationを配信する - Qiita

表題にありますUNNotificationAttachment.init(identifier: String, url: URL, options: [AnyHashable : Any]? = nil)ですが、第二引数のurlに画像や動画などのメディアのURLを渡してUNNotificationContentに付与すると言う使い方をするのが一般的かと思います。

このときどうやらUserNotifications.frameworkはurlのpathExtension、要するに拡張子の情報を利用してメディアの種類を判別しているようで、以下のような状況が発生すると実機でのみUNNotificationAttachment.initの実行が失敗してnilが返却されます。

  • 第二引数のurlに拡張子が付与されておらず、かつoptionsに[UNNotificationAttachmentOptionsTypeHintKey: "<当該URLのメディアの適切なMIME Typeを表す文字列>"]が指定されていない場合。
  • または間違った拡張子やMIME Typeが指定されている場合。

なおシミュレータでは問題ありません。いきなり実機に持っていって困るということが有るかと思いますので十分ご注意ください。

対処法は2つあります。

  1. URLに適切な拡張子を付与する
  2. optionsに[UNNotificationAttachmentOptionsTypeHintKey: "<当該URLのメディアの適切なMIME Typeを表す文字列>"]を付与する

何れにせよURLが指し示すメディアの種類が正確にわかっていないと問題になるため、その点は注意が必要です。

2016年10月4日火曜日

Q. ATS を Debug ビルドでだけ無効にしたいのですが...

A. こちらの内容に従えば一発です。

要するにBuild Phaseにビルドスクリプトを追加してそこでPlistBuddyを使って値を書き換えましょうと言う作戦です。

もうちょっとマシな解説

普段、Info.plistの設定内容をConfiguration毎に書き換えたい場合は、たいていよくやるのがxcconfigファイルを以下のように用意して、
APP_BUNDLE_NAME = MyApp_Dev
でもってInfo.plistに対して
Bundle Display Name = ${APP_BUNDLE_NAME}
こんな風に書いておけばビルド時に自動的にXcodeが環境変数による置換処理を行ってくれる、というのを使うのですが、まいったことにATSの設定を司るNSAllowArbitraryLoadsはString値ではなくBool値でして、このInfo.plistに対するXcodeの環境変数置換処理はString値(と、確かNumber値)にしか使えないというオチがあります。仕方がないので最初に説明した方法が最善になるかと思います。Info.plistファイルをConfiguration毎に用意する方法もありますが、大概そっちのほうが設定のメンテナンスが面倒になるのでオススメしません。

2016年10月3日月曜日

ATS の AVFoundationに対する 例外条項の注意点

2016/10/03段階でのATSを有効にする際の覚書です。基本的には全てTLS 1.2以上をサポートするhttpsにしてしまえばOKですが、例外条項としてAVFoundationのStreamについてはhttpでもよいと例外条項が定められています。

https://developer.apple.com/videos/play/wwdc2016/706/?time=324

これについては設定不要で自動的に適用されるようにWWDCのビデオ上では見えますが、実は落とし穴があり、この例外条項はiOS 10以降でのみしか適用されません。iOS 9については実はAVFoundationのStreamについてATSが完全に適用されているようで、httpの動画をStreamingしようとしても失敗してしまうので注意が必要です。

2016/10/03 16:45 追記
こちらXcode 7.3.1でビルドしたバイナリにて調査した内容ですので、SDKのバージョンが異なるXcode 8.0以降でビルドしたバイナリであればiOS 9.x系でもAVFoundationのStreamについてATSの例外条項が適用されてhttpで通信できるようになるかもしれません。ただ紹介した参照元のWWDCのビデオを見ても分かる通り、基本は例外を考えず全てhttps化するのが良いとされているため、ビデオストリームについても可能であれば全てhttps化するほうがトラブルがないかと思います。

2016年9月11日日曜日

Parse.com が潰れたので新しい mBaaS を探す旅に出た

定番のmBaaSとして人気だったParse.comが終わってしまうことが確定してしまった2016/09/11現在、次はどのようなmBaaSを選ぶのが良いのか調べてみました。なおどのプロダクトも全然試せていないので実戦での詳細な評価についてはできかねます。むしろこれを読んだ皆さんに果敢にトライしていただきたい!\(^o^)/

Niftyクラウド mobile backend


http://mb.cloud.nifty.com

  • 国内だとここ
  • ちょっと前にひとりぼっち惑星がここ使ってるといって有名になった
  • 正直機能的にはダメだと思ってる
    • その話題になったブログ記事の内容が、データストアにトランザクションに相当する処理を新機能として導入しました、とかDBキーによる高速ルックアップを実装しました、とかそういう次元の、あまりにも最初から存在して当然と思われる機能を新規実装として誇っているような内容だったため
    • 逆に言えば今後もこの調子で継続的に改善してくれるかも
  • Niftyが社命賭けてるっぽいしそう簡単には潰さないと信じてる

Google Firebase


https://firebase.google.com

  • なんといってもGoogle様ブランドがある
  • Analyticsが非常に素晴らしい、GAをリプレイスして、Analyticsのためだけに導入する価値がある
  • 一方でiOS/Android/Webプラットフォーム全てで同一の概念を利用できるように、各プラットフォーム独自の箇所をラップして激しく潰しているように見える
    • 特にFCMと呼ばれるPush通知に相当する箇所についてはAPNSが元の影も形も見えないぐらい激しくラップされていて操作できないため、例えば今の時期だとiOS 10向けのUserNotifications.frameworkに対応させるのが実質不可能とかそういうデメリットがある
    • iOS側が割を食いそうな不安はある
    • とはいえ調べてみたところiOS 9からのUniversal LinkはApple仕様に従ってきちんと対応するようになっていて、Firebase側は全く無視する気はなく、対応が遅れるのを許容できるのであれば大丈夫そう
  • Googleだし気合入れてるみたいだしそう簡単には潰れない・・・はず・・・
    • とはいえ最近のGoogleは不採算プロダクトを平然と潰すので油断ならない

backendless


https://backendless.com

  • パッと見一番Parse.comっぽい
  • トップページからして「Migrating from Parse? Welcome to Backendless!」だし
  • 一番Parseっぽくて下手にラップするようなことをしておらず、かつ必要な物は全部あるので最も個人的には好感
    • Firebaseみたいに激しくラップされすぎる構造は、例えるならRuby on RailsやPython Djangoのような感覚があってどうも個人的には好きになれない
  • ただしParseの二の舞いのように潰れる可能性もまた否定出来ない


まとめ

どこも潰れるリスクがワンチャンありそうですね(´・_・`)
ご利用はご計画的に\(^o^)/

iOSのフォントのお話

最近フォント周りについて深く掘り下げる機会がありましたので、その際のメモを残しておこうと思います。かなり読む人置いてけぼりな中身になってますが、フォントを詳しく触り始めるとなるほどーとためになる(と思う)のでどうかご了承ください(´・_・`)

UIFontのプロパティについて

UIFontにはフォントに関する数値を表すプロパティが存在します。いろいろありますが、もっとも重要なのは以下に列挙するプロパティです。

  • pointSize
  • lineHeight
  • ascender
  • descender
  • leading
  • capHeight

以下の画像を見ると非常にわかりやすいかと思います。


参照元: https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/CustomTextProcessing/CustomTextProcessing.html および http://stackoverflow.com/a/35922853

これを踏まえて各プロパティを解説すると以下のとおりになります。
  • pointSize
    • フォントのサイズを表します。
    • ほとんどすべてのケースでpointSize = ascender + descenderとなり、すなわちpointSizeは上端と下端に空白を含まない純粋なフォントの高さだけを表す数値であると言っても良いと思います。
      • 上下に空白を作らない高さでフォントを描画したい場合はこの高さを使うと良い、と言い切って間違いないと思います、ごく一部の変なフォント以外。
  • lineHeight
    • フォントの上部末端(上端空白が存在する場合はそれも)から下部末端(line gapも存在するなら含む)までの完全な高さを表します。
    • 飾り文字などではないまっとうなフォントであれば、必ずこのlineHeightの中に文字の高さが収まるようになっています。
      • 一部飾り文字のあるフォントについてはfont leadingを使って下部末端のline gapを表現していることがあります。
    • pointSizeとの最も大きな違いは、上端と下端にline gapに相当する空白を持っています。特にシステムフォントで顕著にそれが見られます。詳しくは後述。
    • 実際にフォントが画面上にレンダリングされるときはこのlineHeightがすべての基準になっているケースが多いです。
  • ascender
    • 全てのアルファベットの文字の最大高さを表します。
    • この高さまで描画されるのはごく一部の文字だけで、殆どの文字はcapHeightまでの高さしか使いません。詳細はこの後のcapHeightを参照してください。
  • descender
    • アルファベットのBaselineより下に描画される文字、例えばj p q yなどの下付き部分の高さを表します。
    • 日本語の文字でこの領域に描画される文字は私が知っている限りでは存在しません。
    • UIFontが返す値は常にマイナスになるので気をつけてください。
    • CTFontが返す値は常にプラスです。
      • めんどくさいので常にfabs()を通すといいと思います。
  • leading
    • Baselineからフォントの下部末端(line gapを含む)までの高さを表します。
    • 上記の図ですとline gapの部分だけがleadingとなっていますがヒラギノフォントで試した限りは誤りです。Baselineから下全てが対象になっているようです。
    • lineHeightに含まれている下部末端のline gapとは「全く別の値が付与されていることがあります」。
      • 例えば飾り文字のあるフォントについて、lineHeightよりもフォントの描画が大きくなるぐらい下に飾りが伸びていたとしても、leadingの値はそこまで考慮して付与してあるので、usesFontLeadingのオプションを付与すると下の行にはみでなくなる、みたいなケースが存在します。
  • capHeight
    • アルファベットの大文字の最大高さを表します。
    • 日本語の文字は(特に漢字は)基本的にすべてcapHeightの高さに描画されます。
    • capHeightより上に描画されるのは、例えばÅ(Aウムラウト)のように修飾子が付いているアルファベットや、日本語だと濁点および【】隅付き括弧のような一部の文字が、capHeightよりも上に描画されます。
特に覚えておきたいのがleadingというプロパティの働きです。UIKit, TextKit, Core TextにusesFontLeadingというような名前のオプションが存在するケースが多々ありますが(例えばNSAttributedString.boundingRect(with:options:context:)のoptionsとかですね)、これは何かというと、
usesFontLeadingが指定されている場合、lineHeightの計算の際に常にUIFont.leadingの値が使用される。従って、もしattributed stringにparagraph styleを付与してlineSpacingに0を付与していたとしても、フォントにleadingの値が付与されていたら、それ以上に行の高さが低くならず必ず行間が開いてしまう。
というオプションになります。paragraph styleでlineSpacingやparagraphSpacingを0に指定しているにもかかわらずどうしても行間がゼロにならないとか、指定するより広がってしまうみたいな状況になった場合は、このfont leadingを疑ってみてください。

ちなみにiOS 8以上については以下のとおりです。

  • UILabelは常にfont leadingを使用しません。
  • UITextFieldは未調査ですがおそらく常にfont leadingを使用しません。
  • UITextViewはデフォルトfont leadingを使用しますが、UITextView.layoutManager.usesFontLeadingの値をfalseにすることで回避可能です。
  • その他のラベルパーツ(UIButton.titleLabelなど)については完全には調査できていませんが、UIButton.titleLabelに関してのみいうとiOS 9の地点ではfont leadingが常に「使用されてしまいます」。これはおそらくiOS 6以前のUILabelの挙動がそのまま残っているためではないかと思われます。同じUILabelクラスですが中身と挙動がUILabelと異なるため気をつけてください。このようなケースは他にもあると思われます。

iOSビルトインフォントの問題点

幾つかのiOSビルトインフォントについては、先述のUIFontのプロパティの値に疑問がある数値が設定されているケースが有るようです。具体的にご紹介します。

1. ヒラギノフォントの場合

ヒラギノフォント、すなわちヒラギノ角ゴシックW3/W6、それからヒラギノ明朝W3/W6については以下の様なフォントの設定の特徴があります。
  • leadingの値がきちっと付与されています。
  • 常にpointSize == lineHeightになります。
  • おそらくバグだと思うのですが、descenderの値が本来必要な量の半分しか設定されていません。そのためヒラギノフォントを明示的に付与したラベルにg, j, y, qなどの下付き部分があるアルファベット文字を描画すると下が途中で千切れてしまいます。
    • 対処法としてはfont leadingを使用するか、または自力でdescender分だけラベルの高さを高くするか、明示的に指定してヒラギノフォントなんて使うのをやめてシステムフォントを使用してください。

2. システムフォントの場合

次にシステムフォントはどうなっているのかというと、以下の様な特徴があります。
  • 常にleadingの値がゼロになります。従ってfont leadingを使うモードで描画しても使わないモードで描画しても全く同じ結果になります。
    • iOS 8はHelvetica Neue, iOS 9以上はSan Franciscoが採用されていますが、いずれのケースでも全く同様の設定になっています。
  • pointSizeよりlineHeightが高く、ちょうどその差分の分だけ自動的にline gapとして行間が開くような設定になっています。
    • 要するに簡単にいえば、font leadingを使わないで、かつattributed stringでlineSpacingやparagraphSpacingをゼロに設定したとしても、必ず行間がちょっとだけ空いてしまいます。まずやることはないと思いますが、正確に行間ゼロを作るのは著しく困難です。

UIFontのpointSizeについて

UIFont.pointSizeについて、みなさん普段から何気なしに17とか14とか値を指定されているかと思いますが、このpointSizeに指定する値の単位って気にされていますでしょうか?このフォントのpointSizeとはPostScriptポイントないしDTPポイントと呼ばれるものらしいです。一般的に1PostScriptポイント=1/72インチとされています。

ではこの単位はiOS上で使われている論理座標系のpt単位とは全く違うものなのかというと、どうやら大昔のMacintoshのころからの名残で、macOS/iOS上の論理座標系の1pt=1PostScriptポイントとなるように定められていて、これは現在に至るまで常に維持されているということらしいです。

要するに、UIFontのpointSize=iOS上での1ptと完全に一致します。このことはlineHeightが16のフォントを画面上にレンダリングすると論理単位16pt(2倍Retinaなら32pxになる)ことからも確認ができます。便利ですね。


UIFont.leadingのiOS 8以下での不具合

かいつまんで言うと、UIFont.leadingの値は、iOS 8以下のときに常にUIFont.lineHeightと同じ値を返します。これは"font leading"という単語が使われている文脈や文化圏によって意味が異なり、font leading = line heightとなる文化圏があったので誤用されてしまった、ということらしいです。しかしながら実際のiOSでは先述の通り「font leading = BaselineからフォントのLine gapを含む下端までの距離」と定義されていますから、このような値では困ります。

対処法として、iOS 8以下の場合はCTFontを使ってleadingの値を取得してください。こちらにコードを用意しました。
https://gist.github.com/akisute/3c3d162da73abd784525c9ed7859cda2

なお、iOS 9以上はこの問題は発生しません。iOS 8なんてそろそろサポート切れるんで忘れていいかもしれませんが、念のため。

まとめ

デザイナーさんにフォント周りで行間調整がおかしいぞーとかベースラインがずれてるぞーとか突っ込まれまくっていて必死こいて修正方法を調べている時に、こんな記事を見つけました。

https://www.raizlabs.com/dev/2015/08/advanced-ios-typography/

Stop Saying "No" to Designers.
このサイトには英字フォントをBaselineではなくcapHeightの位置で上揃えにするべく奮闘した一人の漢の話が書かれています。おお、海外にも私と同じ境遇の漢が居たのです。

我々もデザイナーにNOと言わないタイポグラフィを実現させたいですね\(^o^)/