手把手帶你擼一個(gè)網(wǎng)易云音樂首頁(下篇)

前言

Hello, 大家好奋构,今天準(zhǔn)備和大家繼續(xù)分享如何利用 Swift 來實(shí)現(xiàn)一個(gè)網(wǎng)易云音樂的首頁听系;上篇文章發(fā)布以后恳谎,我收獲了不少小伙伴的關(guān)注與點(diǎn)贊术浪,同時(shí)也得到了一些非常有用的建議瓢对,在這里再次感謝大家的認(rèn)可, 你們的鼓勵(lì)與建議是我技術(shù)輸出路上最大的動(dòng)力。


MVVM

好了胰苏,回到正題硕蛹,在項(xiàng)目中我們使用了 MVVM 模式,在上一篇文章中硕并,我們講完了 Model 和 ViewModel, 那接下來就開始講 View 吧妓美!如果有小伙伴是從這篇文章進(jìn)入的,不妨先從我的上一篇文章看起鲤孵,這樣看下來才能保證你思路的連貫性壶栋。

View

回到我們的項(xiàng)目工程中來,準(zhǔn)備構(gòu)建我們的表視圖普监。

首先贵试,在我們的首頁視圖控制器 DiscoveryViewController 中創(chuàng)建存儲屬性 HomeViewModel 并初始化它。在我們實(shí)際開發(fā)過程中凯正,數(shù)據(jù)請求的操作必不可少毙玻,必須要先將數(shù)據(jù)提供給 ViewModel,然后在數(shù)據(jù)更新時(shí)重新 Reload TableView廊散。

    // 首頁發(fā)現(xiàn) viewModel
    fileprivate var homeViewModel = HomeViewModel()

接下來桑滩,我們來配置 tableViewDataSource:

    // Mark UITableViewDataSource
    override func numberOfSections(in tableView: UITableView) -> Int {
        if homeViewModel.sections.isEmpty {
            return 0
        }
        return homeViewModel.sections.count
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return homeViewModel.sections[section].rowCount
    }

現(xiàn)在我們就可以開始構(gòu)建 UI 了。根據(jù)網(wǎng)易云音樂的樣式允睹,我們需要?jiǎng)?chuàng)建 12 種不同類型的 Cell, 每種 Cell 對應(yīng)一種 ViewModelItems运准。

為了進(jìn)一步的提高代碼的質(zhì)量,我們可以為這些 Cell 定義一個(gè)基類 BaseViewCell缭受,這樣通過該基類胁澳,我們就可以設(shè)置一些默認(rèn)的屬性,減少一些不必要的編碼工作米者;另外韭畸,通過觀察你會發(fā)現(xiàn),大部分的 Section 都會包含一個(gè) headView蔓搞。關(guān)于 headView 的實(shí)現(xiàn)方式胰丁,想必使用過 UITableView 的同學(xué)都不會陌生,可以通過下面的方法來實(shí)現(xiàn):

- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;   // custom view for header. will be adjusted to default or specified header height

但是喂分,在這個(gè)項(xiàng)目中锦庸,我并不打算使用上面的方法來實(shí)現(xiàn) headView,主要原因是因?yàn)榫W(wǎng)易云音樂的每個(gè) Section 都是有圓角效果的妻顶,如果我們定義了 viewForHeaderInSection酸员,那么我們在實(shí)現(xiàn)圓角的時(shí)候就需要做如下的邏輯:

  • 給 headView 的左上角和右上角添加圓角效果
  • 給 Section 里的 Cell 的左下角和右下腳添加圓角效果

如圖所示:

我們知道蜒车,要為一個(gè)視圖添加圓角是非常有講究的,如果直接調(diào)用 cornerRadius 和 masksToBounds 這倆個(gè)方法設(shè)置圓角就會出現(xiàn)離屏渲染幔嗦,況且我們的首頁有很多圓角視圖酿愧,到時(shí)候首頁加載顯示就會感受到明顯的卡頓,這樣的體驗(yàn)可不好邀泉!而且使用這倆個(gè)方法也無法為視圖指定設(shè)置圓角的方位嬉挡,是要左上角呢還是右下角?

首先作為一個(gè)開發(fā)者汇恤,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要庞钢,這是一個(gè)我的iOS開發(fā)交流群:130 595 548,不管你是小白還是大牛都?xì)g迎入駐 因谎,讓我們一起進(jìn)步基括,共同發(fā)展!(群內(nèi)會免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔2撇怼)

