ในโพสต์นี้เราจะลองสร้างหน้าจอ Home ของ Spotify ใน Swift แบบโปรแกรมมิง (Programmatically) ทำไมต้องแบบโปรแกรมมิง? ผมคิดว่ารู้วิธีสร้างอย่างต่างๆ ดี และชอบเขียนโค้ดทำให้เกิดขึ้น ทักษะเหล่านี้มีประโยชน์มากหากทำงานทีม หรือใช้ Version Control

นี่คือหน้าจอ Home จริงของ Spotify Mobile App เพื่อให้ได้ Layout แบบนี้ เราจะใช้ UICollectionView และอาจใช้ TabBarController สร้าง Tab Navigator ด้วย
ข้อกำหนดพื้นฐาน: ตรวจสอบก่อนว่ามี Xcode 10+ และ Swift 4+ ติดตั้งแล้ว
เริ่มด้วยการสร้าง Xcode Project ใหม่:

สิ่งแรกใน ViewController.swift เปลี่ยน Superclass เป็น UICollectionViewController แทน UIViewController เพราะ Class ของเราจะ Base บน collectionView
import UIKit
class ViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .purple
// Do any additional setup after loading the view.
}
}
ถ้า Run App Build จะ Fail ต้องเพิ่มโค้ดใน AppDelegate.swift ในฟังก์ชัน didFinishLaunchingWithOptions ก่อน return:
let layout = UICollectionViewFlowLayout()
window = UIWindow()
window?.rootViewController = ViewController(collectionViewLayout: layout)
โค้ดเต็มจะเป็นแบบนี้:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let layout = UICollectionViewFlowLayout()
window = UIWindow()
window?.rootViewController = ViewController(collectionViewLayout: layout)
return true
}
ตอนนี้ Run App ได้ และเห็น BackgroundColor เปลี่ยนเป็นม่วง:

ขั้นตอนต่อไป แจกแจง Layout และแบ่งพื้นที่เท่าๆ กันระหว่าง Sections
กำหนดเมธอดของ CollectionView:
- Register Reusable Cell พร้อม Identifier ที่ Unique
- กำหนดจำนวน Items ใน Section
- ใช้ Cell ที่ลงทะเบียนแล้ว
ต้อง Conform UICollectionViewDelegateFlowLayout เป็น Superclass เพื่อรับ AutoComplete เมธอด เริ่ม Register CollectionViewCell
ใน viewDidLoad() เรียก collectionView.register() ลงทะเบียน Reusable Cell:
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
กำหนดจำนวน Cell ใน collectionView ด้วย numberOfItemsInSection ตอนนี้แค่ 5 Items:
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
กำหนด Reusable Cell ด้วย cellForItemAt คืน UICollectionViewCell มี Unique ID ชื่อ cellId:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
cell.backgroundColor = .red
return cell
}
โค้ดเต็ม:
import UIKit
class ViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId: String = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .purple
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 5
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
cell.backgroundColor = .red
return cell
}
}
ควรเห็น 5 Items พื้นหลังสีแดง:

เพิ่ม Width และ Height แบบกำหนดเองให้ Cells
ตอนนี้วาง Cell ให้ถูกต้องและให้ Width, Height แต่ละ Cell รับ Width ของหน้าจอเป็น Width
โชคดีที่มีเมธอด sizeForItemAt ให้กำหนด Width, Height คืนค่า CGSize:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = view.frame.width
let height = CGFloat(200)
return CGSize(width: width, height: height)
}
Cell รับ Width ของหน้าจอด้วย view.frame.width และ Height แบบกำหนดเป็น CGFloat
ผลลัพธ์ใน Simulator:

ดูดีแล้ว คราวนี้สร้าง Custom Cell ที่ Reusable ได้ สร้างไฟล์ Swift ใหม่ชื่อ CustomCell:

CustomCell.swift ควรเป็นแบบนี้:
import UIKit
class CustomCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
แก้ 2 เมธอดรองรับ Reusable Cell: collectionView.register และ cellForItemAt แก้ Register Method ก่อน แทนที่ UICollectionViewCell.self ด้วย CustomCell:
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
Cast cellForItemAt ให้ Conform CustomCell:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CustomCell
Run App อาจไม่เห็นเปลี่ยนแปลง ให้ CustomCell backgroundColor = .yellow ลบบรรทัด cell.backgroundColor = .red ใน cellForItemAt ควรเห็นสีเหลือง 😊

