2017年6月19日月曜日

UIWebView, WKWebView 等において Drag and Drop を禁止する方法


iOS 11よりDrag and Drop APIがUIKitに追加され、UITextView / UITableView / UICollectionViewに簡単にDrag and Dropを実現するためのdelegateが用意されたり、それ以外のUIViewにもDrag and Dropをハンドリングするための仕組みが用意されました。このDrag and Dropは基本的にはアプリをまたいでデータを受け渡しすることを前提として作られていますが、一応は自分のアプリ内でデータを移動したりコピーしたりするためにも使えるようになっているので、積極的に活用していきたいところです。

ところでこのDrag and Drop、基本的にはアプリ側が対応しない限り自動的には対応してくれないのですが、例外があります。それがUIWebView / WKWebView / SFSafariViewController要するにWebViewのたぐいの皆様です。これらについては最初からDragも可能ですしDropも可能なように作られているようです。便利ですね!

ですがここをご覧になっている皆様の中には、大人の事情によりどうしてもそのような便利な機能をユーザーさんにわざと提供したくないと言うケースがある方もいらっしゃるかと思います。そこでUIWebView / WKWebViewのデフォルトのDrag and Dropを無効化したり、頑張って自分のコードでハンドリングしたりすることができるようにする方法をご紹介します。なおSFSafariViewControllerに対しては手も足も出ないのでご了承ください。
if #available(iOS 11.0, *) { 
    webView.scrollView.subviews.first?.interactions = []
}
たったのこれだけです。簡単ですね!

なんのこっちゃなので詳しく説明します。

まずはじめにiOS 11よりUIView.interactionsプロパティが追加になっています。このinteractionsは[UIInteraction]型となっていて、公式のドキュメントによると「ジェスチャベースの挙動をビューに定義する」ことができるらしいです。まさにドラッグ・アンド・ドロップ的なやつですね。
で、Appleによって最初からドラッグ・アンド・ドロップ用のUIInteractionすなわちUIDragInteractionとUIDropInteractionが用意されています。これらのInteractionは内部的にdelegateを持っていて、これらのInteractionがインタラクションを検知したらdelegateにメッセージングを行い、そいつらが実際のドラッグ・アンド・ドロップ処理を行うというような仕組みになっているようです。
調べてみたところUITableViewのような最初からDrag and Drop用のdelegateを追加されているViewについても、このinteractionsに何らかのUIDragInteraction / UIDropInteractionが追加されていて、それが一旦イベントを拾ってから我々が定義したUITableViewDragDelegate / UITableViewDropDelegateに問い合わせを行うという仕組みで動作しているということがわかりました。

ここまで分かれば後は簡単で、UIWebView / WKWebViewも同様にinteractionsで一旦イベントを受け取った後に、内部的に処理を行っていると考えられるのでUIWebViewの中身をどったんばったん探してみたところwebview.scrollView.subviews.firstの中にinteractionsが刺さっているのを発見したと言う次第です。

先程の無効化する例では空配列を渡すことで完全にinteractionsを無効化していますが、ここで自分が作ったカスタムのUIDragInteraction / UIDropInteractionを渡してやればうまい具合に自分でWebViewのDrag and Drop挙動をカスタマイズすることができるかもしれません。また両方を消すのでなくて片方だけを消せばDragによってデータを持ち出すのは禁止するがDropによってデータを持ち込むのは許可するみたいなこともできます。

2017年5月24日水曜日

Xcode 8.3 & Swift 3.1 環境で LLDB を使う Tips

以前も似たような記事書いた気がするんですが、定期的にSwiftのコード上でLLDBを使ってて困ることがあるので定期的に書くことにします。ほとんど https://stackoverflow.com/questions/29441418/lldb-swift-casting-raw-address-into-usable-type からの転載です。

LLDBの言語を指定して実行する

