JSON บน iOS 5

ปัจจุบันเราคงปฏิเสธไม่ได้ว่าเราใช้ JSON กันบ่อยขึ้น และในวงกว้างขึ้นเรื่อยๆ
เดิมหากจะทำอะไรกับ JSON เราก็จะมักจะต้องพึ่งพา third party library อยู่เสมอ แต่เมื่อมาถึงยุคของ iOS 5  Apple ก็ได้เพิ่ม JSON library เข้ามาให้เราพร้อมใช้ได้ง่ายๆ แล้ว

ทำให้เราสามารถแปลง Object อย่าง NSString, NSNumber, NSArray หรือ NSDictionary ให้กลายเป็น JSON ได้ง่ายๆ โดยไม่จำเป็นต้องพึ่งพา third party library อีกต่อไป

พระเอกของงานนี้ก็คือ NSJSONSerialization  เราจะใช้มันช่วยในการแปลง Object ให้กลายเป็น JSON และแปลงจาก JSON กลับมาเป็น Object ได้ ดังนี้


แปลง JSON Data ให้กลายเป็น Object

    // สมมติว่าเราได้รับ data มาจากอินเทอร์เน็ต
    NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString: urlString]];

    // แปลงเป็น Object ด้วยพระเอกของเรา
    NSDictionary* json = [NSJSONSerialization JSONObjectWithData: data
                                                     optionskNilOptions 
                                                       error: &error];


สำหรับ options ของการแปลง JSON ให้กลายเป็น Object นั้น จะเกี่ยวกับเรื่องการความต้องการให้สิ่งที่แปลงออกมามีลักษณะเป็นอย่างไรในลักษณะของ Mutable หรือ Immutable  และอยากจะให้ top level object แยกกันเป็น array หรือ dictionary หรือไม่ อ่านรายละเอียด options ได้ที่นี่


