This is the second part of an article on building a Spotify UI clone with autoLayout programmatically. If you missed the first part, no worries - just please go and check it now.
这是关于以编程方式使用autoLayout构建Spotify UI克隆的文章的第二部分。 如果您错过了第一部分,请不用担心-请立即检查一下 。
In this article, we are going to add some mocked pictures and try to make the UI look the same as Spotify's.
在本文中,我们将添加一些模拟图片,并尝试使UI看起来与Spotify的相同。
This is what we are going to do today 👇
这就是我们今天要做的👇
This is were we left off in the first part:
这是我们在第一部分中省略的内容:
The next step is to create customized cells. So let's start by creating one with the name SubCustomCell
.
下一步是创建自定义单元。 因此,让我们开始创建一个名称为SubCustomCell
的名称。
First, create a new Swift file inside the project folder and name it SubCustomCell.swift
. This file will contain our custom cell that will represent the Playlist. After creating the file, try to add in the code below and initialize the cell, maybe with backgroundColor
, to see the UI changes when we register the cell with the collectionView
.
首先,在项目文件夹中创建一个新的Swift文件,并将其命名为SubCustomCell.swift
。 该文件将包含代表播放列表的自定义单元。 创建文件后,尝试添加以下代码并初始化单元格(也许使用backgroundColor
,以查看在我们向collectionView
注册单元格时UI发生的变化。
import UIKit
class SubCustomCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then we register the SubCustomCell
inside CustomCell.swift
within the init
block. Replace UICollectionViewCell.self
with
like below.SubCustomCell
然后,在init
块内的CustomCell.swift
注册SubCustomCell
。 如下所示,用
替换SubCustomCell
UICollectionViewCell.self
。
collectionView.register(SubCustomCell.self, forCellWithReuseIdentifier: cellId)
Also we need to make a modification on the cellForItemAt
method and make it conform to SubCustomCell
like the following.
同样,我们需要对cellForItemAt
方法进行修改,使其符合SubCustomCell
,如下所示。
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! SubCustomCell
// cell.backgroundColor = .yellow
return cell
}
You should see the backgroundColor
changed to red
.
您应该看到backgroundColor
更改为red
。
Up until this point everything should be straightforward and clear.
到目前为止,一切都应该简单明了。
Now we're going to fill the cells with some mocked pictures and create an ImageView
inside each cell. I already downloaded some random pictures from pexels.com, but feel free to use any pictures you like (including these). You can find them in the project files on Github.
现在,我们将使用一些模拟图片填充单元格,并在每个单元格内创建一个ImageView
。 我已经从pexels.com下载了一些随机图片,但是可以随意使用任何您喜欢的图片(包括这些图片)。 您可以在Github上的项目文件中找到它们。
Let's create the UIImageView
inside SubCustomCell.swift
and make some constraints.
让我们在SubCustomCell.swift
创建UIImageView
并进行一些约束。
let ImageView : UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .yellow
return iv
}()
And add it to the view
within the init
block using addSubView
.
并使用addSubView
将其添加到init
块内的view
中。
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(ImageView)
}
Now let's make the ImageView
take up all the space within the cell with the constraints below.
现在,让我们通过以下约束使ImageView
占据单元格中的所有空间。
ImageView.translatesAutoresizingMaskIntoConstraints = false
ImageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
ImageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
ImageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
ImageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
LeftAnchor
represents the left anchor of the cellLeftAnchor
代表单元格的左锚点rightAnchor
represents the right anchor of the cellrightAnchor
代表单元格的右锚点bottomAnchor
represents the bottom anchor of the cellbottomAnchor
表示单元格的底部锚点topAnchor
represents the top anchor of the celltopAnchor
代表单元格的最高锚点
And by making ImageView
's top anchor equal to the cell's top anchor (and doing the same for ImageView
's left, right, and bottom anchor) it makes the ImageView
take up all the space of the SubCustomCell
(cell).
通过使ImageView
的顶部锚点与单元格的顶部锚点相等(并对ImageView
的左,右和底部锚点进行相同操作),可以使ImageView
占用SubCustomCell
(单元格)的所有空间。
Note: first you need to use translatesAutoresizingMaskIntoConstraints
to be able to apply the constraints to the elements. Also don't forget to call isActive
property and assign it to true
– without doing that the constraints won't work and nothing will change.
注意:首先,您需要使用translatesAutoresizingMaskIntoConstraints
才能将约束应用于元素。 另外,不要忘记调用isActive
属性并将其分配为true
-否则约束将不起作用,并且什么也不会改变。
The ImageView
should have an image, so let's add one.
ImageView
应该有一个图像,所以我们添加一个。
let ImageView : UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .yellow
// we have >image1< file inside the project
iv.image = UIImage(named: "image1")
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
And if you build and run the app, you should see the results and picture we added to the SubCustomCell
.
并且,如果您构建并运行该应用程序,则应看到我们添加到SubCustomCell
的结果和图片。
Cool 😎. Now there is an element we should add to the SubCustomCell
to finish up. We need a title that will represent the title of the playlist: UILabel
.
酷😎。 现在有一个元素应该添加到SubCustomCell
以完成操作。 我们需要一个代表播放列表标题的标题: UILabel
。
For the title it will be like this:
对于标题,它将是这样的:
let TitleLabel : UILabel = {
let lb = UILabel()
lb.textColor = UIColor.lightGray
lb.font = UIFont.systemFont(ofSize: 16)
lb.font = UIFont.boldSystemFont(ofSize: 20)
lb.text = "Evening Music"
return lb
}()
I just put some random text there – you can put whatever you like. The next step is to add the element to the view and give it some constraints. The title will be placed at the bottom of the ImageView
.
我只是在其中放一些随机文本-您可以随便放置任何内容。 下一步是将元素添加到视图中并给它一些约束。 标题将放置在ImageView
的底部。
添加到视图: (Add to view:)
addSubview(TitleLabel)
将约束应用于ImageView
和TitleLabel
(Applying the constraints for both the ImageView
and the TitleLabel
)
ImageView.translatesAutoresizingMaskIntoConstraints = false
ImageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
ImageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
ImageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
ImageView.heightAnchor.constraint(equalToConstant: 240).isActive = true
ImageView.bottomAnchor.constraint(equalTo: TitleLabel.topAnchor).isActive = true
TitleLabel.translatesAutoresizingMaskIntoConstraints = false
TitleLabel.topAnchor.constraint(equalTo: ImageView.bottomAnchor,constant: 10).isActive = true
TitleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 5).isActive = true
TitleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -5).isActive = true
And here we go!
现在我们开始!
We made the picture take up most of the space in the cell, and the rest is taken up by the title. As you can see, you can scroll horizontally in each section and also vertically in the entire screen.
我们使图片占据了单元格中的大部分空间,其余部分则由标题占据。 如您所见,您可以在每个部分中水平滚动,也可以在整个屏幕中垂直滚动。
Now we are put some mock data into the cells to make it feel like it's real. For that I created a JSON
file that contains some random data for sections and playlists.
现在,我们将一些模拟数据放入单元格中,以使其看起来像是真实的。 为此,我创建了一个JSON
文件,其中包含部分和播放列表的一些随机数据。
First let's create a two structs, Section
and Playlist
. We create a separate file for each struct.
首先,我们创建两个结构, Section
和Playlist
。 我们为每个结构创建一个单独的文件。
section.swift
section.swift
import Foundation
struct Section {
var title : String
var playlists : NSArray
init(dictionary:[String : Any]) {
self.title = dictionary["title"] as? String ?? ""
self.playlists = dictionary["playlists"] as? NSArray ?? []
}
}
playlist.swift
playlist.swift
//
// playlist.swift
// spotifyAutoLayout
//
// Created by admin on 12/6/19.
// Copyright © 2019 Said Hayani. All rights reserved.
//
import Foundation
struct PlayList {
var title: String
var image : String
init(dictionary : [String : Any]) {
self.title = dictionary["title"] as? String ?? ""
self.image = dictionary["image"] as? String ?? ""
}
}
And then inside ViewController.swift
we create a function that fetches the JSON for us and stores the results in an array.
然后在ViewController.swift
创建一个函数,该函数为我们获取JSON并将结果存储在数组中。
print("attempt to fetch Json")
if let path = Bundle.main.path(forResource: "test", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
if let jsonResult = jsonResult as? [ Any] {
// do stuff
jsonResult.forEach { (item) in
let section = Section(dictionary: item as! [String : Any])
// print("FEtching",section.playlists)
self.sections.append(section)
}
self.collectionView.reloadData()
}
} catch {
// handle error
}
}
}
The fetchJson
function is called within the ViewDidLoad
method. We also have a variable called sections
where we store the results:
该fetchJson
函数在内部调用ViewDidLoad
的方法。 我们还有一个名为sections
的变量,用于存储结果:
var sections = [Section]()
The next step is to pass the data from ViewController
to CustomCell
. For that we create a variable inside CustomCell
which will receive the data for each section:
下一步是将数据从ViewController
传递到CustomCell
。 为此,我们在CustomCell
创建一个变量,该变量将接收每个部分的数据:
var section : Section?{
didSet{
print("section ✅",self.section)
}
}
We use cellForItemAt
inside the ViewController
method to pass the data directly to the CustomCell
.
我们在ViewController
方法内部使用cellForItemAt
将数据直接传递到CustomCell
。
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CustomCell
cell.section = sections[indexPath.item]
return cell
}
Note: we always call self
.collectionView.reloadData()
every-time fetchJson
is called so the block below, inside CustomCell
, will be called as well. Check the console, shift
+ command + C:
注:我们始终调用self
.collectionView.reloadData()
的每次fetchJson
被称为所以下面的方块,里面CustomCell
,将被称为好。 检查控制台, shift
+ Command + C:
var section : Section? {
didSet{
print("section ✅",self.section)
}
}
The first thing we change is to set the the section title:
我们更改的第一件事是设置节标题:
var section : Section? {
didSet{
print("section ✅",self.section)
guard let section = self.section else {return}
self.titleLabel.text = section.title
}
}
And then you should see that each section has a specific title on the screen 😃.
然后,您应该在屏幕see上看到每个部分都有特定的标题。
Now it's time to pass the data down to SubCustomCell
. We do the same thing as we did above. We need to pass the playlists
array, so we create a variable named playlists
inside CustomCell
.
现在是时候将数据传递给SubCustomCell
。 我们做与上述相同的事情。 我们需要传递playlists
数组,因此我们在CustomCell
创建一个名为playlists
的变量。
var playlists : [PlayList]() //empty
First, we map through the playlists
from the JSON
. Then we add each playlist with the playlists
var.
首先,我们通过JSON
映射playlists
。 然后,我们将每个播放列表与playlists
var相加。
var section : Section? {
didSet{
print("section ✅",self.section)
guard let section = self.section else {return}
self.titleLabel.text = section.title
// append to playlists array
self.section?.playlists.forEach({ (item) in
let playlist = PlayList(dictionary: item as! [String : Any])
self.playlists.append(playlist)
})
self.collectionView.reloadData()
}
}
Attention! If you try to run the app it may crash. This is because we forgot to set the number of sections. Since we are now receiving the data from JSON, the number should be dynamic based on the number of sections we have. The number of sections should be equal to the number of sections inside the JSON
, so we need to modify numberOfItemsInSection
inside ViewController
to the below :
注意! 如果您尝试运行该应用程序,则可能会崩溃。 这是因为我们忘记了设置部分的数量。 由于我们现在正在从JSON接收数据,因此该数量应该根据我们拥有的部分的数量而动态变化。 部分的数量应等于JSON
内部的部分数量,因此我们需要将ViewController
内部的numberOfItemsInSection
修改为以下内容:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sections.count
}
We do the same thing with the same method inside CustomCell.swift
– but here we consider the number of the playlists
instead.
我们在CustomCell.swift
使用相同的方法执行相同的CustomCell.swift
-但在这里,我们考虑playlists
的数量。
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.playlists.count
}
The last step we have to complete is to pass each single playlist Object
to SubCustomCell
within cellForItemAt
in CustomCell.swift
.
我们必须完成的最后一步是将每个播放列表Object
传递给SubCustomCell
中cellForItemAt
中的CustomCell.swift
。
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! SubCustomCell
// here 👇
cell.playlist = playlists[indexPath.item]
return cell
}
And we are going to get that data inside SubCustomCell
via the playlist
variable and finally display the title and image of the playlist.
我们将通过playlist
变量在SubCustomCell
获取该数据,并最终显示playlist
的标题和图像。
var playlist : PlayList? {
didSet{
print("Playlist 🎯",self.playlist)
guard let playlist = self.playlist else {return}
// The Image 👇
self.ImageView.image = UIImage(named: playlist.image)
// the playlist title 👇
self.TitleLabel.text = self.playlist?.title
}
}
I think everything should work fine now, just as below 😃
我认为现在一切正常,就像下面的😃
One last update to the UI: we have to add some padding and margins to the section
and playlist
titles and make the playlist a little bit smaller.
用户界面的最新更新:我们必须在section
和playlist
标题中添加一些填充和边距,并使播放列表更小一些。
Let's first add some padding for the section titles. To do that, we need just to give the constant
property some number value inside the section cell CustomCell
and within setupSubCells
:
首先,为部分标题添加一些填充。 为此,我们只需要在小节单元格CustomCell
内和setupSubCells
内为constant
属性提供一些数值:
collectionView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor,constant: 15).isActive = true
And if you see the entire collectionView
come in at the bottom of the titleLabel
, all we need to do is add more space by adding 15
:
而且,如果您看到整个collectionView
出现在titleLabel
的底部,那么我们要做的就是通过添加15
来添加更多空间:
Next we come to the title of the playlist
. This will be inside SubCustomCell
, and we just need to add more space at the bottom of the ImageView.
接下来,我们进入playlist
的标题。 这将在SubCustomCell
内部,我们只需要在ImageView的底部添加更多空间即可。
ImageView.bottomAnchor.constraint(equalTo: TitleLabel.topAnchor,constant: -15).isActive = true
We already have the constant there. In order for it to work, the value should be -15
我们已经在那里有了常数。 为了使其正常工作,该值应为-15
Finally the playlist needs to be a little bit smaller. This is easy: we just make the playlist
cell's height and width equal to the section
cell's height divided by 2, just like below:
最后,播放列表需要小一些。 这很容易:我们只需使playlist
单元格的高度和宽度等于section
单元格的高度除以2,如下所示:
CustomCell.swift
CustomCell.swift
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = frame.height / 2
let height = frame.height / 2
return CGSize(width: width, height: height)
}
Make the ImageView's height equal to 150
as well.
使ImageView的高度也等于150
。
//SubCutomCell.swift
ImageView.heightAnchor.constraint(equalToConstant: 150).isActive = true
And here we go 👇.
然后我们去👇。
Perfect! I think that's enough for today – I don't want to make this article too long. So we will have another part where we will add the TabBar
and the description, as well as some icons for the playlist.
完善! 我认为今天就足够了–我不想把这篇文章写得太长。 因此,我们将在另一部分中添加TabBar
和说明以及播放列表的一些图标。
View the Full source code on GitHub🔗.
Thanks for your time. I hope I haven't missed anything. If I did please @mention me on Twitter, or if you have any questions or an addition to this post the doors are always open to anyone. Thanks🙏🏼.
谢谢你的时间。 我希望我什么都没错过。 如果我想在Twitter上 @ @提及我,或者如果您对本帖子有任何疑问或补充,则随时对任何人开放。 谢谢🙏🏼。
Subscribe to my email list to be notified when the third part of this tutorial is published.
订阅 我的电子邮件列表,以在本教程的第三部分发布时得到通知。
By the way, I’ve recently worked with a strong group of software engineers for one of my mobile applications. The organization was great, and the product was delivered very quickly, much faster than other firms and freelancers I’ve worked with, and I think I can honestly recommend them for other projects out there. Shoot me an email if you want to get in touch — said@devsdata.com.
顺便说一句,我最近与一群强大的软件工程师一起为我的一个移动应用程序工作。 该组织很棒,产品交付速度非常快,比我所合作的其他公司和自由职业者要快得多,我想我可以诚实地推荐他们用于其他项目。 如果您想与我联系,请给我发送电子邮件-said@devsdata.com 。