building a testable mixed-codebase ios framework · pdf filefrom localytics to google...

49
Building a testable mixed-codebase iOS Framework Nikos Maounis iOS Developer @Psy2k Christos Karaiskos iOS Developer @karaiskc

Upload: buidan

Post on 16-Feb-2018

223 views

Category:

Documents


6 download

TRANSCRIPT

Page 1: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

§

Building a testable mixed-codebase iOS Framework

Nikos MaounisiOS Developer @Psy2k

Christos KaraiskosiOS Developer @karaiskc

Page 2: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Outline

• Introduction and Motivation

• Distributing Libraries and Resources

• Design Considerations

• Testability

• Swift and ObjC Coexistence

Page 3: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Introduction

The App Store evolution

Page 4: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Introduction

The App Store evolution

Page 5: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Introduction

New Horizons

• Extend beyond the traditional app experience

• Quick interactions with your app throughout the OS and throughout different devices

Page 6: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

IntroductionShared Code

LocationLogging

Networking

ReachabilityAnalytics Resources

LocationLogging

Networking

ReachabilityAnalytics Resources

Page 7: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Motivation

• Be at the edge of innovation, follow changes in the app and device landscape (widget, watch, notifications, Siri, Maps)

• Lay the foundations for the future, with focus on code reuse, testability and self-documentation for new team members

• Prepare for complete and safe transition to Swift, with unit-test assurance

Page 8: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Distributing Libraries and Resources

Page 9: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Static Libraries

• Names of the form libname.a • Archives of object files • Statically linked objects become part of executable: Increase in size • Separate distribution of headers and resources • Changes require re-linking • Swift does not support them

Page 10: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Dynamic Libraries

• Names of the form dynamicLibrary.dylib • Single copy of objects is shared among different applications • Symbol binding performed at runtime. Not being able to find and load a required dylib

will crash your app. • Not part of main executable: Static linker only makes note of the install names of

dylibs that are referenced by symbols in your app • Versioned: dylibs may be updated independently of their consumers (i.e., minor

version updates)

Page 11: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Frameworks

• Names of the form frameworkName.framework • Act as dylib wrappers, inherit all their characteristics • Bundle format, relocatable • Include dynamic library, headers, resources, versioning info • Resources include: NIB files, images, localised strings, etc.

Page 12: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

User Frameworks in iOS

• Introduced in iOS 8, support Swift • The official way to package reusable code with resources, without requiring hacks (e.g. static frameworks)

• Due to sandbox restrictions, each application must embed its own copy (along with any further dylib/framework dependencies)

• Frameworks cannot be updated alone, even bumping the minor version requires resubmitting the whole application

• Conversely: They are easier to handle, with the guarantee that the correct version of the framework will run, new versions won’t break anything in previous versions

Page 13: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Design Considerations

Page 14: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Swift vs ObjC in Taxibeat

23.5% 76.5%

26.2% 73.8%

Github Linguist

Passenger

Driver

Page 15: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Our Approach

• Use both ObjC and Swift. Migrate to pure Swift when framework is stable and tested. New classes should be written in Swift.

• Focus on model part of MVC. Determine parts heavily shared among apps and extensions (e.g. location management, networking, logging)

• Treat Framework as a clean room: warnings as errors, unit tests, code reviews, design patterns, double-think before adding functionality

• Code organization. Different project, different repo, same workspace

• Don’t risk client stability. If migration of a class from client to framework is not straightforward, keep both so that nothing breaks and replace only when tests pass.

Page 16: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Project Organization

Page 17: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Extensions

• Check “Allow app extension API only”

• Compiler-enforced restriction on what you can include in framework

• Ensures that what you build can be used in extensions.

ld: warning: linking against a dylib which is not safe for use in application extensions

Page 18: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Architectural Differences

• A framework target can only be built for a single platform, and the respective supported architectures

