Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Building Swift Frameworks

Hermes Pique
November 06, 2014

Building Swift Frameworks

Hermes Pique

November 06, 2014
Tweet

More Decks by Hermes Pique

Other Decks in Technology

Transcript

  1. iOS developers can now create dynamic frameworks. Frameworks are a

    collection of code and resources to encapsulate functionality that is valuable across multiple projects. — New Features in Xcode 6
  2. SWIFT FRAMEWORKS ▸ CAN mix-and-match Objective-C code ▸ CAN import

    all Objective-C system frameworks (e.g., UIKit) ▸ CAN import C libraries defined as modules (e.g., Dispatch) ▸ CAN'T use other libraries directly (e.g., CommonCrypto) See: Swift and Objective-C in the Same Project
  3. LET'S TRY ANYWAY! Add a Swift file to a static

    library and build: error: /Applications/Xcode.app/Contents/Developer/Toolchains/ XcodeDefault.xctoolchain/usr/bin/libtool: unknown option character `X' in: -Xlinker Follow: http://stackoverflow.com/q/24020986/143378
  4. STEP 1: ADD THE FRAMEWORK AS A SUBPROJECT Drag the

    framework project into your app project.
  5. STEP 2: SET THE FRAMEWORK AS BUILD DEPENDENCY 1. Select

    your project and then your app target 2. Open the Build Phases panel 3. Expand Target Dependencies 4. Click + and select the framework
  6. STEP 3: COPY THE FRAMEWORK DURING BUILD From Build Phases:

    1. Click on the + button at the top left of the panel and select New Copy Files Phase 2. Set Destination to Frameworks 3. Click + and select the framework
  7. [I]f you are writing a single-target app, you may not

    need to specify explicit access control levels at all. — The Swift Programming Language
  8. ACCESS LEVELS ▸ Public: accessible everywhere ▸ Internal (default): accessible

    within any source file from the defining module, but not outside of that module ▸ Private: accessible only within its own defining source file
  9. WE KIND* OF HAD THAT ALREADY In Objective-C land: ▸

    Public: declared in public headers ▸ Internal: declared in project headers ▸ Private: declared in implementation files * (without enforcing access control)
  10. GUIDING PRINCIPLE NO ENTITY CAN BE DEFINED IN TERMS OF

    ANOTHER ENTITY THAT HAS A LOWER (MORE RESTRICTIVE) ACCESS LEVEL
  11. BAD private struct PrivateThing {} struct InternalThing {} public struct

    PublicThing { let t1 : PrivateThing public let t2 : InternalThing func doSomething(t : PrivateThing) {} public func doSomethingElse(t : InternalThing) {} }
  12. EXAMPLE Consider a framework with a public function that fetches

    a string asynchronously. The function returns a Fetch object that can have a callback for success. let fetch = fetchSomething().onSuccess { println($0) } Let's define the access levels of the Fetch class.
  13. class Fetch { var onSuccess : ((String) -> ())? var

    state : FetchState = FetchState.Pending func onSuccess(onSuccess : ((String) -> ())) -> Self { self.onSuccess = onSuccess switch self.state { case FetchState.Success(let value): onSuccess(value) default: break } return self } func succeed(value : String) { self.state = FetchState.Success(value) self.onSuccess?(value) } }
  14. public class Fetch { private var onSuccess : ((String) ->

    ())? private var state : FetchState = FetchState.Pending public func onSuccess(onSuccess : ((String) -> ())) -> Self { self.onSuccess = onSuccess switch self.state { case FetchState.Success(let value): onSuccess(value) default: break } return self } func succeed(value : String) { self.state = FetchState.Success(value) self.onSuccess?(value) } }
  15. Namespacing is implicit in swift, all classes (etc) are implicitly

    scoped by the module (Xcode target) they are in. no class prefixes needed — Chris Lattner
  16. FRAMEWORK NAMESPACE The framework name is the namespace. When importing

    a framework types can be accessed implicitly or explicitly.
  17. FUN WITH NAMESPACES Consider: A framework FrameworkA that defines a

    public type Thing. An app MyApp that imports FrameworkA.
  18. import FrameworkA var a : FrameworkA.Thing? var t : Thing?

    The two variables have the same type.
  19. import FrameworkA public class Thing { } var a :

    FrameworkA.Thing? var t : Thing? var t2 : MyApp.Thing? What is the type of t?
  20. NAMESPACE CONFLICT What if MyApp also imports a FrameworkB that

    also defines a public type Thing? import FrameworkA import FrameworkB var a : Thing? We get a nice error.
  21. 'Thing' is ambiguous for type lookup in this context let

    t1 : Thing? = nil ^~~~~ FrameworkA.Thing:1:7: note: found this candidate class Thing ^ FrameworkB.Thing:1:7: note: found this candidate class Thing ^
  22. Solution: use fully qualified names. import FrameworkA import FrameworkB public

    class Thing { } var a : FrameworkA.Thing? var t : Thing? var b : FrameworkB.Thing? t is of type MyApp.Thing.
  23. import FrameworkA import FrameworkB var a : FrameworkA.Thing? var b

    : FrameworkB.Thing? We get a misleading error: 'Thing' is not a member type of 'FrameworkA'
  24. Workaround: define a typealias somewhere else in MyApp. import FrameworkA

    typealias ThingA = Thing Then: import FrameworkA import FrameworkB var a : ThingA? var b : FrameworkB.Thing?
  25. BEST PRACTICES Limit the scope* of types whenever possible. Even

    more so for public types. Don't define a type named like the framework (Haneke oops!). *(THIS IS ALWAYS A GOOD PRACTICE)
  26. BAD public static let ErrorDomain = "io.haneke" GOOD public struct

    HanekeGlobals { public static let ErrorDomain = "io.haneke" } let s = HanekeGlobals.ErrorDomain
  27. BAD public enum ScaleMode { case Fill case AspectFit case

    AspectFill } public struct ImageResizer { public var scaleMode: ScaleMode }
  28. GOOD public struct ImageResizer { public enum ScaleMode { case

    Fill case AspectFit case AspectFill } public let scaleMode: ScaleMode }
  29. WHAT ABOUT GENERICS? Generic types can't define static types. This

    fails to compile: public class NetworkFetcher<T> { public enum ErrorCode : Int { case InvalidData = -400 case MissingData = -401 case InvalidStatusCode = -402 } }
  30. Workaround: abuse the globals struct from before. extend HanekeGlobals {

    public struct NetworkFetcher { public enum ErrorCode : Int { case InvalidData = -400 case MissingData = -401 case InvalidStatusCode = -402 } } } public class NetworkFetcher<T> {}
  31. PREFIXING EXTENSIONS Extensions of Objective-C types (NSObject subclasses) are categories

    and as such need prefixes. extension UIImage { public func hnk_hasAlpha() -> Bool { ... } } See: http://stackoverflow.com/q/25567743/143378
  32. IN AN APP PROJECT Easy. Simply import the library in

    the bridging header: #import <CommonCrypto/CommonCryptor.h> More info: Swift and Objective-C in the Same Project
  33. import works with modules. So let's define a module for

    the C library. Using CommonCrypto as an example...
  34. STEP 1: DEFINE THE MODULE 1. Create a CommonCrypto directory

    inside the framework directory 2. Within, create a module.map file module CommonCrypto [system] { header "/Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator8.0.sdk/us r/include/CommonCrypto/CommonCrypto.h" link "CommonCrypto" export * }
  35. STEP 2: SET A IMPORT PATH 1. Go to the

    framework Build Settings 2. Find Swift Compiler - Search Paths 3. Add the CommonCrypto directory to Import Paths
  36. FINALLY import CommonCrypto func MD5(aString : String) -> String {

    if let data = aString.dataUsingEncoding(NSUTF8StringEncoding) { let result = NSMutableData(length: Int(CC_MD5_DIGEST_LENGTH)) let resultBytes = UnsafeMutablePointer<CUnsignedChar>(result.mutableBytes) CC_MD5(data.bytes, CC_LONG(data.length), resultBytes) let e = UnsafeBufferPointer<CUnsignedChar>(start: resultBytes, length: result.length) let MD5 = NSMutableString() for c in e { MD5.appendFormat("%02x", c) } return MD5 } return "" }
  37. LIMITATIONS Projects that use the framework must repeat step 2.

    The given module map is not platform independent. Is it possible to make the header path relative to the current platform? Follow: http://stackoverflow.com/q/25248598/143378
  38. Objective-C frameworks can use C libraries without any issues. So

    let's create one that wraps the C library. Using CommonCrypto as an example...
  39. STEP 1: CREATE THE BRIDGING FRAMEWORK 1. Add a new

    target to the project. 2. Select Cocoa Touch Framework. 3. Name it MyFrameworkBridge and select Objective-C as a language.
  40. STEP 2: WRAP THE C LIBRARY 1. Add a public

    wrapper class or category. 2. Import and use the library in the implementation file. 3. Import the wrapper header (e.g., NSString+CommonCrypto.h) in the framework header (MyFrameworkBridge.h).
  41. #import "NSString+CommonCrypto.h" #import <CommonCrypto/CommonCrypto.h> @implementation NSString(Haneke) - (NSString*)hnk_MD5String { NSData

    *data = [self dataUsingEncoding:NSUTF8StringEncoding]; unsigned char result[CC_MD5_DIGEST_LENGTH]; CC_MD5(data.bytes, (CC_LONG)data.length, result); NSMutableString *MD5String = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { [MD5String appendFormat:@"%02x",result[i]]; } return MD5String; }
  42. NO CHEATING Importing the C library in a public header

    of the bridging framework will cause a compile error: include of non-modular header inside framework module 'MyFrameworkBridge'
  43. LIMITATIONS Projects that use the framework must also include the

    bridging framework. The wrapper methods of the bridging framework will be visible to projects who use the original framework.
  44. BTW If you need to use simple cryptography such as

    MD5 in a framework, check out CryptoSwift.
  45. "A LIMITATION OF THE ACCESS CONTROL SYSTEM IS THAT UNIT

    TESTS CANNOT INTERACT WITH THE CLASSES AND METHODS IN AN APPLICATION UNLESS THEY ARE MARKED PUBLIC." Xcode 6.0 beta 4 release notes
  46. We're aware that our access control design isn't great for

    unit testing (and this was in the release notes), we're evaluating the situation to see what we can do. — Chris Lattner
  47. WHY TEST INTERNALS? Internals are not public API but they're

    "public" API within the scope of the framework. Do you always work alone? Helper functions/extensions/types will most likely internal and are great candidates for unit testing. extension CGSize { func hnk_aspectFillSize(size: CGSize) -> CGSize { ... } }
  48. WE SHALL UNIT TEST ANYWAY 1. Add all the framework

    files to the test target. 2. Remove the framework from Target Dependencies and Link Binary With Libraries build phases of the test target (to prevent naming collisions). You can now test public and internal elements.