[iOS][Swift3] ニュース系アプリのユーザインタフェース PageMenuKit の実装

# はじめに
[ニュース系アプリのユーザインタフェースの実装](http://qiita.com/magickworx/items/1a0972714f451cfd9ebb)で、Objective-C での実現方法について記した。 ようやく Swift を学び始めたので、練習用に先の記事で紹介した [PageMenuController](https://github.com/magickworx/PageMenuController) を Swift3 で書き直し、**PageMenuKit** としてフレームワーク化してみた。

# PageMenuKit の設計と実装

完成版は github に [PageMenuKitSwift](https://github.com/magickworx/PageMenuKitSwift "PageMenuKitSwift") として公開中。
すぐに動かしてみたい人は、上記 URL をクリックしてダウンロードするか clone するかして、Xcode でビルドして、 iOS Simulator で実行してね。

## メニュー画面の設計

PageMenuController と同じく、画面上部で横スクロールするメニューを実現し、スタイル指定でメニュー部分を切り替え可能な仕組みを採用する。また、メニュー部分は汎用的な親クラス **PMKPageMenuItem** を設計する。各スタイルごとのメニュー表示は PMKPageMenuItem のサブクラスとして実装する。

今回実装するメニュースタイルの定義を以下に示す。とりあえず、iPhone 実機にインストール済みのニュース系アプリのインタフェースを全部実装してみる。

```PMKPageMenuItem.swift
public enum PMKPageMenuControllerStyle: Int {
  case Plain   = 1 // NewsPass  [https://itunes.apple.com/jp/app/id1106788059]
  case Tab     = 2 // Gunosy    [https://itunes.apple.com/jp/app/id590384791]
  case Smart   = 3 // SmartNews [https://itunes.apple.com/jp/app/id579581125]
  case Hacka   = 4 // Hackadoll [https://itunes.apple.com/jp/app/id888231424]
  case Web     = 5 // JCNews    [https://jcnews.tokyo/)
  case Ellipse = 6 // JCNews    [https://itunes.apple.com/jp/app/id1024341813]
  case Suite   = 7 // NewsSuite [https://itunes.apple.com/jp/app/id1176431318]
  case NetLab  = 8 // NLab      [https://itunes.apple.com/jp/app/id949325541]
  case NHK     = 9 // NHK NEWS  [https://itunes.apple.com/jp/app/id1121104608]
}

```
+ メニュー画面は UIScrollView に PMKPageMenuItem を追加する方法で実装する。
+ メニューのタイトルや背景色などの装飾は PMKPageMenuItem で実現する。
+ メニューのタップによるコンテンツ切り替えは PMKPageMenuItem 上での UITapGestureRecognizer を処理する。

## ページ毎のコンテンツの管理方針

+ メニューのタップで切り替わるコンテンツ部分は UIPageViewController を利用する。
+ コンテンツ画面上での左右へのスワイプ操作での画面切り替えは、UIPageViewController の機能をそのまま利用する。
+ UIScrollView と UIPageViewController を管理する PMKPageMenuController を実装する。
+ メニュー画面とコンテンツの境界線と選択されたメニュー項目を示すインジケータの管理も PMKPageMenuController 側で行う。

実際に PMKPageMenuKit を利用する際は、PMKPageMenuController のインスタンスを作成する。
**PMKPageMenuController** は UIViewController のサブクラスで、 initializer は以下の仕様である。


```PMKPageMenuController.swift
public init(controllers: [UIViewController],
              menuStyle: PMKPageMenuControllerStyle,
             menuColors: [UIColor],
           topBarHeight: CGFloat)
```
- __controllers__ 配列は、コンテンツ部分の UIViewController サブクラスである。
- __menuStyle__ は、上述したメニュースタイルのいずれかを指定する。
- __menuColors__ 配列は、メニューのテキストや背景に利用する色を指定する。
- __topBarHeight__ は、statusBar や navigationBar の高さを指定する。

menuColors に [] を指定すると、デフォルトの配色を利用する。通常は、[] の指定で良いだろう。


### アプリでの実装例（抜粋）

```swift
class RootViewController: BaseViewController
{
  var pageMenuController: PMKPageMenuController? = nil

  override func viewDidLoad() {
    super.viewDidLoad()

    var controllers: [UIViewController] = []
    let dateFormatter = DateFormatter()
    for month in dateFormatter.monthSymbols {
      let viewController: DataViewController = DataViewController()
      viewController.title = month
      controllers.append(viewController)
    }

    let statusBarHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
    pageMenuController = PMKPageMenuController(controllers: controllers, menuStyle: .Smart, menuColors: [], topBarHeight: statusBarHeight)
    self.addChildViewController(pageMenuController!)
    self.view.addSubview(pageMenuController!.view)
    pageMenuController?.didMove(toParentViewController: self)
  }

}
```

initializer の引数 menuStyle の指定を変えることで、簡単にメニューを変更できる。以下、デフォルト配色時のメニュースタイルと表示例。

### .Plain
[ニュースパス](https://itunes.apple.com/jp/app/id1106788059) っぽいメニュー画面

![.Plain](https://qiita-image-store.s3.amazonaws.com/0/62583/d128c132-d1bf-4dae-acc2-61d31bd7308b.png "ニュースパス")

### .Tab
[グノシー](https://itunes.apple.com/jp/app/id590384791) っぽいメニュー画面

![.Tab](https://qiita-image-store.s3.amazonaws.com/0/62583/09f38507-8f33-22d3-a392-5da4ba879218.png "グノシー")

### .Smart
[SmartNew](https://itunes.apple.com/jp/app/id579581125) っぽいメニュー画面

![.Smart](https://qiita-image-store.s3.amazonaws.com/0/62583/c103202c-f265-4974-3060-c7d120237f48.png "SmartNews")

### .Hacka
[ハッカドール](https://itunes.apple.com/jp/app/id888231424) っぽいメニュー画面

![.Hacka](https://qiita-image-store.s3.amazonaws.com/0/62583/768b1ffe-641a-63bc-f262-078ba3655d33.png "ハッカドール")

### .Ellipse
[JCnews](https://itunes.apple.com/jp/app/id1024341813) っぽいメニュー画面

![.Ellipse](https://qiita-image-store.s3.amazonaws.com/0/62583/216da3a1-c708-27c4-b185-053d9a9a79fd.png "JCnews iOS App")

### .Web
[JCnews](https://jcnews.tokyo/) のウェブサイトっぽいメニュー画面

![.Web](https://qiita-image-store.s3.amazonaws.com/0/62583/231ffc82-e981-c9a3-e4f1-871a7288012b.png "jcnews.tokyo")

### .Suite
[News Suite](https://itunes.apple.com/jp/app/id1176431318) っぽいメニュー画面（メニューの背景色はグラデーション）

![.Suite](https://qiita-image-store.s3.amazonaws.com/0/62583/7186e125-5e3a-5eab-bd62-7b895bca956b.png "News Suite")

### .NetLab
[ねとらぼ](https://itunes.apple.com/jp/app/id949325541) っぽいメニュー画面（メニューの背景色は透明）

![.NetLab](https://qiita-image-store.s3.amazonaws.com/0/62583/b53504dc-0bdd-7ba6-b73e-9bd35862fdbc.png "ねとらぼ")


### .NHK
[NHK ニュース・防災](https://itunes.apple.com/jp/app/id1121104608) っぽいメニュー画面

![tab_NHK.png](https://qiita-image-store.s3.amazonaws.com/0/62583/9c326ad2-8f04-3e63-cfb1-1fca7b033781.png)






## Delegate

PMKPageMenuController で利用可能な Delegate は次の4つ。現時点では、メニュースタイル **.Hacka** 時のバッジ表示に利用している。

```PMKPageMenuControllerDelegate.swift
public protocol PMKPageMenuControllerDelegate: class
{
  // ページ画面上でスワイプ操作による切り替えが行われる前に呼び出される
  func pageMenuController(_ pageMenuController: PMKPageMenuController, willMoveTo viewController: UIViewController, at menuIndex: Int)
  // ページの切り替えが完了した際に呼び出される
  func pageMenuController(_ pageMenuController: PMKPageMenuController, didMoveTo viewController: UIViewController, at menuIndex: Int)

  // メニュー項目の作成などが完了した際に呼び出される
  func pageMenuController(_ pageMenuController: PMKPageMenuController, didPrepare menuItems: [PMKPageMenuItem])
  // メニューがタップされた際に呼び出される
  func pageMenuController(_ pageMenuController: PMKPageMenuController, didSelect menuItem: PMKPageMenuItem, at menuIndex: Int)
}
```

## メニュー画面のカスタム実装

あらたに自分でメニュー画面を追加する場合は、まず次の項目を検討する。
1. メニュー画面の文字色
2. メニュー画面の背景色
3. メニュー画面とコンテンツ画面の境界線の有無と高さ
4. メニュー項目選択時のインジケータの有無と高さ

以上を決めたら、作成したいメニュー項目のデザインに一番近いメニュースタイルのサブクラスを探す。現在、9種類のメニュースタイルを提供しているが、作りたいメニュー画面は、これらのいずれかをベースに実装するのが簡単である。例えば、メニュー項目の背景をグラデーションにしたい場合は、**.Suite** スタイルの PMKPageMenuItemSuite.swift を、背景色が透明のメニュー画面なら **.NetLab** スタイルの PMKPageMenuItemNetLab.swift をベースに選択すると良い。

以下、 PMKPageMenuItemCustom.swift を実装する手順の概要を示す。

### PMKPageMenuControllerStyle の登録

PMKPageMenuItem.swift 内にあらたに作成するメニューのスタイル名とクラス名を登録する。
ここでは、スタイル名 Custom で識別番号は 999、クラス名は PMKPageMenuItemCustom とする。

```PMKPageMenuItem.swift
public enum PMKPageMenuControllerStyle: Int {
  case Plain   = 1 // NewsPass  [https://itunes.apple.com/jp/app/id1106788059]
  case Tab     = 2 // Gunosy    [https://itunes.apple.com/jp/app/id590384791]
  case Smart   = 3 // SmartNews [https://itunes.apple.com/jp/app/id579581125]
  case Hacka   = 4 // Hackadoll [https://itunes.apple.com/jp/app/id888231424]
  case Web     = 5 // JCNews    [https://jcnews.tokyo/)
  case Ellipse = 6 // JCNews    [https://itunes.apple.com/jp/app/id1024341813]
  case Suite   = 7 // NewsSuite [https://itunes.apple.com/jp/app/id1176431318]
  case NetLab  = 8 // NLab      [https://itunes.apple.com/jp/app/id949325541]
  case NHK     = 9 // NHK NEWS  [https://itunes.apple.com/jp/app/id1121104608]
  case Custom  = 999 // あらたに作成するメニュー （必須!!!）

  func className() -> String {
    switch self {
      case .Plain:   return "PMKPageMenuItemPlain"
      case .Tab:     return "PMKPageMenuItemTab"
      case .Smart:   return "PMKPageMenuItemSmart"
      case .Hacka:   return "PMKPageMenuItemHacka"
      case .Web:     return "PMKPageMenuItemWeb"
      case .Ellipse: return "PMKPageMenuItemEllipse"
      case .Suite:   return "PMKPageMenuItemSuite"
      case .NetLab:  return "PMKPageMenuItemNetLab"
      case .NHK:     return "PMKPageMenuItemNHK"
      case .Custom:  return "PMKPageMenuItemCustom" // あらたに作成するクラス名 （必須!!!）
    }
  }

}

```

ページ画面のインスタンスの作成で利用するので、上記コード内の className() 内にクラス名を忘れずに記述すること。


### PMKPageMenuItemCustom.swift の実装

PMKPageMenuItemCustom.swift は 一番シンプルな PMKPageMenuItemPlain.swift をコピーして改良する。次のようにソースコードを修正する（余計なコメントは削除済み）。

init と render 関数が必須である。render(active: Bool) はメニューの選択時／非選択時の見た目を設定する関数である。今は、メニュー選択時は文字色が init() で指定された design 変数の titleColor を使い、非選択時は .darkGray 色を使う。また、選択／非選択に関わらず、背景色は透明である。ここで、 .darkGray を .lightGray に変更すれば、非選択時のメニューの文字色は、より薄くなる。 PMKPageMenuItemDesign は、メニュー画面の文字色や背景色などを保持するクラスである。

```PMKPageMenuItemCustom.swift
public class PMKPageMenuItemCustom: PMKPageMenuItem {
  public required init(coder  aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  public override init(frame: CGRect, title: String, design: PMKPageMenuItemDesign) {
    super.init(frame: frame, title: title, design: design)

    self.style = .Custom // 自作メニューのスタイル名を指定

    self.titleColor = design.titleColor
  }

  override func render(active: Bool) {
    if (active) {
      self.label?.textColor = self.design?.titleColor
    }
    else {
      self.label?.textColor = .darkGray
    }
    self.label?.backgroundColor = .clear
  }
}

```

### 境界線（menuSeparator）とインジケータ（menuIndicator）の設定

メニュー画面によっては、境界線と選択したメニュー項目を指示するインジケータを表示する場合がある。これらの設定は、PMKPageMenuController.swift の prepareForMenuStyle() 関数で行う。下記ソースコード中の itemMargin はメニュー項目間の隙間である。0 にした場合は、 **.Smart** スタイルのようにメニュー項目同士がぴったりくっついて表示される。

また、separatorHeight は、境界線の太さ（高さ）であり、indicatorHeight はインジケータの太さ（高さ）である。これらの値を変えることで、メニュー画面の見た目を変更できる。 **.Custom** スタイルでは、境界線をなくして、インジケータのみの表示とした。


```PMKPageMenuController.swift
func prepareForMenuStyle(_ menuStyle: PMKPageMenuControllerStyle) {
  switch (menuStyle) {
    case .Plain:
      self.itemMargin = kMenuItemMargin
      self.separatorHeight = 1.0
      self.indicatorHeight = 3.0
      break
    ...
    case .Custom:
      self.itemMargin = 0.0
      self.separatorHeight = 0.0
      self.indicatorHeight = 2.0
  }
}
```

境界線の色の設定は、prepareForMenuSeparator() 関数で行う。 
**.Custom** スタイルでは、先に separatorHeight = 0.0 で境界線を無くしたので今回は利用しないが、**.purple** 色に設定しておくこともできる。ただし、太さが 0 なので意味は無い。

```PMKPageMenuController.swift
func prepareForMenuSeparator() {
  if var menuColor: UIColor = self.menuColors.first {
    ...

    switch (menuStyle) {
      case .Plain, .Web:
        menuColor = .orange
      case .Hacka:
        menuColor = UIColor.hexColor(kHackaHexColor)
      case .Custom:
        menuColor = .purple
      default: break
    }
    ...
  }
}

```

インジケータの色の設定は、prepareForMenuIndicator() 関数で行う。
**.Custom** スタイルでは、**.purple** 色を設定する。

```PMKPageMenuController.swift
func prepareForMenuIndicator() {
  ...
  var color: UIColor? = self.menuColors.first
  switch (menuStyle) {
    case .Plain:
      color = .orange
    ...
      case .Custom:
        color = .purple
    default:
      break
  }
  ...
}
```

### .Custom スタイル指定時のメニュー画面の作成コード

最後に **.Custom** スタイルを指定した際に実際にメニュー画面を作成するコードを prepareForMenuItems() 関数内に記述する。ここでは、メニュー画面の文字色あるいは背景色に利用されるメインカラーである **.purple** を指定する。

```PMKPageMenuController.swift
func prepareForMenuItems() {
  self.menuItems = []

  ...
  let count: Int = self.titles.count
  for i in 0 ..< count {
    let frame: CGRect = CGRect(x: x, y: y, width: w, height: h)
    let title: String = self.titles[i]
    let color: UIColor = .clear
    if self.menuColors.count == 0 { // 色指定がない場合はデフォルト色を使用
      switch (menuStyle) {
        case .Plain:
          color = .orange
        case .Hacka:
          color = UIColor.hexColor(kHackaHexColor)
        ...
        case .Custom:
            color = .purple
        default:
          color = self.menuColor(at: i)
      }
    }
    else {
      ...
    }
    ...
  }
  ...
}
```

以上で、カスタムメニュー画面の実装は完了である。

カスタムメニュー画面作成の要点は次の5つ。

- PMKPageMenuItem.swift 内にスタイルを定義する
- PMKPageMenuController.swift 内の prepareForMenuStyle() 関数で境界線やインジケータのパラメータ値を設定する
- 同様に prepareForMenuSeparator() 関数で境界線の色を指定する
- 次いで prepareForMenuIndicator() 関数でインジケータの色を指定する
- 最後に prepareForMenuItems() 関数で menuColors: [] に対する初期設定コードを記述する

なお、カスタムメニュー実装時に修正する必要がある箇所には、ソースコード内に **CUSTOM:** 付きのコメントがある。
でも実際にソースコードを見た方が、理解しやすいと思う。

# ダウンロード

[Xcode Project 一式](https://github.com/magickworx/PageMenuKitSwift "PageMenuKit Frameworks") は、github から入手可能。
なお、上記 Project では、[Xcode7.3 & SwiftでつくるCocoaTouch Framework（作成編）](http://qiita.com/mfks17/items/b1ae615744e311a7f9eb) の記事を参考にして、フレームワークをビルドできるようにした。そのため、簡単に自作アプリに PageMenuKit を組み込めると思う。

# おわりに

iOS アプリで PageMenuKit で実装していないメニュー画面などを見つけたら、実装してみるので教えて欲しいかな。あるいは、実装したソースコードを github 側に pull してもらうのも嬉しい。