草庐IT

手把手带你撸一个网易云音乐首页(下篇)

iOS是大鑫呀 2023-03-28 原文

前言

Hello, 大家好,今天准备和大家继续分享如何利用 Swift 来实现一个网易云音乐的首页;上篇文章发布以后,我收获了不少小伙伴的关注与点赞,同时也得到了一些非常有用的建议,在这里再次感谢大家的认可, 你们的鼓励与建议是我技术输出路上最大的动力。

MVVM

好了,回到正题,在项目中我们使用了 MVVM 模式,在上一篇文章中,我们讲完了 Model 和 ViewModel, 那接下来就开始讲 View 吧!如果有小伙伴是从这篇文章进入的,不妨先从我的上一篇文章看起,这样看下来才能保证你思路的连贯性。

View

回到我们的项目工程中来,准备构建我们的表视图。

首先,在我们的首页视图控制器 DiscoveryViewController 中创建存储属性 HomeViewModel 并初始化它。在我们实际开发过程中,数据请求的操作必不可少,必须要先将数据提供给 ViewModel,然后在数据更新时重新 Reload TableView。

// 首页发现 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 } 现在我们就可以开始构建 UI 了。根据网易云音乐的样式,我们需要创建 12 种不同类型的 Cell, 每种 Cell 对应一种 ViewModelItems。

为了进一步的提高代码的质量,我们可以为这些 Cell 定义一个基类 BaseViewCell,这样通过该基类,我们就可以设置一些默认的属性,减少一些不必要的编码工作;另外,通过观察你会发现,大部分的 Section 都会包含一个 headView。关于 headView 的实现方式,想必使用过 UITableView 的同学都不会陌生,可以通过下面的方法来实现:

- (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section; // custom view for header. will be adjusted to default or specified header height 但是,在这个项目中,我并不打算使用上面的方法来实现 headView,主要原因是因为网易云音乐的每个 Section 都是有圆角效果的,如果我们定义了 viewForHeaderInSection,那么我们在实现圆角的时候就需要做如下的逻辑:

  • 给 headView 的左上角和右上角添加圆角效果
  • 给 Section 里的 Cell 的左下角和右下脚添加圆角效果
如图所示:

我们知道,要为一个视图添加圆角是非常有讲究的,如果直接调用 cornerRadius 和 masksToBounds 这俩个方法设置圆角就会出现离屏渲染,况且我们的首页有很多圆角视图,到时候首页加载显示就会感受到明显的卡顿,这样的体验可不好!而且使用这俩个方法也无法为视图指定设置圆角的方位,是要左上角呢还是右下角?

首先作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

上面讲到为视图设置圆角一不小心就会造成离屏渲染,那么这个问题该如何解决呢!在这里,我们可以通过利用 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 方法来创建 HeadView,那么我们就要为俩个视图来绘制圆角,分别是 TableViewCell 和 viewForHeaderInSection 创建的 headView。这里我想了一个比较好的办法,只需要调用一次绘制方法即可,那就是将我们的 headView 实现在我们的 tableViewCell 中,如下所示:

另外,因为每个 Section 都有 headView ,所以我们可以在 BaseViewCell 这个基类中去实现这个头视图:

/// 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") } } 接下来,我们来构建具体的 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")) } } /// 首页-发现 圆形按钮 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) } } .... 在现实中,每个 Cell 所展示的视图样式都是非常丰富的,于是我们必须为 Cell 创建不同的 UI 样式,每种样式对应自己的数据 Model。

构建 TableViewCell 样式

图片轮播效果

首先,网易云音乐最上层是一个图片轮播的效果,如何构建这个 Banner 呢!这里就不绕弯子了,当然是用最常用的内容展示神器 UICollectionView 这个控件了,读完本篇文章你会发现真是万物皆可使用 UICollectionView。

具体实现该效果的代码在这里我就不做多阐述了,因为在我之前的文章中,我已经将实现这个效果的教程写出来了。

圆形菜单入口

该效果实现起来很简单,唯一有意思之处在于“每日歌曲推荐”这个按钮上中间的文字是会随着日期改变的,如图:

不过实现起来也简单,中间放一个 Label 即可。如该侧面图所示(图借用自作者 Leo):

整体实现用的控件还是 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) } // 设置菜单圆角 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 } }

推荐歌单/音乐视频/雷达歌单/视频合集等

先看下 UI 效果:

因为这些 UI 的效果是差不多的,第一个冒出来想法就是在 Cell 中放置 UICollectionView,它的布局也很简单,直接用系统提供的即可,不需要我们去自定义布局。

