
Hôm nay mình sẽ nói rõ hơn làm thế nào để làm được điều này bằng thư viện objc-runtime.
Để init data được cho một model tao có 2 cách tiếp cận:
Cách tiếp cận ban đầu có lợi thế là dễ làm nhưng nó dễ gặp nhiều vấn đề như: lặp qua các key dư thừa không có trong model (giả sử dictionary có 5 key nhưng model chỉ có 3 thuộc tính). Không khớp kiểu dữ liệu giữa dictinonary và model (giả sử price trong dictionary là kiểu int trong khi của model là kiểu float). Cụ thể ta có ví dụ như sau:
Dictionary
1 2 3 4 5 6 7 | { name: "Huy", age: 23, city: "Saigon", country: "Vietnam", bio: "Milk Carrot" } |
Model
1 2 3 4 5 | { name: String, age: float, bio: String } |
Ta thấy giả sử nếu viết một hàm init data mà lặp qua kết các keys của Dictionary để set giá trị cho model thì sẽ dư thừa key city, country
cho mỗi lần init data. Hơn nữa kiểu dữ liệu về age sẽ không khớp (không thể xác định được age trong dictionary là kiểu int hay kiểu float). Việc lặp qua hết các keys của Dictionary để set value cho thuộc tính model còn gặp một vấn đề nữa là nếu không handle exception thì chương trình sẽ bị crash khi key đó không có trong model ví dụ key city
và country
Với cách tiếp cận thứ hai là căn cứ vào thuộc tính của model để set giá trị có vẻ như là tốt hơn. Nhưng vấn đề là làm thế nào để lấy được danh sách thuộc tính cũng như kiểu dữ liệu của từng thuộc tính. Sử dụng thư viện objc-runtime. Các bước như sau:
Lấy danh sách thuộc tính -> lấy danh sách kiểu dữ liệu -> Lấy giá trị ứng với thuộc tính trong Dictionary -> Căn cứ vào kiểu dữ liệu ứng với thuộc tính tiến hành init dữ liệu cho thuộc tính.
(HCM) | Urgent - 10 .NET Developer (ASP.NET, MVC) | Attractive Salary — Sutrix Solutions
(HN) | 30 PHP Developers - Salary up to $1000 — Magestore
(HCM) | PHP Developer | Attractive Salary — TEG Consulting
1 2 3 4 5 6 7 8 9 10 11 | # hàm lấy danh sách thuộc tính của một class trong objc-runtime objc_property_t *properties = class_copyPropertyList([self class], &outCount); # lặp qua lần lượt các thuộc tính để lấy ra kiểu dữ liệu for(int i = 0; i < outCount; i++) { objc_property_t property = properties[i]; objc_property_t property = properties[i]; const char *propName = property_getName(property); .... } |
1 2 3 4 5 | # lấy giá trị ứng với thuộc tính trong Dictionary id value = [dictionary valueForKey:propertyName]; # gán giá trị cho thuộc tính đó trong model. [self setValue:value forKey:propertyName]; |
Toàn bộ source code bạn có thể xem ở đây: model
Đôi khi trong lập trình, để dễ dàng hơn, ta muốn khi giá trị một thuộc tính của model thay đổi thì nó sẽ tiến hành gọi một hàm nào đó (callback). Để làm được điều này, chúng ta sử dụng tính năng key-value observing
của ObjC cung cấp. Giả sử ta gọi hàm callback đó là một Action.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @interface SModelAction : NSObject // Đặc tả tên thuộc tính của model mà có sự thay đổi @property (nonatomic, copy) NSString *keyPath; // Đặc tả sự thay đổi (add/remove/init/changed...) @property (nonatomic) SModelEvent event; // Con trỏ của objective gọi hàm callback @property (nonatomic, weak) id target; // Hàm callback sẽ được target gọi @property (nonatomic) SEL selector; @end |
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 | - (void)property:(NSString *)property target:(id)target selector:(SEL)selector onEvent:(SModelEvent)event { // Kiểm tra xem action đã được đăng kí trước hay chưa, nếu đã đăng kí rồi thì báo trùng lập và không làm gì cả if ([[self getActionsOfProperty:property target:target selector:selector onEvent:event] count] > 0) { #ifdef DEBUG NSLog(@"Duplicated register keyPath: %@", property); #endif return; } // Đăng kí observer cho thuộc tính nếu chưa đăng ký [self registerObserverForKeyPath:property]; // Tạo một đối tượng mô tả event và lưu lại SModelAction *modelAction = [[SModelAction alloc] init]; modelAction.keyPath = property; modelAction.target = target; modelAction.selector = selector; modelAction.event = event; [[self actions] addObject:modelAction]; } |
1 2 3 4 5 6 7 8 9 10 11 | - (void)registerObserverForKeyPath:(NSString *)keyPath { if (![self keyPathExisted:keyPath]) { @synchronized(self) { [self addKeyPath:keyPath]; [self addObserver:self forKeyPath:keyPath options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:SPreadContext]; } } } |
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 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { id oldValue = change[@"old"]; id newValue = change[@"new"]; SModelEvent event = SModelEventOnChange; NSMutableArray *actionsToDelete = [NSMutableArray NSArray *actions = [self getActionsOfProperty:keyPath array]; onEvent:event]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ for (SModelAction *action in actions) { id target = action.target; if (target) { ((void (*)(id, SEL))[target methodForSelector:action.selector])(target, action.selector); } else { [actionsToDelete addObject:action]; } } for (SModelAction *action in actionsToDelete) { [self removeActionsForProperty:action.keyPath target:action.target]; } }]; } |
1 2 3 4 5 | User *user = [[User init] alloc]; [user property:"name" target:self selector:@selector(renderNameLabel) onEvent:SModelEventOnChange]; // Gán giá trị mới cho field name user.name = "New name"; |
Sau dòng lệnh gán giá trị mới cho thuộc tính name trong model user, thì model user sẽ tiến hành gọi hàm renderNameLabel. Model cung cấp một cơ chế tự động remove key-observer nên khi sử dụng chỉ cần add target, không cần quan tâm tới việc remove key-observer khi dealloc model. Điều này giảm thiểu việc crash khi lập trình.
Toàn bộ source code bạn có thể xem ở đây
Trong model ở đây, mình chỉ lấy ý tưởng từ phần active record về việc auto mapping giữa tên, kiểu dữ liệu của thuộc tính với dữ liệu trong JSON (Dictionary). Trong khi làm việc với các app có lấy dữ liệu từ mạng về, các bạn rất hay gặp trường hợp cần tải dữ liệu của một đối tượng về mà chỉ biết id của đối tượng đó. Ví dụ khởi tạo đối tượng User có id bằng 1.
1 | User *user = User.findById(1) |
Hoặc bất đồng bộ
1 2 3 | User *user = User() user.id = 1 user.fetchInBackgroud() |
Các công việc cần làm là ta sử dụng một private networking cho class model để tiến hành lấy dữ liệu từ server và kết hợp với phần ở trên để init dữ liệu. Với ý tưởng này, ta có thể đóng gói được phần code làm việc với server gói gọn chỉ trong phần model base mà không cần phải viết lại cho mỗi lần tạo thêm model mới. Từ đó lượng code sinh ra ít hơn và dễ quản lý hơn.
Trong phần model (mình đã update thành opensource) mình đã hiện thực tất cả các phần ở trên, việc sử dụng cũng cực kì đơn giản, chỉ việc kết thừa từ class SModel là có tất cả các tiện ích kể trên. Đồng thời SModel cũng cung cấp thêm nhiều hàm tiện ích khác như mặc định gái trị default cho Model khi giá trị tron Dictionary bị null, convert ngược lại từ class model thành Dictionary cho việc gửi parameters hoặc lưu thành file JSON. Cung cấp hàm callback bằng block, thêm hàm callback khi fetchInBackground.
Techtalk via huypham