preface

Hello, everyone. Today I am going to continue to share with you how to use Swift to achieve a home page of netease Cloud Music. Since the publication of the last article, I have gained a lot of friends’ attention and likes, and also got some very useful suggestions. Thank you again for your recognition. Your encouragement and suggestions are the biggest motivation for my technology output.

MVVM

Ok, back to the topic, we are using the MVVM mode in our project. In the last article, we finished with The Model and ViewModel, so let’s start with the View! For those of you who are starting with this article, you might want to start with my last article, so that you can be sure of continuity.

View

Back in our project, ready to build our table view.

First, create the storage property HomeViewModel in our home page view controller, DiscoveryViewController, and initialize it. In our actual development process, the data request operation is essential, you must first feed the data to the ViewModel, and then rereload the TableView when the data is updated.

ViewModel fileprivate var homeViewModel = homeViewModel ()

Next, let’s configure our 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 }

Now we can start building the UI. According to the style of netease Cloud Music, we need to create 12 different types of cells, and each Cell corresponds to one kind of ViewModelItems.

To further improve the quality of the code, we can define a base class BaseViewCell for these cells. With this base class, we can set some default properties, reducing unnecessary coding. In addition, if you look at it, most sections will contain a headView. To implement a headView, you can use the following method:

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

However, in this project, I am not going to use the above method to implement headView, mainly because every Section of netease Cloud Music has rounded corners, if we define viewForHeaderInSection, So we need to do the following logic to achieve rounded corners:

  • Add rounded corners to the top left and top right corners of the headView
  • Add rounded corners to the lower left and right corners of the Cell in the Section as shown in the figure below:

We know that adding rounded corners to a view is very delicate. Calling the cornerRadius and masksToBounds methods will render the corners off-screen, and our home page has a lot of rounded corners. That’s not a good experience! You can’t specify where to set the rounded corner of the view using either of these methods.

First of all, as a developer, it is particularly important to have a learning atmosphere and a communication circle. This is my iOS development communication group:
710 558 675, no matter you are small white or Daniel welcome to enter, let us progress together, common development! (The group will provide some free learning books collected by the group owner and hundreds of interview questions and answer documents!)

As mentioned above, rounded corners can cause off-screen rendering by mistake. How to solve this problem? In this case, we can use UIBezierPath to draw rounded corners for the view, and we can also specify the direction to draw the rounded corners:

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
    }

Considering that if we’re creating a HeadView through the viewForHeaderInSection method, then we’re going to have to draw rounded corners for two views, the TableViewCell and the HeadView created by the viewForHeaderInSection. A good idea is to implement our headView in our tableViewCell by calling the draw method once, as follows:



In addition, since each Section has a headView, we can implement this header view in the base class BaseViewCell:

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") } }

Next, let’s build the specific Cell. Due to the amount of code, only part of the code is shown here:

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) /// initialize scrollBanner = JJNewsBanner(frame: 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")}} / / / front page - found round button 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) /// Initialize the Home EnU = Home EnU (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) } } ....

In reality, the view styles presented by each Cell are very rich, so we have to create different UI styles for the Cell, each style corresponding to its own data Model.

Build the TableViewCell style

Image rotation effect

First of all, the top layer of netease Cloud Music is a picture rotation effect, how to build this Banner! I’m not going to skip around here, but I’m going to use the most common widget, the UICollectionView. After reading this article, you’ll see that you can use UICollectionView for just about anything.

I won’t go into the details of the code here, because I’ve already written a tutorial to achieve this effect in my previous article.

Round menu entry

The effect is simple to implement, the only interesting thing is that the middle text on the Song of the Day button changes with the date, as shown here:

But it’s easy to implement, just put a Label in the middle. As shown in this side image (borrowed from the author Leo) :



The control used for the overall implementation is UICollectionView. Part of the code is as follows:

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 the make, centerX equalToSuperview () make the width. EqualTo (self. Frame. The size. Width * 0.6) Make. Height. EqualTo (self. Frame. The size, width * 0.6)}. Self menuIcon. SNP. MakeConstraints {(make) in Make. CenterX. EqualToSuperview () make. CenterY. EqualToSuperview () make. Width. EqualTo (self. Frame. The size. Width * 0.6) Make. Height. EqualTo (self. Frame. The size, width * 0.6)}. Self menuText. SNP. MakeConstraints {(make) in Make. CenterX. EqualToSuperview () make. Bottom. EqualToSuperview () make. Height. EqualTo (self. Frame. The size. Width * 0.4) Make. Width. EqualTo (self. Frame. The size, width)} / / set menu rounded self. MenuLayer. Layer. The cornerRadius = self. Frame. The size. The 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 } }

Recommended playlists/music videos/Radar playlists/video collections, etc

Let’s look at the UI:

Since these UIs look pretty much the same, the first idea that comes up is to put a UICollectionView in the Cell, and the layout is very simple, just use what the system provides, we don’t need to customize the layout.

CollectionViewCell (CollectionViewCell (CollectionViewCell))

Import UIKit import SnapKit import Kingfisher class CardViewCell: UICollectionViewCell {/// Cover 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: Desclabel. numberOfLines = 0}() var views: String? /// let float: 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) / / cover style set self. AlbumCover. SNP. MakeConstraints {(make) in the make. Width. EqualTo (width) make.height.equalTo(width) make.centerX.equalToSuperview() make.top.equalToSuperview() } self.albumCover.roundCorners(self.albumCover.bounds, corners: [.allCorners], radius: Let viewsRect = self.getStrboundRect (STR: self.views! , font: self.viewsButton.titleLabel! .font, constrainedSize: 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 the make. Width. EqualTo (width - 10) make.height.equalTo(descHeight) make.centerX.equalToSuperview() make.top.equalTo(self.albumCover.snp.bottom).offset(5) } }... }
Class CardCollectionView: UIView {..... /// layout lazy var cardFlowLayout: UICollectionViewFlowLayout = { let layout = UICollectionViewFlowLayout() layout.minimumLineSpacing = margin layout.minimumInteritemSpacing = 0 layout.sectionInset = UIEdgeInsets.init(top: -20, left: margin, bottom: 0, right: Layout.scrolldirection =.horizontal Return Layout}() horizontal var hotAlbumContainer lazy: 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) / / set the item size 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 } }

Personality recommendation/new songs new disc digital album /

Next, let’s build another style. Let’s start with the UI:

Since the features of “personal recommendation” and “new songs and new digital albums” are similar, they are also mentioned together. Again, I’m going to put a UICollectionView in the Cell. However, by looking at its UI style, you can see that it has to pay attention to, even in the same page, its second item also needs to expose part, how to achieve this!

In order to appear in a page two item, then we have to reduce the width of the itemSize, so after setting UICollectionViewFlowLayout can appear two item in one page.

We know that in the UICollectionView property, we have a pagination property: isPagingEnabled, which, when set to true, is equal to the width of its own frame; When this pagination property is not set, its default value is false, so its scrolling does not have the pagination effect.

OK, is that right? In fact, when you do this, you will find that there is a very annoying bug in the implementation, which is that the item will be blocked when scrolling, which is very considerate of the user.

The UICollectionView control can only page according to the size of the screen. The answer, of course, is no. We can also implement paging scrolling in a custom way. According to the document, Apple in the definition of UICollectionViewFlowLayout provides a rewritable function:

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

The value returned by this function determines the offset at which the UICollectionView stops scrolling. You can override this function for custom paging scrolling. The logic behind overriding this function is as follows:

  1. Define a coordinate point CGPoint to record the offset coordinates of the latest scroll
  2. Define two values: UICollectionView scrollable maximum offset and minimum offset is also 0
  3. The above function func targetContentOffset (…) is called each time the scroll stops In this function, there is a parameter proposedContentOffset that records the target displacement coordinates of the scroll, and the coordinates of the last scroll recorded can be used to determine whether to scroll left or right
  4. If the absolute value of the horizontal subtraction of the two coordinates is greater than some fixed value (such as 1/8 of the item width), paging can be determined, and the number of pages currently scrolling can be calculated using the proposedContentOffset displacement coordinates and the width of the item. If it is less than that fixed value, no paging occurs
  5. Finally, record the latest offset coordinates and return the offset when the UICollectionView stopped scrolling

Code implementation is as follows:

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")} // init func prepare() {super.prepare() self.collectionView? .activity Rate =.fast} // The return value of this activity, Override FUNc targetContentOffset(forProposedContentOffset proposedContentOffset) CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {// page width let pageSpace = self.stepspace () let offsetMax: CGFloat = self.collectionView! .contentSize.width - (pageSpace + self.sectionInset.right + self.minimumLineSpacing) let offsetMin: CGFloat = 0 // Modify the previous record position, 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 False true, left to right let direction: Bool proposedContentOffset. X-ray lastOffset. (x) = 0 > var newProposedContentOffset: CGPoint = CGPoint. Zero if (offsetForCurrentPointX > pageSpace/8.0) && (lastOffset. X >= offsetMin) && (lastOffset Var pageFactor: NSInteger = 0 if velocityX! = 0 {// the faster the slide, PageFactor = abs(NSInteger(velocityX))} else {// Drag pageFactor = abs(NSInteger(offsetForCurrentPointX / PageFactor = pageFactor < 1? PageFactor = pageFactor < 1? PageFactor = pageFactor < 1? PageFactor = pageFactor < 1? PageFactor = pageFactor < 1? 1: (pageFactor < 3 ? 1: 2) let pageOffsetX: CGFloat = pageSpace * CGFloat(pageFactor) newProposedContentOffset = CGPoint(x: lastOffset.x + (direction ? pageOffsetX : -pageOffsetX), y: NewProposedContentOffset = CGPoint(x: lastoffset.x, y:);} else {newProposedContentOffset = CGPoint(x: lastOffset. LastOffset. Y)} lastOffset. X = newProposedContentOffset. X return newProposedContentOffset} / / every page of the spacing sliding public func stepSpace() -> CGFloat { return self.itemSize.width + self.minimumLineSpacing } }

I’ve already written a tutorial to achieve this effect in my previous post, so check it out

Music calendar

The UI is shown in figure:

Music calendar, you don’t need to support horizontal scrolling, so you can choose to put a UIView in the Cell, and for those of you who have a little bit of iOS development background, it shouldn’t be too hard to implement this UI, you can do it either through Xib or code, Xib should be faster to implement, I’m not going to elaborate on that.

podcast

So I’m finally at the end of my UI, so let’s see what it looks like:

After all of the uIs you’ve built up here, you know what this looks like, and what’s an easier way to do it than to use UICollectionView? The same is to build a picture below the Cell, but the podcast needs to add pictures with rounded corners, code implementation is also very simple, here also do not do more elaborated.

search

That’s all about how to build different cells. If you have any questions, please feel free to send me a message in the comment section or my official account.

Next, let’s move on to the last part of the homepage – the search box. There is a view at the top of the home page of netease Cloud Music, which contains three parts: left button, search box and right button. This structure easily reminds us of UINavigationItem. Yes, the UINavigationItem is the most efficient way to implement such a UI structure.

Since our home page controller inherits from UITableViewController, we can directly set the leftBarButtonItem in its UINavigationItem property, TitleView and rightBarButtonItem:

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

Customize UISearchBar with the following code:

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 size using the 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) } }

