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

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

Bas Broek
December 10, 2018

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

How to create better, nicer APIs.

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