นี่คือภาคที่ 2 ของบทความสร้าง UI โคลน Spotify ด้วย AutoLayout แบบโปรแกรมมิง หากพลาดภาคแรก ไม่ต้องกังวล - โปรดไปอ่านก่อนครับ
ในบทความนี้ เราจะเพิ่มรูปภาพจำลอง (Mocked Pictures) และพยายามทำให้ UI ดูเหมือน Spotify จริงๆ
นี่คือสิ่งที่เราจะทำวันนี้ 👇

นี่คือจุดที่เราหยุดในภาคแรก:

ขั้นตอนต่อไปคือสร้าง Custom Cell เริ่มต้นด้วยการสร้างชื่อ SubCustomCell
ขั้นแรก สร้างไฟล์ Swift ใหม่ในโฟลเดอร์โปรเจกต์ ชื่อ SubCustomCell.swift ไฟล์นี้จะมี Custom Cell แทน Playlist หลังสร้างไฟล์ ใส่โค้ดด้านล่างและลอง initialize cell ด้วย backgroundColor เพื่อดูการเปลี่ยนแปลง UI เมื่อลงทะเบียน cell กับ collectionView
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")
}
}
จากนั้นลงทะเบียน SubCustomCell ภายใน CustomCell.swift ในบล็อก init แทนที่ UICollectionViewCell.self ด้วย SubCustomCell ดังนี้
collectionView.register(SubCustomCell.self, forCellWithReuseIdentifier: cellId)
ต้องแก้ไขเมธอด 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
}
ควรเห็น backgroundColor เปลี่ยนเป็นสีแดง

จนถึงตรงนี้ทุกอย่างควรเข้าใจง่ายและชัดเจน
ตอนนี้เราจะเติมรูปภาพจำลองลงใน Cell และสร้าง ImageView ภายในแต่ละ Cell ผมดาวน์โหลดรูปสุ่มจาก pexels.com แต่อยู่ที่คุณ ใช้รูปอะไรก็ได้ (รวมถึงรูปเหล่านี้) หาได้ในไฟล์โปรเจกต์บน GitHub
สร้าง UIImageView ใน SubCustomCell.swift และทำ Constraints
let ImageView: UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .yellow
return iv
}()
เพิ่มลงใน view ภายในบล็อก init ด้วย addSubView
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(ImageView)
}
ทำให้ ImageView ครอบพื้นที่ทั้งหมดใน Cell ด้วย Constraints ด้านล่าง
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= จุดล็อกซ้ายของ CellRightAnchor= จุดล็อกขวาของ CellBottomAnchor= จุดล็อกล่างของ CellTopAnchor= จุดล็อกบนของ Cell
การตั้ง Anchor ของ ImageView เท่ากับ Anchor ของ Cell ทั้ง 4 ด้าน ทำให้ ImageView ขยายเต็มพื้นที่ของ SubCustomCell
หมายเหตุ: ต้องใช้ translatesAutoresizingMaskIntoConstraints ก่อน จึงจะใช้ Constraints ได้ และอย่าลืมตั้ง isActive = true ไม่งั้น Constraints จะไม่ทำงาน
ImageView ควรมีรูปภาพ ให้เพิ่มเข้าไป
let ImageView: UIImageView = {
let iv = UIImageView()
iv.backgroundColor = .yellow
// มีไฟล์ image1 ในโปรเจกต์
iv.image = UIImage(named: "image1")
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
Build และ Run แอป ควรเห็นผลลัพธ์และรูปที่เพิ่มใน SubCustomCell

เยี่ยม 😎 ตอนนี้ยังมีองค์ประกอบที่ต้องเพิ่มใน SubCustomCell อีกหนึ่งอย่างคือ Title แทนชื่อ Playlist: UILabel
สำหรับ Title จะเป็นแบบนี้:
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
}()
ผมใส่ข้อความสุ่ม คุณใส่อะไรก็ได้ ขั้นตอนต่อไปคือเพิ่ม Element ลง View และให้ Constraints Title จะวางที่ด้านล่างของ ImageView
เพิ่มลง View:
addSubview(TitleLabel)
Constraints สำหรับ ImageView และ 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
เสร็จแล้ว!

เราทำให้รูปครอบพื้นที่ส่วนใหญ่ ส่วนที่เหลือสำหรับ Title 如你所见 สามารถเลื่อนแนวนอนในแต่ละ Section และแนวตั้งในหน้าจอทั้งหมด

