LoginSignup
3
4

More than 5 years have passed since last update.

簡易UIPickerViewを自作する

Last updated at Posted at 2015-09-12

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

シミュレータの動作

使い方

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
}
3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4