上面講到為視圖設(shè)置圓角一不小心就會造成離屏渲染风皿,那么這個(gè)問題該如何解決呢!在這里匠璧,我們可以通過利用 UIBezierPath 來為視圖繪制圓角桐款,以及還可以指定畫圓角的方位:

func roundCorners(_ rect: CGRect, corners: UIRectCorner, radius: CGFloat) {
        let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        self.layer.mask = mask
    }

考慮到如果通過 viewForHeaderInSection 方法來創(chuàng)建 HeadView,那么我們就要為倆個(gè)視圖來繪制圓角夷恍,分別是 TableViewCell 和 viewForHeaderInSection 創(chuàng)建的 headView魔眨。這里我想了一個(gè)比較好的辦法,只需要調(diào)用一次繪制方法即可酿雪,那就是將我們的 headView 實(shí)現(xiàn)在我們的 tableViewCell 中遏暴,如下所示:

另外,因?yàn)槊總€(gè) Section 都有 headView 执虹,所以我們可以在 BaseViewCell 這個(gè)基類中去實(shí)現(xiàn)這個(gè)頭視圖:

/// UITableViewCell 的基類
class BaseViewCell: UITableViewCell {

    var headerView: JJTableViewHeader?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.backgroundColor = UIColor.homeCellColor
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

接下來拓挥,我們來構(gòu)建具體的 Cell ,由于代碼過多袋励,這里僅展示部分代碼:

/// 首頁 Bannerl
class ScrollBannerCell: BaseViewCell {
    class var identifier: String {
          return String(describing: self)
    }

    var scrollBanner: JJNewsBanner!

    var item: HomeViewModelSection? {
        didSet {
            guard let item = item as? BannerModel else {
                return
            }
            self.setupUI(model: item)
        }
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        /// 初始化
        scrollBanner = JJNewsBanner(frame: CGRect.zero)
        self.contentView.addSubview(scrollBanner!)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
    }

    func setupUI(model: BannerModel) {
        self.scrollBanner.frame = model.frame
        self.scrollBanner.updateUI(model: model, placeholderImage: UIImage(named: "ad_placeholder"))
    }
}

/// 首頁-發(fā)現(xiàn) 圓形按鈕
class CircleMenusCell: BaseViewCell {
    class var identifier: String {
          return String(describing: self)
    }

    var homeMenu: HomeMenu!

    var item: HomeViewModelSection? {
        didSet {
            guard let item = item as? MenusModel else {
                return
            }
            self.setupUI(model: item)
        }
    }

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        /// 初始化
        homeMenu = HomeMenu(frame: CGRect.zero)
        self.contentView.addSubview(homeMenu!)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
    }

    func setupUI(model: MenusModel) {
        self.homeMenu.frame = model.frame
        self.homeMenu.updateUI(data: model.data)
    }
}

....

在現(xiàn)實(shí)中,每個(gè) Cell 所展示的視圖樣式都是非常豐富的当叭,于是我們必須為 Cell 創(chuàng)建不同的 UI 樣式茬故,每種樣式對應(yīng)自己的數(shù)據(jù) Model。

構(gòu)建 TableViewCell 樣式

圖片輪播效果

首先蚁鳖,網(wǎng)易云音樂最上層是一個(gè)圖片輪播的效果磺芭,如何構(gòu)建這個(gè) Banner 呢!這里就不繞彎子了醉箕,當(dāng)然是用最常用的內(nèi)容展示神器 UICollectionView 這個(gè)控件了钾腺,讀完本篇文章你會發(fā)現(xiàn)真是萬物皆可使用 UICollectionView徙垫。

具體實(shí)現(xiàn)該效果的代碼在這里我就不做多闡述了,因?yàn)樵谖抑暗奈恼轮蟹虐簦乙呀?jīng)將實(shí)現(xiàn)這個(gè)效果的教程寫出來了姻报。

圓形菜單入口

該效果實(shí)現(xiàn)起來很簡單,唯一有意思之處在于“每日歌曲推薦”這個(gè)按鈕上中間的文字是會隨著日期改變的间螟,如圖:

不過實(shí)現(xiàn)起來也簡單吴旋,中間放一個(gè) Label 即可。如該側(cè)面圖所示(圖借用自作者 Leo):

