前言
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ù)的邏輯思路如下:
- 定義一個(gè)坐標(biāo)點(diǎn) CGPoint 來記錄最新滾動(dòng)的偏移坐標(biāo)
- 定義倆個(gè)值分別為 UICollectionView 可滾動(dòng)的最大偏移量與最小偏移量也是就 0
- 每次滾動(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)
- 如果倆坐標(biāo)的水平方向相減的絕對值大于某個(gè)固定值(譬如說 item 寬度的 8 分之一)步悠,則可以判斷發(fā)生了分頁签杈,然后通過 proposedContentOffset 位移坐標(biāo)和 item 的寬度大小來計(jì)算出當(dāng)前滾動(dòng)的頁碼攒菠;如果小于那個(gè)固定值饼问,則不發(fā)生分頁
- 最后記錄最新的偏移坐標(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)了簡單的搜索功能。
好了充易,以上便是本次分享~ 下次見梗脾!