像这种上图下文的 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? /// 内边距 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) // 封面样式设置 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) // 设置按钮样式 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) } } .... } /// 通用的卡片滚动视图,该控件适用于横向滚动并且上图下文形式 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) // 设置 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 } }

个性推荐/新歌新碟数字专辑/

接下来,咱们来构建另外的样式。先来看下 UI:

由于“个性推荐”,“新歌新碟数字专辑”这俩个功能的样式是差不多的,所以也将这俩并在一起说。在这我还是选择在 Cell 中放置 UICollectionView。但是,通过观察你会发现它的 UI 样式其实是有讲究的,就是在同一个页面中,它的第二个 item 也需要露出一部分,这该如何去实现呢!

为了能在一个页面中出现俩个 item,那我们必须要减少 itemSize 的宽度,这样设置 UICollectionViewFlowLayout 后就能在一个页面中出现俩个 item 了。

我们知道在 UICollectionView 的属性中,有一个分页的属性:isPagingEnabled,当设置成 true 时,每次滚动的位移量等于它自身 frame 的宽度;当不设置这个分页属性,它的默认值是 false, 所以它的滚动就不会有分页的效果。

OK,那这个想法是不是正确呢!其实当你动手实践后,你会发现这样实现后会有一个非常头疼的 bug,那就当 item 滚动的时候会出现遮挡,这用户体贴也太差了。

有人要问那是不是 UICollectionView 这个控件就只能按照屏幕的大小来分页呢!答案当然是否定的。我们还可以用自定义的方式来实现分页滚动。根据文档,Apple 在 UICollectionViewFlowLayout 的定义中提供了一个可重写的函数:

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 这个函数的返回值,决定了 UICollectionView 停止滚动时的偏移量,可以通过重写这个函数来实现自定义的分页滚动,重写这个函数的逻辑思路如下:

  1. 定义一个坐标点 CGPoint 来记录最新滚动的偏移坐标
  2. 定义俩个值分别为 UICollectionView 可滚动的最大偏移量与最小偏移量也是就 0
  3. 每次滚动停止都会调用上述的函数 func targetContentOffset(...), 在这个函数中有一个参数 proposedContentOffset 记录了滚动的目标位移坐标,通过这个坐标和记录的上次滚动的坐标可以判断出是向左滚动还是向右滚动
  4. 如果俩坐标的水平方向相减的绝对值大于某个固定值(譬如说 item 宽度的 8 分之一),则可以判断发生了分页,然后通过 proposedContentOffset 位移坐标和 item 的宽度大小来计算出当前滚动的页码;如果小于那个固定值,则不发生分页
  5. 最后记录最新的偏移坐标,然后返回 UICollectionView 停止滚动时的偏移量
代码实现如下:

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 } // 这个方法的返回值,决定了 CollectionView 停止滚动时的偏移量 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 } // 目标位移点距离当前点距离的绝对值 let offsetForCurrentPointX: CGFloat = abs(proposedContentOffset.x - lastOffset.x) let velocityX = velocity.x // 判断当前滑动方向,向左 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) { // 分页因子,用于计算滑过的cell数量 var pageFactor: NSInteger = 0 if velocityX != 0 { // 滑动 // 速率越快,cell 滑过的数量越多 pageFactor = abs(NSInteger(velocityX)) } else { // 拖动 pageFactor = abs(NSInteger(offsetForCurrentPointX / pageSpace)) } //设置 pageFactor 的上限为2,防止滑动速率过大,导致翻页过多 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 { // 滚动距离小于翻页步距,则不进行翻页 newProposedContentOffset = CGPoint(x: lastOffset.x, y: lastOffset.y) } lastOffset.x = newProposedContentOffset.x return newProposedContentOffset } // 每滑动一页的间距 public func stepSpace() -> CGFloat { return self.itemSize.width + self.minimumLineSpacing } } 在我之前的文章中,我已经将实现这个效果的教程写出来了,查看即可

音乐日历

UI 如图:

音乐日历的效果,不需要支持横向滚动,所以这里可以选择在 Cell 中放置一个 UIView,对有一点 iOS 开发基础的同学来说,实现这样的 UI 应该不难,大家可以通过 Xib 或者代码的方式来实现,Xib 实现起来应该更快,这里我就不在多做说明了。

播客

终于讲到最后一个 UI 了,先看下效果:

