How might we create a
simple and intuitive mobile
consumption experience
for Medium?
How might we create a
simple and intuitive
mobile consumption
experience for Medium?
How might we create a
delightful, simple and intuitive
mobile consumption
experience for Medium?
History stack Future stack
Cover Flow
<html>?
• 60 FPS
• Memory
• Parallax
UIKit &
TextKit
{	
"content": {	
...	
"bodyModel": {	
"elements": <Elements>,	
"sections": <Sections>	
},	
...	
},	
}
{	
"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": "38d2",	
"startIndex": 6	
}
{	
"name": "7b28",	
"startIndex": 31,	
"backgroundImage": {	
"id": "1*DhgXe8j0YqltMNM6-pRy6Q.jpeg",	
"originalWidth": 1920,	
"originalHeight": 1200	
},	
"textLayout": 2,	
"imageLayout": 2	
}
CoverFlowView
CoverFlowView
PostView
CoverFlowView
PostView
SectionView
CoverFlowView
PostView
SectionView
SectionImageView
CoverFlowView
PostView
SectionView
SectionImageView
SectionContentView
CoverFlowView
PostView
SectionView
SectionContentView
ElementElementElement
SectionImageView
CoverFlowView
PostView
SectionView
SectionContentView
ElementElementElement
SectionImageView
CoverFlowView
PostView
SectionView
SectionContentView
ElementElementElement
SectionImageView
Element
CoverFlowView
PostView
SectionView
SectionContentView
ElementElementElement
SectionImageView
Element
CoverFlowView
PostView
SectionView
SectionContentView
ElementElement
SectionImageView
Element
CoverFlowView
PostView
SectionView
SectionContentView
ElementElement
SectionImageView
Element
Sect
Section
SectionC
Elemen
CoverFlowView
PostView
View
ntentView
ent
ageView
Element
SectionView
SectionImageView
SectionContentView
ElementElement
CoverFlowView
PostView
SectionView
SectionImageView
SectionContentView
ElementElement
CoverFlowView
PostView
SectionView
SectionImageView
SectionContentView
ElementElement
CoverFlowView
PostView
SectionView
SectionImageView
SectionContentView
ElementElement
Sect
Section
CoverFlowView
PostView
View
ageView
ntentView
lement
SectionView
SectionImageView
SectionC
Elemen
CoverFlowView
PostView
SectionView
SectionImageView
SectionC
Elemen
CoverFlowView
PostView
SectionView
SectionImageView
SectionContentView
Element
CoverFlowView
PostView
SectionView
SectionImageView
SectionContentView
ElementElement
CoverFlowView
PostView
SectionView
SectionImageView
SectionContentView
ElementElement
Thank You!
Questions?
!
Grant Oladipo
grant@medium.com
@kocodude
Shapes & things
Building the data layer of the Medium iOS app
!
!
Elizabeth Ford
@infraredflower
Title
Body text
Other stuff
User
!
Collection
User’s name
Bio
List of stories
• 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?
Speaking of which…
• How should we deal with
merging two objects?
• Merging a full post and
a partial post
Example
Shapes!
Shapes!
Saving objects
Merging objects
Complexity hidden by data layer
• merges objects
• determines when to make
network requests
Complexity hidden by data layer
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!
Thank You!
Questions?
!
Elizabeth Ford
eford@medium.com
@infraredflower
Meet the Singletons
Jimmy O’Neill
Engineer, Medium
@jmyonl
+ (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.
UIApplication
NSFileManager
NSNotificationCenter
NSURLSession
Since singletons are so commonplace in CocoaTouch, many developers overuse them.
A lot of this:
Singletons are bad!!
!
Not a lot of this:
Singletons can be bad because ____. Here’s a real example!
!
!
!
I’m going to focus on 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
PostViewController
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService
PostViewController
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService
Data dependence
Time 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
Example.
PostViewController
PostViewController
LoginViewController
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService Logged in
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];
});
How do we fix this?
Option 1: YOLO
Advantages!
- Very little work
- No testing needed
- Funny later in your career
Disadvantages!
- Horrible UX
- Async bookmark operation still fails
- (void)logOut {
// nuke everything
exit(0);
}
Option 2: Embrace time dependence
Advantages!
- Doesn’t require refactoring
Disadvantages!
- 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
AuthCredentialDataService
Net
Session
Net
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
PostViewController
PostViewController
LoginViewController
1. User bookmark action is queued
PostViewService
PostService
Net
PostViewController
Logged in
Session
dispatch_async(_backgroundQueue, ^{
[[Services postView] bookmark:_postId session:_session];
});
1. User bookmark action is queued
2. User signs out
PostViewService
PostService
Net
PostViewController
Logged in
Session
LoginViewController
Logged 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
LoginViewController
Logged 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
LoginViewController
Logged 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
LoginViewController
Logged 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
LoginViewController
Logged 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
LoginViewController
Logged out
Session
dispatch_async(_backgroundQueue, ^{
[[Services postView] bookmark:_postId session:_session];
});
Think back to the original decision.
Net
AuthCredentialDataService
Keychain
Net
It appears innocent without context.
Net
AuthCredentialDataService
Keychain
Net
Let’s do our best to recognize!
the larger context earlier.
PostViewController
PostViewService
PostService
Net
AuthCredentialDataService
Data dependence
Time dependence
AuthCredentialDataService
PostViewController
Singletons are global state.
Singletons assume that the lifetime of the state they encapsulate is equal
to the lifetime of the program.
Singletons are not evil.
But often, dependency injection is a sounder option.
Thank You!
Questions?
!
Jimmy O’Neill
jimmy@medium.com
@jmyonl
Designing an iOS
Distribution Pipeline
Vinicius Baggio Fuentes
Delivery on the Web
• Feature Toggle
• Tracking and measuring
• Rolling releases
• Multiple deploys a day
Short deployment time
is key
How can we do the
same for apps?
Challenges
• Pre-App Store distribution is complicated
• Slow response time on App Store approvals
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
Feature toggle
• A/B Testing
• Long running feature development
• Emergencies
VARIANT_ENABLE_RECOMMEND_NOTES_COMPOSITION
Feature toggle tools
• GroundControl, SkyLab & related
projects
• Roll-your-own (not recommended)
Versioning system:

semantic versioning
master 640a0a5 638b3b7
master 640a0a5
build-1.3
638b3b7
dde3123
build-1.3.0
master 640a0a5
build-1.3
638b3b7 03dc0b5
dde3123
build-1.3.0
master 640a0a5
build-1.3
638b3b7 03dc0b5 4aee8cd
dde3123 bb24c74
build-1.3.0
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
Distribution channels
Developers Medium Staff
On merge
Version 1.4.90
Distribution channels
Version 1.4.90
Developers Medium Staff Trusted testers
On merge After a day
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
vinny@medium.com
@vinibaggio
Thanks for coming!

Medium TechTalk — iOS