整體實(shí)現(xiàn)用的控件還是 UICollectionView厢破。部分代碼如下:

import UIKit
import Foundation
import SnapKit
import Kingfisher

class HomeMenuCell: UICollectionViewCell {

    lazy var menuLayer: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor.darkModeMenuColor
        return view
    }()

    lazy var menuIcon: UIImageView = {
        let mIcon = UIImageView()
        mIcon.tintColor = UIColor.dragonBallColor
        return mIcon
    }()

    lazy var menuText: UILabel = {
        let mText = UILabel()
        mText.textColor = UIColor.darkModeTextColor
        mText.textAlignment = .center
        mText.font = UIFont.systemFont(ofSize: 12)
        return mText
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        self.contentView.addSubview(self.menuLayer)
        self.menuLayer.addSubview(self.menuIcon)
        self.contentView.addSubview(self.menuText)

        self.menuLayer.snp.makeConstraints { (make) in
            make.centerX.equalToSuperview()
            make.width.equalTo(self.frame.size.width * 0.6)
            make.height.equalTo(self.frame.size.width * 0.6)
        }

        self.menuIcon.snp.makeConstraints { (make) in
            make.centerX.equalToSuperview()
            make.centerY.equalToSuperview()
            make.width.equalTo(self.frame.size.width * 0.6)
            make.height.equalTo(self.frame.size.width * 0.6)
        }

        self.menuText.snp.makeConstraints { (make) in
            make.centerX.equalToSuperview()
            make.bottom.equalToSuperview()
            make.height.equalTo(self.frame.size.width * 0.4)
            make.width.equalTo(self.frame.size.width)
        }

        // 設(shè)置菜單圓角
        self.menuLayer.layer.cornerRadius = self.frame.size.width * 0.6 * 0.5
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupUI(imageUrl: String, title: String) -> Void {
        let cache = KingfisherManager.shared.cache
        let imgModify = RenderingModeImageModifier(renderingMode: .alwaysTemplate)
        let optionsInfo = [KingfisherOptionsInfoItem.imageModifier(imgModify),
                        KingfisherOptionsInfoItem.targetCache(cache)]

        self.menuIcon.kf.setImage(with: URL(string: imageUrl), placeholder: nil, options: optionsInfo, completionHandler:  { ( result ) in

        })
        self.menuText.text = title
    }
}

推薦歌單/音樂視頻/雷達(dá)歌單/視頻合集等

先看下 UI 效果:

因?yàn)檫@些 UI 的效果是差不多的荣瑟,第一個(gè)冒出來想法就是在 Cell 中放置 UICollectionView,它的布局也很簡單摩泪,直接用系統(tǒng)提供的即可笆焰,不需要我們?nèi)プ远x布局。

像這種上圖下文的 CollectionViewCell 也很好定義见坑,這里就不多做闡述嚷掠,部分代碼如下:

import UIKit
import SnapKit
import Kingfisher

class CardViewCell: UICollectionViewCell {
    /// 封面
    lazy var albumCover: UIImageView! = {
        let cover = UIImageView()
        cover.backgroundColor = UIColor.clear
        cover.contentMode = .scaleAspectFill
        return cover
    }()

    /// 描述
    lazy var albumDesc: UILabel! = {
        let descLabel = UILabel()
        descLabel.backgroundColor = UIColor.clear
        descLabel.font = UIFont.systemFont(ofSize: 12)
        descLabel.numberOfLines = 0
        return descLabel
    }()

    /// 閱讀量
    var views: String?

    /// 內(nèi)邊距
    let padding: CGFloat = 5

