こんにちは、@Zhalen(ツァーレン)です。
今日、この「Zhalen」が誤字で、実は「Zahlen」が正しい表記だということを知りました。なので今日から「Zhalen」は私の造語です。
この記事はどこからきたのか、この記事は何か、この記事はどこへ向かうのか
今回の記事では、私が今ちょうどAppleStoreに申請予定(今この記事を書いている途中完成した)のアプリ、「Vote.」に実施したものの一部です。このように白黒つまりグレイスケールを反転させるエクステンションを作ったので紹介します。
ちなみに、GIFの中でそれぞれ切り替えられているボタンも自作のカスタムクラスでその名をRadioButton
と言います。ラジオボタンと検索すれば出てくるアレです。これも、今度また時間があるときに紹介します。
Extension: まずは結果から
まずは最終的な結果から示し、それを解体してゆく要領で説明してゆきます。最後にコピペ用のコード全文を掲載します。
0: 使い方(結果)
このように実行すれば、そのUIViewController
内の全てのグレイスケールを反転します。
self.toggleGrayscale(animated: true)
1: StatusBar, Navigationbar, viewで分割
UIViewControllerの決まり上、
-StatusBar
-NavigationBar
-view
-view1
-view2
-view3
.
.
.
のように、StatusBar
、NavigationBar
、view
の三者それぞれは従属関係にあらず、それぞれが独立していますので、まずはそのそれぞれに対してメソッドを実行します。これがそれで、0: 使い方(結果)
のメソッドです。
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)
}
}
このそれぞれのメソッドに対する中身はこうなっています。
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
内には
override var preferredStatusBarStyle: UIStatusBarStyle {
return /*ダークモードにするための条件*/ ? .darkContent : .lightContent
}
を追加してください。/*ダークモードにするための条件*/
とは、上記のGIFでいう左側のラジオボタンが付いている状態で、実際に私のプロジェクトでは、
override var preferredStatusBarStyle: UIStatusBarStyle {
return modeRadioButtons[0].isRadied ? .darkContent : .lightContent
//左のmodeRadioButtonが付いているかどうか(Radied(造語です))で判定しています。
}
のように、modeRadioButtons[0].isRadied
がダークモードにするための条件となります。
NavigtionBar, view
//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達と自分自身のグレースケールを変換するメソッドです。
そしてこれがその中身です。(長いですがまだコピペ用コード全文ではありません。)
//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)
}
}
この中の一番上の
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の...
といっためんどくさい処理を簡潔に実現します。
イメージとしては、この白丸をビューとして枝が従属関係(点線はその省略)を表すとき、枝が右に存在すればそのさきのビューにメソッドを移行させ、その時に自分自身のグレースケールを反転(self.toggleForEachObject()
)させ、右に枝が存在しなければリターンするという仕組みです。
中身をご覧の通り対象がUIStackView
であるときはarrangedSubview
についても同様のことをします。
ではこのself.toggleForEachObject()
はなんなのかというと、これがグレイスケールを反転する上で主要なもので、文字通りそれぞれのクラスについて記述しなければならなかったため、かなり泥臭いです。これは最初に掲載したGIFの画面においてのみ動作確認をしているので、もちろんもっと他に追加で書かなければならないと気づいたのならばそれを奨励します。
そして一番下の
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: 使い方(結果)
self.toggleGrayscale(animated: true)
という1行に帰着されるわけです。
2: コピペ用コード全文(各自手直しあり)
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)
}
}
宣伝です
つい先日、アンケート特化型SNS「Vote.」をリリースしました!
このアプリは、何かの選択で迷った時・悩んだ時、それだけでなく企業のA/Bテストやテストマーケティングなど実用的な面でも役に立つアプリです! 非常にデザインも洗練されており、誰でも気軽にアンケートや投票を取ることができます。