Solution for deploying to watchOS or tvOS: • Separate framework target for each platform • Use Swift directives (e.g., #if os(watchOS)), different

platforms support different system API subsets

Page 19: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Testability in Mind

Page 20: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

The Importance of Tests

• Confidence to refactor

• Test assisted design promotes good API design. Find flaws early and iterate.

• Modularity and loose coupling by definition

• Simulate edge scenarios using fake objects

• Dynamic documentation

Page 21: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Xcode Server Coverage Stats

We created an Xcode Server to “force” ourselves to write tests

Page 22: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 1: Unit Testing the Location Manager

Page 23: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 1: Unit Testing the Location Manager

• Need to simulate location and authorization scenarios • Override the need for depending on actual hardware

measurements (GPS, A/GPS) • Override the need for user input (Permissions etc)

Page 24: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 1: Unit Testing the Location Manager

• Continuous user tracking not always required throughout app lifecycle, battery drain

• iOS >= 9 provides requestLocation() using delegation

• Closure/block-based approach: ease of use, compact, code not scattered around

• Execute callback when sufficiently accurate location is acquired

@objc(TXBLocationManager) public class LocationManager: NSObject {

public func requestLocationOnce(completion: @escaping ((CLLocation) -> Void)) { // LocationManager saves closure, internally handles inaccurate locations, lack of authorization etc.

// when sufficiently accurate location is retrieved, closure is called } }

Page 25: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 1: Unit Testing the Location Manager

extension LocationManagerTests { public class MockRequestOnceSuccessLocationManager: CLLocationManager { override class func locationServicesEnabled() -> Bool { return true } override class func authorizationStatus() -> CLAuthorizationStatus { return .authorizedWhenInUse } override public func startUpdatingLocation() { delegate?.locationManager?(self, didUpdateLocations: [CLLocation(latitude: 10.0, longitude: 10.0)]) } } }

In order to unit test our LocationManager we need to “control” the CLLocationManager which is used internally

Step 1: Subclass CLLocationManager (or define a protocol with common functions)

Page 26: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 1: Unit Testing the Location Manager

locMgr = LocationManager(locationManagerType: MockRequestOnceSuccessLocationManager.self) // TEST

Step 2: Inject dependency to our LocationManager

init(locationManagerType:CLLocationManager.Type = CLLocationManager.self) { … }

Init for LocationManager

self.locationManagerType.authorizationStatus()

Step 3: Remove hardcoded references of CLLocationManager within LocationManager

CLLocationManager.authorizationStatus()

instead of

Create new instance:

Page 27: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 1: Unit Testing the Location Manager

Step 4: Write an asynchronous test

class LocationManagerTests: XCTestCase { var locMgr: LocationManager! var exp: XCTestExpectation!

func testRequestOnceAllowedWhenInUse() {

exp = self.expectation(description: "Request once callback") locMgr = LocationManager(locationManagerType: MockRequestOnceSuccessLocationManager.self) locMgr.requestLocationOnce(completion: { (location) in XCTAssertEqual(location.coordinate.latitude, 10.0) XCTAssertEqual(location.coordinate.longitude, 10.0) self.exp.fulfill() }) self.waitForExpectations(timeout: 2.0, handler: nil)

} }

Test case for successful scenario

Page 28: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 2: From Localytics to Google Analytics

• Be careful not to become too coupled with 3rd-party applications

• Unit testing revealed that as consumers we just want to log events

• Consumers don’t care if it’s Google Analytics or Localytics under the hood

- (IBAction)favoriteButtonPressed:(id)sender { [Localytics tagEvent:@"favoriteButtonPressed" attributes:@{……}]; // handle action }

- (IBAction)favoriteButtonPressed:(id)sender { id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker]; [tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"someCategory" action:@"favoriteButtonPressed" label:@"someLabel" value:nil] build]]; }

Tight coupling with

3rd party libraries

Page 29: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Example 2: From Localytics to Google Analytics

• Create an abstraction layer, hide actual implementation details

• Migration transparent to consumer (besides the API key initializer)

• Changes required within single class, not scattered throughout project

+ (void)sendHitToProviderWithName:(NSString *)eventName andParameters:(NSDictionary *)params { [Localytics tagEvent:eventName attributes:params]; }

+ (void)sendHitToProviderWithName:(NSString *)eventName andParameters:(NSDictionary *)params { id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker]; [tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"ui_action" action:eventName label:@"app action" value:nil] build]]; }