    /// 閱讀量按鈕
    lazy var viewsButton: UIButton! = {
        let button = UIButton(type: .custom)
        button.titleLabel?.font = UIFont.systemFont(ofSize: 10)
        button.backgroundColor = UIColor(red: 182/255, green: 182/255, blue: 182/255, alpha: 0.6)
        button.setImage(UIImage(named: "Views"), for: .normal)
        button.setTitleColor(.white, for: .normal)
        return button
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.backgroundColor = .clear
        self.addSubview(self.albumCover)
        self.albumCover.addSubview(self.viewsButton)
        self.addSubview(self.albumDesc)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        let height: CGFloat = self.bounds.height
        let width: CGFloat = self.bounds.width

        let descHeight: CGFloat = height * (1/4)

        // 封面樣式設(shè)置
        self.albumCover.snp.makeConstraints { (make) in
            make.width.equalTo(width)
            make.height.equalTo(width)
            make.centerX.equalToSuperview()
            make.top.equalToSuperview()
        }
        self.albumCover.roundCorners(self.albumCover.bounds, corners: [.allCorners], radius: 10)

        // 設(shè)置按鈕樣式
        let viewsRect = self.getStrBoundRect(str: self.views!, font: self.viewsButton.titleLabel!.font, constrainedSize: CGSize.zero)
        let viewsW = viewsRect.width
        let viewsH = viewsRect.height * 1.2
        self.viewsButton.frame = CGRect(x: self.albumCover.frame.width - viewsW - padding, y: padding, width: viewsW, height: viewsH)
        self.viewsButton.moveImageLeftTextCenterWithTinySpace(imagePadding: 5)
        self.viewsButton.roundCorners(self.viewsButton.bounds, corners: [.allCorners], radius: viewsW * 0.2)

        self.albumDesc.snp.makeConstraints { (make) in
            make.width.equalTo(width - 10)
            make.height.equalTo(descHeight)
            make.centerX.equalToSuperview()
            make.top.equalTo(self.albumCover.snp.bottom).offset(5)
        }
    }

    ....
}
/// 通用的卡片滾動(dòng)視圖,該控件適用于橫向滾動(dòng)并且上圖下文形式
class CardCollectionView: UIView {

.....

    /// 布局
    lazy var cardFlowLayout: UICollectionViewFlowLayout = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = margin
        layout.minimumInteritemSpacing = 0
        layout.sectionInset = UIEdgeInsets.init(top: -20, left: margin, bottom: 0, right: 0)
        layout.scrollDirection = .horizontal
        return layout
    }()

    /// 歌單的視圖
    lazy var hotAlbumContainer: UICollectionView = {
        let collectionView = UICollectionView(frame: CGRect.zero, collectionViewLayout: self.cardFlowLayout)
        collectionView.register(CardViewCell.self, forCellWithReuseIdentifier: RecomendAlbumId)
        collectionView.isPagingEnabled = true
        collectionView.showsVerticalScrollIndicator = false
        collectionView.showsHorizontalScrollIndicator = false
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.backgroundColor = UIColor.clear
        collectionView.bounces = false
        return collectionView
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.addSubview(self.hotAlbumContainer)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.hotAlbumContainer.frame = CGRect(x: 0, y: 0, width: self.bounds.width, height: self.bounds.height)
        // 設(shè)置 item size 大小
        self.cardFlowLayout.itemSize = CGSize(width: itemA_width * scaleW, height: self.frame.size.height - 3 * margin)

    }

    deinit {
        self.hotAlbumContainer.delegate = nil
        self.hotAlbumContainer.dataSource = nil
    }
}

// MARK: - UICollectionViewDelegate
extension CardCollectionView: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

    }
}

// MARK: - UICollectionViewDataSource
extension CardCollectionView: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        if self.songList == nil {
            return 0
        }
        return self.songList!.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RecomendAlbumId, for: indexPath) as! CardViewCell
        let result:Creative = self.songList![indexPath.row]
        if result.creativeType == "voiceList" {
            cell.updateUI(coverUrl: (result.uiElement?.image!.imageURL)!, desc: (result.uiElement?.mainTitle!.title)!, views: String((result.creativeEXTInfoVO?.playCount)!))
        } else {
            let element = result.resources?[0]
            cell.updateUI(coverUrl: (element?.uiElement.image.imageURL)!, desc: (element?.uiElement.mainTitle.title)!, views: String((element?.resourceEXTInfo?.playCount)!))
        }

        return cell
    }
}

個(gè)性推薦/新歌新碟數(shù)字專輯/

接下來鳄梅,咱們來構(gòu)建另外的樣式叠国。先來看下 UI:

由于“個(gè)性推薦”,“新歌新碟數(shù)字專輯”這倆個(gè)功能的樣式是差不多的戴尸,所以也將這倆并在一起說粟焊。在這我還是選擇在 Cell 中放置 UICollectionView。但是孙蒙,通過觀察你會發(fā)現(xiàn)它的 UI 樣式其實(shí)是有講究的项棠,就是在同一個(gè)頁面中,它的第二個(gè) item 也需要露出一部分挎峦,這該如何去實(shí)現(xiàn)呢香追!

