Created
February 12, 2018 04:35
-
-
Save KrisYu/3b0490081affce3fa49bc34c9b106100 to your computer and use it in GitHub Desktop.
explains HorizontalScrollerView in https://www.raywenderlich.com/160653/design-patterns-ios-using-swift-part-22
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// HorizontalScrollerView.swift | |
// RWBlueLibrary | |
// | |
// Created by Xue Yu on 2/4/18. | |
// Copyright © 2018 Razeware LLC. All rights reserved. | |
// | |
import UIKit | |
// 把 DataSource 和 Delegate 协议分开,这样更优雅,并且可以减少不必要的 @objc 的使用 | |
// DataSource | |
protocol HorizontalScrollerViewDataSource: class { | |
// 在可以水平滚动的View里面有多少个view | |
func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int | |
// 既然是DataSource,滚到某个index的时候view | |
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView | |
} | |
// Delegate | |
protocol HorizontalScrollerViewDelegate: class { | |
// 选择某个view的时候要做啥 | |
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int) | |
} | |
// 以上就可以看出来UITableView基本上是模板王者级别的,DataSource 和 Delegate 方法都跟它很像 | |
// 定义 | |
class HorizontalScrollerView: UIView { | |
//惯例 : weak var | |
weak var dataSource: HorizontalScrollerViewDataSource? | |
weak var delegate: HorizontalScrollerViewDelegate? | |
// 用 enum 来组织 Constants | |
private enum ViewConstants { | |
// Padding : 与边缘的距离 | |
static let Padding: CGFloat = 10 | |
// Dimensions : 大小 | |
static let Dimensions: CGFloat = 100 | |
// Offset :间距 | |
static let Offset: CGFloat = 100 | |
} | |
// 可滚动的灵魂 : scrollView | |
private let scroller = UIScrollView() | |
// 滚动内容的灵魂 : UIView们 | |
private var contentViews = [UIView]() | |
// 这个init会在 programmatic init view时候被调用 | |
override init(frame: CGRect) { | |
super.init(frame: frame) | |
initializeScrollView() | |
} | |
// 这个会在storyboard init是后被调用 | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
initializeScrollView() | |
} | |
// 某个意义上来说真正的init | |
func initializeScrollView(){ | |
// HorizontalVie 是 scrollView 的delegate | |
// 因为我们需要滚 scrollView 然后调用一些方法 | |
scroller.delegate = self | |
// 把 scroller 添加到HorizontalView | |
addSubview(scroller) | |
// 这个属性设置为false,我们才可以让scoller遵循以下的LayoutConstraint | |
scroller.translatesAutoresizingMaskIntoConstraints = false | |
NSLayoutConstraint.activate([ | |
scroller.leadingAnchor.constraint(equalTo: self.leadingAnchor), | |
scroller.trailingAnchor.constraint(equalTo: self.trailingAnchor), | |
scroller.topAnchor.constraint(equalTo: self.topAnchor), | |
scroller.bottomAnchor.constraint(equalTo: self.bottomAnchor) | |
]) | |
// 添加 tapRecognizer, 这样我们点击里面的某一个子view的时候,调用scrollerTapped 方法 | |
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(scrollerTapped(gesture:))) | |
scroller.addGestureRecognizer(tapRecognizer) | |
} | |
// 这里做计算,centralView 是我们要把它移到中间去的View, 计算出offset,然后我们利用scroller把它移动到它该在的位置 | |
// 给下面scrollerTapped用 | |
func scrollToView(at index: Int, animated: Bool = true) { | |
let centralView = contentViews[index] | |
let targetCenter = centralView.center | |
let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2) | |
scroller.setContentOffset(CGPoint(x: targetOffsetX, y: 0), animated: animated) | |
} | |
// 判定,如果 tap 的点在contentViews的某个,那么我们调用方法,告诉delegate 这个 view被选中了,同时我们把它移到中间去 | |
@objc func scrollerTapped(gesture: UITapGestureRecognizer) { | |
let location = gesture.location(in: scroller) | |
guard | |
let index = contentViews.index(where: { $0.frame.contains(location) }) | |
else { return } | |
delegate?.horizontalScrollerView(self, didSelectViewAt: index) | |
scrollToView(at: index) | |
} | |
// 便利方法,给上面的 scrollerTapped 用 | |
func view(at index: Int) -> UIView { | |
return contentViews[index] | |
} | |
// 类似UITableView,如果 dataSource 改变,那么我们这个HorizontalView里面的东西也要改变 | |
func reload() { | |
guard let dataSource = dataSource else { | |
return | |
} | |
// 移走原本的contentViews | |
contentViews.forEach { $0.removeFromSuperview() } | |
// 开始使用一系列常数计算, xValue是每个子View的水平位置 | |
var xValue = ViewConstants.Offset | |
contentViews = (0..<dataSource.numberOfViews(in: self)).map { | |
index in | |
xValue += ViewConstants.Padding | |
let view = dataSource.horizontalScrollerView(self, viewAt: index) | |
view.frame = CGRect(x: CGFloat(xValue), y: ViewConstants.Padding, width: ViewConstants.Dimensions, height: ViewConstants.Dimensions) | |
// 计算出view的坐标, 添加到 scroller | |
scroller.addSubview(view) | |
xValue += ViewConstants.Dimensions + ViewConstants.Padding | |
return view | |
} | |
//这是极其重要的,如果忘了指定这个,scroller的默认大小就是 (0,0),不会显示 | |
scroller.contentSize = CGSize(width: CGFloat(xValue + ViewConstants.Offset), height: frame.size.height) | |
} | |
private func centerCurrentView() { | |
let centerRect = CGRect( | |
origin: CGPoint(x: scroller.bounds.midX - ViewConstants.Padding, y: 0), | |
size: CGSize(width: ViewConstants.Padding, height: bounds.height) | |
) | |
guard let selectedIndex = contentViews.index(where: { $0.frame.intersects(centerRect)}) | |
else { return } | |
let centralView = contentViews[selectedIndex] | |
let targetCenter = centralView.center | |
let targetOffsetX = targetCenter.x - (scroller.bounds.width / 2) | |
scroller.setContentOffset(CGPoint(x: targetOffsetX, y:0), animated: true) | |
delegate?.horizontalScrollerView(self, didSelectViewAt: selectedIndex) | |
} | |
} | |
// 这个是用于当我们scroll 这个 scroller,会自动center CurrentView | |
// 实际上如果我们不使用 scroller.delegate = self | |
// 和如下方法,App某种程度上也是运行正常,但是水平滑动,并不会改变目前选中的view,并不是很科学 | |
// 但是这个方法会“自动”设置滑动给应该中间的专辑到屏幕中央 | |
extension HorizontalScrollerView: UIScrollViewDelegate { | |
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { | |
if !decelerate { | |
centerCurrentView() | |
} | |
} | |
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { | |
centerCurrentView() | |
} | |
} | |
-------------------------------------------------------------------------------- | |
Extension in ViewController | |
extension ViewController: HorizontalScrollerViewDelegate { | |
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, didSelectViewAt index: Int) { | |
// 当我们选中一个view的时候,把之前选中的view highlight 给变成fasle,把我们选中的view highlight 变成 true,并且让对应的 UITableView 显示专辑数据 | |
let previousAlbumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView | |
previousAlbumView.highlightAlbum(false) | |
currentAlbumIndex = index | |
let albumView = horizontalScrollerView.view(at: currentAlbumIndex) as! AlbumView | |
albumView.highlightAlbum(true) | |
showDataForAlbum(at: index) | |
} | |
} | |
extension ViewController: HorizontalScrollerViewDataSource { | |
// DataSource 方法,同时也hightlight当前选中专辑index | |
func numberOfViews(in horizontalScrollerView: HorizontalScrollerView) -> Int { | |
return allAlbums.count | |
} | |
func horizontalScrollerView(_ horizontalScrollerView: HorizontalScrollerView, viewAt index: Int) -> UIView { | |
let album = allAlbums[index] | |
let albumView = AlbumView(frame: CGRect(x: 0, y: 0, width: 100, height:100), coverUrl: album.coverUrl) | |
if currentAlbumIndex == index { | |
albumView.highlightAlbum(true) | |
} else { | |
albumView.highlightAlbum(false) | |
} | |
return albumView | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment