Swift 3.0 商城開發(fā) —— 圖片滑動(dòng)組件

效果圖

image
image

知識(shí)點(diǎn)

  • 學(xué)習(xí) UIScrollView 和 UIPageControl 的基本用法
  • 學(xué)習(xí) 三個(gè) UIImageView 實(shí)現(xiàn)無限循環(huán)滾動(dòng)思想
  • 學(xué)習(xí) 我的 UIView 常用擴(kuò)展
  • 學(xué)習(xí) 給我們的視圖設(shè)置代理事件

主要設(shè)計(jì)思想

概要介紹我在設(shè)計(jì)此控件的一些具體步驟與思想

  • 創(chuàng)建 UIScrollView 作為滾動(dòng)視圖的容器;
  • 創(chuàng)建 UIPageControl 作為顯示頁數(shù)以及總頁數(shù)的組件
  • 在 UIScrollView 中創(chuàng)建三個(gè) UIImageView 分別作為 上一頁 當(dāng)前頁 下一頁
  • 通過綁定數(shù)據(jù) data 的 didSet 來動(dòng)態(tài)部署 UIScrollView 中的 UIImageView 視圖。(實(shí)現(xiàn)無限循環(huán))
  • 通過 Timer 實(shí)現(xiàn)定時(shí)跳轉(zhuǎn)到下一個(gè)視圖
  • 調(diào)用 UIScrollView 擴(kuò)張 extension 實(shí)現(xiàn)具體操作細(xì)節(jié)

教程開始

  1. UIScrollView 使用

博主習(xí)慣創(chuàng)建組件的方式如下:

// fileprivate 是 Swift 3.0 新增加的訪問控制權(quán)限:文件內(nèi)訪問
// 封裝控件時(shí),我們要主動(dòng)的隱藏掉具體的實(shí)現(xiàn),只開放使用接口即可
fileprivate var scrollView: UIScrollView = {
    let object = UIScrollView()
    // 是否可以拉伸滑動(dòng)
    object.bounces = false
    // 隱藏 水平和垂直 滾動(dòng)條
    object.showsVerticalScrollIndicator = false
    object.showsHorizontalScrollIndicator = false
    // 分頁
    object.isPagingEnabled = true
    return object
}()
  1. UIPageControl 使用

創(chuàng)建 UIPageControl 組件,構(gòu)建如下:

fileprivate var pageControl: UIPageControl = {
    let object = UIPageControl()
    // 單頁面下 隱藏
    object.hidesForSinglePage = true
    // 當(dāng)前頁背景色
    object.currentPageIndicatorTintColor = UIColor.red
    // 其他也背景色
    object.pageIndicatorTintColor = UIColor.gray
    return object
}()
  1. 主視圖配置

主要包括:主視圖的屬性配置、添加子視圖

private func prepareUI() {
    self.backgroundColor = UIColor.white
    self.scrollView.delegate = self
    // 添加 滑動(dòng)試圖
    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)
}

  1. 構(gòu)建 UIScrollView 子視圖

重點(diǎn):通常情況下,開發(fā)者可能根據(jù)圖片數(shù)組的數(shù)量在 UIScrollView 中創(chuàng)建對(duì)應(yīng)數(shù)量的 UIImageView 擴(kuò)充 UIScrollView 的 contentSize 實(shí)現(xiàn)所有圖片的滑動(dòng);這種設(shè)計(jì)思路比較常規(guī),但有兩個(gè)問題:1.如果圖片數(shù)據(jù)過大,此控件將占用過大的內(nèi)存去創(chuàng)建視圖;2.很難實(shí)現(xiàn)無限循環(huán)滑動(dòng)。

我的思想:在 UIScrollView 中只創(chuàng)建三個(gè) UIImageView 作為 上一頁圖片 當(dāng)前頁圖片 下一頁圖片 的容器。通過判斷當(dāng)前頁所在 圖片數(shù)據(jù)中的位置,動(dòng)態(tài)為三個(gè) UIImageView 填充指定圖片。

// 創(chuàng)建 滑動(dòng)視圖子視圖 
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)
    }
}

  1. 重點(diǎn):動(dòng)態(tài)更新 UIImageView 視圖實(shí)現(xiàn)無限滾動(dòng)

核心知識(shí):如果通過三個(gè) UIImageView 實(shí)現(xiàn) 無限圖片的 無限循環(huán)滑動(dòng):

