关于ios:带你手把手撸一个网易云音乐首页下篇

前言

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开发交换群:710 558 675,不论你是小白还是大牛都欢送入驻 ,让咱们一起提高,独特倒退!(群内会收费提供一些群主珍藏的收费学习书籍材料以及整顿好的几百道面试题和答案文档!)

下面讲到为视图设置圆角一不小心就会造成离屏渲染,那么这个问题该如何解决呢!在这里,咱们能够通过利用 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开发交换群:710 558 675,不论你是小白还是大牛都欢送入驻 ,让咱们一起提高,独特倒退!(群内会收费提供一些群主珍藏的收费学习书籍材料以及整顿好的几百道面试题和答案文档!)

数据源有了,接下来就是来实现数据查找性能了,在搜寻栏中输出要搜寻的歌名,并在页面上列出咱们搜寻到的后果。这里就须要来实现 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核2G云服务器,首年99元

阿里云限时活动-1核2G-1M带宽-40-100G ,特惠价87.12元/年(原价1234.2元/年,可以直接买3年),速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

You may also like...

发表评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据