為了能在一個(gè)頁面中出現(xiàn)倆個(gè) item,那我們必須要減少 itemSize 的寬度坦胶,這樣設(shè)置 UICollectionViewFlowLayout 后就能在一個(gè)頁面中出現(xiàn)倆個(gè) item 了透典。

我們知道在 UICollectionView 的屬性中,有一個(gè)分頁的屬性:isPagingEnabled顿苇,當(dāng)設(shè)置成 true 時(shí)峭咒,每次滾動(dòng)的位移量等于它自身 frame 的寬度;當(dāng)不設(shè)置這個(gè)分頁屬性纪岁,它的默認(rèn)值是 false, 所以它的滾動(dòng)就不會有分頁的效果凑队。

OK,那這個(gè)想法是不是正確呢幔翰!其實(shí)當(dāng)你動(dòng)手實(shí)踐后漩氨,你會發(fā)現(xiàn)這樣實(shí)現(xiàn)后會有一個(gè)非常頭疼的 bug西壮,那就當(dāng) item 滾動(dòng)的時(shí)候會出現(xiàn)遮擋,這用戶體貼也太差了叫惊。

有人要問那是不是 UICollectionView 這個(gè)控件就只能按照屏幕的大小來分頁呢款青!答案當(dāng)然是否定的。我們還可以用自定義的方式來實(shí)現(xiàn)分頁滾動(dòng)赋访。根據(jù)文檔可都,Apple 在 UICollectionViewFlowLayout 的定義中提供了一個(gè)可重寫的函數(shù):

func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint // return a point at which to rest after scrolling - for layouts that want snap-to-point scrolling behavior

這個(gè)函數(shù)的返回值,決定了 UICollectionView 停止?jié)L動(dòng)時(shí)的偏移量蚓耽,可以通過重寫這個(gè)函數(shù)來實(shí)現(xiàn)自定義的分頁滾動(dòng)渠牲,重寫這個(gè)函數(shù)的邏輯思路如下:

  1. 定義一個(gè)坐標(biāo)點(diǎn) CGPoint 來記錄最新滾動(dòng)的偏移坐標(biāo)
  2. 定義倆個(gè)值分別為 UICollectionView 可滾動(dòng)的最大偏移量與最小偏移量也是就 0
  3. 每次滾動(dòng)停止都會調(diào)用上述的函數(shù) func targetContentOffset(...), 在這個(gè)函數(shù)中有一個(gè)參數(shù) proposedContentOffset 記錄了滾動(dòng)的目標(biāo)位移坐標(biāo),通過這個(gè)坐標(biāo)和記錄的上次滾動(dòng)的坐標(biāo)可以判斷出是向左滾動(dòng)還是向右滾動(dòng)
  4. 如果倆坐標(biāo)的水平方向相減的絕對值大于某個(gè)固定值(譬如說 item 寬度的 8 分之一)步悠,則可以判斷發(fā)生了分頁签杈,然后通過 proposedContentOffset 位移坐標(biāo)和 item 的寬度大小來計(jì)算出當(dāng)前滾動(dòng)的頁碼攒菠;如果小于那個(gè)固定值饼问,則不發(fā)生分頁
  5. 最后記錄最新的偏移坐標(biāo)筒繁,然后返回 UICollectionView 停止?jié)L動(dòng)時(shí)的偏移量

代碼實(shí)現(xiàn)如下:

class RowStyleLayout: UICollectionViewFlowLayout {

    private var lastOffset: CGPoint!

