- 投稿日:2020-05-30T22:37:41+09:00
NeumorphismなUIButton
※ gifは、影の大きさを調節する関数を実装する前のものです。
はじめに
NeumorphismなUIButtonを作ったときのことを記録に残しておきます。
このButtonのしくみ
見てもらうとすぐわかるんですが、
クリックされていないときは、このボタンは
- ボタンの外側の上と左に光があたっている。
- ボタンの外側の下と右に影がついている。
というような特徴をもっています。
それだけでなく、
- 押されているときはボタンの内側の上と左に影がついている。
- 押されているときはボタンの内側の下と右に光があたっている。
というような特徴をもっています。
この計4つの特徴さえわかってしまえば、あとはそのとおりにUIButtonをいじっていくだけです。
実際のコード
実際のコードを公開します。
- Colors.swift
- PlainSquareButton.swift
という2つのファイルを使って実装しました。
Colors.swiftimport Foundation import UIKit class Colors { static var plainColor = UIColor(hex: "ECF0F3") } extension UIColor { convenience init(hex: String, alpha: CGFloat = 1.0) { let v = Int("000000" + hex, radix: 16) ?? 0 let r = CGFloat(v / Int(powf(256, 2)) % 256) / 255 let g = CGFloat(v / Int(powf(256, 1)) % 256) / 255 let b = CGFloat(v / Int(powf(256, 0)) % 256) / 255 self.init(red: r, green: g, blue: b, alpha: min(max(alpha, 0), 1)) } func brighter() -> UIColor { var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { return UIColor(hue: hue, saturation: saturation * 0.85, brightness: brightness * 1.1, alpha: alpha) } else { return self } } func darker() -> UIColor { var hue: CGFloat = 0, saturation: CGFloat = 0, brightness: CGFloat = 0, alpha: CGFloat = 0 if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) { return UIColor(hue: hue, saturation: saturation * 1.25, brightness: brightness * 0.75, alpha: alpha) } else { return self } } }PlainSquareButton.swiftimport Foundation import UIKit class PlainSquareButton: UIButton { private let highlightLayer = CALayer(), shadowLayer = CALayer() private let dentedHorizontalLayer = CAGradientLayer(), dentedVerticalLayer = CAGradientLayer() required init?(coder: NSCoder) { super.init(coder: coder) self.layer.cornerRadius = 5.0 self.backgroundColor = Colors.plainColor self.putHighlight() self.putShadow() self.addTarget(self, action: #selector(self.onPushed), for: .touchDown) self.addTarget(self, action: #selector(self.onReleased), for: .touchUpInside) } private func putHighlight() { self.highlightLayer.masksToBounds = false self.highlightLayer.frame = self.bounds self.highlightLayer.backgroundColor = Colors.plainColor.cgColor self.highlightLayer.shadowColor = Colors.plainColor.brighter().cgColor self.highlightLayer.cornerRadius = 5.0 self.highlightLayer.shadowOpacity = 0.75 self.highlightLayer.shadowOffset = CGSize(width: -6, height: -6) self.highlightLayer.shadowRadius = 5.0 self.layer.addSublayer(self.highlightLayer) } private func putShadow() { self.shadowLayer.masksToBounds = false self.shadowLayer.frame = self.bounds self.shadowLayer.backgroundColor = Colors.plainColor.cgColor self.shadowLayer.shadowColor = Colors.plainColor.darker().cgColor self.shadowLayer.cornerRadius = 5.0 self.shadowLayer.shadowOpacity = 0.65 self.shadowLayer.shadowOffset = CGSize(width: 6, height: 6) self.shadowLayer.shadowRadius = 5.0 self.layer.addSublayer(self.shadowLayer) } @objc dynamic func onPushed() { self.highlightLayer.removeFromSuperlayer() self.shadowLayer.removeFromSuperlayer() self.putDentedVerticalLayer() self.putDentedHorizontalLayer() } private func putDentedVerticalLayer() { self.dentedVerticalLayer.cornerRadius = 5.0 self.dentedVerticalLayer.frame = self.bounds self.dentedVerticalLayer.colors = [ Colors.plainColor.darker().cgColor, Colors.plainColor.cgColor, Colors.plainColor.cgColor, Colors.plainColor.brighter().cgColor ] self.dentedVerticalLayer.locations = [ 0, 0.15, 0.85, 1 ] self.dentedVerticalLayer.opacity = 1 self.layer.insertSublayer(self.dentedVerticalLayer, at: 0) } private func putDentedHorizontalLayer() { self.dentedHorizontalLayer.cornerRadius = 5.0 self.dentedHorizontalLayer.frame = self.bounds self.dentedHorizontalLayer.colors = [ Colors.plainColor.darker().cgColor, Colors.plainColor.cgColor, Colors.plainColor.cgColor, Colors.plainColor.brighter().cgColor ] self.dentedHorizontalLayer.locations = self.getProperWidthLocations(size: self.bounds.size) self.dentedHorizontalLayer.startPoint = CGPoint(x: 0, y: 0) self.dentedHorizontalLayer.endPoint = CGPoint(x: 1, y: 0) self.dentedHorizontalLayer.opacity = 0.5 self.layer.insertSublayer(self.dentedHorizontalLayer, at: 1) } @objc dynamic func onReleased() { self.dentedVerticalLayer.removeFromSuperlayer() self.dentedHorizontalLayer.removeFromSuperlayer() self.putHighlight() self.putShadow() } private func getProperWidthLocations(size: CGSize) -> [NSNumber] { if (size.width >= size.height*2 && size.width < size.height*3) { return [ 0, 0.075, 0.925, 1 ] } else if (size.width >= size.height*3 && size.width < size.height*4) { return [ 0, 0.05, 0.95, 1 ] } else if (size.width >= size.height*4 && size.width < size.height*5) { return [ 0, 0.0325, 0.9675, 1 ] } else { return [ 0, 0.15, 0.85, 1 ] } } private func getProperHeightLocations(size: CGSize) -> [NSNumber] { if (size.width*2 >= size.height && size.width*3 < size.height) { return [ 0, 0.075, 0.925, 1 ] } else if (size.width*3 >= size.height && size.width*4 < size.height) { return [ 0, 0.05, 0.95, 1 ] } else if (size.width*4 >= size.height && size.width*5 < size.height) { return [ 0, 0.0325, 0.9675, 1 ] } else { return [ 0, 0.15, 0.85, 1 ] } } }解説
まず、イニシャライザで
putHighlight()
というメソッドと、putShadow()
というメソッドを呼び出します。それぞれのメソッドで光と影をボタンの外側につけます。
putHighlight()
では、plainColor
を少し明るくしたものを、ボタンから(-6, -6)くらいの位置まで届くように表示しています。
逆にputShadow()
では、plainColor
を少し暗くしたものを、(6, 6)くらいの位置まで届くように表示しています。
すると、とりあえずこの画像の状態が完成します。凹ませた状態を作るのがちょっと厄介で、
まずCAGradientLayer
を使って、始点の方に光、終点の方に影があたるようにグラデーションを設定します。
これを縦と横につけるのですが、locations
の数値を一緒にしていると、影の長さが縦と横で合わなくなってしまうので、ざっくりと合うようにgetProperHeightLocations()
のようなメソッドを用意しておきました。
そして作成されたレイヤを貼り付けることで、凹んだ状態を再現できます。
表示されていると困る方のレイヤがremoveしてしまいましょう。これで、gifのようなボタンが再現できると思います。
おわり
けっこう楽しいので、この調子で色々な部品を作ってみようと思います。
- 投稿日:2020-05-30T21:20:05+09:00
FlutterアプリでonResume的なタイミングの処理をする
状況
カレンダー式表示のアプリを作成しています。「今日」が分かりやすいように、その日付のセルだけ背景色を変えています。
しかし、特にiOSで問題になるのですが、アプリをホームボタンでバックグラウンドに移動していて、もう一度アプリを立ち上げたときに、そのタイミングで日付が変わっていても「今日のセル」が移動してくれません。
Androidでいう、"onResume"のタイミングで処理ができれば解決できると思い、調査しました。環境など
ツールなど バージョンなど MacBook Air Early2015 macOS Mojave 10.14.6 Android Studio 3.6.1 Java 1.8.0_131 Flutter 1.12.13+hotfix.9 Dart 2.7.2 Xcode 11.3.1 解決策
SystemChannels.lifecycle.setMessageHandler
を使います。
詳しくはこちら。
https://api.flutter.dev/flutter/services/BasicMessageChannel/setMessageHandler.html
Widget
のbuild
内でこの関数にハンドラーを登録しておくと、任意のライフサイクルのタイミングで処理をすることが出来るようになります。/// カレンダーウィジェット class CalendarWidget extends StatelessWidget { @override Widget build(BuildContext context) { final viewModel = Provider.of<MonthlyViewModel>(context, listen: false); SystemChannels.lifecycle.setMessageHandler((msg) { if (msg == 'AppLifecycleState.resumed') { print('Resumed!'); viewModel.updateToday(); } return null; }); return Column(...); } }参考サイト
flutterでonResume
https://qiita.com/sakyoyuto/items/a7a8ecefea1cee5fbac6https://stackoverflow.com/questions/49869873/flutter-update-widgets-on-resume
- 投稿日:2020-05-30T17:05:31+09:00
UITableViewCell の UIView-Encapsulated-Layout-Height について
この記事について
デバッグによって得られたテーブルビューセルの高さの挙動についてのメモです。
環境
Xcode Version 11.5 (11E608c)
Self-sizing Table View Cells
UITableView は次のような設定によって、オートレイアウトベースでセルの高さを決定できるようになります。
tableView.estimatedRowHeight = 85.0 tableView.rowHeight = UITableView.automaticDimension要点は2点です。
estimatedRowHeight
に0
以外の適切な値を設定するrowHeight
にUITableView.automaticDimension
を設定するこの設定によって、オートレイアウトベースでセルの高さを決定することができます。あとは、セルのコンテンツビュー内のレイアウト制約を過不足なく配置するだけです。
このように2つのラベルを配置して、親ビュー(コンテンツビュー)と兄弟ビューとの余白を設定すれば、次のような具合でレイアウトされると思います。UIView-Encapsulated-Layout-Height
デバッグでレイアウト制約を確認してみると、身に覚えのないレイアウト制約が設定されていると思います。
実はこの制約がセルのコンテンツビューの高さを決定しているみたいです。オートレイアウトベースでセルの高さを決定する場合は、このレイアウト制約は次のメソッドに依存しているみたいでした。
func systemLayoutSizeFitting( _ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority ) -> CGSize
systemLayoutSizeFitting
は、対象サイズとそのレイアウト優先度をパラメータとして指定することで、制約ベースにフィットするサイズを算出メソッドです。試しに本来の処理を無視して、高さを固定してみます。
高さを100に固定したサンプルimport UIKit class CustomCell: UITableViewCell { @IBOutlet weak var label1: UILabel! @IBOutlet weak var label2: UILabel! override func systemLayoutSizeFitting( _ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority ) -> CGSize { var size = super.systemLayoutSizeFitting( targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority ) size.height = 100 return size } }オートレイアウトベースで決定するはずのセルの高さが固定になりました。
つまり、オートレイアウトベースで高さが決まるということは、『systemLayoutSizeFitting でセルのサイズを決める』ということだったんですね。Self-sizing で実装するときの注意点
systemLayoutSizeFitting
をオーバーライドしてブレークポイントを貼るなどして、どのようなパラメータが渡されるのかを確認することができます。iPhone SE (2nd generation) の場合
targetSize horizontalFittingPriority verticalFittingPriority (375.0, 0.0) 1000 50 対象サイズの高さを
0
、垂直方向のレイアウト優先度を50
にしていました。垂直方向の優先度を低く設定することで、コンテンツビュー内のレイアウト制約を優先させているようです。こうすることで、対象サイズの高さをブレイクさせてフィットする高さを計算させているみたいです。
以上のことから、コンテンツビュー内部に配置したビューのレイアウト制約の優先度を 50 未満にしてしまうと意図しない高さになるかもしれないこともわかりました。
たとえば次のように、ラベルとコンテンツの下部の制約の優先度を 49 に変更してみます。
この場合、
verticalFittingPriority
の 50 が優先されるので意図しないレイアウトになりました。
通常は、50
未満の優先度を指定することはないと思いますが、優先度の設定には十分に気をつけたほうが良さそうですね。これは iOS SDK の挙動なので、これに従うのがベターだと思いますが、もしも
verticalFittingPriority
の50
が都合悪い場合は、これをオーバーライドすることで意図したセルの高さにすることができました。垂直方向の優先度をもっと小さく指定するoverride func systemLayoutSizeFitting( _ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority ) -> CGSize { return super.systemLayoutSizeFitting( targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: UILayoutPriority(1) ) }こうすれば、インターフェースビルダーなどで配置したビューのレイアウト制約の優先度を
50
未満にしても意味のある値になると思います。まとめ
- オートレイアウトベースで高さを決めるということは、
systemLayoutSizeFitting
でセルの高さを決めるということだった- UIView-Encapsulated-Layout-Height は上記が算出するサイズの高さに一致する
- この計算では、
verticalFittingPriority
に50
が設定される- この点に気をつければ、テーブルビューセルの意図しない制約のブレイクを避けることができる
- 投稿日:2020-05-30T09:52:45+09:00
iosのショートカットで現在時刻の年月日時分を別々の変数に入れる
iosに搭載されているショートカットは魅力的なプログラミング環境なんだけど、情報が少ないので、見つけたtipsを記録しておく
現在時刻の文字列
現在時刻を取得して年月日時分を別々に得るまで
取得
下の図のアクションで現在時刻を取得して、それを文字列テキストに変換します
以下の説明では、最初のアクションを[日時/{現在の日付}] (1)
次のアクションを
[書類/{日時}からテキストを取得] (2)
と表記します。{}は変数を示します
日時はアクション(2)で「日付として」、日付けフォーマット「中」、時間フォーマット「短」としたとき
2020/05/28 22:47 (a)
あるいは
2020/05/28 5:24 (b)
というフォーマットになります年月日の取り出し
正規表現を使って日時文字列から年月日を取り出します
[テキスト/{テキスト}で{[0-9][0-9]}に一致] (3)
日時文字列から数字2文字の組を1要素とするリストを作ります
(a)の例では
20
20
05
28
22
47
(b)の例では
20
20
05
28
24
のリストになります。(b)の例では「時」の要素が1文字なので選ばれません年の抽出
リストの第1,2要素を取り出します
[スクリプティング/{一致}から{Rangeの項目}の{1}から{2}までを取得] (4)
出力は
20
20
となります月の抽出
リストの第3要素を取り出します
[スクリプティング/{一致}から{項目のインデックス}の{3}を取得] (5)
出力は
05
となります日の抽出
リストの第4要素を取り出します
[スクリプティング/{一致}から{項目のインデックス}の{4}を取得] (6)
出力は
28
となります時分の取り出し
正規表現を使って日時文字列から時刻を取り出します
[テキスト/{テキスト}で{[ 1][0-9]:[0-9][0-9]}に一致] (7)
日時文字列からスペースか「1」で始まり「:」で時分を分けた要素を取り出します
(a)の例では
22:47
(b)の例では
5:24
となります
(b)の場合は0パディングしたいので先頭のスペースを「0」に変換します[書類/{一致}の{ ([0-9]):を{0$1:}に置き換え}] (8)
アクション(8)で()で囲まれている正規表現で一致した文字列を「$1」で参照します
出力は(a)の場合は変化なし(マッチしない)、(b)の場合だけ
05:24
となります
次に、「:」で分割してリストを作ります
[テキスト/{アップデートされたテキスト}を{カスタム}{:}で分割]時の抽出
リストの第1要素を取り出します
[スクリプティング/{テキストの分割}から{項目のインデックス}の{1}を取得] (9)
出力は(a)の場合は
22
となります分の抽出
リストの第2要素を取り出します
[スクリプティング/{テキストの分割}から{項目のインデックス}の{2}を取得] (10)
出力は(a)の場合は
47
となりますこれで
アイテム アクション 年 4 月 5 日 6 時 9 分 10 年月日時分が別々の出力として得られました
現在時刻の利用