แปลง Object เป็น JSON Data

    // เริ่มจากเรามี Object ที่ต้องการแปลงเป็น JSON
    NSDictionary* objectInfo = ...

    // แปลงเป็น JSON ด้วยพระเอกของเรา
    NSData* jsonData = [NSJSONSerialization dataWithJSONObject: objectInfo 
                                 optionsNSJSONWritingPrettyPrinted
                                   error: &error];

    // ลองดูหน้าตาของ JSON ที่เราแปลงได้
    NSString *jsonString = [[NSString allocinitWithData:jsonData
                                             encoding:NSUTF8StringEncoding];



กรณีนี้ถ้าหากเราต้องการส่งข้อมูลไปที่อื่นๆ เช่นส่งไปยัง server เราสามารถกำหนด option เป็น kNilOptions ได้ เพื่อตัดอักขระส่วนที่ไม่จำเป็นออกไป แต่ที่เรากำหนด options เป็น NSJSONWritingPrettyPrinted ก็เพื่อให้เราสามารถอ่านได้ง่ายนั่นเอง

เพียงเท่านี้เราก็สามารถทำงานร่วมกับ JSON ได้อย่างมีความสุขแล้ว

พักผ่อนที่ The Living Hill Resort

เคยไปพักผ่อนมาหลายๆ ที่ แต่ที่ The Living Hill Resort นี่ต้องเขียนถึงสักหน่อย เนื่องจากเป็นรีสอร์ทที่มีประวัติโชกโชนอยู่ในเว็บไซต์ชื่อดังหลายเว็บ ลองหาดูจะพบว่าชื่อเสียงในทางลบจะค่อนข้างเยอะนิดนึง ชวนให้คนไม่กล้าไปพักกันสักเท่าไหร่

คำชม
  • พนักงานพูดจาดีมาก บริการก็ดีมากเลย ดูเป็นกันเองคอยช่วยเหลือนู่นนี่ โทรมาถามไถ่เป็นห่วงว่าเราจะหารีสอร์ทไม่เจอด้วยล่ะ (แหงล่ะ ทางไปซับซ้อนมาก)
  • เงียบสงบ ที่พักคนน้อยดี (อาจเพราะไปวันที่คนน้อย) มีที่นั่งชิลๆ ตากลมเย็นๆ สบายดีจัง มีดาดฟ้าให้นอนดูดาวตอนกลางคืนด้วยถ้าโชคดีไปวันที่ฟ้าเปิด
  • ที่พักอยู่บนยอดเนินเขาสูง มองไปเห็นวิวรอบๆ ตัวเป็นแนวเขายาวล้อมรอบ มีลมพัดเย็นตลอด
  • ไม่ได้แย่อย่างที่ในเว็บส่วนใหญ่ที่หาเจอบ่นๆ กัน
  • ถ้าไม่มีดีลคงไม่ไป ห้องแพงเกิน แต่ถ้า 2 พันเหมือนเดิมมีโอกาสอาจไปพักอีกสัก 2-3 วัน
  • สัญญาณ truemove ชัดเจนใช้ได้ ทำให้ผมมี Google Map นำทางไปจนถึงจนได้ ไม่งั้นหลงชัวร์
  • ระหว่างทางแวะกินสเต๊กและไอติมอร่อยๆ ที่ร้าน Dairy Home
  • รูป

คำติ:
  • โทรศัพท์ติดต่อรีสอร์ทติดยากมาก (ดังคนเขาว่า) ถ้าเทียบกับรีสอร์ทที่อื่นๆ ที่เคยไปพัก
  • ตอนเช้างง ไม่รู้ว่าอาหารเช้าไปเอาตรงไหน ที่ไหนได้เดินลงมาแล้วจะมีพนักงานมาถามว่าห้องไหน แล้วจะมีคนเอามาเสริฟให้เลยถึงที่
  • แผนที่ในเว็บ thelivinghill.com การเดินทางไปไม่ชัดเจน เขียนแค่นั้นใครจะเดินทางไปถูก -_-" หลงได้ง่ายๆ เลย เส้นทางก็วกวนซับซ้อน ผมโชคดีที่คนรู้จักไปพักมาก่อน แล้วบันทึก location ไว้เลยเปิด Google Map ตามไปได้ไม่ยาก (ลองเทียบแผนที่จากในเว็บ กับแผนที่ใน Google Map ดูสิ ว่ามันช่างต่างกันมากเหลือเกิน)
  • หาที่นี่ไม่เจอใน Google Map



View Larger Map แผนที่ The Living Hill Resort จากแยกบางพลัด กรุงเทพ ไปถึงทางเข้า The Living Hill Resort


แสดงตำแหน่ง วิธีการเดินทางไป และสถานที่ตั้งของ The Living Hill Resort
ถึงจุด B แล้วให้เลี้ยวขวาไปครับ จะเป็นทางดิน ขึ้นเนินชันมาก

ลาก่อน สตีฟ จ็อบส์

สู่สุขติเถิด ลาก่อนสตีฟ จ็อบส์​





He changed them. He changed us. He changed you.

วาดกราฟใน iOS ด้วย CoreGraphic

เห็นหลายๆ App มีการวาดกราฟสวยๆ กัน โดยเฉพาะ App เกี่ยวกับหุ้นเช่น App ของ Bloomberg เป็นต้น จึงลองวาดเล่นดูสักหน่อย ถือเป็นการทบทวนทักษะทางด้าน CoreGraphic ก็แล้วกัน เพราะตั้งแต่ทำงานมาแทบไม่ได้ใช้ทักษะทางด้านนี้เลย

โดยรูปเป้าหมายที่ต้องการ จะมีหน้าตาดังนี้



หลักการการวาดรูปต่างๆ ลงไปก็คือเราจะเข้าไป override method  - (void)drawRect:(CGRect)rect ของ UIView และสั่งวาดสิ่งต่างๆ ข้างใน method นี้

เมื่อเราต้องการวาดอะไรลงไปก็ตาม เราต้องการสิ่งที่เรียกว่า context และเพื่อให้เข้าใจได้ง่ายๆ ให้มองว่า context ตัวนี้ก็คือผืนผ้าใบที่เราจะวาดสิ่งต่างๆ ลงไปนั่นเอง ดังนั้นเมื่อเราจะเริ่มทำอะไร เราต้องได้ context มาก่อน เพื่อเอาไว้ใช้อ้างอิงในภายหลัง ว่าเราจะทำอะไร จะวาดอะไรลงไปใน context (ผืนผ้าใบ) อันนี้ (context เป็น stateful ซึ่งแปลว่าหากเรากำหนดค่าอะไรให้มันไปแล้ว มันก็จะยังคงค่านั้นเสมอ จนกว่าเราจะเปลี่ยนแปลงค่าให้มันเองในภายหลัง) ดังนี้


    // get current context
    CGContextRef context = UIGraphicsGetCurrentContext();


เริ่มจากการวาดเส้นประ (dash) ที่เป็นพื้นหลังของกราฟก่อน เริ่มวาดเส้นประ (dash) โดยกำหนดความกว้างของเส้น สีที่จะวาด ลักษณะของเส้นประ ดังนี้

    // draw bg line
    CGContextSaveGState(context);
    CGContextSetLineWidth(context, 1.0);
    CGContextSetStrokeColorWithColor(context, [UIColor lightGrayColor].CGColor);
    CGFloat dashArray[] = {4,4,4,4};
    CGContextSetLineDash(context, 3, dashArray, 4);

เมื่อเรากำหนดลักษณะของเส้นที่จะวาดเรียบร้อยแล้ว ต่อไปก็กำหนดเส้นที่จะวาด ว่าจะวาดเส้นโดยเริ่มจากจุดไหน ไปถึงจุดไหนดังนี้

    for(int i = 50; i <= 300; i += 50){
        CGContextMoveToPoint(context, i, 200);
        CGContextAddLineToPoint(context, i, 480);
        
        CGContextMoveToPoint(context, 0, 200 + i);
        CGContextAddLineToPoint(context, 320, 200 + i);
    }


เมื่อกำหนดเส้นที่จะวาดเรียบร้อยแล้ว ก็สั่งให้วาดลงไปใน context (ผืนผ้าใบ) ด้วยฟังก์ชั่น CGContextStrokePath(context) ได้เลย

    CGContextStrokePath(context);
    CGContextRestoreGState(context);


สำหรับความหมายของ CGContextSaveGState(context) และ CGContextRestoreGState(context) ให้อ่านได้จากบทความนี้ สั้นๆ ก็คือมันเป็นการบันทึกสถานนะของการกำหนดค่าต่างๆ ให้กับ context หลังจากนั้นเมื่อเราทำอะไรเสร็จแล้ว เราก็จะย้อนกลับไปสถานะของ context ก่อนหน้าที่เราจะทำอะไรกับมันด้วยการ restore context นั่นเอง เมื่อเราสั่งให้วาดเส้นประแล้วเราจะได้ผลลัพธ์ดังนี้





ถ้าหากเราสั่ง gradient ลงไปใน context ตอนนี้ เราจะได้ gradient เต็มหน้าจอเนื่องจาก context ของเราจะมีรูปร่างเป็นสี่เหลี่ยมผืนผ้าเต็มหน้าจอนั่นเอง แต่ที่เราต้องการจริงๆ ก็คือเราต้องการให้ gradient เฉพาะพื้นที่ใต้กราฟเท่านั้น ซึ่งเทคนิคที่เราจะใช้ก็คือ เราจะสร้าง shape ที่เป็นพื้นที่ใต้กราฟ จากนั้นเราจะ clip context ให้เป็นรูปเดียวกับ shape ที่เราวาดขึ้นมา จากนั้นจึงจะวาด gradient ลงไปใน context ที่ clip แล้ว

สมมติว่าเรามีข้อมูลอยู่ 40 ชุด โดยเราสามารถ generate ข้อมูล 40 ชุดขึ้นมาได้ดังนี้ก่อน


    // generate 40 data, each limited to 150
    int r = 0;
    int array[40];
    for(int i = 0; i <= 40; i ++){
        r = arc4random() % 150;
        array[i] = r;
    }


เก็บ data ไว้ในตัวแปร array ขนาด 40 ต่อไปเราจะวาด Shape ที่ที่เป็นพื้นที่ใต้กราฟโดยอิงจากข้อมูล 40 ชุดที่เรามี ดังนี้


    // draw graph by points stored in array
    CGContextSaveGState(context);
    CGContextMoveToPoint(context, 0, array[0] + graphBase);
    for(int i = 0; i <= 40; i ++){
        CGContextAddLineToPoint(context, i * 8, array[i] + graphBase);
    }
    
    // wrap it as a shape
    CGContextAddLineToPoint(context, 320, 480);
    CGContextAddLineToPoint(context, 0, 480);
    CGContextAddLineToPoint(context, 0, array[0] + graphBase);
    CGContextSetLineWidth(context, 3);
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextStrokePath(context);
    CGContextRestoreGState(context);



เหมือนกับการวาดเส้นประในตอนแรก โดยเราจะวนลูปวาดเส้นไปทีละจุดๆ ใน array เมื่อวาดครบทุกจุดแล้วเราจะวาดย้อนกลับมายังจุดเริ่มต้นใหม่อีกครั้งให้เป็น Shape ปิด และเมื่อเราทดลองวาดเส้นเพื่อตรวจสอบ Shape ที่เราวาดขึ้นมา เราจะได้ Shape หน้าตาดังนี้



Shape ถูกวาดเส้นด้วยสีแดง ต่อไปเราจะทำการ clip Context ด้วย Shape ที่เราวาดขึ้นมาโดยใช้ฟังก์ชั่น CGContextClip(context) และเอาโค้ดในการวาดเส้นสีแดงให้ Shape ออกไป ทำให้จากโค้ดด้านบนเราจะเหลือแบบนี้


    // wrap it as a shape
    CGContextAddLineToPoint(context, 320, 480);
    CGContextAddLineToPoint(context, 0, 480);
    CGContextAddLineToPoint(context, 0, array[0] + graphBase);
    CGContextSetLineWidth(context, 3);
    CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
    CGContextClip(context);


ต่อไปจะวาด Gradient ลงไปใน context ที่เหลือจากการถูก clip โดยเริ่มจากการนิยาม Gradient ก่อนว่าจะให้มีลักษณะเป็นอย่างไร


    // gradient definition
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    CGFloat locations[] = { 0.55, 1.0};
    CGColorRef endColor = [UIColor colorWithRed:0.0f 
                                          green:153.0f/256.0f 
                                           blue:249.0f/256.0f 
                                          alpha:1.0].CGColor;
    
    CGColorRef startColor = [UIColor colorWithRed:1
                                            green:1
                                             blue:1
                                            alpha:0.5].CGColor;
    
    NSArray *colors = [NSArray arrayWithObjects:(id)startColor, (id)endColor, nil];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef) colors, locations);
    CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
    CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));


