Fetch và Parse JSON sử dụng iOS SDK

1.JSON là gì?

JSON(short for JavaScript Object Notation) là một dạng text đơn giản để lữu trữ và trao đổi dữ liệu. Nó thường được sử dụng cho các dịch vụ, ứng dụng dạng client - server như một thay thế cho XML. Rất nhiều các dịch vụ chúng ta sử dụng hàng ngày có sử dụng các APIs trên nền tảng JSON. Phần lớn các ứng dụng iOS bao gồm Twitter, Facebook và Flick gửi dữ liệu tới máy chủ bằng định dạng JSON. Ví dụ về 1 JSON

{
	"title":"Spider-man",
    "release_date": "03/07/2012",
    "director":"Marc Webb",
    "cast": [
    	{
        	"name": "Andrew Garfield",
            "character":"Peter Parker"
        },
        {
        	"name": "Emma Stone",
            "character": "Gwen Stayce"
        },
        {
        	"name": "Rhys Ifans",
            "character": "Dr. Curt Connors"
        }
    ]
}

Như chúng ta có thể thấy ngay thì dữ liệu dang JSON khá dễ hiểu và dễ dàng phân tích hơn dạng XML Từ bản iOS5 thì iOS SDK đã làm cho việc fetch và parse dữ liệud dạng JSON một cách khá dễ dàng. Trong bài viết này sẽ hướng dẫn đơn giản việc xây dựng một ứng dụng sử dụng API dạng JSON.

Chúng ta sẽ xây dựng 1 ứng dụng đơn giản sử dụng các API công khai của Meetup. Ứng dụng của chúng ta sẽ sử dụng dịch vụ Web của Meetup để tra cứu các nhóm gần đó. Ứng dụng sẽ lấy vị trí hiện tại để có thể tìm kiếm tự động các nhóm gần vị trí của chúng ta của Meetup. Việc khởi tạo một project của XCode chúng ta đã khá quen thuộc với nhiều bài viết khác nên trong bài viết này chúng ta sẽ bỏ qua phần đó và đi vào phần chính.

2.Làm việc với Meetup APIs

