2021年10月3日日曜日

UITabBarAppearanceを使って、TabBarの配色をセットする

iOS13以降のTabBarのカスタマイズです。
今回の記事では、次のようにTabBarをカスタマイズしました。

1. UITabBarを継承した、カスタムViewを作成する
2. TabBarItemの配色をUITabBarItemAppearanceで作成する
3. UITabBarAppearanceを生成する
4. UITabBarにセットする
5. UITabBarViewControllerにて、カスタムViewを使う


UITabBarを継承した、カスタムViewは次になります。

final class MyUITabBar: UITabBar {

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }

    private enum Const{
        static let backgroundColor: UIColor = .systemGroupedBackground
        static let tintColor: UIColor = .gray
        static let selectedColor: UIColor = .black
    }
    
    private func setupView() {
        let tabBarItemAppearance = setupTabBarItemAppearance()
        let appearance = UITabBarAppearance()
        appearance.configureWithOpaqueBackground()
        appearance.backgroundColor = Const.backgroundColor
        appearance.stackedLayoutAppearance = tabBarItemAppearance
        appearance.inlineLayoutAppearance = tabBarItemAppearance
        appearance.compactInlineLayoutAppearance = tabBarItemAppearance

        standardAppearance = appearance
        // iOS15: we need to set
        scrollEdgeAppearance = appearance
    }

    private func setupTabBarItemAppearance() -> UITabBarItemAppearance {
        let tabBarItemAppearance = UITabBarItemAppearance()
        // for normal
        tabBarItemAppearance.normal.iconColor = Const.tintColor
        tabBarItemAppearance.normal.titleTextAttributes = [NSAttributedString.Key.foregroundColor: Const.tintColor]
        
        // for selected
        tabBarItemAppearance.selected.iconColor = Const.selectedColor
        tabBarItemAppearance.selected.titleTextAttributes = [NSAttributedString.Key.foregroundColor: Const.selectedColor]
        
        return tabBarItemAppearance
    }
}

UITabBarControllerの子となる各ViewControllerでは、次のようにtabBarItemを指定するのみです。
tabBarItemと配色をするAppearanceをコード上で分け、責務が分離できるので非常に見やすいコードとなりました。

final class FirstViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTabBar()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }

    private func setupTabBar() {
        tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house"))
    }
}

final class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTabBar()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }

    private func setupTabBar() {
        tabBarItem = UITabBarItem(title: "Trash", image: UIImage(systemName: "trash"), selectedImage: UIImage(systemName: "trash"))
    }

}

2021年9月26日日曜日

リンクタップ可能でかつ選択不可なUITextViewを作る

次なような、UITextViewを作成することにしました。

1. リンクタップは可能である、その他のテキストは操作無反応とする
2. ダブルタップによるテキスト選択動作は不可である
3. ロングタップは不可である


UITextViewのカスタムViewを作成する

条件1から3まで満たすようなカスタムViewを作ります。
この方法は、Stackoverflowに書かれているものを参考にしました。
UITextView: Disable selection, allow links


class UnselectableTextView: UITextView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard let position = closestPosition(to: point) else { return false }
        guard let range = tokenizer.rangeEnclosingPosition(
                position,
                with: .character,
                inDirection: UITextDirection(rawValue: UITextLayoutDirection.left.rawValue)
              ) else { return false }
        let startIndex = offset(from: beginningOfDocument, to: range.start)
        return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
    }

    override func becomeFirstResponder() -> Bool {
        return false
    }
}


override func point() にて、タップした位置のテキストがリンクであるかどうか判定し、override func becomeFirstResponder() にてダブルタップとロングタップを抑制しています。
ViewControllerに実装するときは、次のようになります。


class ViewController: UIViewController {

    @IBOutlet weak var textView: UnselectableTextView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupViews()
    }

    private func setupViews() {
        textView.delegate = self
        textView.isEditable = false
        textView.isSelectable = true
        textView.backgroundColor = .clear

        let attributedString = NSMutableAttributedString(string: "this textview has a link test.")
        attributedString.addAttribute(.link, value: "https://www.google.com", range: NSRange(location: 20, length: 4))
        textView.attributedText = attributedString
    }
}

extension UIViewController: UITextViewDelegate {
    public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        print(URL.absoluteString)
        return false
    }
}


ロングタップをハンドリングする

上記で作成したUnselectableTextViewに、独自の UILongPressGestureRecognizer を組み込もうとしたが断念。
UITextViewの GestureRecognizer とConflictしてしまい、良い感じで実装できずでした。
代替案として、UnselectableTextViewの後ろにUIViewを配置して、このUIViewにてロングタップをハンドリングさせます。


class ViewController: UIViewController {
    private func setupGesture() {
        let longPressGesture = UILongPressGestureRecognizer(
            target: self,
            action: #selector(ViewController.longPress(_:))
        )
        longPressGesture.delegate = self
        backgroundView.addGestureRecognizer(longPressGesture)
    }
}

extension UIViewController: UIGestureRecognizerDelegate {
    @objc func longPress(_ sender: UILongPressGestureRecognizer) {
        print(sender.state.rawValue.description)
    }
}

2021年7月5日月曜日

画面幅より短い場合はCenterさせるUIScrollViewを作成する

次のようなViewを条件を満たすViewを作りたくなったので実装してみました。
  • 画面幅より短い場合は、Center表示させる
  • 画面幅より長い場合は、左寄せで表示し横スクロール可能である

完成イメージは次のような感じです。

Viewの構成

Viewの構成は、UIScrollViewの中にコンテンツ用のUIViewを入れるのみです。
    private let scrollView = UIScrollView()
    private let contentView = UIView()


    private func setupViews() {
        scrollView.addSubview(contentView)
        addSubview(scrollView)
    }

実際に表示させたいViewは、次のようにfunc setup(_)メソッドを使って挿入させることを想定しています。

    func setup(_ view: UIView) {
        contentView.addSubview(view)
        view.snp.makeConstraints {
            $0.edges.equalTo(contentView)
        }
    }



制約をセットする

scrollViewの制約は、通常通り親Viewに対して同じ大きさになるよう制約をセットします。
contentViewの制約は工夫が必要で、親Viewに対して横Centerの指定とleftに対してgreaterThanOrEqualToを指定します。
この2つの制約によりCenter表示が可能になります。
    private func setupConstraint() {
        scrollView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        contentView.snp.makeConstraints {
            $0.height.equalTo(scrollView.frameLayoutGuide)
            // 横幅より小さい場合は、Center
            // 横幅より大きい場合は、通常の左寄せ + スクロール
            $0.top.right.bottom.equalTo(scrollView.contentLayoutGuide)
            $0.left.equalTo(scrollView.contentLayoutGuide).priority(.low)
            $0.left.greaterThanOrEqualTo(scrollView.contentLayoutGuide)
            $0.centerX.greaterThanOrEqualToSuperview()
        }
    }


ScrollのContent領域が定まらないというLayoutのwarningが発生したため、次の制約を追加します。
$0.left.equalTo(scrollView.contentLayoutGuide).priority(.low)



完成形のコード

上記の全てを組み合わせると、次のようなコードになります。
final class HorizontalScrollView: UIView {
    private let scrollView = UIScrollView()
    private let contentView = UIView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
        setupConstraint()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupViews()
        setupConstraint()
    }

    private func setupViews() {
        scrollView.addSubview(contentView)
        addSubview(scrollView)
    }

    private func setupConstraint() {
        scrollView.snp.makeConstraints {
            $0.edges.equalToSuperview()
        }
        contentView.snp.makeConstraints {
            $0.height.equalTo(scrollView.frameLayoutGuide)
            // aligin center when content width < frame width
            // aligin left when content width > frame width
            $0.top.right.bottom.equalTo(scrollView.contentLayoutGuide)
            $0.left.equalTo(scrollView.contentLayoutGuide).priority(.low)
            $0.left.greaterThanOrEqualTo(scrollView.contentLayoutGuide)
            $0.centerX.greaterThanOrEqualToSuperview()
        }
    }

    func setup(_ view: UIView) {
        contentView.addSubview(view)
        view.snp.makeConstraints {
            $0.edges.equalTo(contentView)
        }
    }
}

2019年8月17日土曜日

UIScreenEdgePanGestureRecognizerをプログラムで実装する

ViewControllerに実装する


storyboardではなく、プログラムでEdgeスワイプを実装してみました。
UIScreenEdgePanGestureRecognizerをViewController.viewに追加して、selectorを使ってメソッドでハンドリングします。


class ViewController: UIViewController {

    private let closer = SwipeEdgeCloser()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        let edgePan = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleScreenEdgeSwiped))
        edgePan.edges = .left
        view.addGestureRecognizer(edgePan)
    }

    @objc func handleScreenEdgeSwiped(_ recognizer: UIScreenEdgePanGestureRecognizer) {
        if recognizer.state == .recognized {
            print("Screen edge swiped!")
        }
    }
}



クラスに実装する


ViewControllerがファットになりそうなので、クラス化してみました。処理はほぼ同様で、selectorを新しく作成したSwipeLeftEdgePanGestureのメソッドを指定しています。

class ViewController: UIViewController {

    private let closer = SwipeLeftEdgePanGesture()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addGestureRecognizer(closer.panGesture)
    }
}

final class SwipeLeftEdgePanGesture {
    let panGesture: UIScreenEdgePanGestureRecognizer
    
    init() {
        panGesture = UIScreenEdgePanGestureRecognizer()
        panGesture.edges = .left
        panGesture.addTarget(self, action: #selector(SwipeLeftEdgePanGesture.screenEdgeSwiped))
    }
    
    @objc private func screenEdgeSwiped(_ recognizer: UIScreenEdgePanGestureRecognizer) {
        if recognizer.state == .recognized {
            print("SwipeLeftEdgePanGesture: Screen edge swiped!")
        }
    }
}

2016年11月30日水曜日

はじめてのTensorFlow 「Variables: 生成、初期化、保存と復元」その3

原文:
Variables: Creation, Initialization, Saving, and Loading
https://www.tensorflow.org/versions/r0.12/how_tos/variables/index.html#variables-creation-initialization-saving-and-loading


Saving and Restoring

モデルの保存/復元はtf.train.Saverオブジェクトを使用してください。コンストラクタはすべてのグラフ操作、特定のリスト、グラフ内の変数の保存/復元が可能です。Saverオブジェクトは、これらのopsを実行するメソッドを提供し、チェックポイントファイルの書き込みまたは読み取りのパスを指定します。


Checkpoint Files

変数はバイナリファイルに保存され、大まかに、変数名からテンソル値までのマップを含みます。
Saverオブジェクトを作成するときは、チェックポイントファイル内の変数の名前を任意に選択できます。 デフォルトでは、変数ごとにVariable.nameプロパティの値が使用されます。

チェックポイント内の変数を理解するには、inspect_checkpointライブラリ、特にprint_tensors_in_checkpoint_file関数を使用します。


Saving Variables

tf.train.Saver()を使用してSaverを作成し、モデル内のすべての変数を管理します。

# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add an op to initialize the variables.
init_op = tf.global_variables_initializer()

# Add ops to save and restore all the variables.
saver = tf.train.Saver()

# Later, launch the model, initialize the variables, do some work, save the
# variables to disk.
with tf.Session() as sess:
  sess.run(init_op)
  # Do some work with the model.
  ..
  # Save the variables to disk.
  save_path = saver.save(sess, "/tmp/model.ckpt")
  print("Model saved in file: %s" % save_path)


Restoring Variables

同じSaverオブジェクトが変数の復元に使用されます。 ファイルから変数を復元するときは、あらかじめそれらを初期化する必要はありません。


# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add ops to save and restore all the variables.
saver = tf.train.Saver()

# Later, launch the model, use the saver to restore variables from disk, and
# do some work with the model.
with tf.Session() as sess:
  # Restore variables from disk.
  saver.restore(sess, "/tmp/model.ckpt")
  print("Model restored.")
  # Do some work with the model
  ...


Choosing which Variables to Save and Restore

tf.train.Saver()に引数を渡さない場合、グラフのすべての変数がSaverによって処理されます。 それぞれの変数は、変数の作成時に渡された名前で保存されます。
チェックポイントファイル内の変数の名前を明示的に指定すると便利なことがあります。 たとえば、 "weights"という名前の変数を持つモデルを訓練し、その値を "params"という名前の新しい変数にリストアすることができます。

また、モデルによって使用される変数のサブセットの保存または復元のみが有用な場合もあります。 たとえば、5層のニューラルネットを訓練して、6層の新しいモデルを訓練し、以前訓練されたモデルの5つの層のパラメータを新しいモデルの最初の5つの層に復元する必要があります。

tf.train.Saver()コンストラクタにPythonのdictionaryを渡すことで、保存する名前と変数を簡単に指定できます。keysは使用する名前、valueは管理する変数です。

ノート:
モデル変数の異なるサブセットを保存して復元する必要がある場合は、必要な数のセーバーオブジェクトを作成できます。 同じ変数を複数の保存オブジェクトにリストすることができます。その値は、saver restore()メソッドが実行されたときにのみ変更されます。

詳細は、tf.initialize_variablesを参照してください。

# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add ops to save and restore only 'v2' using the name "my_v2"
saver = tf.train.Saver({"my_v2": v2})
# Use the saver object normally after that.
...

はじめてのTensorFlow 「Variables: 生成、初期化、保存と復元」その2

原文:
Variables: Creation, Initialization, Saving, and Loading
https://www.tensorflow.org/versions/r0.12/how_tos/variables/index.html#variables-creation-initialization-saving-and-loading


Initialization

変数の初期化は、他の操作を行う前に明確に実行する必要があります。モデルを使用する前にすべての変数を初期化を実行して下さい。

チェックポイントファイルから変数の値を復元することもできます(以下を参照)
変数を初期化するには、tf.global_variables_initializer()を使用します。
モデルを完全に生成してセッションを起動した後にのみ、そのオペレーションを実行してください。


# Create two variables.
weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35),
                      name="weights")
