What’s new in iOS 7?
Highlights of 1500 new APIs

Jordi Gimenez (@gimix3) & Hermes Pique (@hpique)
NSBarcelona
82% of devices are using iOS 7.

As measured by the App Store during a 7-day period ending February 23, 2014.
Agenda
What we will cover
•
•
•
•
•
•
•

NSTextStorage
UIViewController transitions
iCloud & Core Data
App Store Receipt
S...
NSTextStorage
Text Kit
UILabel

UITextView

Text Kit

Core Text

Core Graphics

UITextField
Text Kit
Classes

NSTextStorage

NSLayoutManager

NSTextContainer

What?

How?

Where?

(text & attributes)

(glyphs & loc...
Text Kit
Use defaults or provide your own classes

textStorage = [NSTextStorage new];
layoutManager = [NSLayoutManager new...
Text Kit
NSTextStorage
• NSMutableAttributedString subclass

• Subclass to provide custom editing
- (void)processEditing
{...
Text Kit
Using images in NSTextStorage
(void)replaceLettersInRange:(NSRange)range
{
[backingStore.string enumerateSubstrin...
Demo

github.com/NSBarcelona/CandyCrushKeyboard
UIViewController Transitions
UIViewController Transitions
Customizable transitions
•
•
•
•

Presentations and dismissals
UINavigationController
UITabBa...
UIViewController Transitions
The container view

Container view
UIViewController A

UIViewController B

View A

View B
UIViewController Transitions
Returning the transition animation

- (id <UIViewControllerAnimatedTransitioning>)navigationC...
UIViewController Transitions
Animating the transition
- (void)animateTransition:(id <UIViewControllerContextTransitioning>...
Demo

github.com/NSBarcelona/Transictures
Adding iCloud to Core Data
“…the average [US] household has 1.6 Apple devices.”

–CNBC in 2012
iCloud
• Using iCloud in iOS 5-6 is a bug
• Sync network setup (!)
• Limited APIs
• Poor support for account changes
• Unr...
iCloud
Core Data with iCloud in 3 steps

1. Add iCloud to Core Data
2. Respond to changes
3. Respond to account changes
iCloud
Adding iCloud to Core Data

NSDictionary *options = @{ NSPersistentStoreUbiquitousContentNameKey : @"name" }
!

[pe...
iCloud
Responding to changes

NSPersistentStoreDidImportUbiquitousContentChangesNotification
!
!

[moc performBlockAndWait...
iCloud
Responding to account changes
NSPersistentStoreCoordinatorStoresWillChangeNotification
!
!

dispatch_sync(dispatch_...
iCloud
Responding to account changes
NSPersistentStoreCoordinatorStoresDidChangeNotification
!
!

dispatch_sync(dispatch_g...
Demo

github.com/NSBarcelona/CloudPhotos
The App Store Receipt
“This is doable.”

–James Wilson (Engineering Manager for the App Store in OS X) at WWDC 2013, about verifying the
App Sto...
App Store Receipt
Verification in 5 steps
1.
2.
3.
4.
5.

Get the receipt data
Verify the receipt signature
Get the receip...
App Store Receipt
Getting the receipt data
const char *cpath = [[path stringByStandardizingPath] fileSystemRepresentation]...
App Store Receipt
Verifying the receipt signature
int result = 0;
OpenSSL_add_all_digests(); // Required for PKCS7_verify ...
App Store Receipt
Getting the receipt fields
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length
u...
App Store Receipt
Verifying the receipt hash
NSUUID *uuid = [[UIDevice currentDevice] identifierForVendor];
unsigned char ...
App Store Receipt
Getting the in-app purchases
[RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length...
Demo

github.com/robotmedia/RMStore
About me
Jordi Giménez
• CTO at Mobile Jazz
• Co-organizer of NSBarcelona
• iOS and Android developer
• High-availability,...
Speech Synthesis with AV Foundation
AV Foundation framework provides essential services for working with time-based audiov...
AV Foundation
Speech synthesis
• AVFoundation allows provides a convenient interface to the speech
synthesizer
AVSpeechSyn...
JavaScriptCore
Evaluate JavaScript in your iOS applications
JavaScriptCore
•
•
•
•

New Objective-C interface
Allows Objective-C Language Binding
Good for JSON parsing/transformation...
JavaScript Execution

// make an execution context
JSContext *context = [[JSContext alloc] initWithVirtualMachine:
[[JSVir...
JavaScript Execution

// declare a function
[context evaluateScript:@"var square = function(x) {return x*x;}”];
!

// run ...
JavaScript Execution

// declare a function as an Objective-C block
context[@"square"] = ^(int x) {
return x*x;
};
Networking APIs
New networking APIs
• Multitasking: background execution of network tasks
• AirDrop: data transfer between nearby devices
...
Multitasking
iOS applications can’t generally run in the background, in order to save
battery and boost performance
• UIBa...
Multitasking
Background fetch: wake up at intervals
// 1. Specify `fetch` in UIBackgroundModes in Info.plist to wake up th...
Multitasking
Remote notifications: wake up on server demand
// 1. Specify `remote-notification` in UIBackgroundModes in In...
Multitasking
Background Transfer Service

Transferring files can be a problem:
• Intermittent connectivity
• User switchin...
Multitasking
Background Transfer Service
Uploads/downloads files even if the application is in background
!

1.
2.
3.
4.

...
Multitasking
Background Transfer Service
The application might be killed while in the background.
!

To deal with backgrou...
AirDrop
AirDrop

AirDrop lets users share photos,
documents, URLs (including custom
schemes), and other kinds of data via
Wifi/Blue...
AirDrop
Sending a file
1. Implement a class with the UIActivityItemSource
protocol

Provides:
• a URL with custom scheme o...
AirDrop
Receiving a file
• Add the document types and URL schemes you want to support (Xcode
Target > Info)
!
!
!
!
!
!

N...
AirDrop
Receiving a file
// handle incoming URLs/files
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)u...
Multipeer connectivity
Multipeer connectivity
• Allows to make ad-hoc groups of up to 8 devices in order to share data
(NSData, streams)
• Ideal ...
iBeacons
iBeacons
• iBeacons are Bluetooth Low Energy devices emitting an identifier
• CoreLocation integrates natively with iBeaco...
iBeacons
Listen for iBeacons
// tell location manager to start monitoring for the beacon region
region = [[CLBeaconRegion ...
iBeacons
Advertising your iDevice
// start the peripheral manager, tells about Bluetooth state
peripheralManager = [[CBPer...
Wrap up
• Speech synthesis
• JavaScript evaluation
• Networking in the background
• Background fetch
• Remote notification...
So… What’s new in iOS 7?
OK, you want the full list
• https://developer.apple.com/ios7/
• https://developer.apple.com/libr...
Thank you!
!

Check out our sample code at:
github.com/nsbarcelona
Hermes Pique!
@hpique

Jordi Giménez!
jordi@mobilejazz....
Upcoming SlideShare
Loading in …5
×

What's new in iOS 7

1,174 views
970 views

Published on

Published in: Technology
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
1,174
On SlideShare
0
From Embeds
0
Number of Embeds
16
Actions
Shares
0
Downloads
9
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide

What's new in iOS 7

  1. 1. What’s new in iOS 7? Highlights of 1500 new APIs Jordi Gimenez (@gimix3) & Hermes Pique (@hpique) NSBarcelona
  2. 2. 82% of devices are using iOS 7. As measured by the App Store during a 7-day period ending February 23, 2014.
  3. 3. Agenda What we will cover • • • • • • • NSTextStorage UIViewController transitions iCloud & Core Data App Store Receipt Speech Synthesis JavaScript Evaluation New Networking Possibilities
  4. 4. NSTextStorage
  5. 5. Text Kit UILabel UITextView Text Kit Core Text Core Graphics UITextField
  6. 6. Text Kit Classes NSTextStorage NSLayoutManager NSTextContainer What? How? Where? (text & attributes) (glyphs & location) (areas)
  7. 7. Text Kit Use defaults or provide your own classes textStorage = [NSTextStorage new]; layoutManager = [NSLayoutManager new]; textContainer = [[NSTextContainer alloc] initWithSize:size]; [layoutManager addTextContainer:textContainer]; [textStorage addLayoutManager:layoutManager]; ! textView = [[UITextView alloc] initWithFrame:frame textContainer:textContainer];
  8. 8. Text Kit NSTextStorage • NSMutableAttributedString subclass • Subclass to provide custom editing - (void)processEditing { [_backingStore beginEditing]; // Custom editing [_backingStore endEditing]; [super processEditing]; }
  9. 9. Text Kit Using images in NSTextStorage (void)replaceLettersInRange:(NSRange)range { [backingStore.string enumerateSubstringsInRange:range options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { ! NSTextAttachment *textAttachment = [NSTextAttachment new]; textAttachment.image = [self imageForString:substring]; NSAttributedString *attributed = [NSAttributedString attributedStringWithAttachment:textAttachment]; [backingStore replaceCharactersInRange:substringRange withAttributedString:attributed]; ! }]; }
  10. 10. Demo github.com/NSBarcelona/CandyCrushKeyboard
  11. 11. UIViewController Transitions
  12. 12. UIViewController Transitions Customizable transitions • • • • Presentations and dismissals UINavigationController UITabBarController UICollectionViewController layout-to-layout
  13. 13. UIViewController Transitions The container view Container view UIViewController A UIViewController B View A View B
  14. 14. UIViewController Transitions Returning the transition animation - (id <UIViewControllerAnimatedTransitioning>)navigationController: (UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC { id<UIViewControllerAnimatedTransitioning> animator = [MyAnimationController new]; return animator; }
  15. 15. UIViewController Transitions Animating the transition - (void)animateTransition:(id <UIViewControllerContextTransitioning>)context { ! UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [context viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *container = context.containerView; [container insertSubview:toViewController.view aboveSubview:fromViewController.view]; ! // Prepare for animation ! NSTimeInterval duration = [self transitionDuration:context]; [UIView animateWithDuration:duration animations:^{ // Animations } completion:^(BOOL finished) { // Cleanup and restore state [context completeTransition:YES]; }]; }
  16. 16. Demo github.com/NSBarcelona/Transictures
  17. 17. Adding iCloud to Core Data
  18. 18. “…the average [US] household has 1.6 Apple devices.” –CNBC in 2012
  19. 19. iCloud • Using iCloud in iOS 5-6 is a bug • Sync network setup (!) • Limited APIs • Poor support for account changes • Unreliability • In iOS 7 it’s actually usable
  20. 20. iCloud Core Data with iCloud in 3 steps 1. Add iCloud to Core Data 2. Respond to changes 3. Respond to account changes
  21. 21. iCloud Adding iCloud to Core Data NSDictionary *options = @{ NSPersistentStoreUbiquitousContentNameKey : @"name" } ! [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:options error:&error];
  22. 22. iCloud Responding to changes NSPersistentStoreDidImportUbiquitousContentChangesNotification ! ! [moc performBlockAndWait:^{ [moc mergeChangesFromContextDidSaveNotification:notification]; }];
  23. 23. iCloud Responding to account changes NSPersistentStoreCoordinatorStoresWillChangeNotification ! ! dispatch_sync(dispatch_get_main_queue(), ^{ // Prepare UI }); [moc performBlockAndWait:^{ NSError *error = nil; if ([moc hasChanges]) { [moc save:&error]; } [moc reset]; }];
  24. 24. iCloud Responding to account changes NSPersistentStoreCoordinatorStoresDidChangeNotification ! ! dispatch_sync(dispatch_get_main_queue(), ^{ // Update UI });
  25. 25. Demo github.com/NSBarcelona/CloudPhotos
  26. 26. The App Store Receipt
  27. 27. “This is doable.” –James Wilson (Engineering Manager for the App Store in OS X) at WWDC 2013, about verifying the App Store receipt
  28. 28. App Store Receipt Verification in 5 steps 1. 2. 3. 4. 5. Get the receipt data Verify the receipt signature Get the receipt fields Verify the receipt hash Get in-app purchases (optional)
  29. 29. App Store Receipt Getting the receipt data const char *cpath = [[path stringByStandardizingPath] fileSystemRepresentation]; FILE *fp = fopen(cpath, "rb"); if (!fp) return nil; ! PKCS7 *p7 = d2i_PKCS7_fp(fp, NULL); fclose(fp); if (!p7) return nil; ! NSURL *certificateURL = [[NSBundle mainBundle] URLForResource:@"AppleIncRootCertificate" withExtension:@"cer"]; NSData *certificateData = [NSData dataWithContentsOfURL:certificateURL]; if ([self verifyPCKS7:p7 withCertificateData:certificateData]) { struct pkcs7_st *contents = p7->d.sign->contents; if (PKCS7_type_is_data(contents)) { ASN1_OCTET_STRING *octets = contents->d.data; data = [NSData dataWithBytes:octets->data length:octets->length]; } } PKCS7_free(p7);
  30. 30. App Store Receipt Verifying the receipt signature int result = 0; OpenSSL_add_all_digests(); // Required for PKCS7_verify to work X509_STORE *store = X509_STORE_new(); if (store) { const uint8_t *certificateBytes = (uint8_t *)(certificateData.bytes); X509 *certificate = d2i_X509(NULL, &certificateBytes (long)certificateData.length); if (certificate){ X509_STORE_add_cert(store, certificate); BIO *payload = BIO_new(BIO_s_mem()); result = PKCS7_verify(container, NULL, store, NULL, payload, 0); BIO_free(payload); X509_free(certificate); } } X509_STORE_free(store); EVP_cleanup();
  31. 31. App Store Receipt Getting the receipt fields [RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) { const uint8_t *s = data.bytes; const NSUInteger length = data.length; switch (type) { case 2: bundleIdData = data; _bundleId = RMASN1ReadUTF8String(&s, length); break; case 3: appVersion = RMASN1ReadUTF8String(&s, length); break; case 4: opaqueValue = data; break; case 5: hash = data; break; case 17: { RMAppReceiptIAP *purchase = [[RMAppReceiptIAP alloc] initWithASN1Data:data]; [purchases addObject:purchase]; break; } case 19: originalAppVersion = RMASN1ReadUTF8String(&s, length); break; case 21: expirationDate = [RMAppReceipt formatRFC3339String:RMASN1ReadIA5SString(&s, length)]; break; } }];
  32. 32. App Store Receipt Verifying the receipt hash NSUUID *uuid = [[UIDevice currentDevice] identifierForVendor]; unsigned char uuidBytes[16]; [uuid getUUIDBytes:uuidBytes]; ! NSMutableData *data = [NSMutableData data]; [data appendBytes:uuidBytes length:sizeof(uuidBytes)]; [data appendData:opaqueValue]; [data appendData:bundleIdData]; ! NSMutableData *expectedHash = [NSMutableData dataWithLength:SHA_DIGEST_LENGTH]; SHA1(data.bytes, data.length, expectedHash.mutableBytes); ! BOOL verified = [expectedHash isEqualToData:hash];
  33. 33. App Store Receipt Getting the in-app purchases [RMAppReceipt enumerateASN1Attributes:asn1Data.bytes length:asn1Data.length usingBlock:^(NSData *data, int type) { const uint8_t *p = data.bytes; const NSUInteger length = data.length; switch (type) { case 1701: quantity = RMASN1ReadInteger(&p, length); break; case 1702: productId = RMASN1ReadUTF8String(&p, length); break; case 1703: transactionIdentifier = RMASN1ReadUTF8String(&p, length); break; case 1704: purchaseDate = [RMAppReceipt formatRFC3339String:RMASN1ReadIA5SString(&p, length)]; case 1705: originalTransactionId = RMASN1ReadUTF8String(&p, length); break; case 1706: originalPurchaseDate = [RMAppReceipt formatRFC3339String:RMASN1ReadIA5SString(&p, length)]; break; case 1708: subscriptionExpirationDate = [RMAppReceipt formatRFC3339String:RMASN1ReadIA5SString(&p, length)]; break; case 1711: webOrderLineItemID = RMASN1ReadInteger(&p, length); break; case 1712: cancellationDate = [RMAppReceipt formatRFC3339String:RMASN1ReadIA5SString(&p, length)]; break; } }];
  34. 34. Demo github.com/robotmedia/RMStore
  35. 35. About me Jordi Giménez • CTO at Mobile Jazz • Co-organizer of NSBarcelona • iOS and Android developer • High-availability, high-scalability • IT Security
  36. 36. Speech Synthesis with AV Foundation AV Foundation framework provides essential services for working with time-based audiovisual media. Play, capture, edit, or encode media.
  37. 37. AV Foundation Speech synthesis • AVFoundation allows provides a convenient interface to the speech synthesizer AVSpeechSynthesizer *synthesizer = [AVSpeechSynthesizer new]; AVSpeechUtterance *utterance = [AVSpeechUtterance speechUtteranceWithString:@"Hello world!"]; [synthesizer speakUtterance:utterance];
  38. 38. JavaScriptCore Evaluate JavaScript in your iOS applications
  39. 39. JavaScriptCore • • • • New Objective-C interface Allows Objective-C Language Binding Good for JSON parsing/transformation Platform independent code
  40. 40. JavaScript Execution // make an execution context JSContext *context = [[JSContext alloc] initWithVirtualMachine: [[JSVirtualMachine alloc] init]]; ! // read and write variables context[@"a"] = @5; // write global variables JSValue *aValue = context[@"a"]; // read global variables double a = [aValue toDouble];
  41. 41. JavaScript Execution // declare a function [context evaluateScript:@"var square = function(x) {return x*x;}”]; ! // run a function JSValue *squareFunction = context[@"square"]; JSValue *result = [squareFunction callWithArguments:@[@3]];
  42. 42. JavaScript Execution // declare a function as an Objective-C block context[@"square"] = ^(int x) { return x*x; };
  43. 43. Networking APIs
  44. 44. New networking APIs • Multitasking: background execution of network tasks • AirDrop: data transfer between nearby devices • Multipeer connectivity: message passing between ad-hoc groups of people
  45. 45. Multitasking iOS applications can’t generally run in the background, in order to save battery and boost performance • UIBackgroundModes specify when the app should wake up • • • • • • • • • audio location newsstand-content external-accessory bluetooth-central bluetooth-peripherial fetch remote-notification
  46. 46. Multitasking Background fetch: wake up at intervals // 1. Specify `fetch` in UIBackgroundModes in Info.plist to wake up the application at intervals. ! // 2. Give an interval hint: [[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum]; ! // 3. Handle fetch wakeup -(void) application:(UIApplication *)application performFetchWithCompletionHandler: (void(^) (UIBackgroundFetchResult))completionHandler { // fetch content from the network if(newContent) { completionHandler(UIBackgroundFetchResultNewData); } else { completionHandler(UIBackgroundFetchResultNoData); } }
  47. 47. Multitasking Remote notifications: wake up on server demand // 1. Specify `remote-notification` in UIBackgroundModes in Info.plist to wake up the application upon reception of push notifications with `content-available=1` attribute. ! // 2. Handle fetch wakeup - (void) application:(UIApplication *)application didReceiveRemoteNotification: (NSDictionary *)userInfo performFetchWithCompletionHandler:(void(^) (UIBackgroundFetchResult))completionHandler { if([[userInfo objectForKey:@"content-available"] intValue] != 1) { completionHandler(UIBackgroundFetchResultNoData); // no content-available! return; } // fetch content from the network if(newContent) { completionHandler(UIBackgroundFetchResultNewData); } else { completionHandler(UIBackgroundFetchResultNoData); } }
  48. 48. Multitasking Background Transfer Service Transferring files can be a problem: • Intermittent connectivity • User switching applications • Network conditioned operations (eg. wifi only)
  49. 49. Multitasking Background Transfer Service Uploads/downloads files even if the application is in background ! 1. 2. 3. 4. Make an NSURLSession with the desired NSURLSessionConfiguration Provide a session identifier and delegate Initiate a task Delegate methods will be called upon progress/completion
  50. 50. Multitasking Background Transfer Service The application might be killed while in the background. ! To deal with background mode, also implement -application:handleEventsForBackgroundURLSession:completionHandler: like this: ! 1. Look at the session identifier provided by the system 2. Make an NSURLSession with the desired NSURLSessionConfiguration, matching the settings for that identifier, with a delegate 3. Delegate methods will be called to notify completion
  51. 51. AirDrop
  52. 52. AirDrop AirDrop lets users share photos, documents, URLs (including custom schemes), and other kinds of data via Wifi/Bluetooth ! (See Apple’s AirDropSample)
  53. 53. AirDrop Sending a file 1. Implement a class with the UIActivityItemSource protocol
 Provides: • a URL with custom scheme or • an UTI + NSData. 2. Display a UIActivityViewController to handle your class instance ! UIActivityViewController already existed before to share data between apps, AirDrop was added
  54. 54. AirDrop Receiving a file • Add the document types and URL schemes you want to support (Xcode Target > Info) ! ! ! ! ! ! No matter where the file comes from (local or AirDrop) you will receive it the same way.
  55. 55. AirDrop Receiving a file // handle incoming URLs/files - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { if ([url.scheme isEqualToString:@"my-custom-scheme"]) { // handle custom scheme URL } else { // read file at URL } return YES; }
  56. 56. Multipeer connectivity
  57. 57. Multipeer connectivity • Allows to make ad-hoc groups of up to 8 devices in order to share data (NSData, streams) • Ideal for games or other kinds of data transfer on the spot
  58. 58. iBeacons
  59. 59. iBeacons • iBeacons are Bluetooth Low Energy devices emitting an identifier • CoreLocation integrates natively with iBeacons to provide regions • Applications can be notified when entering/leaving a region (even in the background) with very little power consumption ! • Useful for: • Locating areas at a retail store • Augmentation of museum • Geocaching/scavenger hunt games • Tourism
  60. 60. iBeacons Listen for iBeacons // tell location manager to start monitoring for the beacon region region = [[CLBeaconRegion alloc] initWithProximityUUID:uuid identifier:@"ibeacon.test"]; ! [locationManager startMonitoringForRegion:region]; [locationManager startRangingBeaconsInRegion:region]; ! // listen for location manager updates -(void)locationManager:(CLLocationManager*)manager didRangeBeacons:(NSArray*)beacons inRegion:(CLBeaconRegion*)region { NSLog(@"Beacons found: %@", beacons); }
  61. 61. iBeacons Advertising your iDevice // start the peripheral manager, tells about Bluetooth state peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil options:nil]; ! // listen for bluetooth peripheral updates -(void)peripheralManagerDidUpdateState:(CBPeripheralManager*)peripheral { if (peripheral.state == CBPeripheralManagerStatePoweredOn) { // makes a region describing this device CLBeaconRegion *advertisingRegion = [[CLBeaconRegion alloc] initWithProximityUUID:uuid major:33 minor:44 identifier:@"ibeacon.test"]; // gets this device's data to use as beacon NSDictionary* beaconData = [advertisingRegion peripheralDataWithMeasuredPower:nil]; // advertises [self.peripheralManager startAdvertising:beaconData]; } else if (peripheral.state == CBPeripheralManagerStatePoweredOff) { [self.peripheralManager stopAdvertising]; } }
  62. 62. Wrap up • Speech synthesis • JavaScript evaluation • Networking in the background • Background fetch • Remote notification background mode • Background Transfer Service • AirDrop & Multi-peer connectivity • iBeacons
  63. 63. So… What’s new in iOS 7? OK, you want the full list • https://developer.apple.com/ios7/ • https://developer.apple.com/library/prerelease/ios/releasenotes/General/ WhatsNewIniOS/Article
  64. 64. Thank you! ! Check out our sample code at: github.com/nsbarcelona Hermes Pique! @hpique Jordi Giménez! jordi@mobilejazz.cat

×