When we click on the search box at the top, the page needs to jump to the real search page, so we need to implement the UISearchBarDelegate function:

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

Build the search page after the jump

First, you need to realize the search view, we view controller MusicSearchViewController inherited from UITableViewController, so its UINavigationItem with searchController himself. However, since the search bar needs to be styled, we can define a UISearchController member variable, initialize its properties, and then assign values as follows:

   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

In this project, we only implement a simple search demonstration function, because to really do a good job in the search requirement, we need the server’s “strong” cooperation, in this project, we only use some static data to do the demonstration:

Musics = [Results(name: "love "), Results(name:" love "), Results(name: "love "), Results(name:" love "), Results(name: "love "), Results(name:" love "), Results(name: "love "), Results(name:" love "), Results(name: "Love "), Results(name:" Love "), Results(name: "Love "), "The end of the world "), Results(name:" love in B.C. "), Results(name: "wait for you to finish class "), Results(name:" I don't deserve it ")]

First of all, as a developer, it is particularly important to have a learning atmosphere and a communication circle. This is my iOS development communication group: 710 558 675. No matter you are a small white or a big ox, you are welcome to join us. (The group will provide some free learning books collected by the group owner and hundreds of interview questions and answer documents!)

With the data source in place, the next step is to implement the data search function. Enter the song name to search in the search bar and list the results we find on the page. We need to implement the UISearchResultsUpdating and UISearchBarDelegate proxies, retrieve the input values from the UISearchBar, look them up in the provided data source, and reload our table view:

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() }

At the end

Here, the use of MVVM to build netease Cloud Music home page is almost finished, let’s summarize again, in this article we mainly explain how to build UI view, because in our home page Cell style is different but there are also similarities. So we created a base class, BaseViewCell, to show the same place in the Cell; Then we built different styles of UI in each Cell, using the UICollectionView artifact to achieve these effects; Finally, a simple search function is implemented.

Well, that’s all for this share ~ see you next time!