20200227のSwiftに関する記事は8件です。

【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で続きを読む

【Xcode】ひらがな化APIを使ったアプリを作りました【Swift】

はじめに

はい、はるうさぎです。今回はSwiftとXcodeを使ってgooのひらがな化APIを使用してアプリを作りました。簡単な備忘録というか、自己満な記事です。
学生の頃にSwift3を触って以来久しぶりに書きました。Xcodeも変わっているところが何点もあって慣れるのに苦労しました。

何もわからない初心者がアプリを開発をするという全くタメにならない記事になってますので、興味本位でみていただけると幸いです。

参考サイト

調べてみると数名の方が記事などを書いていました。

http://harumi.sakura.ne.jp/wordpress/2019/06/29/%E3%81%B2%E3%82%89%E3%81%8C%E3%81%AA%E5%8C%96api%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B/

https://qiita.com/sventouz/items/318d8370ab489724c454

以上のサイトを参考とさせていただきました。

gooのひらがな化APIについて

こちらにAPIについて概要が書かれています。

その名の通り、漢字の含んだ文をひらがなにするという簡単なものです。

まず見た目だ👀

アプリ起動時の画面(LaunchScreen.storyboard)

スクリーンショット 2020-02-27 18.44.20.png
自分のロゴが最初に出るようにしました(自己主張が激しい)

肝心のアプリ画面(Main.storyboard)

スクリーンショット 2020-02-27 18.45.31.png
いらすとや三昧ですねw
操作は後から書きますので抜粋します。

ざっとコードを書きます

ViewController.swift
import 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.swift
import 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
}

操作

ezgif.com-video-to-gif.gif

1.オレンジ色の吹き出しはtextfieldなので、ここに漢字を含んだ文を入力します。
2.女の子がボタンになっています。タップします。
3.青色の吹き出しにひらがな化された文が表示されます。

以上、簡単だね!イエイ!

反省点

  • textfieldとbuttonがわかりにくい
  • textfieldをタップしても文字が消えない(自分で消さないとだめ)
  • そもそものデザインが悪い
  • MVC,MMVCになっていない
  • オブジェクト指向わからん
  • Xcodeの操作わからん
  • Swiftわからん
  • autolayout何それ美味しいの?

改善したい点

  • 反省点を改善したい
  • オブジェクト指向とXcodeとSwift完全に理解した。になりたい
  • autolayoutの理解を深める

まとめ

書いてないですが、SwiftUIで苦戦しました。
わからんことだらけで、とりあえずアプリなんて作るもんじゃないです()
でもわからないなりにフォロワーさんに教えていただいたり、調べたりで、楽しい時間でした。
少しは自走力もついたのでは????と思っています。
今度はもっとUIがしっかりした何かを作りたいと思います。

「MVC,MMVCこう書くといいよ!!!!」とか「こうした方がいいよ」という点がありましたら、優しく教えていただけると幸いです。

最後まで読んでいただきありがとうございました。

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

[Swift] pre-commit でコミット時に SwiftFormat を適用する

Tl;Dr

SwiftFormatpre-commit に対応しているので .pre-commit-config.yaml を作成し、

.pre-commit-config.yaml
repos:
-   repo: https://github.com/nicklockwood/SwiftFormat
    rev: 0.44.2
    hooks:
    -   id: swiftformat

pre-commit installすればコミット時に適用してくれるようになる。
CDF672A6-9B7B-43A7-A75B-7B6857080291.png

なぜ?

通常の 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すれば導入は完了である。

私のリポジトリではMakefileBrewfileを用意し、make setupの1コマンドで導入できるようにしている。

Makefile
setup:
  brew bundle
  pre-commit install
Brewfile
brew "pre-commit"

使い方

コミット時に SwiftFormat により変更が入った場合、コミットは失敗して変更自体はアンステージされた状態になる。

冒頭の画像の再掲となるが、 Failed と右上に表示されているのはそういう意味である。
CDF672A6-9B7B-43A7-A75B-7B6857080291.png

そのため、適用された変更は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 で一貫したコードスタイルを。

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

横スクロールカレンダーサンプル

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