Trước khi bạn sử dụng Meetup APIs, đầu tiên chúng ta cần khởi tạo một tài khoản mới trên Meetup. Chúng ta sẽ sử dụng một trong các Meetup APIs (ví dụ https://api.meetup.com/2/groups) để lấy được vị trí của các nhóm. Lần gọi API này cho phép các nhà phát triển xác định vị trí bằng cách sử dụng kinh độ và vĩ độ. Bạn có thể thử nghiệm lời gọi API này bằng cách sử dụng API console. Ví dụ dưới đây với 1 request có link như sau : https://api.meetup.com/2/groups?&sign=true&lat=51.509980&lon=-0.133700&page=1 thì JSON trả về chúng ta sẽ nhận được là

{
"results": [
	{
		"lon": -0.10000000149011612,
        "visibility": "public",
        "organizer": {
        	"name": "William Brown",
            "member_id": 3817216
        },
        "link": "http://www.meetup.com/french-32/",
        "state": "17",
        "join_mode": "approval",
        "who": "LFM members",
        "country": "GB",
        "city": "London",
        "id": 63974,
        "category": {
        	"id": 16,
            "name": "language/ethnic identity",
            "shortname": "language"
        },
        "topics": [
        	{
            	"id": 185,
                "urlkey": "french",
                "name": "French Language"
            },
            {
            	"id": 264,
                "urlkey": "friends",
                "name": "Friends"
            },
            {
            	"id": 3304,
                "urlkey": "expatfrench",
                "name": "Expat French"
            }
        ],
        "timezone": "Europe/London",
        "group_photo": {
        	"photo_link": "http://photos3.meetupstatic.com/photos/event/7/4/a/b/600_929867.jpeg",
            "highres_link": "http://photos3.meetupstatic.com/photos/event/7/4/a/b/highres_929867.jpeg",
            "thumb_link": "http://photos3.meetupstatic.com/photos/event/7/4/a/b/thumb_929867.jpeg",
            "photo_id": 929867
        },
        "created": 1034097734000,
        "description": "<p>The London French Meetup is the biggest group of French speakers of all ages and nationalities in London. We hold regular events to meet up, talk in French and share interests in French culture whilst having a good time.</p><p>We have two main events per month where we have the whole of the upstairs of a pub.</p><p>In addition, we organise other regular events such as outings to: restaurants, trendy bars, french films, live music, sports related activities, outdoor events and more...</p><p>The organising team is made of volunteers from different nationalities and ages. Our members are made up of: 1/3 French nationals, 1/3 British nationals and 1/3 other nationalities and francophone countries. If you have any ideas or suggestions for events or would like to help please let us know.</p><p>A bientôt.</p><p>LFM Team.</p>",
        "name": "London French Meetup",
        "rating": 4.37,
        "urlname": "french-32",
        "lat": 51.52000045776367,
        "members": 4889
    }
    ],
    "meta": {
    	"lon": -0.1337,
        "count": 1,
        "signed_url": "http://api.meetup.com/2/groups?radius=25.0&order=id&desc=false&offset=0&format=json&lat=51.50998&page=1&fields=&lon=-0.1337&sig_id=109020062&sig=4532ed8f987f940748ebfba0f483a26f756dcba3",
        "link": "http://www.meetup.com/2/groups",
        "next": "http://www.meetup.com/2/groups?radius=25.0&order=id&format=json&lat=51.50998&page=1&desc=false&offset=1&fields=&sign=true&lon=-0.1337",
        "total_count": 4501,
        "url": "http://www.meetup.com/2/groups?radius=25.0&order=id&format=json&lat=51.50998&page=1&desc=false&offset=0&fields=&sign=true&lon=-0.1337",
        "id": "",
        "title": "Meetup Groups v2",
        "updated": 1377876449000,
        "description": """",
        "method": "Groups",
        "lat": 51.50998
    }
}

3.Thiết kế ứng dụng và các làm việc

Như đã nói trên, Meetup API cung cấp cho chúng ta phương thức để request được các nhóm ở một vị trí cụ thể. Dữ liệu trả về sẽ được gửi về là dạng JSON. Chúng ta cần một đối tượng để có thể lấy được dữ liệu và cần các đối tượng để có để lưu trữ dữ liệu. Chúng ta sẽ tạo một lớp MeetupManager, trong đó sẽ request đến các nhóm Meetup cho một khu vực cụ thể. Lớp MeetupCommunicator để có thể giao tiếp với các Meetup API. Mỗi lần Meetup trả về dữ liệu là dữ liệu dạng JSON, chúng ta sẽ truyền nó sang GroupBuilder bởi hàm dựng đối tượng Group. MasterViewController sử dụng Core Location để cấu hình vị trí hiện tại và thông báo cho MeetupManager để có thể lấy được các nhóm Meetup trong khu vực đó. Khi các nhóm được lấy ra thì nó sẽ giao tiếp với MasterViewController qua các delegate và thông qua các nhóm được tìm thấy. Sau đó MasterViewController sẽ thể hiện các nhóm trong một bảng.

Chúng ta sẽ bắt đầu bằng việc hiện thực lớp model. Lớp Group đại diện cho các thông tin về nhóm trong ứng dụng và được sử dụng để lưu trữ thông tin nhóm được trả lại từ Meetup. Chúng ta sẽ không sử dụng hết toàn bộ dữ liệu được trả về từ Meetup trong JSON mà chúng ta chỉ cần một số thông tin cần thiết như "name", "description", "who", "country" và "city". Các trường này đủ thông tin để hiển thị trên tableView của ứng dụng. Bây giờ chúng ta tạo một lớp Objective-C đặt tên là Group, và để là lớp con kế thừa lớp NSObject và thêm các dòng code sau vào file header:

@interface Group : NSObject
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *description;
@property (strong, nonatomic) NSString *who;
@property (strong, nonatomic) NSString *country;
@property (strong, nonatomic) NSString *city;
@end

Sau đó chúng ta sẽ tạo một Objective-C protocol với tên là MeetupCommunicatorDelegate và thêm vào đoạn code sau:

@protocol MeetupCommunicatorDelegate
- (void)receivedGroupsJSON:(NSData *)objectNotation;
- (void)fetchingGroupsFailedWithError:(NSError *)error;
@end

Protocol MeetupCommunicator chịu trách nhiệm giao tiếp với các Meetup API và lấy các dữ liệu dạng JSON. Nó dựa vào các delegate của MeetupCommunicatiorDelegate để xử lý các phân tích dữ liệu JSON. Các phương thức đều không có phần thân hàm để xử lý dữ liệu dạng JSON. Ở đây công việc chính là tạo ra các kết nối đến Meetup API và lấy dữ liệu từ nó. Với các delegate này thì chúng ta sẽ tạo ra một lớp có tên là MeetupCommunicator. Và thêm phần code sau vào file header của nó:

#import <CoreLocation/CoreLocation.h>

@protocol MeetupCommunicatorDelegate;

@interface MeetupCommunicator : NSObject
@property (weak, nonatomic) id<MeetupCommunicatorDelegate> delegate;

- (void)searchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate;
@end

Chúng ta tạo ra một thuộc tính để giữ delegate, Sau đó định nghĩ một phương thức cho việc tìm kiếm các nhóm Meetup. Tiếp đó mở file MeetupCommunicator.m và thêm vào đoạn code sau:

#import "MeetupCommunicator.h"
#import "MeetupCommunicatorDelegate.h"

#define API_KEY @"1f5718c16a7fb3a5452f45193232"
#define PAGE_COUNT 20

@implementation MeetupCommunicator

- (void)searchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate
{
    NSString *urlAsString = [NSString stringWithFormat:@"https://api.meetup.com/2/groups?lat=%f&lon=%f&page=%d&key=%@", coordinate.latitude, coordinate.longitude, PAGE_COUNT, API_KEY];
    NSURL *url = [[NSURL alloc] initWithString:urlAsString];
    NSLog(@"%@", urlAsString);

    [NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:url] queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {

        if (error) {
            [self.delegate fetchingGroupsFailedWithError:error];
        } else {
            [self.delegate receivedGroupsJSON:data];
        }
    }];
}

@end

Ở đây chúng ta thấy có khai báo 1 hằng API_KEY. Meetup API cần có một key để hoạt động. Khi bạn đăng kí một tài khoản thì bạn có thể lấy nó bằng việc tới trang API Key. Sau khi lấy được key này chúng ta chỉ việc thay vào phần giữa hai dấu nháy kép trên chỗ khai báo hằng API_KEY. Như đã nói phần trên thì chúng ta sẽ sử dụng Meetup API để tìm kiếm các nhóm trong một khu vực riêng biệt. API chấp nhận vị trí khi thông báo trong form có kinh độ và vĩ độ. Trong phần hiện thực phương thức, đầu tiên chúng ta dựng một Meetup API URL cùng với kinh độ và vĩ độ, số lượng nhóm và API key. Ở đây chúng ta có sử dụng một block, chúng ta load dữ liệu cho URL request bằng phương thức sendAsynchronousRequest: của lớp NSURLConnection. Cuối cùng khi chúng ta nhận dữ liệu về thì chúng ta sẽ xử lý chúng bằng việc gửi qua delegate.

Parsing JSON và xây dựng đối tượng Group

Khi MeetupManager nhận dữ liệu JSON về, chúng ta sử dụng phương thức của lớp GroupBuilder để chuyển đổi dữ liệu từ Data sang đối tượng Group. Trước tiên chúng ta cần tạo một file ObjectiveC có tên là GroupBuilder. Mở file header và thêm đoạn code sau:

#import <Foundation/Foundation.h>

@interface GroupBuilder : NSObject
+ (NSArray *)groupsFromJSON:(NSData *)objectNotation error:(NSError **)error;
@end

Sau đó trong file GroupBuilder.m chúng ta hiện thực hàm đã được khai báo trong file header.

#import "GroupBuilder.h"
#import "Group.h"

@implementation GroupBuilder
+ (NSArray *)groupsFromJSON:(NSData *)objectNotation error:(NSError **)error
{
    NSError *localError = nil;
    NSDictionary *parsedObject = [NSJSONSerialization JSONObjectWithData:objectNotation options:0 error:&localError];

    if (localError != nil) {
        *error = localError;
        return nil;
    }

    NSMutableArray *groups = [[NSMutableArray alloc] init];

    NSArray *results = [parsedObject valueForKey:@"results"];
    NSLog(@"Count %d", results.count);

    for (NSDictionary *groupDic in results) {
        Group *group = [[Group alloc] init];

        for (NSString *key in groupDic) {
            if ([group respondsToSelector:NSSelectorFromString(key)]) {
                [group setValue:[groupDic valueForKey:key] forKey:key];
            }
        }

        [groups addObject:group];
    }

    return groups;
}
@end

phương thức "groupsFromJSON:" dùng để chuyển đổi dữ liệu từ JSON sang mảng đối tượng Group. Từ phiên bản iOS5, iOS SDK cung cấp chúng ta một lớp là NSJSONSerialization để parsing JSON. Các nhà phát triển có thể sử dụng lớp này để chuyển đổi từ JSON sang đối tượng Foundation hoặc ngược lại.

Khi đọc dữ liệu JSON bằng cách sử dụng NSJSONSerialization, tất cả các key được đưa vào danh sách một cách từ động vào đối tượng NSDictionary. Đối với mảng thì nó sẽ tự động chuyển đổi sang NSArray, các chuỗi thì được chuyển đổi thành đối tượng NSString, các chuỗi số chuyển đổi sang NSNumber và các giá trị null được chuyển đổi sang dạng NSNull.

Như vậy chúng ta đã hiểu cách làm việc với JSON, cách parse dữ liệu dạng JSON và chuyển đổi chúng sang các đối tượng. Bây giờ chúng ta sẽ hiện thực lớp MeetupManagerMeetupManagerDelegate bằng cách tạo một file Objectivec-C Protocol với tên là MeetupManagerDelegate. Thêm đoạn code sau vào file header:

@protocol MeetupManagerDelegate
- (void)didReceiveGroups:(NSArray *)groups;
- (void)fetchingGroupsFailedWithError:(NSError *)error;
@end

Trong file header của protocol này có khai báo hai phương thức và nó sẽ được gọi bởi MeetupManager khi mà các nhóm có thể hoạt động. Phương thức đầu tiên được gọi khi danh sách các nhóm nhận được từ Meetup, trong khi đó phương thức thứ hai được gọi khi có lỗi xảy ra. Các phương thức delegate này sẽ được gọi trong MasterViewController. Tiếp theo chúng ta tạo một lớp Objective-C tên gọi là MeetupManager. Sau đó thêm đoạn code sau vào file header:

#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>

#import "MeetupManagerDelegate.h"
#import "MeetupCommunicatorDelegate.h"

@class MeetupCommunicator;

@interface MeetupManager : NSObject<MeetupCommunicatorDelegate>
@property (strong, nonatomic) MeetupCommunicator *communicator;
@property (weak, nonatomic) id<MeetupManagerDelegate> delegate;

- (void)fetchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate;
@end

Tiếp theo ta cần hiện thực các hàm đã được khai báo trong file header như sau:

#import "GroupBuilder.h"
#import "MeetupCommunicator.h"

@implementation MeetupManager
- (void)fetchGroupsAtCoordinate:(CLLocationCoordinate2D)coordinate
{
    [self.communicator searchGroupsAtCoordinate:coordinate];
}

#pragma mark - MeetupCommunicatorDelegate

- (void)receivedGroupsJSON:(NSData *)objectNotation
{
    NSError *error = nil;
    NSArray *groups = [GroupBuilder groupsFromJSON:objectNotation error:&error];

    if (error != nil) {
        [self.delegate fetchingGroupsFailedWithError:error];

    } else {
        [self.delegate didReceiveGroups:groups];
    }
}

- (void)fetchingGroupsFailedWithError:(NSError *)error
{
    [self.delegate fetchingGroupsFailedWithError:error];
}
@end

Phần hiện thực phương thức fetchGroupsAtCoordinate:coordinate để lấy được những nhóm trong một khu vực xác định sử dụng phương thức searchGroupsAtCoordinate: của comunicator. Chúng ta cũng hiện thực các phương thức của MeetupCommunicatorDelegate để xử lý JSON. Phần code trong phương thức đầu tiên của protocol sử dụng phương thức của lớp GroupBuilder để chuyển từ JSON vào các đối tượng Group và sau đó thông báo với delegate của nó với các đối tượng Group. Nếu có một vài vấn đề xảy ra trong quá trình request, thì chúng ta sẽ gọi một phương thức khác của delegate để thông báo với Controller một vấn đề đã xảy ra.

Hiển thị danh sách các nhóm

Việc đầu tiên cần làm chúng ta cần import các file vào file MasterViewController.m sau:

#import "Group.h"
#import "MeetupManager.h"
#import "MeetupCommunicator.h"

@interface MasterViewController () <MeetupManagerDelegate> {
    NSArray *_groups;
    MeetupManager *_manager;
}

Sau đó chúng ta sẽ hiện thực các phương thức của lớp này:

- (void)viewDidLoad
{
    [super viewDidLoad];

    _manager = [[MeetupManager alloc] init];
    _manager.communicator = [[MeetupCommunicator alloc] init];
    _manager.communicator.delegate = _manager;
    _manager.delegate = self;

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(startFetchingGroups:)
                                                 name:@"kCLAuthorizationStatusAuthorized"
                                               object:nil];
}

Trong đoạn code trên khá đơn giản để hiểu nó thực hiện việc khởi tạo một manager mới và khởi tạo thuộc tính communicator của nó là một MeetupCommunicator mới. Tiếp theo là phương thức startFetchingGroups:

- (void)startFetchingGroups:(NSNotification *)notification
{
    [_manager fetchGroupsAtCoordinate:self.locationManager.location.coordinate];
}

và tiếp theo là các phương thức của MeetupManagerDelegate protocol

- (void)didReceiveGroups:(NSArray *)groups
{
    _groups = groups;
    [self.tableView reloadData];
}

- (void)fetchingGroupsFailedWithError:(NSError *)error
{
    NSLog(@"Error %@; %@", error, [error localizedDescription]);
}

Cuối cùng là các phương thức delegate của tableView:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return _groups.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];

    Group *group = _groups[indexPath.row];
    [cell.nameLabel setText:group.name];
    [cell.whoLabel setText:group.who];
    [cell.locationLabel setText:[NSString stringWithFormat:@"%@, %@", group.city, group.country]];
    [cell.descriptionLabel setText:group.description];

    return cell;
}

Bài viết hướng dẫn cách để làm việc với JSON một kiểu dữ liệu khá phổ biến để trao đổi dữ liệu giữa client và server.