標準ではbreakpointを挟んでbreakした行がSwiftであればSwift、Objective-CであればObjective-CでLLDBの言語が選択されるようなのですが、これでは毎回毎回breakした地点に応じてデバッグの仕方が変わって大変で仕方がないので、Swiftに統一してしまうのが良いと思っています。
expr -l swift -- {Swift Command}
e -l swift -- {Swift Command}
とすると常にSwiftでLLDBに対してコマンドを送る事が可能になります。逆にObjective-C側に寄せたい場合は
expr -l objc++ -O -- {Objective-C Command}
e -l objc++ -O -- {Objective-C Command}
とすれば良いです。-Oオプションがないと結果がいい感じにObjective-Cのオブジェクトとして扱われないので注意。

importを忘れずに

以前も書いた気がしますがLLDBでbreakした環境はそのままだとFoundation.frameworkすらまともに使えない(シンボルを認識しない)状態なので、importを忘れずにしましょう。
e -l swift -- import UIKit
e -l swift -- import MyApp
e -l swift -- import MyAppLib
という具合でしょうか。他必要なものがあればその都度。注意点として自分のアプリ自体に含まれるシンボル、例えばAppDelegateだとかViewControllerのたぐいだとかもimportしないと触れないので忘れないように。

unsafeBitCast(_:to:)を使ってポインタからオブジェクトを取得する

これも以前書いた気がしますが、Swift 3でまたシグネチャが変わったらしいので念のため。
e -l swift -- let $obj = unsafeBitCast(0x00000000, to: MyClass.self)
e -l swift -- print($obj)
とかやるといい具合になります。

エイリアスを作ろう

毎回e -l swift -- とかタイプするの面倒ですし、poがe -O -- のエイリアスなのと同様に、eswiftとかpswiftとかそういうエイリアスを作ってしまえばいいと思います。~/.lldbinitの中に、
command alias eswift e -l swift -- 
とか書いておけば良い気がします。

2017年3月7日火曜日

NSAttributedString に lineSpace を付与したとき、1行であるにも関わらず lineSpace が下端に付与されて困っている人はこれを読んだら直ります



突然ですが皆さん、NSAttributedStringには2017/03/07付のiOS 10.3現在でも致命的にバグっている箇所があります。

症状:

  • NSAttributedStringにNSParagraphStyleAttributeNameを利用してNSParagraphStyle経由でlineSpaceが付与されている。
  • NSAttributedStringに複数の「区間」が存在する。例えばNSAttributedString全体が単一のAttributeによって構成されているときはこの問題は発生しません。2つ以上の異なるAttributeの区間が必要です。
  • NSAttributedStringを描画するときに、横幅及び文字列の長さの都合で、1行で描画される。複数行になるときにはこの問題は発生しません。
  • NSAttributedStringにNSBackgroundColorAttributeNameが付与されていない。または、NSBackgroundColorAttributeNameとNSKerningAttributeNameの両方が付与されている。

上記の条件を全て満たすとき、
本来、1行の文字列はレンダリングしたときに下端にlineSpaceが付与されてはいけませんが、この条件が満たされていると下端にlineSpaceが付与されてしまいます。先頭の画像の左上のケースがこの問題に相当します。

具体的な問題としては、
  • 問題が発生するNSAttributedStringを使用したUILabel, UITextField, UITextViewの描画内容が思いっきり上にずれます(下に無駄なスペースが発生するので)
  • NSAttributedStringの地点で壊れているので、Core Textを利用したり、boundingRect(with size: CGSize, options: NSStringDrawingOptions = [], context: NSStringDrawingContext?) -> CGRectを使っても一切回避できません

解決策: 

この問題に対する正しいワークアラウンドの方法は一つしかありません。

NSAttributedStringの全Attribute区間にNSBackgroundColorAttributeNameを付与し、かつどの区間にもNSKerningAttributeNameを付与しない。

背景色が不要の場合はUIColor.clearでも付与してごまかしてください。
以上です。よろしくお願いします。

参考: