2021年10月3日日曜日

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

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

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


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

  1. final class MyUITabBar: UITabBar {
  2.  
  3. override init(frame: CGRect) {
  4. super.init(frame: frame)
  5. setupView()
  6. }
  7. required init?(coder: NSCoder) {
  8. super.init(coder: coder)
  9. setupView()
  10. }
  11.  
  12. private enum Const{
  13. static let backgroundColor: UIColor = .systemGroupedBackground
  14. static let tintColor: UIColor = .gray
  15. static let selectedColor: UIColor = .black
  16. }
  17. private func setupView() {
  18. let tabBarItemAppearance = setupTabBarItemAppearance()
  19. let appearance = UITabBarAppearance()
  20. appearance.configureWithOpaqueBackground()
  21. appearance.backgroundColor = Const.backgroundColor
  22. appearance.stackedLayoutAppearance = tabBarItemAppearance
  23. appearance.inlineLayoutAppearance = tabBarItemAppearance
  24. appearance.compactInlineLayoutAppearance = tabBarItemAppearance
  25.  
  26. standardAppearance = appearance
  27. // iOS15: we need to set
  28. scrollEdgeAppearance = appearance
  29. }
  30.  
  31. private func setupTabBarItemAppearance() -> UITabBarItemAppearance {
  32. let tabBarItemAppearance = UITabBarItemAppearance()
  33. // for normal
  34. tabBarItemAppearance.normal.iconColor = Const.tintColor
  35. tabBarItemAppearance.normal.titleTextAttributes = [NSAttributedString.Key.foregroundColor: Const.tintColor]
  36. // for selected
  37. tabBarItemAppearance.selected.iconColor = Const.selectedColor
  38. tabBarItemAppearance.selected.titleTextAttributes = [NSAttributedString.Key.foregroundColor: Const.selectedColor]
  39. return tabBarItemAppearance
  40. }
  41. }

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

  1. final class FirstViewController: UIViewController {
  2.  
  3. override func viewDidLoad() {
  4. super.viewDidLoad()
  5. setupTabBar()
  6. }
  7. override func viewWillAppear(_ animated: Bool) {
  8. super.viewWillAppear(animated)
  9. }
  10.  
  11. private func setupTabBar() {
  12. tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house"))
  13. }
  14. }
  15.  
  16. final class SecondViewController: UIViewController {
  17.  
  18. override func viewDidLoad() {
  19. super.viewDidLoad()
  20. setupTabBar()
  21. }
  22. override func viewWillAppear(_ animated: Bool) {
  23. super.viewWillAppear(animated)
  24. }
  25.  
  26. private func setupTabBar() {
  27. tabBarItem = UITabBarItem(title: "Trash", image: UIImage(systemName: "trash"), selectedImage: UIImage(systemName: "trash"))
  28. }
  29.  
  30. }

2021年9月26日日曜日

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

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

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


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

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


  1. class UnselectableTextView: UITextView {
  2. override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
  3. guard let position = closestPosition(to: point) else { return false }
  4. guard let range = tokenizer.rangeEnclosingPosition(
  5. position,
  6. with: .character,
  7. inDirection: UITextDirection(rawValue: UITextLayoutDirection.left.rawValue)
  8. ) else { return false }
  9. let startIndex = offset(from: beginningOfDocument, to: range.start)
  10. return attributedText.attribute(.link, at: startIndex, effectiveRange: nil) != nil
  11. }
  12.  
  13. override func becomeFirstResponder() -> Bool {
  14. return false
  15. }
  16. }


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


  1. class ViewController: UIViewController {
  2.  
  3. @IBOutlet weak var textView: UnselectableTextView!
  4. override func viewDidLoad() {
  5. super.viewDidLoad()
  6. setupViews()
  7. }
  8.  
  9. private func setupViews() {
  10. textView.delegate = self
  11. textView.isEditable = false
  12. textView.isSelectable = true
  13. textView.backgroundColor = .clear
  14.  
  15. let attributedString = NSMutableAttributedString(string: "this textview has a link test.")
  16. attributedString.addAttribute(.link, value: "https://www.google.com", range: NSRange(location: 20, length: 4))
  17. textView.attributedText = attributedString
  18. }
  19. }
  20.  
  21. extension UIViewController: UITextViewDelegate {
  22. public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
  23. print(URL.absoluteString)
  24. return false
  25. }
  26. }


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

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


  1. class ViewController: UIViewController {
  2. private func setupGesture() {
  3. let longPressGesture = UILongPressGestureRecognizer(
  4. target: self,
  5. action: #selector(ViewController.longPress(_:))
  6. )
  7. longPressGesture.delegate = self
  8. backgroundView.addGestureRecognizer(longPressGesture)
  9. }
  10. }
  11.  
  12. extension UIViewController: UIGestureRecognizerDelegate {
  13. @objc func longPress(_ sender: UILongPressGestureRecognizer) {
  14. print(sender.state.rawValue.description)
  15. }
  16. }

2021年7月5日月曜日

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

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

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

Viewの構成

Viewの構成は、UIScrollViewの中にコンテンツ用のUIViewを入れるのみです。
  1. private let scrollView = UIScrollView()
  2. private let contentView = UIView()
  3.  
  4.  
  5. private func setupViews() {
  6. scrollView.addSubview(contentView)
  7. addSubview(scrollView)
  8. }

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

  1. func setup(_ view: UIView) {
  2. contentView.addSubview(view)
  3. view.snp.makeConstraints {
  4. $0.edges.equalTo(contentView)
  5. }
  6. }



制約をセットする

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


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



完成形のコード

上記の全てを組み合わせると、次のようなコードになります。
  1. final class HorizontalScrollView: UIView {
  2. private let scrollView = UIScrollView()
  3. private let contentView = UIView()
  4.  
  5. override init(frame: CGRect) {
  6. super.init(frame: frame)
  7. setupViews()
  8. setupConstraint()
  9. }
  10.  
  11. required init?(coder aDecoder: NSCoder) {
  12. super.init(coder: aDecoder)
  13. setupViews()
  14. setupConstraint()
  15. }
  16.  
  17. private func setupViews() {
  18. scrollView.addSubview(contentView)
  19. addSubview(scrollView)
  20. }
  21.  
  22. private func setupConstraint() {
  23. scrollView.snp.makeConstraints {
  24. $0.edges.equalToSuperview()
  25. }
  26. contentView.snp.makeConstraints {
  27. $0.height.equalTo(scrollView.frameLayoutGuide)
  28. // aligin center when content width < frame width
  29. // aligin left when content width > frame width
  30. $0.top.right.bottom.equalTo(scrollView.contentLayoutGuide)
  31. $0.left.equalTo(scrollView.contentLayoutGuide).priority(.low)
  32. $0.left.greaterThanOrEqualTo(scrollView.contentLayoutGuide)
  33. $0.centerX.greaterThanOrEqualToSuperview()
  34. }
  35. }
  36.  
  37. func setup(_ view: UIView) {
  38. contentView.addSubview(view)
  39. view.snp.makeConstraints {
  40. $0.edges.equalTo(contentView)
  41. }
  42. }
  43. }