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

So You're Writing a Framework... (more Swift ve...

Avatar for Bas Broek Bas Broek
December 10, 2018

So You're Writing a Framework... (more Swift version)

How to create better, nicer APIs.

Avatar for Bas Broek

Bas Broek

December 10, 2018
Tweet

More Decks by Bas Broek

Other Decks in Programming

Transcript

  1. Who am I → iOS Developer @ XING → I

    curate Swift Weekly Brief → I do some open source things → I like continuous integration systems, too 8 — @basthomas
  2. Have you ever tried... → using UIKit without documentation? (Xcode

    "helps" sometimes) → using someone's (undocumented) API without looking at the implementation? → using an undocumented API you wrote in the past, without looking at the implementation? 9 — @basthomas
  3. Looking into a pull request that came up in my

    GitHub notifications... 10 — @basthomas
  4. class Lists { let array1: [String] let array2: [String] init(array1:

    [String], array2: [String]) { self.array1 = array1 self.array2 = array2 } func combineOutputs() -> [String] { return concatArrays() } private var occurences: Int { // https://github.com/apple/swift-evolution/blob/master/proposals/0220-count-where.md return array1.filter({ $0 == array2.first }).count } private func occurredAtIndex(array: [String]) -> Int? { guard let first = array2.first else { return nil } array.firstIndex(of: first) } private func occurrenceChunks() -> [String] { var chunks: [String] = [] var array = array1 for _ in 0...occurences { guard let occurrenceIndex = occurredAtIndex(array) else { continue } chunks.append(array[occurrenceIndex...array.count]) array.remove(at: occurrenceIndex) } return chunks } // more of this } 12 — @basthomas
  5. This pull request adds a possibility to concat arrays without

    removing the duplicates in the parts that are not overlap completely (in the meaning that we care only if the last x values of 1st array overlaps with the same x values of the 2nd, but we don't care if some duplicates are randomly exist in the body of array) 16 — @basthomas
  6. This pull request adds a possibility to concat arrays without

    removing the duplicates in the parts that are not overlap completely 17 — @basthomas
  7. /// - Parameter array1: an array that contain all the

    string collected till /// this moment /// - Parameter array2: an array that contain new elements that should be /// added in array1 init(array1: [String], array2: [String]) { self.array1 = array1 self.array2 = array2 } /// Combines two arrays with partial removal of duplicates /// /// - Returns: An array of complate `array1` and partially (removing the first /// occurances in both arrays) added `array2` func combineOutputs() -> [String] { return concatArrays() } 18 — @basthomas
  8. class ListsTestCase: XCTestCase { func testCombineOutputs() { let array1 =

    ["a", "b", "c"] let array2 = ["c", "d", "e"] let expected = ["a", "b", "c", "d", "e"] XCTAssertEqual(Lists(array1, array2).combineOutputs(), expected) let array3 = ["x", "y", "z"] // Could not find comparable data. XCTAssertThrows(try Lists(array1, array3).combineOutputs()) } } 19 — @basthomas
  9. class Lists { enum ListError: Error { case noOverlap }

    /// Adds two arrays with removing the overlap between them /// /// - Parameter existing: the exsting elemets. /// - Parameter newEntries: the elements that should be added to the front /// - Returns: the sum of both array with `newEntries` at the front static func concatenateWithoutOverlap( existing: [String], newEntries: [String] ) throws -> [String] { let occourences = newEntries.enumerated().filter { index, entry in newEntries[index] == existing.first } let _overlapStart = occourences.first(where: { index in existing.starts(with: newEntries[index..<-1]) }) guard let overlapStart = _overlapStart else { throw ListError.noOverlap } return [Array(arr3.prefix(through: 1)), arr2].flatMap { $0 } } } 20 — @basthomas
  10. class Lists { enum ListError: Error { case noOverlap }

    /// Adds two arrays with removing the overlap between them /// /// - Parameter existing: the exsting elemets. /// - Parameter newEntries: the elements that should be added to the front /// - Returns: the sum of both array with `newEntries` at the front static func concatenateWithoutOverlap( existing: [String], newEntries: [String] ) throws -> [String] { // implementation } } 22 — @basthomas
  11. /// Prepends an array to front of an existing one,

    /// without adding the overlap twice. /// /// Example: /// /// ```swift /// let existing = [4, 3, 2, 1] /// let newEntries = [6, 5, 4, 3] /// Lists.concatenateWithoutOverlap( /// existing: existing, /// newEntries: newEntries /// ) // Returns [6, 5, 4, 3, 2, 1] /// ``` /// /// - Parameter existing: the existing array. /// - Parameter newEntries: the array that should be added to the front /// - Returns: returns the new array 23 — @basthomas
  12. class Lists { enum ListError: Error { case noOverlap }

    /// Prepends an array to front of an existing one, /// without adding the overlap twice. /// /// Example: /// /// ```swift /// let existing = [4, 3, 2, 1] /// let newEntries = [6, 5, 4, 3] /// Lists.concatenateWithoutOverlap( /// existing: existing, /// newEntries: newEntries /// ) // Returns [6, 5, 4, 3, 2, 1] /// ``` /// /// - Parameter existing: the existing array. /// - Parameter newEntries: the array that should be added to the front /// - Returns: returns the new array static func stitch( existing: [String], with newEntries: [String] ) throws -> [String] { // Get the index of all occurrences of the first element from the // `newEntries` array in `existing` let occurrences = newEntries.enumerated().filter { index, entry in newEntries[index] == existing.first } // Check if one of the slices of `existing` is prefix of // the `newEntries` array let _overlapStart = occurrences.first(where: { index in existing.starts(with: newEntries[index..<-1]) }) guard let overlapStart = _overlapStart else { throw ListError.noOverlap } // Add the `newEntries` without the overlap at the end of the existing array return [Array(arr3.prefix(through: 1)), arr2].flatMap { $0 } } } 24 — @basthomas
  13. The order does not matter* * But do figure out

    what works best for your team. There's a high chance starting with tests is a good idea. 36 — @basthomas
  14. It is successful if a failure or success case that

    is discovered or added in the future can be solved via Test Driven Development — Me 38 — @basthomas
  15. What more can we do? → Give examples → Write

    simple, non near-code documentation → "No raw loops" → Self-enforced best practice → Strongly typed APIs → Scope → Compassion → ..? 41 — @basthomas
  16. OK, not yet... → Component System → Component (Text) Configuration

    → Strong typing → Objective-C interop... → (is a good thing?) 42 — @basthomas
  17. @objc public class TextConfiguration: NSObject { @objc public init( color:

    TextColor, weight: TextWeight, size: TextSize, numberOfLines: Int ) { self._color = color self.weight = weight self.size = size self.numberOfLines = numberOfLines } } 43 — @basthomas
  18. /// Creates a new `TextConfiguration` with the specified values. ///

    /// - Parameter color: the text color to use. Available colors are a subset /// of `XINGColor` and are safe to use for text. /// - Parameter weight: the text weight to use. This will be used for the text's font. /// - Parameter size: the text size to use. This will be used for the text's font. /// - Parameter numberOfLines: The number of lines of text to render. /// To remove any maximum limit, and use as many lines as needed, /// set the value of this property to 0. @objc public init( color: TextColor, weight: TextWeight, size: TextSize, numberOfLines: Int ) { self._color = color self.weight = weight self.size = size self.numberOfLines = numberOfLines } 44 — @basthomas
  19. /// - Paramter label: the label to generate a text

    configuration from. /// /// - Returns: a `TextConfiguration` based on the label, if it uses valid /// text configuration values. If not, it will assert and revert to default options. @objc public static func from(label: UILabel) -> TextConfiguration { func textColor(from color: UIColor) -> TextColor { guard let _textColor = TextColor.all.first(where: { $0.uiColor == label.textColor }) else { assertionFailure("Could not convert label's color to a valid TextColor. This will default to `.grey800` in production, which may be unexpected.") return .grey800 } return _textColor } func textWeight(from font: UIFont) -> TextWeight { guard let _textStyle = TextWeight.from(font: font) else { assertionFailure("Could not convert font's weight to a valid TextWeight. This will default to `.regular` in production, which may be unexpected.") return .regular } return _textStyle } func textSize(from font: UIFont) -> TextSize { guard let _textSize = TextSize.from(font: font) else { assertionFailure("Could not convert font's size to a valid TextSize. This will default to `.default12` in production, which may be unexpected.") return .default12 } return _textSize } return .init( color: textColor(from: label.textColor), weight: textWeight(from: label.font), size: textSize(from: label.font), numberOfLines: label.numberOfLines ) } 46 — @basthomas
  20. func textColor(from color: UIColor) -> TextColor { guard let _textColor

    = TextColor.all.first(where: { $0.uiColor == label.textColor }) else { assertionFailure("Could not convert label's color to a valid TextColor.") return .grey800 } return _textColor } func textWeight(from font: UIFont) -> TextWeight { guard let _textStyle = TextWeight.from(font: font) else { assertionFailure("Could not convert font's weight to a valid TextWeight.") return .regular } return _textStyle } func textSize(from font: UIFont) -> TextSize { guard let _textSize = TextSize.from(font: font) else { assertionFailure("Could not convert font's size to a valid TextSize.") return .default12 } return _textSize } 47 — @basthomas
  21. @objc public static func from(label: UILabel) -> TextConfiguration { return

    .init( color: textColor(from: label.textColor), weight: textWeight(from: label.font), size: textSize(from: label.font), numberOfLines: label.numberOfLines ) } 48 — @basthomas
  22. /// Applies the current `TextConfiguration` to the supplied label. ///

    /// - Parameter label: the `UILabel` instance to apply the /// text configuration to. /// /// - Returns: a reference to the label that has the configuration /// aplied to it. @discardableResult @objc public func apply(to label: UILabel) -> UILabel { label.textColor = color label.font = font label.numberOfLines = numberOfLines return label } 49 — @basthomas
  23. Outcome → Scoped → Testable (and bugs and edge cases

    fixed / covered during development) → Objective-C interop → Chainable API → It's not perfect (and that is OK) → Architecture design is missing 51 — @basthomas
  24. Outcome, part 2 → The compiler is your friend →

    Don't be afraid to use types → The compiler is your enemy → We should explain the architectural design → Prevent "rest of the damn owl" syndrom → Use assertions! 52 — @basthomas
  25. Things to keep in mind → I don't want to

    write the correct code. There is no correct code. There is no silver bullet. → I want to help the most correct code to be written. → I want to prevent incorrect code to be written. → I want to fail early and often. → (but for that, I need it to be testable) → Iterate, iterate, iterate 53 — @basthomas
  26. Takeaways → Optimize code for reading, not writing → Work

    together and bounce off ideas → Documentation is helpful for yourself when writing code, too → Documentation helps you with naming, scope, testability and readability → Having testable code feels awesome → Snapshot tests are the root of all evil 54 — @basthomas
  27. Further reading "Combinators" by Daniel Steinberg https://vimeo.com/290272240 "Denotational Design" by

    Conal Elliot https://github.com/conal/talk-2014-lambdajam-denotational-design "... But That Should Work?" by me https://basthomas.github.io/but-that-should-work Point-Free by Brandon Williams & Stephen Celis https://www.pointfree.co 56 — @basthomas