簡易UIPickerViewを自作する

既存のUIPickerViewではサイズ調整がアフィン変換をしたりと面倒なので、自作してみました。今回は実装していませんが、自作すればデータをループにするなどの拡張も手軽にできそうですね。

![シミュレータの動作](https://i.gyazo.com/44458737c8921bd25a904a69c0d3361d.gif)

## 使い方

```ViewController.swift
  override func viewDidLoad() {
    ...
    // view1, ... は SelectionView を継承する

    data += [Data(view: view1, id: "v1"), Data(view: view2, id: "v2"), Data(view: view3, id: "v3"), Data(view: view4, id: "v4")]
    
    pickerView = ExPickerView(frame: CGRectMake(50, 300, 300, 125))
    pickerView.dataSource = self
    pickerView.delegate   = self
    pickerView.reload()
    
  }

...


extension ViewController : ExPickerViewDataSource {
  func numberOfRowsInExPickerView(exPickerView: ExPickerView) -> Int {
    return data.count
  }
}

extension ViewController : ExPickerViewDelegate {
  func exPickerView(exPickerView: ExPickerView, viewForRow row: Int) -> SelectionView {
    return data[row].view
  }
  
  func exPickerView(exPickerView: ExPickerView, didSelectRow row: Int) {
    print("select: \(data[row].id)")
  }
  
  func widthInExPickerView(exPickerView: ExPickerView) -> CGFloat {
    return pickerView.frame.width
  }
  
  func rowHeightInExPickerView(exPickerView: ExPickerView) -> CGFloat {
    return data[0].view.frame.height
  }
}
```

という感じに使います。

しかし、これでは本物のUIPickerViewのように、滑らかにUIViewが拡大縮小しないので出来はイマイチかなぁと思っています。良いアイデアが有れば教えていただけると幸いです。
## コード

```ExPickerView.swift

import UIKit

// スクロール時、各行のヴューが拡大縮小を可能とする
public protocol Scalable {  // For subclass of UIView
  func scaleWithRatio(ratio: CGFloat)
}

// Name: SelectionView: 各行のヴュー(RowViewの方が分かりやすいか？)
// Desc: Scalableプロトコル を持つ UIView なら任意の実装でOK
public class SelectionView : UIView, Scalable {
  
  private var label:      UILabel!
  private var imageView:  UIImageView!
  
  private var labelRawX:          CGFloat!
  private var labelRawPointSize:  CGFloat!
  
  private var imageViewRawX:      CGFloat!
  private var imageViewRawSize:   CGSize!
  
  var text: String! {
    didSet {
      label.text = text
      label.sizeToFit()
      label.frame.origin.y = (frame.height - label.frame.height) / 2
    }
  }
  
  var image: UIImage! {
    didSet {
      imageView.image = image
    }
  }
  
  required public init?(coder aDecoder: NSCoder) { fatalError() }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    label     = UILabel()
    label.frame.origin.x = frame.width * 0.1
    labelRawX = label.frame.origin.x
    labelRawPointSize = label.font.pointSize
    addSubview(label)
    
    imageView = UIImageView()
    imageView.frame.origin.x = frame.width * 0.8
    imageViewRawX = imageView.frame.origin.x
    imageView.frame.size = CGSizeMake(frame.height * 0.9, frame.height * 0.9)
    imageView.frame.origin.y = (frame.height - imageView.frame.size.height) / 2
    imageViewRawSize = imageView.frame.size
    addSubview(imageView)
  }
  
  public func scaleWithRatio(ratio: CGFloat) {
    
    guard 0 <= ratio && ratio <= 1 else {
      print("SelectionView.scaleWithRatio: invalid ratio")
      return
    }
    
    label.font = UIFont(name: label.font.fontName, size: labelRawPointSize * ratio)
    label.sizeToFit()
    label.frame.origin.x = labelRawX + label.frame.width * (1.0 - ratio)
    label.frame.origin.y = (frame.height - label.frame.height) / 2
    
    imageView.frame.size.width  = imageViewRawSize.width  * ratio
    imageView.frame.size.height = imageViewRawSize.height * ratio
    imageView.frame.origin.x    = imageViewRawX - imageView.frame.width / 3 * (1.0 - ratio)
    imageView.frame.origin.y    = (frame.height - imageView.frame.size.height) / 2
    
  }
  
}

// Name: MovingConstrainedView
// Desc: ExPickerView 内の contentView: MovingConstrainedView の移動制限
class MovingConstrainedView : UIView {
  var top:    CGFloat = 0
  var bottom: CGFloat = 0
  
  override var frame: CGRect {
    didSet {
      if frame.origin.y < top {
        frame.origin.y = top
      }
      if frame.maxY > bottom {
        frame.origin.y = bottom - frame.height
      }
    }
  }
}

// Name: ExPickerView
// Desc: 自作UIPickerView本体
//       ExPickerViewDataSource と ExPickerViewDelegate を要求する
public class ExPickerView : UIView {
  
  weak public var dataSource: ExPickerViewDataSource?
  weak public var delegate:   ExPickerViewDelegate?
  
  // returns selected row.
  public var selectedRow: Int = 0
  
  override public init(frame: CGRect) {
    super.init(frame: frame)
    clipsToBounds = true
    layer.borderColor = UIColor.grayColor().CGColor
    layer.borderWidth = 1
    layer.cornerRadius = 2.0
  }
  
  required public init?(coder aDecoder: NSCoder) { fatalError() }
  
  public func viewForRow(row: Int) -> SelectionView? {
    return delegate?.exPickerView(self, viewForRow: row)
  }
  
  // Reloading whole view or single component
  public func reload() {
    
    contentView = MovingConstrainedView()
    addSubview(contentView)
    contentView.top     = bounds.midY - rowHeight / 2 - CGFloat(numberOfRows - 1) * rowHeight
    contentView.bottom  = bounds.midY + rowHeight / 2 + CGFloat(numberOfRows - 1) * rowHeight
    contentView.frame = CGRect(
      x:      0,
      y:      bounds.midY - rowHeight / 2 - CGFloat(selectedRow) * rowHeight,
      width:  width,
      height: CGFloat(numberOfRows) * rowHeight
    )
    
    for row in 0..<numberOfRows {
      let view = viewForRow(row)! ?? SelectionView()
      view.frame.origin.x = (width - view.frame.width) / 2
      view.frame.origin.y = CGFloat(row) * rowHeight + (rowHeight - view.frame.height) / 2
      rowViews[row] = view
      contentView.addSubview(view)
    }
    
    showSelectionViews()
    
    let selectionRowHeight = frame.height
    frontImageView = UIImageView(frame: CGRectMake(0, bounds.midY - selectionRowHeight / 2, width, selectionRowHeight))
    frontImageView.image = UIImage(named: "selection")
    frontImageView.alpha = 0.7
    
//    addSubview(frontImageView)  // SelectionView の前面に表示したい場合
    insertSubview(frontImageView, atIndex: 0) // SelectionView の背面に表示したい場合
    
  }
  
  private func showSelectionViews() {
    for row in 0..<numberOfRows {
      let y = visibleLimitYForRow(row)
      if 0 <= contentView.frame.origin.y + y && contentView.frame.origin.y + y <= bounds.height {
        let view = rowViews[row]!
        view.frame.origin.x = (width - view.frame.width) / 2
        view.frame.origin.y = CGFloat(row) * rowHeight + (rowHeight - view.frame.height) / 2
        
        let reduction = CGFloat(abs(row - containingSelectionViewRow())) * 0.1
        view.scaleWithRatio(1.0 - reduction)
      }
    }
  }
  
  // selection. in this case, it means showing the appropriate row in the middle
  // scrolls the specified row to center.
  public func selectRow(row: Int, animated: Bool) {
    // アニメーションしないのでanimated使ってません。

    contentView.frame.origin.y = bounds.midY - rowHeight / 2 - CGFloat(row) * rowHeight
    
    showSelectionViews()
    
    selectedRow = row
    delegate?.exPickerView(self, didSelectRow: row)
  }
  
  // MARK: - private -
  
  private var contentView:    MovingConstrainedView!
  private var frontImageView: UIImageView!
  private var rowViews        = [Int : SelectionView]()
  
  private func visibleLimitYForRow(row: Int) -> CGFloat {
    return rowHeight * (2 * CGFloat(row) + 1) / 2
  }
  
  private func containingSelectionViewRow() -> Int {
    return Int(floor((bounds.midY - contentView.frame.origin.y) / rowHeight))
  }
  
  private var width: CGFloat {
    get {
      guard let delegate = delegate else { return 200.0 }
      return delegate.widthInExPickerView(self)
    }
  }
  
  private var rowHeight: CGFloat {
    get {
      guard let delegate = delegate else { return 30.0 }
      return delegate.rowHeightInExPickerView(self)
    }
  }
  
  private var numberOfRows: Int {
    get {
      guard let dataSource = dataSource else { return 0 }
      return dataSource.numberOfRowsInExPickerView(self)
    }
  }
  
  private var previousLocationOfTouch: CGPoint!
  
}

// MARK: - touches responder -
extension ExPickerView {
  
  override public func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    super.touchesBegan(touches, withEvent: event)
    let touch = touches.first!
    let location = touch.locationInView(self)
    previousLocationOfTouch = location
  }
  
  override public func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    super.touchesMoved(touches, withEvent: event)
    let touch = touches.first!
    let location = touch.locationInView(self)
    let len = location.y - previousLocationOfTouch.y
    contentView.frame.origin.y += len
    previousLocationOfTouch = location
    showSelectionViews()
  }
  
  override public func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    super.touchesEnded(touches, withEvent: event)
    selectRow(containingSelectionViewRow(), animated: true)
  }
  
}

public protocol ExPickerViewDataSource: class {
  func numberOfRowsInExPickerView(exPickerView: ExPickerView) -> Int
}

public protocol ExPickerViewDelegate: class {
  // dataSource との違いは view に関わる delegate であること
  func exPickerView(exPickerView: ExPickerView, viewForRow row: Int) -> SelectionView
  func exPickerView(exPickerView: ExPickerView, didSelectRow row: Int)
  func widthInExPickerView(exPickerView: ExPickerView) -> CGFloat
  func rowHeightInExPickerView(exPickerView: ExPickerView) -> CGFloat
}
```