- (IBAction)favoriteButtonPressed:(id)sender { [BKAnalyticsHelper sendHitToProviderWithName:@"favoriteButtonPressed" andParameters:@{}]; // handle action }

Page 30: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Swift and ObjC Coexistence

Page 31: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Exposing ObjC Symbols

• Umbrella header, named MyFramework.h • Exposes Objective-C/C classes as public • Similar to Bridging header of mixed-codebase app target

• Should include all the ObjC headers you want disclosed in your public API

• Be sure to mark the header file as Public in the inspector

Page 32: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Exposing Swift Symbols

• Use access control to restrict visibility to parts of your code from code in other source files and modules

• Explicitly mark classes and methods as public if you want them exposed (default is internal)

Page 33: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Namespaces

• Swift supports namespaces • ObjC does not. Convention is 3-character prefixing Solution for exposing Swift symbols:

Seen in ObjC as:

@objc(TXBLogger) public class Logger: NSObject {

public func logDebug(_ params:Any...) { // .... } }

SWIFT_CLASS_NAMED("Logger") @interface TXBLogger : NSObject + (void)logDebug; - (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER; @end

Page 34: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Pure Swift Features

• As long as there is ObjC we cannot fully take advantage of Swift’s powerful features

• We could not use optional primitives, associated value enums, structs, tuples, nested classes, default function params etc.

Solution: • Use pure Swift features only for internal Swift classes that do not

interact with ObjC. • If at some point they need to, write wrapper methods (e.g. to expose

a String enum to ObjC)

Page 35: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Delegation in Swift (1/2)

• Handling weak references in delegates

Solution(??): declare delegate var weak, as in ObjC

strong reference to A

Page 36: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Delegation in Swift (2/2)

Solution: Declare the protocol to inherit from class and property weak

delegate released when it goes out of method scope

Page 37: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Being Swifty

• Use guard to exit functions early • Use type inference • Use the trailing closure syntax • Follow case conventions. Names of types and protocols are

UpperCamelCase. Everything else is lowerCamelCase. • Default to structs unless you really need a class • Favor immutable variables

https://swift.org/documentation/api-design-guidelines/

Page 38: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Being Swifty Example 1

•Map reduce filter vs C-style loop NSArray<NSNumber *> *sampleArray = @[@1, @2, @3, @4, @5]; NSMutableArray *finalArray = [[NSMutableArray alloc] init]; for (NSInteger i = 0; i <= sampleArray.count; i++) { NSNumber *iDoubled = [NSNumber numberWithInteger: sampleArray[i].integerValue * 2]; [finalArray addObject:iDoubled]; }

let sampleArray = [1, 2, 3, 4, 5]

let finalArray = sampleArray.map { return $0*2 }

Page 39: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Being Swifty Example 2

Naming conventions

typedef NS_ENUM(NSInteger,TXBStatusViewState) { kStatusViewStateNone, kStatusViewStateWaiting, kStatusViewStateNoAvailableDrivers, kStatusViewStateUnsupportedArea, kStatusViewStateMain, kStatusViewStateAddress, kStatusViewStateBlocked, kStatusViewStateNoInternet };

let statusViewMode: TXBStatusViewState = .statusViewStateBlocked //NOT SWIFTY

typedef NS_ENUM(NSInteger,TXBStatusViewState) { TXBStatusViewStateNone, TXBStatusViewStateWaiting, TXBStatusViewStateNoAvailableDrivers, TXBStatusViewStateUnsupportedArea, TXBStatusViewStateMain, TXBStatusViewStateAddress, TXBStatusViewStateBlocked, TXBStatusViewStateNoInternet };

let statusViewMode: TXBStatusViewState = .blocked //SWIFTY

😭

😍

Page 40: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Being Swifty Example 3

Type Safety[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_badRequestNotification:) name:@"kRequestBadNotification" object:nil];

NotificationCenter.default.addObserver(self, selector: #selector(self.badRequestNotification), name: Notification.Name.requestBad, object: nil)

Page 41: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Coming Soon…

Taxibeat Widget

Page 42: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

iOS.Conf app

The .Conf app is built using embedded frameworks for the Watch App

and…

we made it open source on Github https://github.com/taxibeat/ios-conference

Page 43: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

iOS.Conf app

we made it open source on Github https://github.com/taxibeat/ios-conference

Questions? enum SupportedLanguages { case # case $ }

Page 44: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Questions

Page 45: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

CocoaPods

•Single Podfile for both projects, same workspace

•pod update acts on both projects

source 'https://github.com/CocoaPods/Specs.git' workspace 'MyWorkspace' platform :ios, '9.0' use_frameworks!

target 'MyProject' do project 'MyProject.xcodeproj' pod 'FBSDKCoreKit' pod 'FBSDKLoginKit' pod 'FBSDKShareKit' pod 'FBSDKMessengerShareKit' pod 'Fabric' pod 'Crashlytics' end

target 'MyFramework' do project ‘../iOS-Framework/MyFramework/MyFramework.xcodeproj' pod 'CocoaLumberjack' end

Page 46: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Warnings as errors

•Static Analyzer (ObjC): Localized Strings missing comments

•Treat warning as errors

Page 47: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

API Design Example 1: Request Location Once

[[TXBLocationManager sharedInstance] requestLocationOnceWithCompletion:^(CLLocation * _Nonnull location) {

// center map to user location

}];

Page 48: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Mock URLSession

class MockSession: URLSession { var completionHandler:((Data?, URLResponse?, Error?) -> Void)? static var mockResponse: (data: Data?, urlResponse: URLResponse?, error: NSError?) override class var shared: URLSession { return MockSession() } override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { self.completionHandler = completionHandler return MockTask(response: MockSession.mockResponse, completionHandler: completionHandler) } class MockTask: URLSessionDataTask { typealias Response = (data: Data?, urlResponse: URLResponse?, error: NSError?) var mockResponse: Response let completionHandler: ((Data?, URLResponse?, Error?) -> Void)? init(response: Response, completionHandler:((Data?, URLResponse?, Error?) -> Void)?) { self.mockResponse = response self.completionHandler = completionHandler } override func resume() { completionHandler!(mockResponse.data, mockResponse.urlResponse, mockResponse.error) } } }

Page 49: Building a testable mixed-codebase iOS Framework · PDF fileFrom Localytics to Google Analytics • Be careful not to become too coupled with 3rd-party applications

Organization

•Separate project and git repo

•Same workspace (already setup due to CocoaPods)

•Alternatives: Subproject, different target [1]

[1] https://developer.apple.com/library/content/technotes/tn2435