โค๊ด เป็นการกำหนด color space (ส่วนใหญ่แทบทั้งหมดจะใช้ RGB)  กำหนดตำแหน่งของสีที่จะใช้วาด gradient กำหนดสีเริ่มต้น และสีปลายของ gradient กำหนดจุดเริ่มต้นของ gradient และจุดปลายของ gradient

วาด gradient ลงไปใน context ที่ clip ไปแล้ว และ restore state เดินกลับมาแบบนี้


    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
    CGContextRestoreGState(context);


จะได้ผลลัพธ์ดังนี้



เราได้ gradient ใต้กราฟมาแล้ว ต่อไปเราจะวาดเส้นกันล่ะ โดยจะวนลูปวาดเส้นจากข้อมูลทั้ง 40 จุดที่เรามี ดังนี้


    // draw graph line
    CGColorRef graphColor = [UIColor colorWithRed:0.0f 
                                            green:153.0f/256.0f 
                                             blue:249.0f/256.0f 
                                            alpha:1].CGColor;
    CGContextSaveGState(context);
    CGContextSetStrokeColorWithColor(context, graphColor);
    CGContextSetLineWidth(context, 3);
    CGContextMoveToPoint(context, 0, array[0]);
    for(int i = 0; i <= 40; i ++){
        CGContextAddLineToPoint(context, i * 8, array[i]);
    }
    CGContextStrokePath(context);
    CGContextRestoreGState(context);


