Download - Making Swift even safer
Making Swift even safer
18 months of development
120k+ lines of Swift
≈40 releases
6 months in production
Juno Rider iOS
Stricter interface
extension Array { public subscript (safe index: Int) -> Element? { … }}
extension Int { public init?(safe value: Float) { … } public init?(safe value: Double) { … } public init?(safe value: UInt) { … }}
!
NonEmptyString
NonEmptyArray
NonNegativeDouble
Stronger types
struct LocationCoordinate<A> { let value: Double init(_ value: Double) { self.value = value }}
enum Lat {}enum Lon {}
struct Location { let latitude: LocationCoordinate<Lat> let longitude: LocationCoordinate<Lon>}
Phantom types
struct TaggedValue<ValueType, Tag> { let value: ValueType init(_ value: ValueType) { self.value = value }}
enum PickupTimeTag {}typealias PickupTime = TaggedValue<NSDate, PickupTimeTag>
enum DropoffTimeTag {}typealias DropoffTime = TaggedValue<NSDate, DropoffTimeTag>
Phantom types
enum PhoneTag {}typealias Phone = TaggedValue<String, PhoneTag>
extension TaggedValueType where Tag == PhoneTag, ValueType == String {
func trimCountryCode() -> Phone { return Phone(trimCountryCode(self.taggedValue.value)) }}
private func trimCountryCode(phone: String) -> String { … }
Phantom types
extension TaggedValueType where ValueType: JSONEncoding {
func encodeJSON() -> AnyObject { return self.taggedValue.value.encodeJSON() }}
Phantom types
enum StringKey { case AccessibilityHomeConfirmButton case DialogInsufficientFundsTitle
...}
func localized(value: Strings) -> String {switch value {case .AccessibilityHomeConfirmButton:
return String.localized(“Accessibility.Home.Confirm.Button”)
...
}}
Static resources
extension HTTP {
enum Error: ErrorType {
case InvalidResponse(request: NSURLRequest, response: NSURLResponse?) case TransportError(request: NSURLRequest, error: NSError) case HTTPError(request: NSURLRequest, response: NSHTTPURLResponse, responseData: NSData?)
case CannotCreateURL(components: NSURLComponents) case InvalidURL(urlString: String) case AuthServiceFailure
case CannotBindStreamPair(request: NSURLRequest) case StreamWriting(request: NSURLRequest, error: NSError?) case StreamGzipEncoding(request: NSURLRequest, operation: HTTP.Error.GzipOperation) }}
Strong ErrorType
extension JSON.Error {
struct Encode: ErrorType { public let error: NSError public let source: Any }
enum Decode: ErrorType { case Unexpected case Serialization(error: NSError, data: NSData) case SchemeMismatch(error: JSON.Error.SchemeMismatch, body: AnyObject?) }
struct SchemeMismatch: ErrorType { public let pathComponents: [String] public let reason: String }}
Strong ErrorType
public enum JSONTaskError: ErrorType { case Task(error: HTTP.Error) case Request(error: JSON.Error.Encode) case Response(response: HTTP.Response, error: JSON.Error.Decode)}
Strong ErrorType
Changing code
Components
Context
App
ContextMay contain dirty things dealing with global state
ContextMay contain dirty things dealing with global state
Unit tests
Contexttypealias AppContext = protocol< StringsServiceContainer, StaticImageServicesContainer, BundleImagesServiceContainer, ReachabilityServiceContainer, AnalyticsServiceContainer, SchedulerContainer, RemoteNotificationsContainer, RemoteNotificationsPermissionContainer, RemoteNotificationClearActionContainer, LocationServiceContainer, ApplicationServiceContainer, DeviceServiceContainer,
…>
class ElDependor: AppContext { … }
class MockContext: AppContext { … }
App
Pure state machine
App
Pure state machine
- Unit tests- Integrated acceptance tests
Acceptance tests via TestAppclass Allow_Rider_to_Have_Max_X_Cards_per_Account_Spec: QuickSpec { override func spec() { var app: TestApp! beforeEach { app = TestApp() } given("rider is entering CC details on Add CC screen") { beforeEach { app.login() app.goToHomeScreen() app.receiveSomePaymentMethods() app.openPayments() app.payments.paymentMethods.tapAddPayment() app.payments.addPayment.enterSomeCC() app.payments.addPayment.tapNext() app.payments.addPayment.enterSomeZipCode() } when("rider taps Add CC button") { beforeEach { app.payments.addPayment.tapDone() } and("BE returns the message about max number of active cards") { beforeEach { app._context.addCreditCard.receive(.Failed(.TooManyPaymentMethods)) }
then("present the Max Cards Added alert") { app.payments.addPayment.expectToPresentAlert() } when("rider taps Ok button") { beforeEach { app.payments.addPayment.alert.tapOK() } then("dismiss the alert") { app.payments.addPayment.expectToNotPresentAlert() }
View: screenshot testing
View: screenshot testing
View: screenshot testing
Detecting & investigating bugs in the field
- smart assertions- diligent logging- daily duty
junoAssert
in Debug - crash 🙀in Release - log.error(), trackNonFatal() and recover 🙏
Logging
switch error { case let .InvalidResponse(value): log.warn( category: logCategory, message: "Unexpected response", payload: [ .RequestIdentifier: error.requestIdentifier, .ResponseIdentifier: error.responseIdentifier, .Description: "\(value.response)", .Path: value.request.path, .URL: value.request.absoluteURL ] )
…
Logging
Analytics junoAssert ↓
↓log.verbose log.info log.warn log.error ↓ ↓ ↓ ↓
{'key0':'value0','key1':‘value1'} ↓
append to txt file ↓ roll & upload to AWS ↓ Elasticsearch + Kibana
Logging
Production quality comes at a price
- takes up to x2 dev effort- challenging for new team members- performance considerations
But it brings satisfaction
Thank you