medium techtalk — ios
TRANSCRIPT
{ "name": "8419", "type": 1, "text": "...song Walking in LA?", "markups": [ { "type": 3, "start": 300, "end": 315, "href": "https://somewhere.com", "title": "", "rel": "" } ]}
{ "name": "24cc", "type": 4, "text": "", "markups": [], "layout": 1, "metadata": { "id": "1*WwSQ20QNvf-WovuMLLslEA.jpeg", "originalWidth": 453, "originalHeight": 301 }}
{ "name": "7b28", "startIndex": 31, "backgroundImage": { "id": "1*DhgXe8j0YqltMNM6-pRy6Q.jpeg", "originalWidth": 1920, "originalHeight": 1200 }, "textLayout": 2, "imageLayout": 2}
Cover Flow
View
Post View
Section View
Section Content View
Element
Element
Element
Section Image View
Cover Flow
View
Post View
Section View
Section Content View
Element
Element
Element
Section Image View
Cover Flow
View
Post View
Section View
Section Content View
Element
Element
Element
Section Image View
Element
Cover Flow
View
Post View
Section View
Section Content View
Element
Element
Element
Section Image View Elem
ent
Cover Flow
View
Post View
Section View
Section Content View
Element
Element
Section Image View Elem
ent
Cover Flow
View
Post View
Section View
Section Content View
Element
Element
Section Image View Elem
ent
Section View
Section Image View
Section Content View
Element
Cover Flow
View
Post View
Section View
Section Content View
Element
Section Image View Elem
ent
Section View
Section Image View
Section Content View
Element
Element
Cover Flow
View
Post View
Section View
Section Image View
Section Content View
Element
Element
Section View
Section Image View
Cover Flow
View
Post View
Section View
Section Image View
Section Content View
Element
Section View
Section Image View
Section Content View
Element
• How will we deal with the possibility of API changes?
• especially since the app will be long-lived • How do we minimize the number of requests
from the iOS app to the server? • How can we avoid making separate API routes
for the iOS app (and every client thereafter)?
Questions…
Clearly bad ideas
• Make multiple requests to display one Post, User, or Collection
• Make new iOS routes with exactly what we need
• Only save full objects
Clearly bad ideas
• Save partial objects and if we need a full object, just check to see if all the fields are set in the object in the cache
• Is the field supposed to be “” or was it never set?
What about dealing with a changing API?
• explicit definitions for the resources returned by API endpoints
• tests based on these definitions
Possible areas for improvement
• one service to return any shape (instead of a separate data service for each type of object)
• autogenerate shape definitions from protocol buffers (on both server and client)
• easy to add features on the iOS side • easy to make changes on the server
side without breaking the iOS client • minimize # of server requests
Shapes!
+ (id)sharedResource { static id _instance; static dispatch_once_t onceToken; ! dispatch_once(&onceToken, ^{ _instance = [[Resource alloc] init]; }); ! return _instance; }
A singleton restricts the instantiation of a class to one object.
They are often lazily instantiated.
They can be accessed globally by any module of a program.
A lot of this: Singletons are bad!!
!
Not a lot of this: Singletons can be bad because ____. Here’s a real example!
- (AuthCredential *)mediumAuthCredential; !- (void)saveMediumAuthCredential:(AuthCredential *)credential;
AuthCredentialDataService
Keychain
AuthCredentialDataService
- (void)fetch:(NetRequest *)request completion:(void (^)(NetResponse *, NSError *))completion;
Net
AuthCredentialDataService
NetNet
- (void)fetchPostById:(NSString *)postId completion:(void(^)(Post *post, NSError *error))completion;
PostService
Net
AuthCredentialDataService
PostService
- (void)fetchPostViewDataWithPostId:(NSString *)postId completion:(void(^)(Post *post, PostViewModel *postViewModel, PostUserData *postUserData, NSError *error))callback;
PostViewService
PostService
Net
AuthCredentialDataService
PostViewService
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService
Data dependenceTime dependence
AuthCredentialDataService
PostViewController
Time dependence isn’t an issue until the underlying
data starts changing.
PostViewController
PostViewService
PostService
Net
AuthCredentialDataServiceAuthCredentialDataService
PostViewController
If your app supports sign out, the underlying authentication
data is changing.
PostViewController
PostViewService
PostService
Net
AuthCredentialDataServiceAuthCredentialDataService
PostViewController
What problems does time dependence cause?
- Functional impurity - Leaky abstraction - Background task failures - Difficult to test
PostViewController
PostViewService
PostService
Net
AuthCredentialDataServiceAuthCredentialDataService
PostViewController
1. User bookmark action is queued
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged in
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
1. User bookmark action is queued 2. User signs out
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged out
LoginViewController
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged out
LoginViewController
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged out
LoginViewController
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged out
LoginViewController
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged out
LoginViewController
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued 4. Bookmark request 401s
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged out
LoginViewController
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued 4. Bookmark request 401s 5. PostViewController is cleaned up
PostViewService
PostService
Net
AuthCredentialDataService Logged out
LoginViewController
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId]; });
Option 1: YOLO
Advantages!- Very little work - No testing needed - Funny later in your careerDisadvantages!- Horrible UX - Async bookmark operation still fails
- (void)logOut { // nuke everything exit(0); }
Option 2: Embrace time dependence
Advantages!- Doesn’t require refactoringDisadvantages!- Requires global knowledge of all the apps components - Affects other, disjunct service consumers - Difficult to test - Async bookmark operation still fails
- (void)logOut { // Synchronously wipe all the user-dependent data [[Services authCredentialDataService] wipeData]; // … ! // Reload UI with new global data [_appController reload]; }
Option 3: Dependency inject your user-dependent data
- Eliminates time dependence in the service layer - Gracefully supports switching authentication contexts - Easy to test interfaces with a mock Session object - Async bookmark operation succeeds
- (void)logOut { // Create an unauthenticated session Session *newSession = [Session loggedOutSession]; ! // Reload UI with logged out session [_appController loadSession:newSession]; }
@interface Session : NSObject !- (instancetype)initWithAuth:(AuthCredential *)authCredential; !@property (nonatomic, strong, readonly) NSString *sessionId; @property (nonatomic, strong, readonly) AuthCredential *authCredential; !- (BOOL)isAuthenticated; !@end
The Session object directly models user-dependent state, and is injected into objects that need it.
- (void)fetch:(NetRequest *)request completion:(void (^)(NetResponse *, NSError *))completion;
Net
AuthCredentialDataService
Net
Net
Session
- (void)fetch:(NetRequest *)request session:(Session *)session completion:(void (^)(NetResponse *, NSError *))completion;
Net
PostService
Net
Session
- (void)fetchPostById:(NSString *)postId session:(Session *)session completion:(void(^)(Post *post, NSError *error))completion;
PostService
PostViewService
PostService
Session
Net
- (void)fetchPostViewDataWithPostId:(NSString *)postId session:(Session *)session completion:(void(^)(Post *post, PostViewModel *postViewModel, PostUserData *postUserData, NSError *error))callback;
PostViewService
PostViewService
PostService
Net
PostViewController
Session
[[PostViewController alloc] initWithPostId:postId session:session];
PostViewController
Session
1. User bookmark action is queued
PostViewService
PostService
Net
PostViewControllerLogged in Session
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId session:_session]; });
1. User bookmark action is queued 2. User signs out
PostViewService
PostService
Net
PostViewControllerLogged in Session
LoginViewControllerLogged out
Session
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId session:_session]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued
PostViewService
PostService
Net
PostViewControllerLogged in Session
LoginViewControllerLogged out
Session
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId session:_session]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued
PostViewService
PostService
Net
PostViewController
Logged in Session
LoginViewControllerLogged out
Session
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId session:_session]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued
PostViewService
PostService
Net
PostViewController
Logged in Session
LoginViewControllerLogged out
Session
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId session:_session]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued 4. Bookmark action succeeds
PostViewService
PostService
Net
PostViewController
Logged in Session
LoginViewControllerLogged out
Session
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId session:_session]; });
1. User bookmark action is queued 2. User signs out 3. Bookmark action is dequeued 4. Bookmark action succeeds 5. PostViewController and logged in Session are cleaned up
PostViewService
PostService
Net
LoginViewControllerLogged out
Session
dispatch_async(_backgroundQueue, ^{ [[Services postView] bookmark:_postId session:_session]; });
Let’s do our best to recognize!the larger context earlier.
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService
Data dependenceTime dependence
AuthCredentialDataService
PostViewController
Singletons assume that the lifetime of the state they encapsulate is equal to the lifetime of the program.
Delivery on the Web
• Feature Toggle
• Tracking and measuring
• Rolling releases
• Multiple deploys a day
Fast release requirements• Over-the-air updates
• Crash report system
• Authorization server
• Feature toggles
• Versioning system
• Distribution channels
OTA updates, crash reports and authorization
• Crashlytics
• TestFlight / Apple’s upcoming beta program
• HockeyApp
master 640a0a5
build-1.3
638b3b7 03dc0b5 4aee8cd 04d773b
dde3123 bb24c74 f1e48af
build-1.
3.1
build-1.
3.0
master 640a0a5
build-1.3
build-1.4
638b3b7 03dc0b5 4aee8cd 04d773b
dde3123 bb24c74 f1e48af
build-1.
3.1
f1e48af
build-1.
3.0
build-1.
4.0
Distribution channels
Version 1.4.90
Developers Medium Staff Trusted testers Beta testers
On merge After a day After a week
Lessons Learned
• Xcconfig files and -derivedDataPath compiler flag helps builds being deterministic
• Plist manipulation in Python, nomad-cli tools (esp. Shenzhen, Cupertino)
Xcconfig example
#include "base.xcconfig" #include "Servers/medium.xcconfig" #include "Build/config-debug.xcconfig" !
MEDIUM_DISTRIBUTION_CHANNEL = mediumStaff
Lessons Learned [2]
• Make it easy for everyone to see which version of the app each group has
• Easy visibility and communication on features of each of these groups
• Automate everything, consider Jenkins CI over Xcode Bots
Thanks!• References:
• www.nomad-cli.com
• jenkins-ci.org
• crashlytics.com
• https://github.com/mattt/GroundControl
• http://semver.org
• hockeyapp.net
• http://techcrunch.com/2014/06/02/ios-testflight/
Vinícius Baggio Fuentes [email protected] @vinibaggio