20200227のiOSに関する記事は6件です。

【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遷移 tab切り替え page遷移
遷移時のアクション 指定なし 指定なし タブアイテムをタップ 左右スワイプ
遷移先の一般的なデザイン UINavigationBar左部分に「< 戻る」
or
「< (遷移元のtitle)」
画面左上に「キャンセル」
or
「閉じる」ボタンを配置
選択中のタブアイテムがTint Colorで色付けされる -
戻る方法 「< 戻る」ボタンタップ
or
右スワイプ
画面を破棄する処理を実行
or
下スワイプ(iOS13~)
- -
一般的なアニメーション 右から画面が出現 下から画面が出現 瞬時に切り替わる 左右から画面が出現
GIF push.gif modal.gif tabbar.gif page.gif

遷移を実行する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を使用する方法もあります。

簡単な手順

  1. storyboard 上でView Controller or Storyboard Reference を配置
  2. 親のView Controllerと接続する: この接続がsegueの本体
  3. segue の identifier を設定
  4. 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構築方法の考察などは続けていきたいです。

最後に

今回は、ごくごく簡単に実装しながら遷移方法をまとめてみました。

遷移に限った話ではありませんが、様々な方法の中からベストな方法を抽出したり組み合わせたりできるよう精進します!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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遷移
遷移時のアクション 指定なし 指定なし タブアイテムをタップ 左右スワイプ
遷移先の一般的なデザイン UINavigationBar左部分に「< 戻る」
or
「< (遷移元のtitle)」
画面左上に「キャンセル」
or
「閉じる」ボタンを配置
選択中のタブアイテムがTint Colorで色付けされる -
戻る方法 「< 戻る」ボタンタップ
or
右スワイプ
画面を破棄する処理を実行
or
下スワイプ(iOS13~)
- -
一般的なアニメーション 右から画面が出現 下から画面が出現 瞬時に切り替わる 左右から画面が出現
GIF push.gif modal.gif tabbar.gif page.gif

遷移を実行する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を使用する方法もあります。

簡単な手順

  1. storyboard 上でView Controller or Storyboard Reference を配置
  2. 親のView Controllerと接続する: この接続がsegueの本体
  3. segue の identifier を設定
  4. 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構築方法の考察などは続けていきたいです。

最後に

今回は、ごくごく簡単に実装しながら遷移方法をまとめてみました。

遷移に限った話ではありませんが、様々な方法の中からベストな方法を抽出したり組み合わせたりできるよう精進します!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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])
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS][Swift]UIColorからUIImageを生成する(ダークモード対応版)

概要

参考 https://qiita.com/akatsuki174/items/c0b8b5126b6c12f62001

参考リンクに挙げた記事の通りUIKitUIButtonのハイライト時の背景色を指定したい場合など、色から指定したいが仕様上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もダークモード対応できるようになるわけです。よかったですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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。
image.png

読み込み

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
}()
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を設定すれば実装できる。
ドラッグ中のスタイルの設定はまた別でデリゲートがあります。

※このコードは動作確認してません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む