กำหนดสีที่จะวาด จากนั้นลูปวาดเส้นตามจุดต่างๆ ตามข้อมูลทั้ง 40 จุดที่เรามี เราก็จะได้กราฟหน้าตาแบบนี้ออกมาแล้ว



ส่วนการวาด text หรือข้อความลงไปด้วย CoreGraphic นั้น ถ้าหากเราสั่งวาดแบบปกติลงไป เราจะได้ข้อความที่กลับหัว ต้อง transform มันด้วยโดยการกำหนด text matrix ก่อนการสั่งวาด ดังนี้


    // draw text
    char* text ="Apple API";
    CGContextSelectFont(context, "Helvetica", 16, kCGEncodingMacRoman); 
    CGContextSetTextDrawingMode(context, kCGTextFill);
    CGContextSetRGBFillColor(context, 1, 1, 1, 1);
    
    CGAffineTransform xform = CGAffineTransformMake(1.00.0,
                                                    0.0, -1.0,
                                                    0.00.0);
    CGContextSetTextMatrix(context, xform);
    CGContextShowTextAtPoint(context, 120, 450, text, strlen(text));



ซอสโค้ดของตัวอย่างนี้ดาวโหลดได้ที่นี่ หรือดาวน์โหลดจาก GitHub ก็ได้

อ้างอิง:
An iOS 4 iPad Graphics Drawing Tutorial using Quartz 2D
Quartz 2D Programming Guide

สั่งให้ UIView เคลื่อนที่แนวโค้ง

การสั่งให้ UIView ทั้งหลายแหล่เคลื่อนที่เป็นแนวเส้นตรงนั้นไม่ยาก เพียงแต่กำหนดจุดเริ่มต้น กับจุดปลายผ่านบล็อค beginAnimations:context: และ commitAnimations เท่านั้น

