效果圖

image
知識點
- 學習 UIScrollView 和 UIPageControl 的基本用法
- 學習 三個 UIImageView 實現無限循環滾動思想
- 學習 我的 UIView 常用擴展
- 學習 給我們的視圖設置代理事件
主要設計思想
概要介紹我在設計此控件的一些具體步驟與思想
- 創建 UIScrollView 作為滾動視圖的容器;
- 創建 UIPageControl 作為顯示頁數以及總頁數的組件
- 在 UIScrollView 中創建三個 UIImageView 分別作為 上一頁 當前頁 下一頁
- 通過綁定數據 data 的 didSet 來動態部署 UIScrollView 中的 UIImageView 視圖。(實現無限循環)
- 通過 Timer 實現定時跳轉到下一個視圖
- 調用 UIScrollView 擴張 extension 實現具體操作細節
教程開始
- UIScrollView 使用
博主習慣創建組件的方式如下:
// fileprivate 是 Swift 3.0 新增加的訪問控制權限:文件內訪問
// 封裝控件時,我們要主動的隱藏掉具體的實現,只開放使用接口即可
fileprivate var scrollView: UIScrollView = {
let object = UIScrollView()
// 是否可以拉伸滑動
object.bounces = false
// 隱藏 水平和垂直 滾動條
object.showsVerticalScrollIndicator = false
object.showsHorizontalScrollIndicator = false
// 分頁
object.isPagingEnabled = true
return object
}()
- UIPageControl 使用
創建 UIPageControl 組件,構建如下:
fileprivate var pageControl: UIPageControl = {
let object = UIPageControl()
// 單頁面下 隱藏
object.hidesForSinglePage = true
// 當前頁背景色
object.currentPageIndicatorTintColor = UIColor.red
// 其他也背景色
object.pageIndicatorTintColor = UIColor.gray
return object
}()
- 主視圖配置
主要包括:主視圖的屬性配置、添加子視圖
private func prepareUI() {
self.backgroundColor = UIColor.white
self.scrollView.delegate = self
// 添加 滑動試圖
self.addSubview(scrollView)
createScrollView()
// 添加 頁面控制
self.addSubview(pageControl)
}
private func layoutUI() {
scrollView.frame = self.bounds
pageControl.frame = CGRect(x: self.frame.width - 85, y: self.frame.height - 25, width: 80, height: 20)
scrollView.contentSize = CGSize(width: CGFloat(imageCount) * viewSize.width, height: viewSize.height)
}
- 構建 UIScrollView 子視圖
重點:通常情況下,開發者可能根據圖片數組的數量在 UIScrollView 中創建對應數量的 UIImageView 擴充 UIScrollView 的 contentSize 實現所有圖片的滑動;這種設計思路比較常規,但有兩個問題:1.如果圖片數據過大,此控件將占用過大的內存去創建視圖;2.很難實現無限循環滑動。
我的思想:在 UIScrollView 中只創建三個 UIImageView 作為 上一頁圖片 當前頁圖片 下一頁圖片 的容器。通過判斷當前頁所在 圖片數據中的位置,動態為三個 UIImageView 填充指定圖片。
// 創建 滑動視圖子視圖
func createScrollView() {
for i in 0 ..< 3 {
let imageView: UIImageView = {
let object = UIImageView()
object.isUserInteractionEnabled = true
object.contentMode = UIViewContentMode.scaleToFill
return object
}()
imageView.frame = CGRect(x: CGFloat(i) * viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
let tap = UITapGestureRecognizer(target: self, action: #selector(touchImage))
imageView.addGestureRecognizer(tap)
scrollView.addSubview(imageView)
}
}
- 重點:動態更新 UIImageView 視圖實現無限滾動
核心知識:如果通過三個 UIImageView 實現 無限圖片的 無限循環滑動:
func updateScrollView() {
// 遍歷三次
for i in 0 ..< 3 {
獲取 UIScrollView 中 UIImageView
let imageView = scrollView.subviews[i] as! UIImageView
// 獲取 當前展示的圖片是 序號
var index = pageControl.currentPage
// 如果 UIImageView UIScrollView 中的第二個視圖,即 上一個圖片
if i == 0 {
// 則 index 等于 當前圖片序號 - 1
index -= 1
}
// 如果 UIImageView 是 UIScrollView 中的第三個視圖,即下一個圖片
if i == 2 {
// 則 index 等于 當前圖片序號 + 1
index += 1
}
// 越界操作 操作
if index < 0 {
index = pageControl.numberOfPages - 1
}
if index >= pageControl.numberOfPages {
index = 0
}
imageView.tag = index
if let currentData = data {
imageView.image = UIImage(named: currentData[index].imageUrl!)
}
}
// 調整 UIScrollView 偏移量 使其永遠只顯示中間的 UIImageView
scrollView.contentOffset = CGPoint(x: viewSize.width, y: 0)
}
糊涂的童鞋,奉上我畫的示意圖
假如我們的圖片數據只有 6 張圖片;
紅色代表原始數組,藍色代表我們的 UIScrollView 兩邊越界是 填充收尾圖片

image
最后部分
設置定時器
// MARK: Timer
fileprivate func startTimer() {
let selector = #selector(nextImage)
timer = Timer(timeInterval: 3.0, target: self, selector: selector, userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: RunLoopMode.commonModes)
}
fileprivate func stopTimer() {
timer?.invalidate()
timer = nil
}
擴展完善交互時的事件
extension ImageScrollView: UIScrollViewDelegate {
// 當 scrollView 有偏移量時出發
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var page: Int = 0
var minDistance: CGFloat = CGFloat(MAXFLOAT)
for i in 0 ..< 3 {
let imageView = scrollView.subviews[i] as! UIImageView
// 由于當前頁有用都是中間頁,所以當 偏移量==圖片寬度時,就是當前頁。
let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
if distance < minDistance {
minDistance = distance
page = imageView.tag
}
}
pageControl.currentPage = page
}
// 開始拖動 UIScrollView 事件
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
stopTimer()
}
// 結束拖動 UIScrollView 事件
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
startTimer()
}
//
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updateScrollView()
}
// UIScrollView 結束滾動
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
updateScrollView()
}
}
學會設置代理
具體步驟:
- 定義代理 delegate
- 配置代理事件
- 實例對象,調用對象代理
// 1
@objc protocol ImageScrollViewDelegate {
@objc optional func touchAt(index: Int)
}
// 2
class ImageScrollView: UIView {
var delegate: ImageScrollViewDelegate?
// 觸發代理
func touchImage(tap: UITapGestureRecognizer) {
if let index = tap.view?.tag {
delegate?.touchAt!(index: index)
}
}
...... }
// 3
// 實現代理:在調用 ImageScrollView 控件的 VC 中擴展,具體看源碼
extension ImageScrollViewController: ImageScrollViewDelegate {
func touchAt(index: Int) {
print(index)
}
}
源碼
組件源碼
import UIKit
@objc protocol ImageScrollViewDelegate {
@objc optional func touchAt(index: Int)
}
class ImageScrollView: UIView {
var delegate: ImageScrollViewDelegate?
fileprivate var timer: Timer?
var viewSize: CGSize!
var data: [ImageScrollData]? {
didSet {
if timer != nil {
timer!.invalidate()
timer = nil
}
if let scrollData = data {
pageControl.numberOfPages = scrollData.count
pageControl.currentPage = 0
updateScrollView()
startTimer()
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.frame = frame
viewSize = frame.size
prepareUI()
layoutUI()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func prepareUI() {
self.backgroundColor = UIColor.white
self.scrollView.delegate = self
// 添加 滑動試圖
self.addSubview(scrollView)
createScrollView()
// 添加 頁面控制
self.addSubview(pageControl)
}
private func layoutUI() {
scrollView.frame = self.bounds
pageControl.frame = CGRect(x: self.frame.width - 85, y: self.frame.height - 25, width: 80, height: 20)
scrollView.contentSize = CGSize(width: CGFloat(3) * viewSize.width, height: viewSize.height)
}
// 創建 滑動視圖子視圖
func createScrollView() {
for i in 0 ..< 3 {
let imageView: UIImageView = {
let object = UIImageView()
object.isUserInteractionEnabled = true
object.contentMode = UIViewContentMode.scaleToFill
return object
}()
imageView.frame = CGRect(x: CGFloat(i) * viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
let tap = UITapGestureRecognizer(target: self, action: #selector(touchImage))
imageView.addGestureRecognizer(tap)
scrollView.addSubview(imageView)
}
}
func updateScrollView() {
for i in 0 ..< 3 {
let imageView = scrollView.subviews[i] as! UIImageView
var index = pageControl.currentPage
if i == 0 {
index -= 1
}
if i == 2 {
index += 1
}
if index < 0 {
index = pageControl.numberOfPages - 1
}
if index >= pageControl.numberOfPages {
index = 0
}
imageView.tag = index
if let currentData = data {
imageView.image = UIImage(named: currentData[index].imageUrl!)
}
}
scrollView.contentOffset = CGPoint(x: viewSize.width, y: 0)
}
// MARK: Timer
fileprivate func startTimer() {
let selector = #selector(nextImage)
timer = Timer(timeInterval: 3.0, target: self, selector: selector, userInfo: nil, repeats: true)
RunLoop.main.add(timer!, forMode: RunLoopMode.commonModes)
}
fileprivate func stopTimer() {
timer?.invalidate()
timer = nil
}
func nextImage() {
scrollView.setContentOffset(CGPoint(x: 2.0 * viewSize.width, y: 0), animated: true)
}
func touchImage(tap: UITapGestureRecognizer) {
if let index = tap.view?.tag {
delegate?.touchAt!(index: index)
}
}
// 初始化 滑動視圖
fileprivate var scrollView: UIScrollView = {
let object = UIScrollView()
object.bounces = false
object.showsVerticalScrollIndicator = false
object.showsHorizontalScrollIndicator = false
object.isPagingEnabled = true
return object
}()
fileprivate var pageControl: UIPageControl = {
let object = UIPageControl()
object.hidesForSinglePage = true
object.currentPageIndicatorTintColor = UIColor.red
object.pageIndicatorTintColor = UIColor.gray
return object
}()
}
extension ImageScrollView: UIScrollViewDelegate {
// 當 scrollView 有偏移量時出發
func scrollViewDidScroll(_ scrollView: UIScrollView) {
var page: Int = 0
var minDistance: CGFloat = CGFloat(MAXFLOAT)
for i in 0 ..< 3 {
let imageView = scrollView.subviews[i] as! UIImageView
// 由于當前頁有用都是中間頁,所以當 偏移量==圖片寬度時,就是當前頁。
let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
if distance < minDistance {
minDistance = distance
page = imageView.tag
}
}
pageControl.currentPage = page
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
stopTimer()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
startTimer()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updateScrollView()
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
updateScrollView()
}
}
extension UIView {
/// X值
open var x: CGFloat {
return self.frame.origin.x
}
/// Y值
open var y: CGFloat {
return self.frame.origin.y
}
/// 寬度
open var width: CGFloat {
return self.frame.size.width
}
///高度
open var height: CGFloat {
return self.frame.size.height
}
open var size: CGSize {
return self.frame.size
}
open var origin: CGPoint {
return self.frame.origin
}
}
調用的 ViewController
import UIKit
class ImageScrollViewController: UIViewController {
var imageScrollView = ImageScrollView(frame: CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: 150))
var data = [ImageScrollData]()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.white
edgesForExtendedLayout = .init(rawValue: 0)
self.title = "圖片無限滾動"
self.view.addSubview(imageScrollView)
for i in 1 ... 6 {
let item = ImageScrollData(imageUrl: "image_scroll_0\(i).jpg", imageDescribe: nil)
data.append(item)
}
imageScrollView.data = data
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ImageScrollViewController: ImageScrollViewDelegate {
func touchAt(index: Int) {
print(index)
}
}