【Swift 5】グレイスケール反転

こんにちは、@Zhalen（ツァーレン）です。

今日、この「Zhalen」が誤字で、実は「Zahlen」が正しい表記だということを知りました。なので今日から「Zhalen」は私の造語です。


#この記事はどこからきたのか、この記事は何か、この記事はどこへ向かうのか
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540507/c2f5aba3-f105-04fd-3408-dd66bc0db2bf.gif)

今回の記事では、私が今ちょうどAppleStoreに申請予定（今この記事を書いている途中完成した）のアプリ、「Vote.」に実施したものの一部です。このように白黒つまりグレイスケールを反転させるエクステンションを作ったので紹介します。

ちなみに、GIFの中でそれぞれ切り替えられているボタンも自作のカスタムクラスでその名を```RadioButton```と言います。ラジオボタンと検索すれば出てくるアレです。これも、今度また時間があるときに紹介します。


#Extension: まずは結果から

まずは最終的な結果から示し、それを解体してゆく要領で説明してゆきます。最後にコピペ用のコード全文を掲載します。

##0: 使い方（結果）
このように実行すれば、その```UIViewController```内の全てのグレイスケールを反転します。

```swift:UIViewController-viewDidLoad

self.toggleGrayscale(animated: true)
```

##1: StatusBar, Navigationbar, viewで分割

UIViewControllerの決まり上、

```
-StatusBar
-NavigationBar
-view
    -view1
    -view2
    -view3
    .
    .
    .
```


のように、```StatusBar```、```NavigationBar```、```view```の三者それぞれは従属関係にあらず、それぞれが独立していますので、まずはそのそれぞれに対してメソッドを実行します。これがそれで、```0: 使い方（結果）```のメソッドです。

```swift

extension UIViewController {
    func toggleGrayscale(animated: Bool) {
        //StatusBar
        self.toggleStatusBarAppearance(animated: animated)
        //navigationBar
        if let navigationBar: UINavigationBar = self.navigationController?.navigationBar {
            navigationBar.toggleGrayscale(animated: animated)
        }
        //view
        self.view.toggleGrayscale(animated: animated)
    }
}
```

このそれぞれのメソッドに対する中身はこうなっています。

```swift:StatusBar

extension UIViewController {
    
    func toggleStatusBarAppearance(animated: Bool) {
        UIView.animate(
            withDuration: animated ? 0.5 : 0.0,
            delay: 0.0,
            options: .curveEaseOut) {
            self.setNeedsStatusBarAppearanceUpdate()
            self.setStatusBarBackgroundColor(self.preferredStatusBarStyle == .darkContent ? /*ダークモードの時の色*/ : /*ライトモードの時の色*/)
        }
    }

    private final class StatusBarView: UIView { }
    func setStatusBarBackgroundColor(_ color: UIColor?) {
        for subView in self.view.subviews where subView is StatusBarView {
            subView.removeFromSuperview()
        }
        guard let color = color else {
            return
        }
        let statusBarView = StatusBarView()
        statusBarView.backgroundColor = color
        self.view.addSubview(statusBarView)
        statusBarView.translatesAutoresizingMaskIntoConstraints = false
        statusBarView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        statusBarView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        statusBarView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        statusBarView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
    }
}

```

####StatusBar

```setStatusBarBackgroundColor``` は、https://qiita.com/uhooi/items/a4fd1e115196d6b48949 を参考にさせていただきました。

```toggleStatusBarAppearance``` 内で、```animated```による分岐を掛けその中にステータスバーの色をアップデートするメソッドを記述します。```/*ダークモードの時の色*/``` , ```/*ライトモードの時の色*/```にはそれぞれ、ご自身の目的の色を代入してください。

また、対象の```UIViewController```内には

```swift:UIViewController

override var preferredStatusBarStyle: UIStatusBarStyle {
    return /*ダークモードにするための条件*/ ? .darkContent : .lightContent
}
```

を追加してください。```/*ダークモードにするための条件*/```とは、上記のGIFでいう左側のラジオボタンが付いている状態で、実際に私のプロジェクトでは、

```swift:俺の例

override var preferredStatusBarStyle: UIStatusBarStyle {
    return modeRadioButtons[0].isRadied ? .darkContent : .lightContent
    //左のmodeRadioButtonが付いているかどうか(Radied(造語です))で判定しています。
}
```

のように、```modeRadioButtons[0].isRadied``` がダークモードにするための条件となります。

####NavigtionBar, view

```swift:さっきのやつ

//navigationBar
if let navigationBar: UINavigationBar = self.navigationController?.navigationBar {
    navigationBar.toggleGrayscale(animated: animated)
}
//view
self.view.toggleGrayscale(animated: animated)
```

