Bạn có chắc chắn muốn xóa bài viết này không ?
Bạn có chắc chắn muốn xóa bình luận này không ?
UI Components và bước đầu hướng đến MicroViewControllers trong lập trình iOS
UI Components là 1 khái niệm không có gì mới và đã có ứng dụng rộng rãi trong lập trình Web (FrontEnd), Tuy nhiên trong lập trình mobile nói chung và iOS nói riêng đôi khi vẫn có những ranh giới cần phải vượt qua để hiện thực hoá được phong cách lập trình này.
Trong bài này mình sẽ đưa ra những khó khăn cụ thể bạn sẽ gặp phải và cách để vượt qua, dựa theo Prototype App mình đang làm cho chính Kipalog. Đây là demo của phần vỏ UI:
Quy ước
Trước hết mình sẽ trả lời trước 1 số câu hỏi định hình cho bài viết
Để làm giao diện nên dùng UITableView hay UICollectionVIew?
Trả lời: UICollectionVIew. Tất cả những gì làm được với UITableView đều làm được với UICollectionView. Với UICollectionVIew khả năng điều chỉnh layout linh hoạt hơn, và khi cần có thể tạo UICollectionView bên trong cell của UICollectionView 1 cách đơn giản.
Design cell của UICollectionView như thế nào?
Trả lời: Mình tách cell ra làm 1 file xib và 1 file ViewController riêng (ViewController chứ không phải View nhé) sau đó cài cắm vào trong ViewController chính.
Lập trình giao diện bằng code, storyboard hay xib?
Trả lời: Xib. Lập trình layout bằng code tốn công, thiếu trực quan và quan trọng nhất là không nhìn thấy layout warning, Storyboard khiến cho khởi tạo ViewController bị rối. Trong khi đó Xib hiện giờ đã hỗ trợ Safe Area Layout như Storyboard và không có nhược điểm nào như 2 cách còn lại.
Reactive Programming dùng thư viện gì?
Trả lời: RxSwift. Tuy vậy ReactiveSwift cũng okie và nội dung về UI Components trong bài viết này không liên quan và không bị ảnh hưởng bởi thư viện Reactive Programming nào
Dùng architecture gì thế?
Trả lời: MVVM ở mức tối gỉản.
Đây là Layout capture dùng Reveal
Khởi tạo ViewController từ xib
Phần này không có gì khó và bạn có thể tìm thấy câu trả lời trên StackOverFlow rất dễ dàng. Đây là protocol mình hay sử dụng
protocol XibViewController { | |
static var className: String { get } | |
static func make() -> Self | |
} | |
extension XibViewController where Self: UIViewController { | |
static var className: String { | |
return String(describing: self).components(separatedBy: ".").last! | |
} | |
static func make() -> Self { | |
return self.init(nibName: Self.className, bundle: nil) | |
} | |
} | |
extension UIViewController: XibViewController {} |
Như vậy để mỗi lần tạo 1 file ABCViewController.swift
và ABCViewController.xib
, mình sẽ
- Mở file xib > Identity Inspedctor > set class thành file swift
- Mở file xib > Connection Inspector > kéo nối vIew của file owner vào View chính của xib
- Khởi tạo ViewController bằng
let vc = ABCViewController.make()
Khởi tạo CollectionViewCell bằng ViewController
Nếu bạn làm iOS theo sách giáo khoa của Apple chắc sẽ biết cách đơn giản nhất khi tạo UICollectionViewCell là design trực tiếp vào storyboard đúng không? Khi quen hơn chút nữa chắc sẽ thiết kế Cell trong 1 file xib riêng rồi tạo 1 file View kế thừa lớp UICollectionViewCell
và cài cắm vào lớp chính kế thừa UICollectionViewController
.
Nhưng ở đây mình sẽ tạo Cell bằng xib và ViewController như bước trên, và đem cả ViewController đó bỏ vào trong 1 UIView của lớp UICollectionViewCell
. Để làm điều đó mình sẽ trừu tượng hoá Cell 1 chút. Trước hết là 1 protocol
protocol UIComponent: class { | |
associatedtype Content: UIViewController | |
associatedtype Parent: UIViewController | |
var contentView: UIView { get } | |
var content: Content? { get set } | |
var parent: Parent? { get set } | |
} | |
extension UIComponent where Self: UIView { | |
func embed(content: Content, parent: Parent) { | |
self.content = content | |
self.parent = parent | |
content.view.translatesAutoresizingMaskIntoConstraints = false | |
if superview == nil { | |
content.willMove(toParent: parent) | |
} else { | |
parent.addChild(content) | |
} | |
contentView.addSubview(content.view) | |
NSLayoutConstraint.activate( | |
[ | |
content.view.topAnchor.constraint(equalTo: contentView.topAnchor), | |
content.view.leftAnchor.constraint(equalTo: contentView.leftAnchor), | |
content.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), | |
content.view.rightAnchor.constraint(equalTo: contentView.rightAnchor) | |
] | |
) | |
if superview == nil { | |
content.removeFromParent() | |
} else { | |
content.didMove(toParent: parent) | |
} | |
} | |
} |
Đoạn code bên trên làm gì ở đây?
Trước hết mình có 1 protocol UIComponent
, luôn chắc chắn có biến số content
và parent
với kiểu kế thừa UIVIewController
. Bạn có thể hiểu nôm na content
sẽ là ViewController của cell, và parent
là ViewController của lớp chứa UICollectionView. Cả 2 đều là ViewController.
TIếp đến trong phương thức embed()
, mình để content
trở thành con của parent
, cho view của content
trờ thành subView của contentView
và tạo 4 constraints để ghim view của content
vào contentView
. contentView
sẽ là biến số có sẵn nếu như UIComponent
cũng là 1 lớp kế thừa UICollectionViewCell
Tiếp theo sẽ là 1 lớp CollectionViewCell tổng quát
https://gist.github.com/orakaro/176f84057dc12dea990b7184c511a58e
Bạn sẽ muốn để ý thấy lớp CollectionViewCell
vừa là UICollectionViewCell
vừa là UIComponent
. phương thức register
đơn giản là đăng ký Cell và phương thức deque
sẽ gọi dequeueReusableCell()
bên trong cùng với embed()
để cài đặt content
vào trong cell
và nối luôn với parent
.
Nghe có vẻ hay đấy, vậy sử dụng lớp này như thế nào
Bạn sẽ chỉ cần đăng ký và hàm cellForItemAt indexPath
mặt mũi sẽ như này:
override func viewDidLoad() { | |
super.viewDidLoad() | |
CollectionViewCell<FeedCellViewController, FeedViewController>.register(to: collectionView) | |
} | |
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { | |
let post = posts[indexPath.row] | |
let cell = CollectionViewCell<FeedCellViewController, FeedViewController>.dequeue(from: collectionView, for: indexPath, parent: parent) | |
cell.content?.post = post | |
return cell | |
} |
Lợi ích
Đọc đến đây các bạn có thể sẽ tự hỏi, chúng ta làm những thứ loằng con nhà bà ngoằng này để làm gì?
Lợi ích lớn nhất thu được ở thời điểm này là, mọi thành phần UI nhỏ đều có thể cắt ra thành Xib+ViewController.
Bạn có 1 TableViewCell
? Cắt ra thành XIb+ViewController. Bạn có CollectionVIewCell
? Cắt ra thành Xib+ViewController. Bạn có TableVIewHeader/Footer
? hay là CollectionViewHeader/Footer
? Cắt tiếp ra thành Xib+ViewController. Bạn có 1 VIew lẻ? Cắt ra thành XIb+ViewController.
Và trong mỗi XIb+ViewController, bạn có thể tuỳ ý cắt tiếp, hay ném vào 1 CollectionView nữa, và rồi mỗi Cell lại có thể là Xib+ViewController. Tiềm năng là vô hạn. Mặc dù mình thấy code đến 3 mức CollectionVIew bên trong CollectionView bên trong CollectionVIew là quá lắm rồi
Và vì bản thân mỗi thành phần là 1 ViewController, bạn có thể làm gì tuỳ ý, gói logic vào bên trong, gọi API, lưu dữ liệu, dùng mô hình cấu trúc khác nhau v.v... Và bức tranh hoàn chỉnh hiện ra là bạn sẽ có rất nhiều các MicroViewControllers. Nghe na ná MicroServices phải không nào?
Tuy nhiên đây mới là bước đầu của MicroViewControllers. Để có tính ứng dụng cao hơn chúng ta còn cần StackView, Container, và cách lưu chuyển dữ liệu giữa các MicroVIewControllers dùng kiểu Redux v.v... Mình sẽ cố gắng chia sẻ nếu nhiều bạn quan tâm.
Tham khảo
- Code mẫu và Prototype của Kipalog ở đây: https://github.com/orakaro/kipalog-ios
- Ý tưởng về VC bên trong Cell: http://khanlou.com/2015/04/view-controllers-in-cells/






