$30 off During Our Annual Pro Sale. View Details »

Mobile App Showcase: Tuenti

Tuenti
November 05, 2013

Mobile App Showcase: Tuenti

Tuenti has one of the most popular Spanish mobile apps with millions of monthly active users. Want to know how it was built?

Tuenti

November 05, 2013
Tweet

More Decks by Tuenti

Other Decks in Technology

Transcript

  1. Mobile App Showcase

    View Slide

  2. Who are we?
    @luisrecuenco
    Luis Recuenco
    iOS Voice Team
    Eduardo González
    @ieduardogf
    iOS Core Team

    View Slide

  3. 1. A little bit of history
    !
    2. Process & release workflow
    !
    3. Testing
    !
    4. Libraries and architecture

    View Slide

  4. A little bit of history

    View Slide

  5. Tuenti 1.0
    • 21 January, 2010.
    • No API at the beginning. It was created along the way.
    • Only one dev.

    View Slide

  6. View Slide

  7. Tuenti 2.0
    • 13 April, 2011, a year later.
    • Big feature: push notifications.
    • As Tuenti 1.0, almost no help from design.
    • Again, only one dev. Created in his free time.

    View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. Tuenti 2.X
    • Comms & social iterations.
    • Too much work for one person…
    • Mobile team started to grow.
    • Three people in september 2011
    Dani Miguel Luis

    View Slide

  12. Change of strategy

    View Slide

  13. Messenger
    • Started development in december, 2011.
    • Focused on communication with a slight social
    layer.
    • Objective: communications tool

    View Slide

  14. Meanwhile… Tuenti app was frozen

    View Slide

  15. Not even adapted to the iPhone 5!

    View Slide

  16. It never was…

    View Slide

  17. View Slide

  18. • Change of direction: giving back some importance to social.
    • Claim: The easiest way to share experiences with the people
    that matter right now.
    • October 2012: Messenger hit the app store.
    • Rebranding: Tuenti Social Messenger | Tuenti Classic

    View Slide

  19. View Slide

  20. 3.5

    View Slide

  21. Problems
    • Tuenti brand is “strong”. Difficult to be considered a
    messenger.
    • 1.0 quite incomplete.
    • Mobile Team getting bigger.
    • Not Mobile First first yet.

    View Slide

  22. Dani Miguel Luis
    Sergio
    Víctor Edu

    View Slide

  23. Sharing
    Comms
    Core

    View Slide

  24. Almost 1 year later…

    View Slide

  25. Fully mobile first philosophy

    View Slide

  26. Sharing
    Cloud Phone
    Core
    Growth Tent
    Comms

    View Slide

  27. 12 developers!

    View Slide

  28. 1 app!!!

    View Slide

  29. View Slide

  30. View Slide

  31. 30 developers

    View Slide

  32. New Mobile
    • One Core Team: support
    • Product teams:
    • Sharing (MAD)
    • Comms (MAD)
    • Cloud Phone (MAD)
    • Growth (BCN)
    • Tent (BCN)

    View Slide

  33. Change of strategy

    View Slide

  34. Yes, another one

    View Slide

  35. Two forces…

    View Slide

  36. View Slide

  37. Social

    View Slide

  38. View Slide

  39. Telco 2.0

    View Slide

  40. Users

    View Slide

  41. View Slide

  42. Customers

    View Slide

  43. View Slide

  44. • Core Team
    • Voice
    • Messaging
    • Account
    • Social
    • Onboarding (BCN)
    • Acquisition (BCN)

    View Slide

  45. Process & release workflow

    View Slide

  46. Plan—>Develop—>Distribute

    View Slide

  47. Plan—>Develop—>Distribute

    View Slide

  48. AGILE
    Scrum

    View Slide

  49. Stop starting, Start finishing

    View Slide

  50. 2 weeks sprint

    View Slide

  51. Plan
    Jira

    View Slide

  52. Plan
    • Support for manage SCRUM boards
    • Create a user story / bug / task
    • Every commit needs a ticket (hook on repository)
    • Estimate. Visualize remaining time
    • Assign directly to developer in charge of the feature

    View Slide

  53. View Slide

  54. View Slide

  55. Plan—>Develop—>Distribute

    View Slide

  56. Version control
    GIT

    View Slide

  57. View Slide

  58. Development Process Flow
    • Develop features
    • commit and push code to repository
    • create unit/acceptant test. Run them!!!
    • Fix things.
    • Meetings!!
    • More and more code

    View Slide

  59. No matter who. No matter what. No matter when. Short term. Long term. Any
    term. Writing good code is ALWAYS faster than writing bad code.!
    ROBERT MARTIN (@UNCLEBOB)

    View Slide

  60. Code Reviews
    FishEye & Crucible

    View Slide

  61. Code Reviews
    • Ensure developers follow coding standards
    • At least 2 developers must “pass” the review to be
    able to close it.
    • “It’s nothing personal”
    • Be proactive with your comments.

    View Slide

  62. View Slide

  63. Continuous Integration
    Jenkins

    View Slide

  64. Continuous Integration
    • Jenkins / 2 mac minis
    • Jobs for server and client
    • Jobs create on command line
    • tu-jenkins create-dev (unit)
    • tu-jenkins create-nightly (full regresion)
    • tu-jenkins pre-integrate
    • Unit test every push / instant feedback for the developer
    • Every night unit and acceptant test execute
    • Email every morning to the branch owner.

    View Slide

  65. Branch Integration Process
    • Feature is code complete
    • Tested and QA approved
    • All the API dependencies are working in live
    • Translations added and reviewed by QA/PM
    • Unit and acceptant test!!
    • Has latest integration merged (with blue ball)

    View Slide

  66. View Slide

  67. Flow
    (Jira+Jenkins+SCM)

    View Slide

  68. Plan—>Build—>Distribute

    View Slide

  69. Distribution
    HockeyApp

    View Slide

  70. Use your app!

    View Slide

  71. Distribution
    • Rake hotapps (everything automated)
    • Clean, checkstyle, archive & upload to Hockeyapp
    • Distribute betas internally in an easy way
    • Also used for collect crash reports, find bugs
    • Previously we had Crashlytics
    • Rake appstore

    View Slide

  72. Go to market

    View Slide

  73. Release Process Flow
    • We have enough to go live (Product Team)
    • We take latest stable (blue ball) changeset from
    integration and create a new Release Candidate
    branch
    • QA team makes a regression
    • 3 days, if something is discovered, release cancelled
    • Send to Apple!!
    • Merge back to master, integration and TAG

    View Slide

  74. Release Process Flow

    View Slide

  75. View Slide

  76. 1 week…

    View Slide

  77. –Edsger W. Dijkstra
    “Program testing can be a very effective way to
    show the presence of bugs, but is hopelessly
    inadequate for showing their absence”

    View Slide

  78. Testing

    View Slide

  79. How many of you test your code?

    View Slide

  80. Unit + Acceptant Tests

    View Slide

  81. Unit Tets
    • Mocking: OCMockito
    !
    • Assertions: OCHamcrest

    View Slide

  82. Jon Reid

    View Slide

  83. http://qualitycoding.org/

    View Slide

  84. View Slide

  85. View Slide

  86. View Slide

  87. Acceptant Tests
    • Before: Frank + cucumber
    !
    • Now: KIF

    View Slide

  88. Frank + cucumber

    View Slide

  89. # coding: utf-8
    # Author:: David Pastor (mailto:[email protected])
    !
    Feature: Login Screen
    !
    Background:
    Given I launch the app
    And in the start screen I go to the login
    !
    # Regular login
    !
    @QAke(1982)
    Scenario: Verified new messenger user logs in and skips sync process
    Given a user: "myself" exists with verified: true
    When in the login screen I do login skipping synchronization with "myself"
    Then in the conversations screen I check that is the current screen

    View Slide

  90. class LoginScreen < Base
    !
    element :incorrect_credentials_label, "view:'UILabel' marked:'Oops! Make sure your…’”
    element :login_button, "view:'UIButton' marked:'Log In'"
    element :forgot_password_button, "view:'UIButton' marked:'UITextFieldForgotButton'"
    !
    def navigation_bar
    @navigation_bar ||= NavigationBar.new(:title => "Log In") do |bar|
    bar.add_element :join, "view:'UIButton' marked:'Join Tuenti'"
    end
    end
    !
    def email_text_field
    TextField.new("view:'TMPaddingTextField' index:0", :placeholder => "Email")
    end
    !
    def password_text_field
    TextField.new("view:'TMPaddingTextField' index:1", :placeholder => "Password")
    end

    View Slide

  91. def check_that_is_the_current_screen
    navigation_bar.check_displayed
    end
    def submit_credentials(email, password)
    fill_the_credentials(email, password)
    login_button.touch
    end

    View Slide

  92. Problems
    • Very slow…
    • We didn’t like cucumber.
    • We wanted something in Objective-C if possible, not
    ruby.

    View Slide

  93. KIF

    View Slide

  94. #import "TMKIFScreen.h"
    !
    @interface TMLoginKIFScreen : TMKIFScreen
    !
    // Convenience
    - (void)loginWithEmail:(NSString *)email password:(NSString *)password;
    !
    // Login button
    - (void)tapOnTheLoginButton;
    - (void)checkLoginButtonEnabled:(BOOL)expectedEnabled;
    !
    // Email text field
    - (void)typeIntoEmailTextField:(NSString *)string;
    - (void)clearEmailTextField;
    !
    @end

    View Slide

  95. - (void)loginWithEmail:(NSString *)email password:(NSString *)password
    {
    [self typeIntoEmailTextField:email];
    [self typeIntoPasswordTextField:password];
    [self tapOnTheLoginButton];
    }

    View Slide

  96. - (void)typeIntoEmailTextField:(NSString *)string
    {
    [self.actor enterText:string
    intoViewWithAccessibilityLabel:kEmailTextFieldAccessibilityLabel];
    }
    - (void)typeIntoPasswordTextField:(NSString *)string
    {
    [self.actor enterText:string
    intoViewWithAccessibilityLabel:kPasswordTextFieldAccessibilityLabel];
    }

    View Slide

  97. - (void)checkForIncorrectCredentialsErrorFeedback
    {
    [self.actor
    waitForViewWithAccessibilityLabel:kIncorrectCredentialsFeed
    backAccessibilityLabel];
    }

    View Slide

  98. Dependency Injection

    View Slide

  99. sharedInstance is “bad”

    View Slide

  100. Typhoon

    View Slide

  101. When we didn’t test…

    View Slide

  102. @interface TMGetEventInfoOperation : TMServiceOperation
    !
    @end
    @implementation TMGetEventInfoOperation
    !
    - (void)doMain
    {
    ...
    NSArray *unknowns = [[TMUserParser parser]
    parseUsers:unknownsFromJSON];
    !
    [[TMUserStorage sharedStorage] addAsUnknowns:unknowns];
    ...
    }
    !
    @end

    View Slide

  103. Now that we do test…

    View Slide

  104. @interface TMGetEventInfoOperation : TMServiceOperation
    @property (nonatomic, strong) TMUserParser *userParser;
    @property (nonatomic, strong) TMUserStorage *userStorage;
    @end
    @implementation TMGetEventInfoOperation
    !
    - (void)doMain
    {
    ...
    NSArray *unknowns = [self.userParser
    parseUsers:unknownsFromJSON];
    !
    [self.userStorage addAsUnknowns:unknowns];
    ...
    }
    !
    @end

    View Slide

  105. View Slide

  106. View Slide

  107. View Slide

  108. Not so clean headers
    Mock dependencies

    View Slide

  109. View Slide

  110. Libraries & architecture

    View Slide

  111. Libraries

    View Slide

  112. View Slide

  113. Not yet

    View Slide

  114. git submodules

    View Slide

  115. View Slide

  116. • XMPPFramework
    • Reachability
    • Mixpanel
    • Cordova
    • HockeySDK
    • TheAmazingAudioEngine
    • Typhoon
    • KIF
    • libextobjc

    View Slide

  117. libextobjc

    View Slide

  118. __weak typeof(self) wself = self;
    __strong typeof(wself) sself = wself;

    View Slide

  119. @weakify(self)
    @strongify(self)

    View Slide

  120. @weakify(self);
    self.albumKVOToken = [self.album
    addObserverForKeyPath:@“photoMoments”
    block:^(id obj, NSDictionary *change) {
    [self doWhatever];
    }];

    View Slide

  121. @weakify(self);
    self.albumKVOToken = [self.album
    addObserverForKeyPath:@“photoMoments”
    block:^(id obj, NSDictionary *change) {
    @strongify(self);
    [self doWhatever];
    }];

    View Slide

  122. @weakify(self);
    self.albumKVOToken = [self.album
    addObserverForKeyPath:@“photoMoments”
    block:^(id obj, NSDictionary *change) {
    @strongify(self);
    dispatch_async(dispatch_get_main_queue(), ^{
    [self doWhatever];
    })
    }];

    View Slide

  123. MULTITHREADING IS EVIL

    View Slide

  124. WE LOVE FIRING NSOPERATIONS

    View Slide

  125. 300 THREADS!

    View Slide

  126. @synthesizeAssociation(CLASS, PROPERTY)

    View Slide

  127. View Slide

  128. KVO & NSNotifications

    View Slide

  129. @interface NSObject
    (TUKVODoneRight)

    View Slide

  130. self.commentKVOToken = [comment
    addObserverForKeyPath:@keypath(comment.state)
    block:^(id obj, NSDictionary *change) {
    // Comment state just changed
    }];
    @property (nonatomic, copy) NSString *commentKVOToken;

    View Slide

  131. - (void)dealloc
    {
    [_comment
    removeObserverForKeyPath:@keypath(state)
    token:_commentKVOToken];
    }

    View Slide

  132. View Slide

  133. THObserversAndBinders
    https://github.com/th-in-gs/THObserversAndBinders

    View Slide

  134. self.commentObserver = [THObserver
    observerForObject:comment
    keyPath:@keypath(comment.state) block:^{
    // Comment state just changed
    !
    }];
    @property (nonatomic, strong) THObserver *commentObserver;

    View Slide

  135. No hacky swizzling to unsubscribe!

    View Slide

  136. LRNotificationObserver
    https://github.com/luisrecuenco/LRNotificationObserver/

    View Slide

  137. Networking

    View Slide

  138. View Slide

  139. We love Mattt

    View Slide

  140. NSHipster

    View Slide

  141. We also love
    reinventing the wheel

    View Slide

  142. Tuenti API Client

    View Slide

  143. NSOperationQueue based
    Aware of network changes to handle
    concurrency
    Cancellation, ops dependencies

    View Slide

  144. NSInteger const kWiFiMaxOperationCount = 6;
    NSInteger const kWWANMaxOperationCount = 2;
    NSInteger const kWiFiMaxUploadOperationCount = 2;
    NSInteger const kWWANMaxUploadOperationCount = 1;

    View Slide

  145. Persistence

    View Slide

  146. Core Data

    View Slide

  147. Mogenerator

    View Slide

  148. Saving plists is fine

    View Slide

  149. Architecture

    View Slide

  150. View Slide

  151. http://cleancoders.com/

    View Slide

  152. Massive View Controller

    View Slide

  153. View Slide

  154. MVP

    View Slide

  155. MVVM

    View Slide

  156. Reactive Cocoa

    View Slide

  157. View Slide

  158. View Slide

  159. Storages

    View Slide

  160. View Slide

  161. Just intelligent arrays

    View Slide

  162. Multicast delegate

    View Slide

  163. @protocol TMAlbumStorageDelegate
    !
    @optional
    - (void)albumStorage:(TMAlbumStorage *)albumStorage
    didUpdateAlbumsForUser:(TMUser *)user withAlbums:(NSArray *)albums;
    !
    - (void)albumStorage:(TMAlbumStorage *)albumStorage
    didUpdateAlbumWithId:(NSString *)albumId;
    !
    @end

    View Slide

  164. @interface TMAlbumStorage : NSObject
    !
    - (void)addAlbum:(TMAlbum *)album;
    - (void)removeAlbum:(TMAlbum *)album;
    !
    - (NSArray *)albumsForUser:(TMUser *)user;
    !
    - (void)addDelegate:(id)delegate
    delegateQueue:(dispatch_queue_t)delegateQueue;
    !
    - (void)removeDelegate:(id)delegate
    delegateQueue:(dispatch_queue_t)delegateQueue;
    !
    - (void)removeDelegate:(id)delegate;
    !
    @end

    View Slide

  165. GCDMulticastDelegate
    *multicastDelegate

    View Slide

  166. Networking Service Layer

    View Slide

  167. View Slide

  168. Dictionary with ongoing operations
    Operations must have a id token
    Listen to isFinished keypath via KVO to remove
    operation from dictionary

    View Slide

  169. - (id)getAlbumInfoForAlbumId:(NSString *)albumId
    user:(TMUser *)user
    {
    return [self cachedOperationWithFallback:
    [self.getAlbumInfoOperationProvider operationWithAlbumId:albumId
    user:user]];
    }

    View Slide

  170. - (void)doMain
    {
    TURequestOperation *getAlbumInfoOp = [self.requestOperationProvider
    operationWithName:kGetAlbumInfoOperationName];
    [getAlbumInfoOp addValue:self.albumId forArgument:kAlbumIdParameterName];
    getAlbumInfoOp.successBlock = ^(TURequestOperation *operation, NSDictionary *response) {
    TMAlbumParser *parser = [[TMAlbumParser alloc] init];
    TMAlbum *album = [parser parseAlbumForUser:self.user
    withDictionary:response[kAlbumResponseKey]];
    [[TMAlbumStorage sharedStorage] updateAlbumsForUser:self.user withAlbums:@[album]];
    self.album = [[TMAlbumStorage sharedStorage] albumForId:album.albumId user:self.user];
    };
    getAlbumInfoOp.failureBlock = ^(TURequestOperation *operation, NSDictionary *response,
    NSError *error) {
    };
    [self startRequest:[getAlbumInfoOp asRequest]];
    }

    View Slide

  171. Cell architecture

    View Slide

  172. View Slide

  173. View Slide

  174. View Slide

  175. View Slide

  176. TMFeedItemCellControllerFactory
    Introspection of model object
    isKindOfClass, double dispatch,
    polymorphism

    View Slide


  177. creates cell
    handle tap on cells
    height of cell

    View Slide

  178. TMFeedItemCellController
    injected handlers/interactors
    invoked when events in the cell
    forwarded to the controller

    View Slide

  179. Cell Controllers are reusable
    Handlers are reusable

    View Slide

  180. Tuenti Image Client

    View Slide

  181. Two level cache (mem + disk)

    View Slide

  182. View Slide

  183. View Slide

  184. View Slide

  185. @interface UIImageView (TMNetImagePresenter)
    !
    - (void)tm_presentImageFromURL:(NSURL *)imageURL;
    !
    @end

    View Slide

  186. - (void)tm_presentImageFromURL:(NSURL *)imageURL
    {
    TMNetImagePresenter *presenter = [TMNetImagePresenter
    netImagePresenterWithURL:imageURL];
    self.imagePresenter = presenter;
    [presenter presentImage];
    }

    View Slide

  187. - (void)presentImage
    {
    [self cancelOperation];
    !
    CGSize size = self.imageView.frame.size;
    !
    UIImage *image = [TUNetImageProvider imageFromURL:self.imageURL size:size];
    if (image)
    {
    // Image in memcache.
    [self updateViewWithImage:image];
    }
    else
    {
    [self updateViewWithImage:self.defaultImage];
    self.operation = [TUNetImageProvider obtainImageFromURL:self.imageURL
    size:size
    delegate:self];
    }
    }

    View Slide

  188. LRImageManager
    https://github.com/luisrecuenco/LRImageManager

    View Slide

  189. QUESTIONS?

    View Slide

  190. We are hiring!
    [email protected]

    View Slide