上記のこのコードを見てわかるように、```navigationBar```と```view```は同じ```toggleGrayscale(animated: animated)``` というメソッドが割り当てられます。これは両方とも```UIView```の配下に置かれていることに起因します。このメソッドは、対象の```UIView```に乗せられたsubview達と自分自身のグレースケールを変換するメソッドです。

そしてこれがその中身です。（長いですがまだコピペ用コード全文ではありません。）

```swift

//MARK:- ToggleGrayscale
extension UIView {
    
    func toggleGrayscale(animated: Bool) {
        UIView.animate(
            withDuration: animated ? 0.5 : 0.0,
            delay: 0.0,
            options: .curveEaseOut) {
            self.toggleForEachObject()
        }
        if type(of: self) == UIStackView.self {
            guard !(self as! UIStackView).arrangedSubviews.isEmpty else { return }
            (self as! UIStackView).arrangedSubviews.forEach { (arrangedSubviews) in arrangedSubviews.toggleGrayscale(animated: animated) }
        }
        guard !self.subviews.isEmpty else { return }
        self.subviews.forEach { (subview) in subview.toggleGrayscale(animated: animated) }
    }
    private func toggleForEachObject() {
        if let backgroundColor: UIColor = self.backgroundColor {
            self.backgroundColor = backgroundColor.toggled()
        }
        if let tintColor: UIColor = self.tintColor {
            self.tintColor = tintColor.toggled()
        }
        if let borderColor: CGColor = self.layer.borderColor {
            self.layer.borderColor = borderColor.toggled()
        }
        if let label: UILabel = self as? UILabel, let textColor: UIColor = label.textColor {
            label.textColor = textColor.toggled()
        } else if let button: UIButton = self as? UIButton, let titleColor: UIColor = button.titleColor(for: .normal) {
            button.setTitleColor(titleColor.toggled(), for: .normal)
        } else if let navigationBar: UINavigationBar = self as? UINavigationBar {
            if let barTintColor: UIColor = navigationBar.barTintColor {
                navigationBar.barTintColor = barTintColor.toggled()
            }
            if let backgroundColor: UIColor = navigationBar.backgroundColor {
                navigationBar.backgroundColor = backgroundColor.toggled()
            }
            if let tintColor: UIColor = navigationBar.tintColor {
                navigationBar.tintColor = tintColor.toggled()
            }
            if let titleTextAttributes = navigationBar.titleTextAttributes {
                navigationBar.titleTextAttributes = [.foregroundColor: (titleTextAttributes[.foregroundColor] as! UIColor).toggled()]
            }
        } else if let switchControll: UISwitch = self as? UISwitch, let onTintColor: UIColor = switchControll.onTintColor {
            switchControll.onTintColor = onTintColor.toggled()
        } else if let radioButton: RadioButton = self as? RadioButton {
            radioButton.radioColor = radioButton.radioColor.toggled()
            radioButton.borderColor = radioButton.borderColor.toggled()
        }
    }
}
extension UIColor {
    func toggled() -> UIColor {
        let grayScale: (white: CGFloat, alpha: CGFloat) = self.grayScale()
        return UIColor.init(white: 1-grayScale.white, alpha: grayScale.alpha)
    }
    func grayScale() -> (white: CGFloat, alpha: CGFloat) {
        var components: (white: CGFloat, alpha: CGFloat) = (white: CGFloat(), alpha: CGFloat())
        self.getWhite(&components.white, alpha: &components.alpha)
        return (white: components.white, alpha: components.alpha)
    }
}
extension CGColor {
    func toggled() -> CGColor {
        let grayScale: (white: CGFloat, alpha: CGFloat) = self.grayScale()
        return UIColor.init(white: 1-grayScale.white, alpha: grayScale.alpha).cgColor
    }
    func grayScale() -> (white: CGFloat, alpha: CGFloat) {
        var components: (white: CGFloat, alpha: CGFloat) = (white: CGFloat(), alpha: CGFloat())
        UIColor(cgColor: self).getWhite(&components.white, alpha: &components.alpha)
        return (white: components.white, alpha: components.alpha)
    }
}
```


この中の一番上の

```swift

func toggleGrayscale(animated: Bool) {
    UIView.animate(
        withDuration: animated ? 0.5 : 0.0,
        delay: 0.0,
        options: .curveEaseOut) {
        self.toggleForEachObject()
    }
    if type(of: self) == UIStackView.self {
        guard !(self as! UIStackView).arrangedSubviews.isEmpty else { return }
        (self as! UIStackView).arrangedSubviews.forEach { (arrangedSubviews) in arrangedSubviews.toggleGrayscale(animated: animated) }
    }
    guard !self.subviews.isEmpty else { return }
    self.subviews.forEach { (subview) in subview.toggleGrayscale(animated: animated) }
}
```