    override init() {
        super.init()
        lastOffset = CGPoint.zero
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // 初始化
    override func prepare() {

        super.prepare()
        self.collectionView?.decelerationRate = .fast
    }

    // 這個(gè)方法的返回值酒贬,決定了 CollectionView 停止?jié)L動(dòng)時(shí)的偏移量
    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {

        // 分頁的 width
        let pageSpace = self.stepSpace()
        let offsetMax: CGFloat = self.collectionView!.contentSize.width - (pageSpace + self.sectionInset.right + self.minimumLineSpacing)
        let offsetMin: CGFloat = 0

        // 修改之前記錄的位置,如果小于最小的contentsize或者最大的contentsize則重置值
        if lastOffset.x < offsetMin {
            lastOffset.x = offsetMin
        } else if lastOffset.x > offsetMax{
            lastOffset.x = offsetMax
        }

        // 目標(biāo)位移點(diǎn)距離當(dāng)前點(diǎn)距離的絕對值
        let offsetForCurrentPointX: CGFloat = abs(proposedContentOffset.x - lastOffset.x)
        let velocityX = velocity.x

        // 判斷當(dāng)前滑動(dòng)方向竞端,向左 true, 向右 fasle
        let direction: Bool = (proposedContentOffset.x - lastOffset.x) > 0

        var newProposedContentOffset: CGPoint = CGPoint.zero

        if (offsetForCurrentPointX > pageSpace/8.0) && (lastOffset.x >= offsetMin) && (lastOffset.x <= offsetMax) {
            // 分頁因子被因,用于計(jì)算滑過的cell數(shù)量
            var pageFactor: NSInteger = 0
            if velocityX != 0 {
                // 滑動(dòng)
                // 速率越快蝗蛙,cell 滑過的數(shù)量越多
                pageFactor = abs(NSInteger(velocityX))
            } else {
                // 拖動(dòng)
                pageFactor = abs(NSInteger(offsetForCurrentPointX / pageSpace))
            }

            //設(shè)置 pageFactor 的上限為2择卦,防止滑動(dòng)速率過大敲长,導(dǎo)致翻頁過多
            pageFactor = pageFactor < 1 ? 1: (pageFactor < 3 ? 1: 2)

            let pageOffsetX: CGFloat = pageSpace * CGFloat(pageFactor)
            newProposedContentOffset = CGPoint(x: lastOffset.x + (direction ? pageOffsetX : -pageOffsetX), y: proposedContentOffset.y)
        } else {
            // 滾動(dòng)距離小于翻頁步距,則不進(jìn)行翻頁
            newProposedContentOffset = CGPoint(x: lastOffset.x, y: lastOffset.y)
        }

        lastOffset.x = newProposedContentOffset.x
        return newProposedContentOffset
    }

    // 每滑動(dòng)一頁的間距
    public func stepSpace() -> CGFloat {
        return self.itemSize.width + self.minimumLineSpacing
    }
}

在我之前的文章中秉继,我已經(jīng)將實(shí)現(xiàn)這個(gè)效果的教程寫出來了祈噪,查看即可

音樂日歷

UI 如圖:

音樂日歷的效果,不需要支持橫向滾動(dòng)尚辑,所以這里可以選擇在 Cell 中放置一個(gè) UIView辑鲤,對有一點(diǎn) iOS 開發(fā)基礎(chǔ)的同學(xué)來說,實(shí)現(xiàn)這樣的 UI 應(yīng)該不難杠茬,大家可以通過 Xib 或者代碼的方式來實(shí)現(xiàn)月褥,Xib 實(shí)現(xiàn)起來應(yīng)該更快,這里我就不在多做說明了瓢喉。

播客

終于講到最后一個(gè) UI 了吓坚,先看下效果:

經(jīng)歷過構(gòu)建上面這么多 UI 后,想必看到這個(gè)效果灯荧,大家都心知肚明了,還有比用 UICollectionView 更簡單的方式了嗎盐杂? 同樣是構(gòu)建一個(gè)上圖下文的 Cell, 只不過播客需要將圖片加上圓角逗载,代碼實(shí)現(xiàn)起來也很簡單哆窿,這里也不做多闡述了。

搜索

關(guān)于如何構(gòu)建不同的 Cell 到這里就講完了厉斟,如果大家有疑問的話挚躯,歡迎在評論區(qū)或者我的公號中發(fā)信息給我。

接下來擦秽,我們開始講首頁的最后一部分---搜索框码荔。在網(wǎng)易云音樂首頁的最頂層有一個(gè)視圖,視圖包含的內(nèi)容有三部分:左按鈕感挥,搜索框缩搅,右按鈕,這種結(jié)構(gòu)很容易讓我們聯(lián)想到 UINavigationItem触幼。沒錯(cuò)硼瓣,利用 UINavigationItem 來實(shí)現(xiàn)這樣的 UI 結(jié)構(gòu)是最有效的。

由于我們工程里首頁控制器是繼承自 UITableViewController 的置谦,所以我們可以直接設(shè)置它 UINavigationItem 屬性中的 leftBarButtonItem堂鲤,titleView 和 rightBarButtonItem:

// 設(shè)置搜索視圖
    func setupSearchController () {
        let leftItem = UIBarButtonItem(image: UIImage(named: "menu")?.withRenderingMode(.alwaysOriginal), style: UIBarButtonItem.Style.plain, target: self, action: #selector(menuBtnClicked))
        let rightItem = UIBarButtonItem(image: UIImage(named: "microphone")?.withRenderingMode(.alwaysOriginal), style: UIBarButtonItem.Style.plain, target: self, action: #selector(microphoneBtnClicked))
        self.navigationItem.leftBarButtonItem = leftItem
        self.navigationItem.rightBarButtonItem = rightItem

        self.cusSearchBar = JJCustomSearchbar(frame: CGRect(x: 0, y: 0, width: 200, height: 50))
        self.cusSearchBar.delegate = self
        self.navigationItem.titleView = self.cusSearchBar
    }

自定義 UISearchBar,代碼如下:

class JJCustomSearchbar: UISearchBar {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.searchTextField.placeholder = "has not been"
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func adjustPosition() {
        var frame :CGRect
        frame = self.searchTextField.frame
        // 獲取 placeholder 大小
        let r = self.searchTextField.placeholderRect(forBounds: self.searchTextField.bounds)
        let offset = UIOffset(horizontal: (frame.size.width - r.width - 40)/2, vertical: 0)
        self.setPositionAdjustment(offset, for: .search)
    }
}

當(dāng)我們點(diǎn)擊頂部的搜索框時(shí)媒峡,頁面需要跳轉(zhuǎn)到真正的搜索頁面瘟栖,所以我們需要實(shí)現(xiàn) UISearchBarDelegate 代理函數(shù):

extension DiscoveryViewController: UISearchBarDelegate {
    // 點(diǎn)擊跳轉(zhuǎn)
    func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
        self.musicSearchController = MusicSearchViewController()
        self.navigationController?.pushViewController(self.musicSearchController, animated: false)
        return true
    }
}

構(gòu)建跳轉(zhuǎn)后的搜索頁面

首先,需要實(shí)現(xiàn)搜索視圖谅阿,我們的視圖控制器 MusicSearchViewController 繼承自 UITableViewController半哟,所以它的 UINavigationItem 中自己帶有 searchController。不過奔穿,由于搜索欄需要自定義一些樣式镜沽,我們可以先定義一個(gè) UISearchController 的成員變量,將它的屬性初始化好以后贱田,再進(jìn)行賦值缅茉,代碼如下:

   self.searchController = UISearchController(searchResultsController: nil)
   self.searchController.delegate = self
   self.searchController.searchResultsUpdater = self
   self.searchController.searchBar.delegate = self
   self.searchController.searchBar.placeholder = "Search"
   self.searchController.searchBar.autocapitalizationType = .none
   self.searchController.dimsBackgroundDuringPresentation = false

   self.navigationItem.hidesBackButton = true
   self.navigationItem.searchController = self.searchController
   self.navigationItem.searchController?.isActive = true
   self.navigationItem.hidesSearchBarWhenScrolling = false
   definesPresentationContext = true

在本工程,我們僅實(shí)現(xiàn)一個(gè)簡單的搜索演示功能男摧,因?yàn)橐娴淖龊盟阉鬟@個(gè)需求蔬墩,需要服務(wù)器的”大力“配合,在本工程中耗拓,我們僅用一些靜態(tài)數(shù)據(jù)來做演示:

musics = [
            Results(name: "如果愛"),
            Results(name: "情書"),
            Results(name: "龍卷風(fēng)"),
            Results(name: "半島鐵盒"),
            Results(name: "世界末日"),
            Results(name: "愛在西元前"),
            Results(name: "等你下課"),
            Results(name: "黑色幽默"),
            Results(name: "我不配")
        ]

首先作為一個(gè)開發(fā)者拇颅,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要,這是一個(gè)我的iOS開發(fā)交流群:130 595 548乔询,不管你是小白還是大牛都?xì)g迎入駐 樟插,讓我們一起進(jìn)步,共同發(fā)展!(群內(nèi)會免費(fèi)提供一些群主收藏的免費(fèi)學(xué)習(xí)書籍資料以及整理好的幾百道面試題和答案文檔;拼浮)
數(shù)據(jù)源有了搪缨,接下來就是來實(shí)現(xiàn)數(shù)據(jù)查找功能了,在搜索欄中輸入要搜索的歌名鸵熟,并在頁面上列出我們搜索到的結(jié)果副编。這里就需要來實(shí)現(xiàn) UISearchResultsUpdating 和 UISearchBarDelegate 這倆個(gè)代理了,通過 UISearchBar 獲取到輸入值流强,然后在提供的數(shù)據(jù)源中查找痹届,并 reload 我們的表視圖:

