Sử dụng MVVM để tableView của bạn trở nên mượt mà hơn
Như các bạn đã biết thì hiển thị một tập hợp các dữ liệu là một trong những task phổ biến nhất trong quá trình xây dựng một ứng dụng. Apple SDK đã cung cấp cho chúng ta 2 công cụ để làm việc này, đó là UITableView và UICollectionView.
Table view và collection view đều được thiết kế để hỗ trợ việc hiển thị dữ liệu mà có thể cuộn được. Tuy nhiên khi khối lượng dữ liệu cần hiển thị là rất lớn thì chúng ta còn cần phải đảm bảo việc mượt mà trong các thao tác vuốt, cuộn nữa. Bài viết này dựa trên kinh nghiệm của riêng tôi với table view và collection view trong việc hiển thị dữ liệu lớn một cách mượt mà.
Đầu tiên chúng ta hãy cùng nhau nhìn lướt qua 2 component trên để nắm được những thành phần cơ bản của chúng. UITableView được tối ưu để hiển thị dữ liệu theo các dòng – row hay còn gọi là cell. Việc hiển thị dữ liệu được thực hiện qua các delegates.
UICollectionView thì linh hoạt hơn, chúng ta có thể tùy chỉnh bố cục, layout cho các thành phần được hiển thị trong đó. Tuy nhiên, để có được sự linh hoạt đó thì chúng ta cũng phải thực hiện nhiều tác vụ chi tiết mà vẫn đảm bảo được performance của ứng dụng.
Ở trong bài viết này, tôi sẽ áp dụng phương pháp cho TableView, và tất nhiên các bạn cũng có thể áp dụng nó cho Collection View
Sự tương tác giữa UITableView và UITableViewCell được mô tả qua những event sau:
- TableView gọi tới cell sẽ được hiển thị trên view:
- (tableView(_:cellForRowAt:)).
- TableView chuẩn bị hiển thị cell:
- (tableView(_:willDisplay:forRowAt:)).
- Cell đã được gỡ ra khỏi TableView
- (tableView(_:didEndDisplaying:forRowAt:)).
Trong tất cả những event trên, Table View sẽ truyền vào một giá trị index (row) tương ứng với từng dòng dữ liệu. Dưới đây là mô tả một vòng đời của một đối tượng UITableViewCell:
Đầu tiên, method tableView(_:cellForRowAt:) sẽ cần phải thực thi càng nhanh càng tốt. Method này được gọi mỗi lần một cell chuẩn bị được hiển thị, tốc độ thực thi càng nhanh thì tác vụ cuộn hay vuốt sẽ càng trở nên mượt mà.
Để làm được việc này thì chúng ta có thể làm một vài thao tác theo chỉ dẫn của tài liệu Apple như sau:
1
2
3
4
5
6
7
8
9
10
|
override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
// Table view cells are reused and should be dequeued using a cell identifier.
let cell = tableView . dequeueReusableCell ( withIdentifier : "reuseIdentifier" , for : indexPath )
// Configure the cell ...
return cell
}
|
Sau khi khai báo một cell instance có thể tái sử dụng được (dequeueReusableCell(withIdentifier:for:)), chúng ta cần phải config nó bằng việc gán những giá trị cần thiết cho các property của nó.
Khởi tạo View Model cho đối tượng Cell
Có một cách để tất cả những property mà chúng ta cần hiển thị trở nên dễ dàng truy cập và gán giá trị vào hơn đó là sử dụng mô hình MVVM. Giả dụ rằng khi chúng ta cần hiển thị một tập hợp những user trong table view thì có thể định nghĩa lớp Model cho user như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
enum Role : String {
case Unknown = "Unknown"
case User = "User"
case Owner = "Owner"
case Admin = "Admin"
static func get ( from : String ) -> Role {
if from == User . rawValue {
return . User
} else if from == Owner . rawValue {
return . Owner
} else if from == Admin . rawValue {
return . Admin
}
return . Unknown
}
}
struct User {
let avatarUrl : String
let username : String
let role : Role
init ( avatarUrl : String , username : String , role : Role ) {
self . avatarUrl = avatarUrl
self . username = username
self . role = role
}
}
|
Sau đó chúng ta định nghĩa View Model cho User như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
struct UserViewModel {
let avatarUrl : String
let username : String
let role : Role
let roleText : String
init ( user : User ) {
// Avatar
avatarUrl = user . avatarUrl
// Username
username = user . username
// Role
role = user . role
roleText = user . role . rawValue
}
|
Đổ dữ liệu theo cách bất đồng bộ và Cache View Models:
Sau khi chúng ta đã khai báo Model và View Model, hãy dung chúng để đổ dữ liệu cho user qua web service, và tất nhiên chúng ta muốn mang đến trải nghiệm tốt nhất có thể, do đó chúng ta phải lưu ý những thứ sau:
- Tránh block main thread trong khi đổ dữ liệu.
- Update table View ngay sau khi nhận dữ liệu về.
Điều này có nghĩa rằng chúng ta sẽ đổ dữ liệu một theo hướng bất đồng bộ. Task này sẽ được thực thi qua một controller riêng biệt để tách những logic của việc đổ dữ liệu ra khỏi Model và ViewModel :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
class UserViewModelController {
fileprivate var viewModels : [ UserViewModel ? ] = [ ]
func retrieveUsers ( _ completionBlock : @ escaping ( _ success : Bool , _ error : NSError ? ) -> ( ) ) {
let urlString = . . . // Users Web Service URL
let session = URLSession . shared
guard let url = URL ( string : urlString ) else {
completionBlock ( false , nil )
return
}
let task = session . dataTask ( with : url ) { [ weak self ] ( data , response , error ) in
guard let strongSelf = self else { return }
guard let data = data else {
completionBlock ( false , error as NSError ? )
return
}
let error = . . . // Define a NSError for failed parsing
if let jsonData = try ? JSONSerialization . jsonObject ( with : data , options : . allowFragments ) as ? [ [ String : AnyObject ] ] {
guard let jsonData = jsonData else {
completionBlock ( false , error )
return
}
var users = [ User ? ] ( )
for json in jsonData {
if let user = UserViewModelController . parse ( json ) {
users . append ( user )
}
}
strongSelf . viewModels = UserViewModelController . initViewModels ( users )
completionBlock ( true , nil )
} else {
completionBlock ( false , error )
}
}
task . resume ( )
}
var viewModelsCount : Int {
return viewModels . count
}
func viewModel ( at index : Int ) -> UserViewModel ? {
guard index >= 0 && index < viewModelsCount else { return nil }
return viewModels [ index ]
}
}
private extension UserViewModelController {
static func parse ( _ json : [ String : AnyObject ] ) -> User ? {
let avatarUrl = json [ "avatar" ] as ? String ? ? ""
let username = json [ "username" ] as ? String ? ? ""
let role = json [ "role" ] as ? String ? ? ""
return User ( avatarUrl : avatarUrl , username : username , role : Role . get ( from : role ) )
}
static func initViewModels ( _ users : [ User ? ] ) -> [ UserViewModel ? ] {
return users . map { user in
if let user = user {
return UserViewModel ( user : user )
} else {
return nil
}
}
}
}
|
Bây giờ chúng ta có thể nhận dữ liệu và update tableView theo hướng bất đồng bộ như sau:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
class MainViewController : UITableViewController {
fileprivate let userViewModelController = UserViewModelController ( )
override func viewDidLoad ( ) {
super . viewDidLoad ( )
userViewModelController . retrieveUsers { [ weak self ] ( success , error ) in
guard let strongSelf = self else { return }
if ! success {
DispatchQueue . main . async {
let title = "Error"
if let error = error {
strongSelf . showError ( title , message : error . localizedDescription )
} else {
strongSelf . showError ( title , message : NSLocalizedString ( "Can't retrieve contacts." , comment : "The message displayed when contacts can’t be retrieved." ) )
}
}
} else {
DispatchQueue . main . async {
strongSelf . tableView . reloadData ( )
}
}
}
}
[ . . . ]
}
|
Chúng ta có thể dùng đoạn mã trên để truyền dữ liệu theo nhiều cách khác nhau:
- Đặt vào viewDidLoad(). khi chúng ta load table view một lần duy nhất.
- Đặt vào viewWillAppear(_:). khi tableView cần phải load nhiều lần.
- Còn lại tùy theo yêu cầu của người dùng, chúng ta có thể đặt khối lệnh này ở trong method thực thi yêu cầu đó. Ví dụ như kéo xuống để refesh..vv
Load ảnh bất đồng bộ và lưu lại cache
Việc load và hiển thị ảnh ở trong cell là rất phổ biến, và để cho ra một tác vụ mượt mà nhất có thể, chúng ta hiển nhiên không muốn block main thread để tải ảnh. Có một cách để load ảnh bất đồng bộ đó là tạo một lớp wrapper qua URLSession:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
extension UIImageView {
func downloadImageFromUrl ( _ url : String , defaultImage : UIImage ? = UIImageView . defaultAvatarImage ( ) ) {
guard let url = URL ( string : url ) else { return }
URLSession . shared . dataTask ( with : url , completionHandler : { [ weak self ] ( data , response , error ) -> Void in
guard let httpURLResponse = response as ? NSHTTPURLResponse where httpURLResponse . statusCode == 200 ,
let mimeType = response ? . mimeType , mimeType . hasPrefix ( "image" ) ,
let data = data where error == nil ,
let image = UIImage ( data : data )
else {
return
}
} ) . resume ( )
}
}
|
Ở đay chúng ta sẽ load từng tấm ảnh tại background thread và chỉ update UI khi mà đã có đủ data cần có. Bên cạnh đó chúng ta cũng có thể sử dụng những thư viện như SDWebImage hay AlamofireImage.
Tùy chỉnh Cell
Để tận dụng hoàn toàn những lợi ích từ View Model, chúng ta có thể tùy chỉnh User cell bằng cách subclass nó ( từ UITableViewCell cho TableView và từ UICollectionViewCell cho collection view). Hướng tiếp cận ở đây đó là tạo một outlet cho từng property của Model mà cần được hiển thị và khởi tạo chúng từ View Model:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class UserCell : UITableViewCell {
@ IBOutlet weak var avatar : UIImageView !
@ IBOutlet weak var username : UILabel !
@ IBOutlet weak var role : UILabel !
func configure ( _ viewModel : UserViewModel ) {
avatar . downloadImageFromUrl ( viewModel . avatarUrl )
username . text = viewModel . username
role . text = viewModel . roleText
}
}
|
Sử dụng Opaque Layers và tránh dùng Gradients
Sử dụng layer trong suốt hay kiểu Gradients đòi hỏi một khối lượng tính toán lớn, do vậy cũng sẽ ảnh hưởng đến performance. Vì thế nếu có thể, chúng ta nên tránh sử dụng chúng, cũ thể chúng ta có thể dùng màu RGB, ví dụ như UIColor.clear:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
class UserCell : UITableViewCell {
@ IBOutlet weak var avatar : UIImageView !
@ IBOutlet weak var username : UILabel !
@ IBOutlet weak var role : UILabel !
func configure ( _ viewModel : UserViewModel ) {
setOpaqueBackground ( )
[ . . . ]
}
}
private extension UserCell {
static let defaultBackgroundColor = UIColor . groupTableViewBackgroundColor
func setOpaqueBackground ( ) {
alpha = 1.0
backgroundColor = UserCell . defaultBackgroundColor
avatar . alpha = 1.0
avatar . backgroundColor = UserCell . defaultBackgroundColor
}
|
Sau khi chỉnh sửa xong, chúng ta tổng hợp tất cả các thứ lại như sau:
1
2
3
4
5
6
7
8
9
10
11
|
override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
let cell = tableView . dequeueReusableCell ( withIdentifier : "UserCell" , for : indexPath ) as ! UserCell
if let viewModel = userViewModelController . viewModel ( at : ( indexPath as NSIndexPath ) . row ) {
cell . configure ( viewModel )
}
return cell
}
|
Tổng kết
Như vậy là qua bài viết trên, chúng ta đã có thể tìm ra một trong những ccách làm tối ưu performance cho tableView, collectionView nói riêng và ứng dụng của chúng ta nói chung. Bạn có thể download ví dụ trên tại đây.
- Viết ứng dụng Smartphone và Tablet
- 5 lý do sở hữu một ứng dụng di động là cần thiết đối với doanh nghiệp vừa và nhỏ
- Các nền tảng công nghệ hỗ trợ cho khởi nghiệp tiết kiệm, hiệu quả,...
- Hệ thống điều hành, tìm gọi và quản lý xe sử dụng công nghệ mới
- Ứng dụng bán hàng trên smartphone, smart TV, mạng xã hội...
- Hướng dẫn cài ứng dụng, phần mềm cho Android trực tiếp bằng tập tin APK
- IoT là gì? ứng dụng của IoT trong cuộc sống hiện đại
- Khắc phục lỗi đăng nhập Windows 10, không thể login vào Windows 10
- Platform là gì?
- Cách đổi tên thiết bị Android
- Hệ thống order chuyên nghiệp cho quán ăn, cafe, nhà hàng...
- 100 Website đặt backlink miễn phí chất lượng
DVMS chuyên:
- Tư vấn, xây dựng, chuyển giao công nghệ Blockchain, mạng xã hội,...
- Tư vấn ứng dụng cho smartphone và máy tính bảng, tư vấn ứng dụng vận tải thông minh, thực tế ảo, game mobile,...
- Tư vấn các hệ thống theo mô hình kinh tế chia sẻ như Uber, Grab, ứng dụng giúp việc,...
- Xây dựng các giải pháp quản lý vận tải, quản lý xe công vụ, quản lý xe doanh nghiệp, phần mềm và ứng dụng logistics, kho vận, vé xe điện tử,...
- Tư vấn và xây dựng mạng xã hội, tư vấn giải pháp CNTT cho doanh nghiệp, startup,...
Vì sao chọn DVMS?
- DVMS nắm vững nhiều công nghệ phần mềm, mạng và viễn thông. Như Payment gateway, SMS gateway, GIS, VOIP, iOS, Android, Blackberry, Windows Phone, cloud computing,…
- DVMS có kinh nghiệm triển khai các hệ thống trên các nền tảng điện toán đám mây nổi tiếng như Google, Amazon, Microsoft,…
- DVMS có kinh nghiệm thực tế tư vấn, xây dựng, triển khai, chuyển giao, gia công các giải pháp phần mềm cho khách hàng Việt Nam, USA, Singapore, Germany, France, các tập đoàn của nước ngoài tại Việt Nam,…
Quý khách xem Hồ sơ năng lực của DVMS tại đây >>
Quý khách gửi yêu cầu tư vấn và báo giá tại đây >>