は、私の他の記事であるこれ
https://qiita.com/Zhalen/items/a887991c524d4e4bff61 
のように、自分自身のsubview全てに対して再起的に同じメソッドを割り当てることで```subviewのsubviewのsubviewの...```といっためんどくさい処理を簡潔に実現します。

![スクリーンショット 2020-12-16 8.13.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540507/8a6eee51-6937-a152-f680-524434ea0afd.png)

イメージとしては、この白丸をビューとして枝が従属関係（点線はその省略）を表すとき、枝が右に存在すればそのさきのビューにメソッドを移行させ、その時に自分自身のグレースケールを反転(```self.toggleForEachObject()```)させ、右に枝が存在しなければリターンするという仕組みです。

中身をご覧の通り対象が```UIStackView```であるときは```arrangedSubview```についても同様のことをします。


ではこの```self.toggleForEachObject()```はなんなのかというと、これがグレイスケールを反転する上で主要なもので、文字通りそれぞれのクラスについて記述しなければならなかったため、かなり泥臭いです。これは最初に掲載したGIFの画面においてのみ動作確認をしているので、もちろんもっと他に追加で書かなければならないと気づいたのならばそれを奨励します。

そして一番下の

```swift

extension UIColor {
    func toggled() -> UIColor {
        let grayScale: (white: CGFloat, alpha: CGFloat) = self.grayScale()
        return UIColor.init(white: 1-grayScale.white, alpha: grayScale.alpha)
    }
    func grayScale() -> (white: CGFloat, alpha: CGFloat) {
        var components: (white: CGFloat, alpha: CGFloat) = (white: CGFloat(), alpha: CGFloat())
        self.getWhite(&components.white, alpha: &components.alpha)
        return (white: components.white, alpha: components.alpha)
    }
}
extension CGColor {
    func toggled() -> CGColor {
        let grayScale: (white: CGFloat, alpha: CGFloat) = self.grayScale()
        return UIColor.init(white: 1-grayScale.white, alpha: grayScale.alpha).cgColor
    }
    func grayScale() -> (white: CGFloat, alpha: CGFloat) {
        var components: (white: CGFloat, alpha: CGFloat) = (white: CGFloat(), alpha: CGFloat())
        UIColor(cgColor: self).getWhite(&components.white, alpha: &components.alpha)
        return (white: components.white, alpha: components.alpha)
    }
}
```

これで、実際に白黒を反転させます。本当は```view.backgroundColor.toggle()```のようにミュータブル(mutable。mutating func とかのミュートがエイブルでミュータブル)にやりたかったのですが、普通にできなかったのと、そもそも上記にもある```<UIButton>.setTitleColor(_:, for:)```や、```<UITextView>.attributedString = [.foregroundColor : <UIColor>]```のようなものを含めながらにして柔軟に対応することが困難だったため、反転したものを代入するという形になりました。


そして、これら上記全てが最初の```0: 使い方（結果）```

```swift:UIViewController-viewDidLoad

self.toggleGrayscale(animated: true)
```

という１行に帰着されるわけです。

##2: コピペ用コード全文（各自手直しあり）