extension MusicSearchViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        let searchBar = searchController.searchBar
        filterContentForSearchText(searchBar.text!)
    }
}

extension MusicSearchViewController: UISearchBarDelegate{
    func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
        filterContentForSearchText(searchBar.text!)
    }

    func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        self.navigationController?.popViewController(animated: true)
    }

    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        self.searchController.searchBar.resignFirstResponder()
    }
}

func filterContentForSearchText(_ searchText: String){
        filteredMusic = musics.filter{ music in
            return music.name.lowercased().contains(searchText.lowercased()) || searchText == ""
        }

        tableView.reloadData()
    }

結(jié)尾

到此,使用 MVVM 來構(gòu)建網(wǎng)易云音樂首頁就差不多講完了打月,我們再總結(jié)一下队腐,在本文中我們主要講解了如何來構(gòu)建 UI 視圖, 由于在我們首頁里的 Cell 的樣式有不同之處但也有相似的地方僵控,所以我們創(chuàng)建了一個(gè)基類 BaseViewCell, 用于展示 Cell 中相同的地方香到;然后我們在各個(gè) Cell 中構(gòu)建不同樣式的 UI,利用 UICollectionView 這一神器實(shí)現(xiàn)了這些效果报破;最后悠就,實(shí)現(xiàn)了簡單的搜索功能。

