iOS
Swift
MaterialDesign

[MDC]TabBarのindicatorの高さを変更する

Material Design Components for iOSMDCTabBarを使っていたのですが、タブ下のインジケータがデフォルトで2ptというとても細い線しか描画できず困っていました。
が、40.1.0より自由に変更できるようになっていました。

使い方
let tabBar = MDCTabBar()
tabBar.items = ....
tabBar.selectionIndicatorTemplate = MDCTabBarUnderlineIndicatorTemplate()

アンダーラインはMDCTabBarUnderlineIndicatorTemplateとしてデフォルトで用意してありますが、高さが2.0fと固定されているため、高さを変更する場合は独自で用意する必要があります。

TabIndicator.swift
import Foundation
import MaterialComponents

class TabIndicator: NSObject, MDCTabBarIndicatorTemplate {
    /// タブインジケーター高さ
    private let underlineHeight: CGFloat = 4.0
    func indicatorAttributes(for context: MDCTabBarIndicatorContext) -> MDCTabBarIndicatorAttributes {
        let bounds = context.bounds
        let attributes = MDCTabBarIndicatorAttributes()
        let underlineFrame = CGRect(x: bounds.minX,
                                    y: bounds.maxY - underlineHeight,
                                    width: bounds.width,
                                    height: underlineHeight)
        attributes.path = UIBezierPath(rect: underlineFrame)
        return attributes
    }
}

MDCTabBarIndicatorTemplateを継承し、indicatorAttributesを実装してその中で自由にインジケータを描画してあげればOKです。

ただし、注意点としてtabのitemsをsetした後に設定しないと反映されません。
(上の例だと、boundsがzeroになってしまいます)
そのため、私は下記のようにカスタムクラスを用意しています。

TabBar.swift
import MaterialComponents

class TabBar: MDCTabBar {
    override var items: [UITabBarItem] {
        get {
            return super.items
        }
        set {
            super.items = newValue
            /// itemsがセットされた後でないと反映されません
            selectionIndicatorTemplate = TabIndicator()
        }
    }
}

また、underlineだけではなく、色々なインジケータを表現できます。
下記はサンプルから拝借しました。

SampleIndicatorTemplate.swift
class IndicatorTemplate: NSObject, MDCTabBarIndicatorTemplate {
    func indicatorAttributes(for context: MDCTabBarIndicatorContext) -> MDCTabBarIndicatorAttributes {
      let attributes = MDCTabBarIndicatorAttributes()
      // Outset frame, round corners, and stroke.
      let indicatorFrame = context.contentFrame.insetBy(dx: -8, dy: -4)
      let path = UIBezierPath(roundedRect: indicatorFrame, cornerRadius: 4)
      attributes.path = path.stroked(withWidth: 2)
      return attributes
    }
}

extension UIBezierPath {
  /// Returns a copy of the path, stroked with the given line width.
  func stroked(withWidth width: CGFloat) -> UIBezierPath {
    let strokedPath = cgPath.copy(
      strokingWithWidth: width,
      lineCap: .butt,
      lineJoin: .miter,
      miterLimit: 0)
    return UIBezierPath(cgPath: strokedPath)
  }
}

上記例だと下記のようになります。
sample.png

アンダーラインの高さを変えたいだけなのに手間がかかってしまいますが、自由度は高いので色々面白いこともできそうです。