- 投稿日: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遷移: 番外編: 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-27T19:12:18+09:00
【Xcode】ひらがな化APIを使ったアプリを作りました【Swift】
はじめに
はい、はるうさぎです。今回はSwiftとXcodeを使ってgooのひらがな化APIを使用してアプリを作りました。簡単な備忘録というか、自己満な記事です。
学生の頃にSwift3を触って以来久しぶりに書きました。Xcodeも変わっているところが何点もあって慣れるのに苦労しました。何もわからない初心者がアプリを開発をするという全くタメにならない記事になってますので、興味本位でみていただけると幸いです。
参考サイト
調べてみると数名の方が記事などを書いていました。
https://qiita.com/sventouz/items/318d8370ab489724c454
以上のサイトを参考とさせていただきました。
gooのひらがな化APIについて
こちらにAPIについて概要が書かれています。
その名の通り、漢字の含んだ文をひらがなにするという簡単なものです。
まず見た目だ?
アプリ起動時の画面(LaunchScreen.storyboard)
肝心のアプリ画面(Main.storyboard)
いらすとや三昧ですねw
操作は後から書きますので抜粋します。ざっとコードを書きます
ViewController.swiftimport UIKit class ViewController: UIViewController ,UITextFieldDelegate{ @IBOutlet weak var convertText: UITextField! @IBOutlet weak var convertedText: UILabel! @IBOutlet weak var errorText: UILabel! let api = API() override func viewDidLoad() { super.viewDidLoad() } @IBAction func convertButton(_ sender: Any) { guard let convertTextForApi = convertText.text else { return } //api通信 self.api.convertHiragana(convertTextForApi: convertTextForApi) { (convertedStr) in guard let _convertedStr = convertedStr else { //コンバート失敗 //アラートを出す return } DispatchQueue.main.async { self.convertedText.text = _convertedStr } } } }Api.swiftimport Foundation class API { let host = "https://labs.goo.ne.jp/api" let appID = "自分で取得したAPIのアレ" let requestID = "record003" let postMethod = "POST" func convertHiragana(convertTextForApi: String, completion:((String?) -> Void)?) { let url = "https://labs.goo.ne.jp/api/hiragana" let outputType = "hiragana" let postData = PostData(app_id: self.appID, request_id: requestID, sentence: convertTextForApi, output_type: outputType) self.request(method: "POST", url: url, postData: postData, completion: completion) } func request(method: String, url: String, postData:PostData, completion:((String?) -> Void)?) { guard let _url = URL(string: url) else { return } // URLRequstの設定 var request = URLRequest(url: _url) request.httpMethod = method request.addValue("application/json", forHTTPHeaderField: "Content-Type") //POSTするデータをURLRequestに持たせる guard let uploadData = try? JSONEncoder().encode(postData) else { debugPrint("json生成に失敗しました") return } request.httpBody = uploadData //APIへPOSTしてresponseを受け取る let task = URLSession.shared.uploadTask(with: request, from: uploadData) { data, response, error in if let error = error { debugPrint ("error: \(error)") completion?(nil) return } guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else { debugPrint ("server error") completion?(nil) return } guard response.statusCode == 200 else { debugPrint("サーバエラー ステータスコード: \(response.statusCode)\n") completion?(nil) return } guard let data = data, let jsonData = try? JSONDecoder().decode(Rubi.self, from: data) else { debugPrint("json変換に失敗しました") completion?(nil) return } debugPrint(jsonData.converted) completion?(jsonData.converted) } task.resume() } } struct Rubi:Codable { var request_id: String var output_type: String var converted: String } struct PostData: Codable { var app_id:String var request_id: String var sentence: String var output_type: String }操作
1.オレンジ色の吹き出しはtextfieldなので、ここに漢字を含んだ文を入力します。
2.女の子がボタンになっています。タップします。
3.青色の吹き出しにひらがな化された文が表示されます。以上、簡単だね!イエイ!
反省点
- textfieldとbuttonがわかりにくい
- textfieldをタップしても文字が消えない(自分で消さないとだめ)
- そもそものデザインが悪い
- MVC,MMVCになっていない
- オブジェクト指向わからん
- Xcodeの操作わからん
- Swiftわからん
- autolayout何それ美味しいの?
改善したい点
- 反省点を改善したい
- オブジェクト指向とXcodeとSwift完全に理解した。になりたい
- autolayoutの理解を深める
まとめ
書いてないですが、SwiftUIで苦戦しました。
わからんことだらけで、とりあえずアプリなんて作るもんじゃないです()
でもわからないなりにフォロワーさんに教えていただいたり、調べたりで、楽しい時間でした。
少しは自走力もついたのでは????と思っています。
今度はもっとUIがしっかりした何かを作りたいと思います。「MVC,MMVCこう書くといいよ!!!!」とか「こうした方がいいよ」という点がありましたら、優しく教えていただけると幸いです。
最後まで読んでいただきありがとうございました。
- 投稿日: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-27T15:19:06+09:00
[Swift] pre-commit でコミット時に SwiftFormat を適用する
Tl;Dr
SwiftFormatは pre-commit に対応しているので
.pre-commit-config.yaml
を作成し、.pre-commit-config.yamlrepos: - repo: https://github.com/nicklockwood/SwiftFormat rev: 0.44.2 hooks: - id: swiftformat
pre-commit install
すればコミット時に適用してくれるようになる。
なぜ?
通常の iOSアプリプロジェクトであれば SwiftFormat は Xcode における Build Phases に組み込んで適用するのがセオリーといえる。
しかし、Swift Package Manager の場合、Xcodeプロジェクトは
Package.swift
およびディレクトリツリーから自動生成するため、Xcodeの設定を変更するやり方はあまりベターではない(Xcode 11.4 からLSP が同梱されるようになったことから、そもそも Xcode の利用はオプショナルになりつつもある)。そのため、Git の pre-commit でフックするやり方も考えられる。
pre-commit
pre-commit は Python製のツールで、Git の pre-commit にフックするスクリプトを管理するためのエコシステムのようなものらしい。Swift Package Manager が Swift 製のライブラリを管理するエコシステムであれば、それの pre-commit hook 版、といったところだろうか。
以下の記事がわかりやすかった。
Python製のツールpre-commitでGitのpre-commit hookを楽々管理!!導入
pre-commit 自体は Homebrew でインストールできるので
brew install pre-commit
するだけでよい。あとは冒頭に書いたように
.pre-commit-config.yaml
を作成し、pre-commit install
すれば導入は完了である。私のリポジトリでは
Makefile
とBrewfile
を用意し、make setup
の1コマンドで導入できるようにしている。Makefilesetup: brew bundle pre-commit installBrewfilebrew "pre-commit"使い方
コミット時に SwiftFormat により変更が入った場合、コミットは失敗して変更自体はアンステージされた状態になる。
冒頭の画像の再掲となるが、
Failed
と右上に表示されているのはそういう意味である。
そのため、適用された変更は
git add
でステージした上で再度コミットする必要がある。補足
SwiftFormat の README には通常のpre-commit hook を利用した手順も記載されている。
pre-commit#!/bin/bash git diff —diff-filter=d —staged —name-only | grep -e ‘\(.*\).swift$’ | while read line; do swiftformat “${line}”; git add "$line"; doneこのスクリプトは
swiftformat
を実行後に自動的にgit add
する作りになっているため、 pre-commit のようにコミットに失敗したらステージして再度コミットする、といった手間がない。一方、ファイル全体を
git add
されてしまう為、部分的にステージ(git add -p
)・コミットしたい場合などに、意図しなかった変更までコミットしてしまうリスクもある。このあたりはトレードオフではあるが、個人的には pre-commit の方が安全であると感じる。
終わりに
Swift Package Manager のプロジェクトでも、SwiftFormat で一貫したコードスタイルを。
- 投稿日: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を設定すれば実装できる。
ドラッグ中のスタイルの設定はまた別でデリゲートがあります。※このコードは動作確認してません。
- 投稿日:2020-02-27T01:03:24+09:00