```swift

import UIKit

class /*手直しポイント1: クラス名*/: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        //グレイスケール反転
        self.toggleGrayscale()
    }
    override var preferredStatusBarStyle: UIStatusBarStyle {
        return /*手直しポイント2（ラスト）: ダークモードにするための条件*/ ? .darkContent : .lightContent
    }
}

//エクステンション集
extension UIViewController {
    func toggleGrayscale(animated: Bool) {
        //StatusBar
        self.toggleStatusBarAppearance(animated: animated)
        //navigationBar
        if let navigationBar: UINavigationBar = self.navigationController?.navigationBar {
            navigationBar.toggleGrayscale(animated: animated)
        }
        //view
        self.view.toggleGrayscale(animated: animated)
    }
    func toggleStatusBarAppearance(animated: Bool) {
        UIView.animate(
            withDuration: animated ? 0.5 : 0.0,
            delay: 0.0,
            options: .curveEaseOut) {
            self.setNeedsStatusBarAppearanceUpdate()
            self.setStatusBarBackgroundColor(self.preferredStatusBarStyle == .darkContent ? /*ダークモードの時の色*/ : /*ライトモードの時の色*/)
        }
    }

    private final class StatusBarView: UIView { }
    func setStatusBarBackgroundColor(_ color: UIColor?) {
        for subView in self.view.subviews where subView is StatusBarView {
            subView.removeFromSuperview()
        }
        guard let color = color else {
            return
        }
        let statusBarView = StatusBarView()
        statusBarView.backgroundColor = color
        self.view.addSubview(statusBarView)
        statusBarView.translatesAutoresizingMaskIntoConstraints = false
        statusBarView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        statusBarView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true
        statusBarView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true
        statusBarView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
    }
}
extension UIView {

    func toggleGrayscale(animated: Bool) {
        UIView.animate(
            withDuration: animated ? 0.5 : 0.0,
            delay: 0.0,
            options: .curveEaseOut) {
            self.toggleForEachObject()
        }
        if type(of: self) == UIStackView.self {
            guard !(self as! UIStackView).arrangedSubviews.isEmpty else { return }
            (self as! UIStackView).arrangedSubviews.forEach { (arrangedSubviews) in arrangedSubviews.toggleGrayscale(animated: animated) }
        }
        guard !self.subviews.isEmpty else { return }
        self.subviews.forEach { (subview) in subview.toggleGrayscale(animated: animated) }
    }
    private func toggleForEachObject() {
        if let backgroundColor: UIColor = self.backgroundColor {
            self.backgroundColor = backgroundColor.toggled()
        }
        if let tintColor: UIColor = self.tintColor {
            self.tintColor = tintColor.toggled()
        }
        if let borderColor: CGColor = self.layer.borderColor {
            self.layer.borderColor = borderColor.toggled()
        }
        if let label: UILabel = self as? UILabel, let textColor: UIColor = label.textColor {
            label.textColor = textColor.toggled()
        } else if let button: UIButton = self as? UIButton, let titleColor: UIColor = button.titleColor(for: .normal) {
            button.setTitleColor(titleColor.toggled(), for: .normal)
        } else if let navigationBar: UINavigationBar = self as? UINavigationBar {
            if let barTintColor: UIColor = navigationBar.barTintColor {
                navigationBar.barTintColor = barTintColor.toggled()
            }
            if let backgroundColor: UIColor = navigationBar.backgroundColor {
                navigationBar.backgroundColor = backgroundColor.toggled()
            }
            if let tintColor: UIColor = navigationBar.tintColor {
                navigationBar.tintColor = tintColor.toggled()
            }
            if let titleTextAttributes = navigationBar.titleTextAttributes {
                navigationBar.titleTextAttributes = [.foregroundColor: (titleTextAttributes[.foregroundColor] as! UIColor).toggled()]
            }
        } else if let switchControll: UISwitch = self as? UISwitch, let onTintColor: UIColor = switchControll.onTintColor {
            switchControll.onTintColor = onTintColor.toggled()
        } else if let radioButton: RadioButton = self as? RadioButton {
            radioButton.radioColor = radioButton.radioColor.toggled()
            radioButton.borderColor = radioButton.borderColor.toggled()
        }
    }
}
extension UIColor {
    func toggled() -> UIColor {
        let grayScale: (white: CGFloat, alpha: CGFloat) = self.grayScale()
        return UIColor.init(white: 1-grayScale.white, alpha: grayScale.alpha)
    }
    func grayScale() -> (white: CGFloat, alpha: CGFloat) {
        var components: (white: CGFloat, alpha: CGFloat) = (white: CGFloat(), alpha: CGFloat())
        self.getWhite(&components.white, alpha: &components.alpha)
        return (white: components.white, alpha: components.alpha)
    }
}
extension CGColor {
    func toggled() -> CGColor {
        let grayScale: (white: CGFloat, alpha: CGFloat) = self.grayScale()
        return UIColor.init(white: 1-grayScale.white, alpha: grayScale.alpha).cgColor
    }
    func grayScale() -> (white: CGFloat, alpha: CGFloat) {
        var components: (white: CGFloat, alpha: CGFloat) = (white: CGFloat(), alpha: CGFloat())
        UIColor(cgColor: self).getWhite(&components.white, alpha: &components.alpha)
        return (white: components.white, alpha: components.alpha)
    }
}
```


#宣伝です
![スクリーンショット 2020-12-30 17.42.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540507/32438993-4e40-e9ae-474f-718d6732c5fc.png)


つい先日、アンケート特化型SNS「<b>Vote.</b>」をリリースしました！

このアプリは、何かの選択で迷った時・悩んだ時、それだけでなく企業のA/Bテストやテストマーケティングなど実用的な面でも役に立つアプリです！　非常にデザインも洗練されており、誰でも気軽にアンケートや投票を取ることができます。

#####インストールはこちらから：https://apps.apple.com/us/app/vote/id1542436046