สำหรับการเคลื่อนที่ของ UIView เป็นเส้นโค้งนั้น หลักการก็คือเราจะกำหนดเส้นทางการเคลื่อนที่ให้มัน จากนั้นกำหนด animation ให้กับ layer ของ UIView ที่ต้องการให้เคลื่อนที่

ค้นหาเส้นทางการเคลื่อนที่
เราจะกำหนดเส้นทางการเคลื่อนที่ แบบเดียวกับการวาดเส้นโค้งอย่าง Bézier curve ซึ่งเจ้า Bézier curve นี้ก็จะประกอบด้วยจุดเริ่ม จุดปลาย และจุดควบคุมหรือ control point (อ่านรายละเอียดเกี่ยวกับลักษณะของ Bézier curve ที่นี่)

ผมจะวาดเส้นโค้งขึ้น จากขอบทางซ้าย ไปทางขวาของจอ iPad ผมสามารถกำหนดเส้นโค้งได้ประมาณนี้

CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, 0, 512);
CGPathAddQuadCurveToPoint(curvedPath, NULL, 768/2, 0, 768, 512);

ซึ่งขนาดความโค้งว่าจะโค้งมากหรือน้อย โค้งขึ้นหรือโค้งลง สามารถกำหนดได้ที่จุดควบคุม จากโค๊ดด้านบนลองวาดออกมาเป็นเส้นดูจะได้เส้นหน้าตาแบบนี้



ส่วนวิธีการวาดนั้น สามารถทำได้ดังนี้

//Create a bitmap graphics context, you will later get a UIImage from this
UIGraphicsBeginImageContext(CGSizeMake(768,1024));
CGContextRef ctx = UIGraphicsGetCurrentContext();

//Set variables in the context for drawing
CGContextSetLineWidth(ctx, 1.5);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);

CGContextMoveToPoint(ctx, 0, 512);
CGContextAddQuadCurveToPoint(ctx, 768/2, 0, 768, 512);

//Draw the line
CGContextDrawPath(ctx, kCGPathStroke);

//Get a UIImage from the current bitmap context we created at the start and then end the image context
UIImage *curve = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

//With the image, we need a UIImageView
UIImageView *curveView = [[UIImageView alloc] initWithImage:curve];
//Set the frame of the view - which is used to position it when we add it to our current UIView
curveView.frame = CGRectMake(0, 0, 768, 1024);
curveView.backgroundColor = [UIColor clearColor];

[self.view addSubview:curveView];

เมื่อดูจากเส้นที่วาดออกมาแล้ว พบว่าเป็นเส้นทางการเคลื่อนที่ในแนวโค้งที่เราต้องการ เราก็เอาจุดที่นิยาม Bézier curve นี้แหละมาใช้งาน

กำหนดเส้นทางการเคลื่อนที่
เราจะใช้ CAKeyframeAnimation ในการกำหนดเส้นทางการเคลื่อนที่ โดยการกำหนดค่าต่างๆ ให้กับ CAKeyframeAnimation ดังนี้

//Prepare the animation - we use keyframe animation for animations of this complexity
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.duration = 5.0;
//Lets loop continuously for the demonstration
pathAnimation.repeatCount = 1000;

จากโค๊ดจะบอกว่าให้เล่น animation นี้ 5 นาทีต่อ 1 รอบ กำหนดให้เล่น 1000 รอบ สำหรับรายละเอียดของ property ตัวอื่นๆ ที่ไม่ได้กล่าวถึงแต่ละตัวสามารถอ่านได้จากที่นี่เลย

ต่อไปก็กำหนดเส้นทางเดินให้ animation ตัวนี้ล่ะ โดยใช้เส้นที่เราได้เลือกแล้วดังกล่าวไปข้างต้น ดังนี้

CGMutablePathRef curvedPath = CGPathCreateMutable();
CGPathMoveToPoint(curvedPath, NULL, 0, 512);
CGPathAddQuadCurveToPoint(curvedPath, NULL, 768/2, 0, 768, 512);

pathAnimation.path = curvedPath;
CGPathRelease(curvedPath);

เมื่อ animation และเส้นทางการเคลื่อนที่พร้อมแล้ว เราก็กำหนดเส้น animation นี้ให้กับ UIView ที่ต้องการได้เลย ดังนี้

[myView.layer addAnimation:pathAnimation forKey:@"position"];

เราก็จะได้การเคลื่อนที่เป็นเส้นโค้งจากจุดต้นของเส้นทางซ้ายสุด ไปจุดปลายของเส้นทางขวาสุดสมใจ

แอบเอาโค๊ดมาจากที่นี่