好了充易,以上便是本次分享~ 下次見梗脾!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者盹靴。
  • 序言:七十年代末炸茧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子稿静,更是在濱河造成了極大的恐慌梭冠,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件改备,死亡現(xiàn)場離奇詭異控漠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)悬钳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門盐捷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人默勾,你說我怎么就攤上這事碉渡。” “怎么了母剥?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵滞诺,是天一觀的道長形导。 經(jīng)常有香客問我,道長铭段,這世上最難降的妖魔是什么骤宣? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮序愚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘等限。我一直安慰自己爸吮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布望门。 她就那樣靜靜地躺著形娇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪筹误。 梳的紋絲不亂的頭發(fā)上桐早,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天,我揣著相機(jī)與錄音厨剪,去河邊找鬼哄酝。 笑死,一個(gè)胖子當(dāng)著我的面吹牛祷膳,可吹牛的內(nèi)容都是我干的陶衅。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼直晨,長吁一口氣:“原來是場噩夢啊……” “哼搀军!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起勇皇,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤罩句,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后敛摘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體门烂,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年着撩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了诅福。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拖叙,死狀恐怖氓润,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情薯鳍,我是刑警寧澤咖气,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布挨措,位于F島的核電站,受9級特大地震影響崩溪,放射性物質(zhì)發(fā)生泄漏浅役。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一伶唯、第九天 我趴在偏房一處隱蔽的房頂上張望觉既。 院中可真熱鬧,春花似錦乳幸、人聲如沸瞪讼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽符欠。三九已至,卻和暖如春瓶埋,著一層夾襖步出監(jiān)牢的瞬間希柿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工养筒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留曾撤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓闽颇,卻偏偏與公主長得像盾戴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子兵多,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

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

  • 前言 Hello尖啡,大家好,近期我一直在學(xué)習(xí)用 Swift 編碼剩膘,由于之前很多項(xiàng)目我都是用 OC 實(shí)現(xiàn)的衅斩,所以導(dǎo)致我...
    iOS鑫閱讀 1,234評論 0 7
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒怠褐。表情可以傳達(dá)很多信息畏梆。高興了當(dāng)然就笑了,難過就哭了奈懒。兩者是相互影響密不可...
    Persistenc_6aea閱讀 124,213評論 2 7
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者奠涌,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn)磷杏,也就放棄了無數(shù)的可能溜畅。 ...
    yichen大刀閱讀 6,033評論 0 4