既存の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
}