ตอนนี้ใส่ Mock Data ลง Cell ให้รู้สึกเหมือนจริง ผมสร้างไฟล์ JSON ที่มีข้อมูลสุ่มสำหรับ Sections และ Playlists
ขั้นแรก สร้าง Struct 2 ตัว: Section และ Playlist สร้างไฟล์แยกสำหรับแต่ละ Struct
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
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 ?? ""
}
}
ใน ViewController.swift สร้างฟังก์ชันดึง JSON และเก็บผลลัพธ์ใน Array
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] {
jsonResult.forEach { (item) in
let section = Section(dictionary: item as! [String: Any])
self.sections.append(section)
}
self.collectionView.reloadData()
}
} catch {
// handle error
}
}
ฟังก์ชัน fetchJson เรียกใน ViewDidLoad มีตัวแปร sections เก็บผลลัพธ์:
var sections = [Section]()
ขั้นตอนต่อไป ส่งข้อมูลจาก ViewController ไป CustomCell สร้างตัวแปรใน CustomCell รับข้อมูลแต่ละ Section:
var section: Section? {
didSet {
print("section ✅", self.section)
}
}
ใช้ cellForItemAt ใน ViewController ส่งข้อมูลตรงไป 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
}
หมายเหตุ: เรียก self.collectionView.reloadData() ทุกครั้งที่ fetchJson ทำงาน บล็อกด้านล่างใน CustomCell จะถูกเรียก ดู Console: shift + command + C
var section: Section? {
didSet {
print("section ✅", self.section)
}
}
แก้ไขแรกคือตั้งค่า Section Title:
var section: Section? {
didSet {
print("section ✅", self.section)
guard let section = self.section else { return }
self.titleLabel.text = section.title
}
}
ควรเห็นแต่ละ Section มี Title เฉพาะตัวบนหน้าจอ 😊

ส่งข้อมูลต่อไป SubCustomCell ทำเหมือนเดิม ส่ง Array playlists สร้างตัวแปร playlists ใน CustomCell
var playlists: [PlayList]() // empty
Map ผ่าน playlists จาก JSON เพิ่มแต่ละ Playlist ลงตัวแปร playlists
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()
}
}
ระวัง! ถ้า Run อาจ Crash เพราะลืมตั้งจำนวน Sections ตอนรับข้อมูลจาก JSON จำนวนต้องเป็น Dynamic ตามจำนวน Sections ใน JSON ต้องแก้ numberOfItemsInSection ใน ViewController:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sections.count
}
ทำเหมือนกันใน CustomCell.swift แต่พิจารณาจำนวน playlists
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.playlists.count
}
ขั้นสุดท้าย ส่ง Playlist 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
}
รับข้อมูลใน SubCustomCell ผ่านตัวแปร playlist แสดง Title และ Image ของ 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
}
}
คิดว่าทุกอย่างควรทำงานได้แล้ว 👇

อัปเดต UI สุดท้าย: เพิ่ม Padding และ Margin ให้ Section และ Playlist Title และทำให้ Playlist เล็กลงนิดหน่อย
ก่อนอื่น เพิ่ม Padding ให้ Section Title ใน CustomCell ภายใน setupSubCells:
collectionView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15).isActive = true
ถ้า CollectionView ติดกับ TitleLabel เกินไป ให้เพิ่ม Space 15

ต่อมา Title ของ Playlist ใน SubCustomCell เพิ่ม Space ใต้ ImageView
ImageView.bottomAnchor.constraint(equalTo: TitleLabel.topAnchor, constant: -15).isActive = true
มี Constant อยู่แล้ว ต้องเป็น -15 จึงจะทำงาน

สุดท้าย ทำ Playlist ให้เล็กลงง่ายๆ คือ ทำ Height และ Width ของ Playlist Cell เท่ากับ Section Cell Height หาร 2
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)
}
ตั้งค่า ImageView Height = 150
// SubCustomCell.swift
ImageView.heightAnchor.constraint(equalToConstant: 150).isActive = true
เสร็จแล้ว 😎

พอดีครับ ไม่อยากทำยาวเกินไป จะมีภาคต่อเพิ่ม TabBar คำอธิบาย และไอคอน Playlist
ดู Full Source Code บน GitHub👇
ขอบคุณสำหรับเวลา หวังไม่ได้พลาดอะไร ถ้ามี @mention ผมที่ Twitter ได้เลย หรือมีคำถาม/เพิ่มเติม ประตูเปิดให้ทุกคน ขอบคุณครับ 🙏🙏
สมัครรับ อีเมลเพื่อรับแจ้งตอนเผยแพร่ภาคที่ 3 ของบทเรียนนี้
โดยดרך แค่เพิ่งทำงานกับกลุ่มวิศวกรซอฟต์แวร์ที่เก่งมากสำหรับแอปมือถือของผม องค์กรดีเยี่ยม ส่งมอบผลิตภัณฑ์เร็วมากกว่าบริษัทและฟรีแลนซ์อื่นๆ ที่เคยทำงาน ผมแนะนำพวกเขาได้เลย ส่งอีเมลมาที่ said@devsdata.com ถ้าอยากติดต่อ