ตอนนี้ใส่เนื้อหาลง CustomCell :D
ดู Spotify Home Screen แต่ละ Section (คือ CustomCell ของเรา) มี Section Title, Sub Cells และเลื่อนแนวนอน:

เพิ่ม Section Title
เพิ่ม Title Label ใน Cell สร้าง titleLabel ใน Class CustomCell:
let titleLabel: UILabel = {
let lb = UILabel()
lb.text = "Section Title"
lb.font = UIFont.boldSystemFont(ofSize: 14)
lb.font = UIFont.boldSystemFont(ofSize: 14)
return lb
}()
เพิ่ม Element ลง View ในบล็อก init():
addSubview(titleLabel)
Run App ไม่เห็นการเปลี่ยนแปลง เพราะยังไม่มี Constraint ให้ Element เพิ่ม Property lb.translatesAutoresizingMaskIntoConstraints = false ให้ titleLabel เพื่อใช้ Constraints ได้
หลังเพิ่ม titleLabel ลง View กำหนด Constraints:
addSubview(titleLabel)
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true
titleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true
ต้องใส่ .isActive = true เสมอ ไม่งั้น Constraint ไม่ทำงาน!

ก่อนต่อไป เปลี่ยน Background ของหน้าจอเป็นดำ และลบสีเหลืองของ Cells:

ส่วนสำคัญ: ใส่ Sub Cells ลงในแต่ละ Cell จะเพิ่ม CollectionView ภายใน CustomCell
เพื่อเพิ่ม CollectionView ใน UICollectionViewCell ต้องเพิ่ม Properties UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource เป็น Superclass ของ CustomCell
สร้าง Element collectionView เหมือน View ธรรมดา:
let collectionView: UICollectionView = {
// init layout
let layout = UICollectionViewFlowLayout()
// set direction horizontal
layout.scrollDirection = .horizontal
// instance of collectionView
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
// Activate constraints
cv.translatesAutoresizingMaskIntoConstraints = false
return cv
}()
สังเกตว่าเพิ่ม layout ให้ collectionView ใน Initializer เหมือนครั้งแรกกับ viewController.swift ที่นี่ระบุทิศทาง FlowLayout เป็น .horizontal
เพิ่ม collectionView ลง View เป็น SubView ทำฟังก์ชันแยกเพื่อ Code สะอาด:
fileprivate func setupSubCells() {
// add collectionView to view
addSubview(collectionView)
collectionView.dataSource = self
collectionView.delegate = self
// setup constraints
// make it fit all space of CustomCell
collectionView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor).isActive = true
collectionView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
}
ตั้งค่า Delegate และ DataSource ให้ collectionView:
collectionView.dataSource = self
collectionView.delegate = self
เรียกฟังก์ชันในบล็อก init
Xcode จะ Error ถ้า Build เพราะยังไม่ Conform Protocols UICollectionViewDelegate และ UICollectionViewDelegateFlowLayout แก้โดย Register Sub Cell เป็น Reusable Cell
สร้างตัวแปรบนสุด Class ชื่อ cellId:
let cellId: String = "subCellID"
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: cellId)
ขาดอีก 2 เมธอด: numberOfItemsInSection กำหนดจำนวน Cell ใน Section และ cellForItemAt กำหนด Reusable Cell เมธอดเหล่านี้จำเป็นสำห collectionView ทำงาน:
// number of cells
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
// reusable Cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath)
cell.backgroundColor = .yellow
return cell
}
ผลลัพธ์:

เห็น collectionView พื้นหลังม่วง Sub Cells สีเหลือง
สิ่งสุดท้ายก่อนจบบทความ: ทำให้ subCells มี Height เท่า Section และ Width ใช้ sizeForItemAt กำหนด Height, Width:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = frame.height
let height = frame.height
return CGSize(width: width, height: height)
}
เสร็จแล้ว 😎:

เยี่ยม! หยุดที่นี่ก่อน เพราะไม่อยากยาวเกิน ไป ภาค 2 จะเพิ่มรูปภาพจำลองและเติมข้อมูล
Full Source Code 👇
ถ้ามีเพิ่มเติม คำถาม หรือแก้ไข Comment ได้ด้านล่าง 👇 หรือแท็กผมที่ Twitter
Subscribe อีเมลเพื่อรับแจ้งตอนเผยแพร่ภาค 2 ของบทเรียนนี้