经历过构建上面这么多 UI 后,想必看到这个效果,大家都心知肚明了,还有比用 UICollectionView 更简单的方式了吗? 同样是构建一个上图下文的 Cell, 只不过播客需要将图片加上圆角,代码实现起来也很简单,这里也不做多阐述了。

搜索

关于如何构建不同的 Cell 到这里就讲完了,如果大家有疑问的话,欢迎在评论区或者我的公号中发信息给我。

接下来,我们开始讲首页的最后一部分---搜索框。在网易云音乐首页的最顶层有一个视图,视图包含的内容有三部分:左按钮,搜索框,右按钮,这种结构很容易让我们联想到 UINavigationItem。没错,利用 UINavigationItem 来实现这样的 UI 结构是最有效的。

由于我们工程里首页控制器是继承自 UITableViewController 的,所以我们可以直接设置它 UINavigationItem 属性中的 leftBarButtonItem,titleView 和 rightBarButtonItem:

// 设置搜索视图 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) } } 当我们点击顶部的搜索框时,页面需要跳转到真正的搜索页面,所以我们需要实现 UISearchBarDelegate 代理函数:

extension DiscoveryViewController: UISearchBarDelegate { // 点击跳转 func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool { self.musicSearchController = MusicSearchViewController() self.navigationController?.pushViewController(self.musicSearchController, animated: false) return true } }

构建跳转后的搜索页面

首先,需要实现搜索视图,我们的视图控制器 MusicSearchViewController 继承自 UITableViewController,所以它的 UINavigationItem 中自己带有 searchController。不过,由于搜索栏需要自定义一些样式,我们可以先定义一个 UISearchController 的成员变量,将它的属性初始化好以后,再进行赋值,代码如下:

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 在本工程,我们仅实现一个简单的搜索演示功能,因为要真的做好搜索这个需求,需要服务器的”大力“配合,在本工程中,我们仅用一些静态数据来做演示:

musics = [ Results(name: "如果爱"), Results(name: "情书"), Results(name: "龙卷风"), Results(name: "半岛铁盒"), Results(name: "世界末日"), Results(name: "爱在西元前"), Results(name: "等你下课"), Results(name: "黑色幽默"), Results(name: "我不配") ]
首先作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!) 数据源有了,接下来就是来实现数据查找功能了,在搜索栏中输入要搜索的歌名,并在页面上列出我们搜索到的结果。这里就需要来实现 UISearchResultsUpdating 和 UISearchBarDelegate 这俩个代理了,通过 UISearchBar 获取到输入值,然后在提供的数据源中查找,并 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() }

结尾

到此,使用 MVVM 来构建网易云音乐首页就差不多讲完了,我们再总结一下,在本文中我们主要讲解了如何来构建 UI 视图, 由于在我们首页里的 Cell 的样式有不同之处但也有相似的地方,所以我们创建了一个基类 BaseViewCell, 用于展示 Cell 中相同的地方;然后我们在各个 Cell 中构建不同样式的 UI,利用 UICollectionView 这一神器实现了这些效果;最后,实现了简单的搜索功能。

好了,以上便是本次分享~ 下次见!

有关手把手带你撸一个网易云音乐首页(下篇)的更多相关文章

  1. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  2. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  3. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  4. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  5. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  6. ruby-on-rails - Rails - 从另一个模型中创建一个模型的实例 - 2

    我有一个正在构建的应用程序,我需要一个模型来创建另一个模型的实例。我希望每辆车都有4个轮胎。汽车模型classCar轮胎模型classTire但是,在make_tires内部有一个错误,如果我为Tire尝试它,则没有用于创建或新建的activerecord方法。当我检查轮胎时,它没有这些方法。我该如何补救?错误是这样的:未定义的方法'create'forActiveRecord::AttributeMethods::Serialization::Tire::Module我测试了两个环境:测试和开发,它们都因相同的错误而失败。 最佳答案

  7. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  8. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  9. ruby - Rails 关联 - 同一个类的多个 has_one 关系 - 2

    我的问题的一个例子是体育游戏。一场体育比赛有两支球队,一支主队和一支客队。我的事件记录模型如下:classTeam"Team"has_one:away_team,:class_name=>"Team"end我希望能够通过游戏访问一个团队,例如:Game.find(1).home_team但我收到一个单元化常量错误:Game::team。谁能告诉我我做错了什么?谢谢, 最佳答案 如果Gamehas_one:team那么Rails假设您的teams表有一个game_id列。不过,您想要的是games表有一个team_id列,在这种情况下

  10. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

随机推荐