func updateScrollView() {
    // 遍歷三次
    for i in 0 ..< 3 {
        獲取 UIScrollView 中 UIImageView
        let imageView = scrollView.subviews[i] as! UIImageView
        // 獲取 當(dāng)前展示的圖片是 序號(hào)
        var index = pageControl.currentPage
        // 如果 UIImageView UIScrollView 中的第二個(gè)視圖,即 上一個(gè)圖片
        if i == 0 {
            // 則 index 等于 當(dāng)前圖片序號(hào) - 1
            index -= 1
        }
        // 如果 UIImageView 是 UIScrollView 中的第三個(gè)視圖,即下一個(gè)圖片
        if i == 2 {
            // 則 index 等于 當(dāng)前圖片序號(hào) + 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!)
        }
    }
    // 調(diào)整 UIScrollView 偏移量 使其永遠(yuǎn)只顯示中間的 UIImageView
    scrollView.contentOffset = CGPoint(x: viewSize.width, y: 0)
}

糊涂的童鞋,奉上我畫的示意圖

假如我們的圖片數(shù)據(jù)只有 6 張圖片;
紅色代表原始數(shù)組,藍(lán)色代表我們的 UIScrollView 兩邊越界是 填充收尾圖片

image
image

最后部分

設(shè)置定時(shí)器

// 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
}

擴(kuò)展完善交互時(shí)的事件

extension ImageScrollView: UIScrollViewDelegate {
    
    // 當(dāng) scrollView 有偏移量時(shí)出發(fā)
    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
            // 由于當(dāng)前頁有用都是中間頁,所以當(dāng) 偏移量==圖片寬度時(shí),就是當(dāng)前頁。
            let distance:CGFloat = abs(imageView.x - scrollView.contentOffset.x)
            if distance < minDistance {
                minDistance = distance
                page = imageView.tag
            }
        }
        pageControl.currentPage = page
    }
    
    // 開始拖動(dòng) UIScrollView 事件
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        stopTimer()
    }
    
    // 結(jié)束拖動(dòng) UIScrollView 事件
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        startTimer()
    }
    
    // 
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        updateScrollView()
    }
    
    // UIScrollView 結(jié)束滾動(dòng)
    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        updateScrollView()
    }
    
}

學(xué)會(huì)設(shè)置代理

具體步驟:

  1. 定義代理 delegate
  2. 配置代理事件
  3. 實(shí)例對(duì)象,調(diào)用對(duì)象代理
// 1
@objc protocol ImageScrollViewDelegate {
    @objc optional func touchAt(index: Int)
}
// 2
class ImageScrollView: UIView {
    var delegate: ImageScrollViewDelegate?
    
    // 觸發(fā)代理
    func touchImage(tap: UITapGestureRecognizer) {
        if let index = tap.view?.tag {
            delegate?.touchAt!(index: index)
        }
    }
    ...... }
// 3
// 實(shí)現(xiàn)代理:在調(diào)用 ImageScrollView 控件的 VC 中擴(kuò)展,具體看源碼
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
        // 添加 滑動(dòng)試圖
        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)
    }
    
    // 創(chuàng)建 滑動(dòng)視圖子視圖
    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)
        }
    }
    
    // 初始化 滑動(dòng)視圖
    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 {
    
    // 當(dāng) scrollView 有偏移量時(shí)出發(fā)
    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
            // 由于當(dāng)前頁有用都是中間頁,所以當(dāng) 偏移量==圖片寬度時(shí),就是當(dāng)前頁。
            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
    }
}


調(diào)用的 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 = "圖片無限滾動(dòng)"
        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)
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評(píng)論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,855評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,833評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,016評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,273評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評(píng)論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,730評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,006評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,154評(píng)論 4 61
  • 轉(zhuǎn)眼間我們都打卡第6天了,組長說了,這周作業(yè)是記錄一下讓你情緒失控的時(shí)刻。 我一直在想,我們?yōu)槭裁赐瑯拥氖虑椋l(fā)生...
    笑莉說閱讀 372評(píng)論 5 18
  • 不知何時(shí)開始,焦慮已經(jīng)不是一種簡簡單單的心情。很多人開始發(fā)脾氣,有些人開始郁悶,有些人開始急躁。但是,焦慮是一種常...
    小簡貓閱讀 419評(píng)論 0 1
  • 首先要做到“知”。有三條建議: 摸底大調(diào)查。學(xué)生總有同校和同鄉(xiāng),學(xué)生入校時(shí),班主任一般會(huì)有一個(gè)調(diào)查小問卷,此時(shí)可以...
    侯志強(qiáng)閱讀 294評(píng)論 0 0
  • 兩個(gè)月前曹蕾跟我說要弄些“好玩”的事情,然后我看到了她的“玩好市集”。 一個(gè)月前,我看到曹蕾在朋友圈說要開始挑戰(zhàn)每...
    demi小貓閱讀 608評(píng)論 0 1