biases = tf.Variable(tf.zeros([200]), name="biases")
...
# Add an op to initialize the variables.
init_op = tf.global_variables_initializer()

# Later, when launching the model
with tf.Session() as sess:
  # Run the init operation.
  sess.run(init_op)
  ...
  # Use the model
  ...


Initialization from another Variable

他の変数の初期値で変数を初期化することも可能です。tf.global_variables_initializerによって追加された演算子はすべての変数を並列に初期化するので、必要なときは注意して下さい。

他の変数の値から変数を初期化は変数のinitialized_value()プロパティを使います。初期化された値は、新しい変数の初期値として直接使用することも、他のテンソルとして使用して新しい変数の値を計算することもできます。


# Create a variable with a random value.
weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35),
                      name="weights")
# Create another variable with the same value as 'weights'.
w2 = tf.Variable(weights.initialized_value(), name="w2")
# Create another variable with twice the value of 'weights'
w_twice = tf.Variable(weights.initialized_value() * 2.0, name="w_twice")



Custom Initialization

tf.global_variables_initializerは、モデル内のすべての変数を初期化するための演算子を追加します。 また、初期化する変数の明示的なリストを渡すこともできます。 変数が初期化されているかどうかのチェックなど、その他のオプションについては変数ドキュメントを参照してください。

はじめてのTensorFlow 「Variables: 生成、初期化、保存と復元」その1

原文:
Variables: Creation, Initialization, Saving, and Loading
https://www.tensorflow.org/versions/r0.12/how_tos/variables/index.html#variables-creation-initialization-saving-and-loading


Variables: 生成、初期化、保存と復元

モデルを列挙にする際、パラメータを保持、更新するためにVariables(以後、変数)を使います。Variablesは行列としてメモリ上に存在します。変数は明示的に初期化する必要があり、トレーニング中かつトレーニング後はディスク上に可能です。保存された値を後で復元して、モデルを実行または分析することができます。

このドキュメントでは、次のTensorFlowクラスを参照しています。 APIの完全な説明についてはリファレンスマニュアルのリンクをクリックしてください:
The tf.Variable class.
The tf.train.Saver class.


Creation

変数を生成する際、初期値としてTensrに値をVariable()コンストラクタに渡します。
TensorFlowは、定数やランダムな値から初期化するためによく使われるテンソルを生成するopsのコレクションを提供します。

これらすべての操作では、テンソルの形状を指定する必要があります。 その形状は自動的に変数の形になります。 変数は一般的に固定された形状を持ちますが、TensorFlowは変数を再構成する高度なメカニズムを提供します。

# Create two variables.
weights = tf.Variable(tf.random_normal([784, 200], stddev=0.35),
                      name="weights")
biases = tf.Variable(tf.zeros([200]), name="biases")

tf.Variable()をコールすると、グラフにopsが追加されます。
・保持:値を保持する
・初期化:変数を初期化する。実際にはtf.assignで操作する。
・初期値:biases変数のゼロ初期化など。この例ではグラフに追加もしている

tf.Variable()の値が返す値は、Pythonクラスtf.Variableのインスタンスです。



Device placement

変数は、生成するときに特定のデバイスに固定することができます。tf.deviceを使用します。

# Pin a variable to CPU.
with tf.device("/cpu:0"):
  v = tf.Variable(...)

# Pin a variable to GPU.
with tf.device("/gpu:0"):
  v = tf.Variable(...)

# Pin a variable to a particular parameter server task.
with tf.device("/job:ps/task:7"):
  v = tf.Variable(...)

変数をmutateする操作、v.assign()やtf.train.Optimizer内でパラメータを更新する操作は同じデバイス上で実行する必要があります。互換性のないデバイス配置指定は、これらの操作を生成するときに無視されます。

複製された設定で実行する場合、デバイス配置は特に重要です。
複製されたされたモデルのデバイス設定を簡素化できるデバイス機能の詳細は、tf.train.replica_device_setterを参照してください。