- 投稿日:2020-02-27T23:05:29+09:00
【iOS】遷移方法まとめ
iOS遷移方法まとめ
案件にて、主に Navigation Controller に関する懸念点を考慮し Controller の構成を考える必要があったため、今回は iOSにおける一般的な遷移方法をまとめてみました。
遷移方法・Controller の hierarchy を決定するために役立てられればいいなと思っています。
また、最後に独り言も記載しています。
ご指摘・ご感想・たわいもないコメントなどいただければ嬉しいです!
環境
- Xcode 11.2.1
- Swift 5.0
- iPhone 11 Pro Max (iOS13.2.2, シミュレータ)
遷移の種類
一般的な遷移の種類は大きく分けて以下の4つです。
- push遷移
- modal遷移: 番外編: modalとは?
- tab切り替え
- page遷移
遷移を実行するController
前項の各遷移を実行するのは以下のような Controller たちです。
- UIViewController: 以下すべての Controller の親
- UINavigationController
- UITabBarController
- UIPageViewController
すべての Controller がすべての遷移を行えるわけではないので、次に遷移実行の可否を表にまとめます
遷移実行の可否
遷移を実行するController push遷移 modal遷移 tab切り替え page遷移 UIViewController × ○ × × UINavigationController ○ ○ × × UITabBarController × ○ ○ × UIPageViewController × ○ × ○ 遷移実行の方法
遷移を実行するController push遷移 modal遷移 tab切り替え page遷移 UIViewController × vc.present(_ viewControllerToPresent:, animated:)
↑ UIViewControllerを継承した Controller (以下の3つ含む)で使用可能× × UINavigationController nc.pushViewController(_ viewController:, animated:)
or
nc.show(_ vc:, sender:)nc.present(_ viewControllerToPresent:, animated:)
or
nc.showDetail(_ vc:, sender:)× × UITabBarController × tc.present(_ viewControllerToPresent:, animated:) UITabBarController に UITabBar, UITabBarItem, 対象の View Controller を設定する。遷移時は UITabBarItem をタップ。 × UIPageViewController × pc.present(_ viewControllerToPresent:, animated:) × UIPageViewController に View Controller を設定する。遷移時はスワイプ。 segue
遷移の実装方法として、
segue
を使用する方法もあります。簡単な手順
- storyboard 上でView Controller or Storyboard Reference を配置
- 親のView Controllerと接続する: この接続が
segue
の本体segue
の identifier を設定- storyboard 上でなんらかの Action と結びつける
or
コードから UIViewController 内で performSegue(withIdentifier:, sender:)を呼ぶ。
(このメソッドはUIViewController
で定義されているので)* push遷移は、segue が定義された View Controller が親に UINavigationController を持っていないと実行できず、UINavigationController を持たない場合は storyboard で push遷移を設定していたとしても modal遷移する(iOS13で確認)。
ライブラリ
また、ライブラリを使用する手段もあります。
ここでは、標準の機能では実装が難しいような遷移を実装するためのライブラリを少しだけですが添付しておきます。
- XLPagerTabStrip:画面上部にタブバーを設定する
- FloatingPanel:モーダルビュー、セミモーダルビューを実装できる。iOS13より前バージョンでも。
独り言
最後に、独り言をつらつらと書きたいと思います。
少し長いので、読み物として読んでいただければと思います。
最近はHuman Interface Guidelines などを見ながら、各コンポーネントは何のために作られたのか、どのような特徴を持っているのかを学習するのが楽しいです。
それらを意識して実装するのが奥深くて面白いなと未熟ながら感じています。例えば、今回の「遷移」について言えば、モーダル遷移に関して色々と考えを巡らせることができました。
番外編: モーダルとは?
モーダル遷移のモーダルは英語で書くと mode の形容詞形である
modal
なので、モーダル遷移はつまり〇〇モードを実行するための遷移です。アクションシートなどのアラートも一種のモーダルな遷移・デザイン手法で、ユーザーの注意を引きそのときに必要な情報を提供したり、必要なアクションを起こさせたりすることができます。
モードに入っているときは他の作業は行えません。
遷移先の画面では 通常 Navigation Bar は存在せず、前の画面に
戻る
ためのボタンもなく、配置されるのは一般的にキャンセル
や閉じる
ボタンです。モーダル遷移後の処理は独立していて前画面とは切り離されている証拠です。
そう考えると、アクションシートや(セミ)モーダルビュー表示時にそれ以外の部分が薄暗くなりタップできなくなるようにした方が良いという考えにも納得できます。
このように考えを巡らせ論理的に理解することで、綺麗にそしてユーザーに優しく実装することができるのではないかと考えています。
色々と実装をデザインするのは難しい!と同時に面白い
この記事を書いた動機は、あるデザイン要件を満たすためにはどのような Controller を、どの階層に配置し、どのような手段を選べば、期待通りの遷移を実装できるかを考えやすくしようと思ったからです。
そこで、Human Interface Guidelines を読んだり、既存の有名なアプリをよく観察したりしました。
しかし、今の自分にとっては既存のアプリがどのように実装されているのかを判断することは少し難しく感じました。
(色々と考え、真似してみることは勉強になるとは思っています!)遷移に関して言えば、様々な Controller を共存すること、画面ごとに異なるデザインの Navigation Bar を構築することが難しかったです。
画面ごと、遷移ごとに以下のような問題について考える必要があり、さらには滑らかなアニメーションで遷移することが理想とされるからです。
- Tab Bar の表示/非表示
- Tab Bar のデザイン
- Navigation Bar の表示/非表示
- Navigation Bar のデザイン
- (セミ)モーダルビューを最前面に表示する
特に、Navigation Bar のデザインが統一されていない場合、push遷移時に Navigation Bar の背景色を滑らかに切り替えるのは難しいです。
それを実現するための1つの案は、Navigation Bar ごとフルスクリーンで遷移することです(伝わりますでしょうか...)。そのためには、UINavigationBarを使用せず自作 Navigation Bar なるものを作成する他ないと思われます。
もう1つの案は、遷移時のアニメーションを実装することです。そのためには、ぼかし具合などを計算し自力でコードを書く(or 何らかのライブラリを使用する)しかありません。
シンプル・イズ・ザ・ベストな考え方が好きなので、基本的に標準でできないこと推奨されないことは無理に実装したくありませんが、デザインがそうなっていれば仕方ありません。
各コンポーネントの用途を理解してスッキリ実装したものが、開発者にもユーザーにも分かりやすくて『良い』と私は思いますが、『良い』という言葉は主観量なのでその人、その企業にとっての『良い』デザインは異なるんだろうなぁと思ってます。。
なので、仕方ないですね。。
やっぱり自分にはまだまだ難しいです。。
が、絵的な意味のデザインを実装するための手段をデザインすることは奥深く面白いなぁと感じています!
今後も、Human Interface Guidelines やデザインの実装方法に関する記事を読むこと、既存アプリやUI構築方法の考察などは続けていきたいです。
最後に
今回は、ごくごく簡単に実装しながら遷移方法をまとめてみました。
遷移に限った話ではありませんが、様々な方法の中からベストな方法を抽出したり組み合わせたりできるよう精進します!
参考
- 投稿日:2020-02-27T23:05:29+09:00
【iOS】遷移方法まとめとエンジニア1年生の独り言
iOS遷移方法まとめ
案件にて、主に Navigation Controller に関する懸念点を考慮し Controller の構成を考える必要があったため、今回は iOSにおける一般的な遷移方法をまとめてみました。
遷移方法・Controller の hierarchy を決定するために役立てられればいいなと思っています。
また、最後にエンジニア1年生の独り言も記載しています。
ご指摘・ご感想・たわいもないコメントなどいただければ嬉しいです!
環境
- Xcode 11.2.1
- Swift 5.0
- iPhone 11 Pro Max (iOS13.2.2, シミュレータ)
遷移の種類
一般的な遷移の種類は大きく分けて以下の4つです。
- push遷移
- modal遷移: 番外編: モーダルとは?
- tab切り替え
- page遷移
遷移を実行するController
前項の各遷移を実行するのは以下のような Controller たちです。
- UIViewController: 以下すべての Controller の親
- UINavigationController
- UITabBarController
- UIPageViewController
すべての Controller がすべての遷移を行えるわけではないので、次に遷移実行の可否を表にまとめます
遷移実行の可否
遷移を実行するController push遷移 modal遷移 tab切り替え page遷移 UIViewController × ○ × × UINavigationController ○ ○ × × UITabBarController × ○ ○ × UIPageViewController × ○ × ○ 遷移実行の方法
遷移を実行するController push遷移 modal遷移 tab切り替え page遷移 UIViewController × vc.present(_ viewControllerToPresent:, animated:)
↑ UIViewControllerを継承した Controller (以下の3つ含む)で使用可能× × UINavigationController nc.pushViewController(_ viewController:, animated:)
or
nc.show(_ vc:, sender:)nc.present(_ viewControllerToPresent:, animated:)
or
nc.showDetail(_ vc:, sender:)× × UITabBarController × tc.present(_ viewControllerToPresent:, animated:) UITabBarController に UITabBar, UITabBarItem, 対象の View Controller を設定する。遷移時は UITabBarItem をタップ。 × UIPageViewController × pc.present(_ viewControllerToPresent:, animated:) × UIPageViewController に View Controller を設定する。遷移時はスワイプ。 segue
遷移の実装方法として、
segue
を使用する方法もあります。簡単な手順
- storyboard 上でView Controller or Storyboard Reference を配置
- 親のView Controllerと接続する: この接続が
segue
の本体segue
の identifier を設定- storyboard 上でなんらかの Action と結びつける
or
コードから UIViewController 内で performSegue(withIdentifier:, sender:)を呼ぶ。
(このメソッドはUIViewController
で定義されているので)* push遷移は、segue が定義された View Controller が親に UINavigationController を持っていないと実行できず、UINavigationController を持たない場合は storyboard で push遷移を設定していたとしても modal遷移する(iOS13で確認)。
ライブラリ
また、ライブラリを使用する手段もあります。
ここでは、標準の機能では実装が難しいような遷移を実装するためのライブラリを少しだけですが添付しておきます。
- XLPagerTabStrip:画面上部にタブバーを設定する
- FloatingPanel:モーダルビュー、セミモーダルビューを実装できる。iOS13より前バージョンでも。
エンジニア1年生の独り言
最後に、独り言をつらつらと書きたいと思います。
少し長いので、読み物として読んでいただければと思います。
最近はHuman Interface Guidelines などを見ながら、各コンポーネントは何のために作られたのか、どのような特徴を持っているのかを学習するのが楽しいです。
それらを意識して実装するのが奥深くて面白いなと未熟ながら感じています。例えば、今回の「遷移」について言えば、モーダル遷移に関して色々と考えを巡らせることができました。
番外編: モーダルとは?
モーダル遷移のモーダルは英語で書くと mode の形容詞形である
modal
なので、モーダル遷移はつまり〇〇モードを実行するための遷移です。アクションシートなどのアラートも一種のモーダルな遷移・デザイン手法で、ユーザーの注意を引きそのときに必要な情報を提供したり、必要なアクションを起こさせたりすることができます。
モードに入っているときは他の作業は行えません。
遷移先の画面では 通常 Navigation Bar は存在せず、前の画面に
戻る
ためのボタンもなく、配置されるのは一般的にキャンセル
や閉じる
ボタンです。モーダル遷移後の処理は独立していて前画面とは切り離されている証拠です。
そう考えると、アクションシートや(セミ)モーダルビュー表示時にそれ以外の部分を薄暗くしタップできなくなるようにした方が良いという考えにも納得できます。
このように考えを巡らせ論理的に理解することで、綺麗にそしてユーザーに優しく実装することができるのではないかと考えています。
色々と実装をデザインするのは難しい!と同時に面白い
この記事を書いた動機は、あるデザイン要件を満たすためにはどのような Controller を、どの階層に配置し、どのような手段を選べば、期待通りの遷移を実装できるかを考えやすくしようと思ったからです。
そこで、Human Interface Guidelines を読んだり、既存の有名なアプリをよく観察したりしました。
しかし、今の自分にとっては既存のアプリがどのように実装されているのかを判断することは少し難しく感じました。
(色々と考え、真似してみることは勉強になるとは思っています!)遷移に関して言えば、様々な Controller を共存すること、画面ごとに異なるデザインの Navigation Bar を構築することが難しかったです。
画面ごと、遷移ごとに以下のような問題について考える必要があり、さらには滑らかなアニメーションで遷移することが理想とされるからです。
- Tab Bar の表示/非表示
- Tab Bar のデザイン
- Navigation Bar の表示/非表示
- Navigation Bar のデザイン
- (セミ)モーダルビューを最前面に表示する
特に、Navigation Bar のデザインが統一されていない場合、push遷移時に Navigation Bar の背景色を滑らかに切り替えるのは難しいです。
それを実現するための1つの案は、Navigation Bar ごとフルスクリーンで遷移することです(伝わりますでしょうか...)。そのためには、UINavigationBarを使用せず自作 Navigation Bar なるものを作成する他ないと思われます。
もう1つの案は、遷移時のアニメーションを実装することです。そのためには、ぼかし具合などを計算し自力でコードを書く(or 何らかのライブラリを使用する)しかありません。
シンプル・イズ・ザ・ベストな考え方が好きなので、基本的に標準でできないこと推奨されないことは無理に実装したくありませんが、デザインがそうなっていれば仕方ありません。
各コンポーネントの用途を理解してスッキリ実装したものが、開発者にもユーザーにも分かりやすくて『良い』と私は思いますが、『良い』という言葉は主観量なのでその人、その企業にとっての『良い』デザインは異なるんだろうなぁと思ってます。。
なので、仕方ないですね。。
やっぱり自分にはまだまだ難しいです。。
が、絵的な意味のデザインを実装するための手段をデザインすることは奥深く面白いなぁと感じています!
今後も、Human Interface Guidelines やデザインの実装方法に関する記事を読むこと、既存アプリやUI構築方法の考察などは続けていきたいです。
最後に
今回は、ごくごく簡単に実装しながら遷移方法をまとめてみました。
遷移に限った話ではありませんが、様々な方法の中からベストな方法を抽出したり組み合わせたりできるよう精進します!
参考
- 投稿日:2020-02-27T18:09:49+09:00
iPhoneアプリで自己証明書のサーバーにリクエストを送りたい
はじめに
開発環境ではlocalhostに対してAPIリクエストを送って情報を得るケースがあります。
しかし、iPhoneのデフォルトではどのサーバーにもhttps
であればきちんとした証明書を求めます。
証明書関連をしっかりするのをチーム全員に求めるのも酷なので(たまにしか触らない人も出てくるし)、アプリ側で吸収する方法を調べました。環境
iOS 13.3
ライブラリを使う
Alamofire
というライブラリを使用するのが一番楽という結論になりました。
Security関連の設定を楽にできる仕組みが入っているためです。Alamofireインストール
https://qiita.com/ume1126/items/9ec378c02ca1b06287e9
上記記事を参照ください。
少し前の記事ですが、この記事を記載している時点で同じやり方で特に困りませんでした。Sessionの生成
Alamofire
はSessionを元に動きます。
公式ではAF.request
という形でリクエストを作っていますが、AF
の正体はSession.defaltut
です。
一般的なSessionを作り出しているので、ここを自分で作ったSessionに変えればOKです。
Sessionを作るコードは以下です。
ServerTrustManager
が肝で、これにevaluatorを変更したいhostとServerTrustEvaluting
を渡せば勝手に判定してやってくれます。
Evaluatorはいくつか種類がありますので、詳しくは公式を参照ください。https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#security
今回は証明書判定をスルーして欲しいので、
DisabledEvaluator
を使用します。
DisabledEvaluator
は完全無視なので使用の際には要注意なEvaluatorです。private let session: Session = { let manager = ServerTrustManager(evaluators: ["localhost": DisabledEvaluator()]) return Session(configuration: URLSessionConfiguration.af.default, serverTrustManager: manager) }()こちらはStack Overflowにあったコードを使わせてもらいました。特に変える必要はないのでそのまま使ってます。
https://stackoverflow.com/questions/55543462/how-to-use-alamofires-servertrustpolicy-disableevaluation-in-swift-5-alamofire-5リクエスト部分
リクエスト部分は上で作成したSessionを使ってリクエストすればOKです。
self.session.request("https://localhost/api/v1/login", method: .post).response { response in print(response) }Evaluatorのhostを変更してリクエストを送ってエラーが出て、
localhost
の際にはエラー出ずにリクエストが送れていますので、これで大丈夫そうです。
なお、証明書エラーになった際は以下のようなエラーが出ます。failure(Alamofire.AFError.serverTrustEvaluationFailed(reason: Alamofire.AFError.ServerTrustFailureReason.noRequiredEvaluator(host: "localhost"))) 2020-02-27 18:07:19.829669+0900 qasee-ios[92585:4214289] Task <630403A5-077F-481E-AD99-9CB312F60676>.<1> HTTP load failed, 0/0 bytes (error code: -999 [1:89])
- 投稿日:2020-02-27T16:12:40+09:00
[iOS][Swift]UIColorからUIImageを生成する(ダークモード対応版)
概要
参考 https://qiita.com/akatsuki174/items/c0b8b5126b6c12f62001
参考リンクに挙げた記事の通り
UIKit
のUIButton
のハイライト時の背景色を指定したい場合など、色から指定したいが仕様上UIImage
でしか設定することができないという状況では、UIColor
からUIImage
を生成する必要があります。
iOS12までは参考リンクの通りで良いのですが、iOS13ではダークモードが搭載されたため特定のケースで上手く動かなくなることがあります。
本記事ではダークモードに対応したUIColorからUIImageを生成する方法を紹介していきます。方法
先に方法だけ提示します。解説は以下の項に続きます。
extension UIImage { static func filledImage(byColor color: UIColor) -> UIImage { let createImage = { (rawColor: UIColor) -> UIImage in let rect = CGRect(x: 0, y: 0, width: 1, height: 1) UIGraphicsBeginImageContext(rect.size) let context = UIGraphicsGetCurrentContext()! context.setFillColor(rawColor.cgColor) context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return image } if #available(iOS 13.0, *) { //ダークモードはiOS13からなので分岐する必要がある let image = UIImage() let appearances: [UIUserInterfaceStyle] = [.light, .dark] appearances.forEach { let traitCollection = UITraitCollection(userInterfaceStyle: $0) image.imageAsset?.register(createImage(color.resolvedColor(with: traitCollection)), with: traitCollection) // ライトモードとダークモードの色を直接指定してImageを生成している } return image } else { return createImage(color) } } } extension UIColor { var image: UIImage { UIImage.filledImage(byColor: self) } }実装
まず従来の手順で実装してみましょう。
以下のような、普通のボタンと反転した見た目のボタンを表示する機能を実装します。
- 文字色がUIColor.systemBackground
- 通常時の背景色がUIColor.label
- ハイライト時の背景色がUIColor.secondaryLabel
従来の手順
XCodeで
Single View App
を選択しプロジェクトを作成したら、まずUIColor
からUIImage
を生成するExtensionを実装します。// UIImage+Color.swift import UIKit extension UIImage { static func filledImage(byColor color: UIColor) -> UIImage { let rect = CGRect(x: 0, y: 0, width: 1, height: 1) UIGraphicsBeginImageContext(rect.size) let context = UIGraphicsGetCurrentContext()! context.setFillColor(color.cgColor) context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return image } } extension UIColor { var image: UIImage { UIImage.filledImage(byColor: self) } }これを利用し、適当にStoryBoard上で
UIButton
を中央に置いたUIViewController
に対してボタン色を設定します。// ViewController.swift import UIKit class ViewController: UIViewController { @IBOutlet var button: UIButton! override func viewDidLoad() { super.viewDidLoad() button.setTitleColor(.systemBackground, for: .normal) button.setBackgroundImage(UIColor.label.image, for: .normal) button.setBackgroundImage(UIColor.secondaryLabel.image, for: .normal) } }これをライトモード、ダークモードでそれぞれアプリを起動すると以下のような表示になります。上手く動いているように見えますね。
Light Dark では、この
UIViewController
を表示したままiOSの設定を変更し、ライトモード/ダークモードを切り替えるとどうなるでしょうか?
Light -> Dark Dark -> Light 様子がおかしいですね。画面の背景色やボタンのテキストはモード変更に追従しているのにボタンの背景色だけが追従できていないようです。
なぜならUIButton
の背景色を設定した段階のUIImage
で固定されてしまうためです。
iOS12まで通用していた方法では、画面の表示後に表示モードを切り替えられると不具合が発生してしまうのです。対策の手がかり
UIImage.imageAsset
ライトモード/ダークモード変更に対処するための仕組みが
UIImage
には有り、以下のような形で利用することができます。
UIImage.imageAsset
を利用するとそれぞれのモードに設定された適切な画像を自動で選択し、表示に反映します。let image = UIImage() image.imageAsset?.register(UIImage(named: "light.png")!, with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.light)) image.imageAsset?.register(UIImage(named: "dark.png")!, with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.dark))UIColor.resolvedColor
UIColor
はモード別の色を内包したクラスですが、UIColor.resolvedColor
を利用すると特定のモードの色を直接取り出すことが可能です。let color = UIColor.label let lightColor = color.resolvedColor(with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.light)) let darkColor = color.resolvedColor(with: UITraitCollection(userInterfaceStyle: UIUserInterfaceStyle.dark))解決
前述の内容を踏まえて冒頭に紹介したとおりにExtensionを書き換えてみましょう。
// UIImage+Color.swift extension UIImage { static func filledImage(byColor color: UIColor) -> UIImage { let createImage = { (rawColor: UIColor) -> UIImage in let rect = CGRect(x: 0, y: 0, width: 1, height: 1) UIGraphicsBeginImageContext(rect.size) let context = UIGraphicsGetCurrentContext()! context.setFillColor(rawColor.cgColor) context.fill(rect) let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return image } if #available(iOS 13.0, *) { //ダークモードはiOS13からなので分岐する必要がある let image = UIImage() let appearances: [UIUserInterfaceStyle] = [.light, .dark] appearances.forEach { let traitCollection = UITraitCollection(userInterfaceStyle: $0) image.imageAsset?.register(createImage(color.resolvedColor(with: traitCollection)), with: traitCollection) // ライトモードとダークモードの色を直接指定してImageを生成している } return image } else { return createImage(color) } } } extension UIColor { var image: UIImage { UIImage.filledImage(byColor: self) } }この実装をビルドしてアプリを起動し、
UIViewController
を表示したままiOSの設定を変更し、ライトモード/ダークモードを切り替えてみましょう。
Light -> Dark Dark -> Light どうでしょう。ボタンの背景色がモード変更に追従できているようです。
今回紹介した方法を使えば、UIColor
から生成した単色のUIImage
もダークモード対応できるようになるわけです。よかったですね。
- 投稿日:2020-02-27T13:18:30+09:00
iOSアプリに必要なデータをCoreDataでbundleする
iOSアプリで読み取り専用データをbundleするための方法
データ作成
実際に使うものと同様のデータモデルで作成する。
https://qiita.com/kenmaz/items/818d61cd0ece8664c017
の設定をしておくと、SQLiteのファイルパスがログに出力されるので便利。サイズを小さくするためにVACUUMする。
sqlite> VACUUM;Bundleのやり方
普通にSQLiteのファイルをドラッグアンドドロップしてbundleする。
書き込みたい場合は Application Support とかにコピーしてから使う必要があるが、読み取り専用なのでその必要はない。
ただし、sqliteの一時ファイルである Hogehoge.sqlite-wal と Hogehoge.sqlite-shm も一緒にbundleしてあげる必要がある。
これが存在しないと、Unable to open って怒られる。
両方とも空ファイルでOK。
読み込み
private(set) lazy var container: NSPersistentContainer = { let container = NSPersistentContainer(name: "Bundle") let description = NSPersistentStoreDescription() description.url = Bundle.main.url(forResource: "Bundle", withExtension: "sqlite")! description.isReadOnly = true container.persistentStoreDescriptions = [description] container.loadPersistentStores(completionHandler: { (_, error) in if let error = error { fatalError("Unresolved error \(error)") } }) return container }()
- 投稿日:2020-02-27T01:10:17+09:00
UICollectionViewのドラッグで、元の位置に戻すときに一瞬ちらつく現象
ドラッグ時にちらつく
Xcode9時代にUICollectionViewのドラッグアンドドロップによる並び替え機能を実装していた画面にて、Xcode11.3でビルドしたところ、一見問題なく動いているものの、
- ドラッグ開始
- 元の位置に移動する
- 瞬時に元の位置に戻ったり、ドラッグ位置に戻ったりとちらつく
という現象に遭遇しました。
実はというと、Autolayoutで警告メッセージが表示されていたので、多分そっちを解決すべきなんですが、どうしてもAutolayoutが解決しなかった。で、iOS11以降から使用できるというUICollectionViewDragDelegate/DropDelegateがあるためこちらに書き換えたところ、現象が解消されました。
UICollectionViewDragDelegate
ドラッグ開始のデリゲート。
UIDragItemのitemProviderを通じてドラッグ後イベントにパラメータを渡したりできる。
よくある書き方はこんなかんじ。func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { let index = indexPath.row.description let itemProvider = NSItemProvider(object: index as NSString) let dragItem = UIDragItem(itemProvider: itemProvider) return [dragItem] }並び替え処理の場合、このデリゲート内で
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal{ return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) }と返してあげる。
(ホントわかりにくい)ドラッグ後のデリゲートはこちら。
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { // 遷移先のindexPathはこうして取得 let destinationIndexPath: IndexPath = coordinator.destinationIndexPath switch coordinator.proposal.operation { case .move: // UIDragItemは複数ある let items = coordinator.items // 先頭の1要素目から遷移元indexPathの取得はこういう感じで let firstIndexPath = items.first!.sourceIndexPath // performBatchUpdatesの中で、データの更新とCollectionViewのセルの増減操作をする。 collectionView.performBatchUpdates({ // データソースの更新 let n = datalist.remove(at: sourceIndexPath.item) datalist.insert(n, at: destinationIndexPath.item) //セルの移動 collectionView.deleteItems(at: [sourceIndexPath]) collectionView.insertItems(at: [destinationIndexPath]) }) // dropを呼ぶと、指定したindexPathの位置にCellがスッと入る動きをしてくれる coordinator.drop(item.dragItem, toItemAt: destinationIndexPath) default: return } } }という感じ。
あとはUICollectionViewのDrag/DropDelegateを設定すれば実装できる。
ドラッグ中のスタイルの設定はまた別でデリゲートがあります。※このコードは動作確認してません。