20200530のiOSに関する記事は4件です。

NeumorphismなUIButton

gifは、影の大きさを調節する関数を実装する前のものです。

はじめに

NeumorphismなUIButtonを作ったときのことを記録に残しておきます。
index.gif

このButtonのしくみ

見てもらうとすぐわかるんですが、
image.png
クリックされていないときは、このボタンは

  • ボタンの外側の上と左に光があたっている。
  • ボタンの外側の下と右に影がついている。

というような特徴をもっています。
それだけでなく、

image.png

  • 押されているときはボタンの内側の上と左に影がついている。
  • 押されているときはボタンの内側の下と右に光があたっている。

というような特徴をもっています。

この計4つの特徴さえわかってしまえば、あとはそのとおりにUIButtonをいじっていくだけです。

実際のコード

実際のコードを公開します。

  • Colors.swift
  • PlainSquareButton.swift

という2つのファイルを使って実装しました。

Colors.swift
import 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.swift
import 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)くらいの位置まで届くように表示しています。
すると、とりあえずこの画像の状態が完成します。

image.png

凹ませた状態を作るのがちょっと厄介で、
まずCAGradientLayerを使って、始点の方に光、終点の方に影があたるようにグラデーションを設定します。
これを縦と横につけるのですが、locationsの数値を一緒にしていると、影の長さが縦と横で合わなくなってしまうので、ざっくりと合うようにgetProperHeightLocations()のようなメソッドを用意しておきました。
そして作成されたレイヤを貼り付けることで、凹んだ状態を再現できます。
表示されていると困る方のレイヤがremoveしてしまいましょう。

image.png

これで、gifのようなボタンが再現できると思います。

おわり

けっこう楽しいので、この調子で色々な部品を作ってみようと思います。

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

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

Widgetbuild内でこの関数にハンドラーを登録しておくと、任意のライフサイクルのタイミングで処理をすることが出来るようになります。

/// カレンダーウィジェット
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/a7a8ecefea1cee5fbac6

https://stackoverflow.com/questions/49869873/flutter-update-widgets-on-resume

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

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点です。

  • estimatedRowHeight0 以外の適切な値を設定する
  • rowHeightUITableView.automaticDimension を設定する

この設定によって、オートレイアウトベースでセルの高さを決定することができます。あとは、セルのコンテンツビュー内のレイアウト制約を過不足なく配置するだけです。
carbon (3).png
このように2つのラベルを配置して、親ビュー(コンテンツビュー)と兄弟ビューとの余白を設定すれば、次のような具合でレイアウトされると思います。

Screen Shot 2020-05-30 at 16.48.06.png

UIView-Encapsulated-Layout-Height

デバッグでレイアウト制約を確認してみると、身に覚えのないレイアウト制約が設定されていると思います。
Screen_Shot_2020-05-30_at_16_53_38.png
実はこの制約がセルのコンテンツビューの高さを決定しているみたいです。

オートレイアウトベースでセルの高さを決定する場合は、このレイアウト制約は次のメソッドに依存しているみたいでした。

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
  }
}

オートレイアウトベースで決定するはずのセルの高さが固定になりました。
Screen Shot 2020-05-30 at 17.01.23.png
つまり、オートレイアウトベースで高さが決まるということは、『systemLayoutSizeFitting でセルのサイズを決める』ということだったんですね。

Self-sizing で実装するときの注意点

systemLayoutSizeFitting をオーバーライドしてブレークポイントを貼るなどして、どのようなパラメータが渡されるのかを確認することができます。

iPhone SE (2nd generation) の場合

targetSize horizontalFittingPriority verticalFittingPriority
(375.0, 0.0) 1000 50

対象サイズの高さを 0、垂直方向のレイアウト優先度を 50 にしていました。垂直方向の優先度を低く設定することで、コンテンツビュー内のレイアウト制約を優先させているようです。

こうすることで、対象サイズの高さをブレイクさせてフィットする高さを計算させているみたいです。

以上のことから、コンテンツビュー内部に配置したビューのレイアウト制約の優先度を 50 未満にしてしまうと意図しない高さになるかもしれないこともわかりました。

たとえば次のように、ラベルとコンテンツの下部の制約の優先度を 49 に変更してみます。
carbon (2).png

この場合、verticalFittingPriority の 50 が優先されるので意図しないレイアウトになりました。
Screen Shot 2020-05-30 at 17.04.07.png
通常は、50 未満の優先度を指定することはないと思いますが、優先度の設定には十分に気をつけたほうが良さそうですね。

これは iOS SDK の挙動なので、これに従うのがベターだと思いますが、もしも verticalFittingPriority50 が都合悪い場合は、これをオーバーライドすることで意図したセルの高さにすることができました。

垂直方向の優先度をもっと小さく指定する
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 は上記が算出するサイズの高さに一致する
  • この計算では、verticalFittingPriority50 が設定される
  • この点に気をつければ、テーブルビューセルの意図しない制約のブレイクを避けることができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iosのショートカットで現在時刻の年月日時分を別々の変数に入れる

iosに搭載されているショートカットは魅力的なプログラミング環境なんだけど、情報が少ないので、見つけたtipsを記録しておく

現在時刻の文字列

現在時刻を取得して年月日時分を別々に得るまで

取得

下の図のアクションで現在時刻を取得して、それを文字列テキストに変換します
キャプチャ.JPG
以下の説明では、最初のアクションを

[日時/{現在の日付}] (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

年月日時分が別々の出力として得られました

現在時刻の利用

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