$30 off During Our Annual Pro Sale. View Details »

try! Swift Tokyo 2018: ​Optimizing Swift code for separation of concerns and simplicity

try! Swift Tokyo 2018: ​Optimizing Swift code for separation of concerns and simplicity

Separating concerns in code is often seen as a premature optimization when code doesn’t need to be reused, but it has huge implications in our ability to comprehend what code does. Let’s explore examples of this in Swift in what I like to call “separating what code does from how it does what it does”.

Javier Soto

March 01, 2018
Tweet

More Decks by Javier Soto

Other Decks in Programming

Transcript

  1. Optimizing Swift code for
    separation of concerns and
    simplicity
    樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄SwiftπЄϖ΄๋晒۸
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 1

    View Slide

  2. Intro
    - Pebble
    - Twitter (Fabric)
    -
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 2

    View Slide

  3. Coding Principles
    πЄϔΰЀν΄ܻ㳷
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 3

    View Slide

  4. Coding Principles
    πЄϔΰЀν΄ܻ㳷
    - Simplicity / 㶨奈
    - Conciseness / 墋侜
    - Clarity / ก嘦
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 4

    View Slide

  5. Separation of Concerns
    樛ஞ΄ړ櫝
    - Code is read much more often than it is written
    πЄϖ΅䨗͡΢Ρͩ;ΞΠΘ抎Δ΢Ρͩ;΄ො͢ग़͚
    - Separating the "what" from the "how"
    How ͡Ο what ΨڔΠ櫝ͯ
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 5

    View Slide

  6. Example 1 (Before)
    func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange,
    replacementText text: String) -> Bool {
    sendButton.isEnabled =
    textView.text.utf16.count
    + text.utf16.count
    - range.length <= 140
    return true
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 6

    View Slide

  7. Example 1 (After)
    private extension String {
    var characterCountUsingBackendPolicy: Int {
    return utf16.count
    }
    }
    func textView(_ textView: UITextView,
    shouldChangeTextIn range: NSRange,
    replacementText text: String) -> Bool {
    let characters =
    textView.text.characterCountUsingBackendPolicy
    + text.characterCountUsingBackendPolicy
    - range.length
    let characterLimit = 140
    sendButton.isEnabled = characters <= characterLimit
    return true
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 7

    View Slide

  8. Example 2 (Before)
    api.requestReplies(postID: 4815162342) { [weak self] result in
    switch result {
    case .success(let replies):
    var filteredReplies: [Reply] = []
    for reply in replies {
    if !user.isBlocking(reply.author) {
    filteredReplies.append(reply)
    }
    }
    self?.replies = filteredReplies
    case .failure:
    // ...
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 8

    View Slide

  9. Example 2 (After)
    extension Collection where Element == Reply {
    var filteringBlockedContent: [Reply] {
    return filter { !user.isBlocking($0.author) }
    }
    }
    api.requestReplies(postID: 4815162342) { [weak self] result in
    switch result {
    case .success(let replies):
    self?.replies = replies.filteringBlockedContent
    case .failure:
    // ...
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 9

    View Slide

  10. Example 3 (Before)
    NSLayoutConstraint.activate([
    subview.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left),
    subview.topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top),
    view.trailingAnchor.constraint(equalTo: subview.trailingAnchor, constant: insets.right),
    view.bottomAnchor.constraint(equalTo: subview.bottomAnchor, constant: insets.bottom)
    ])
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 10

    View Slide

  11. Example 3 (After)
    NSLayoutConstraint.activate([
    subview.leadingAnchor ≈ view.leadingAnchor + insets.left,
    subview.topAnchor ≈ view.topAnchor + insets.top,
    view.trailingAnchor ≈ subview.trailingAnchor + insets.right,
    view.bottomAnchor ≈ subview.bottomAnchor + insets.bottom
    ])
    infix operator ≈ : LayoutAnchorPrecedence
    func ≈ (lhs: NSLayoutAnchor,
    rhs: LayoutAnchorTransform) -> NSLayoutConstraint {
    return lhs.constraint(equalTo: rhs.anchor, constant: rhs.constant)
    }
    func + (lhs: NSLayoutAnchor,
    rhs: CGFloat) -> LayoutAnchorTransform {
    return LayoutAnchorTransform(anchor: lhs, constant: rhs)
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 11

    View Slide

  12. Example 3 (After 2)
    NSLayoutConstraint.activate(NSLayoutConstraint.anchoring(subview, within: view))
    extension NSLayoutConstraint {
    static func anchoring(_ subview: UIView,
    within view: UIView,
    insets: UIEdgeInsets = .zero) -> [NSLayoutConstraint] {
    return [
    subview.leadingAnchor ≈ view.leadingAnchor + insets.left,
    subview.topAnchor ≈ view.topAnchor + insets.top,
    view.trailingAnchor ≈ subview.trailingAnchor + insets.right,
    view.bottomAnchor ≈ subview.bottomAnchor + insets.bottom
    ]
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 12

    View Slide

  13. Example 3 (Before and After)
    // Before
    NSLayoutConstraint.activate([
    subview.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: insets.left),
    subview.topAnchor.constraint(equalTo: view.topAnchor, constant: insets.top),
    view.trailingAnchor.constraint(equalTo: subview.trailingAnchor, constant: insets.right),
    view.bottomAnchor.constraint(equalTo: subview.bottomAnchor, constant: insets.bottom)
    ])
    // After
    NSLayoutConstraint.activate(NSLayoutConstraint.anchoring(subview, within: view))
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 13

    View Slide

  14. Example 4
    class MyView: UIView {
    var activeConstraints: [NSLayoutConstraint] = [] {
    willSet { NSLayoutConstraint.deactivate(activeConstraints) }
    didSet { NSLayoutConstraint.activate(activeConstraints) }
    }
    var headerVisible: Bool { didSet {
    activeConstraints = [
    view.topAnchor ≈ (headerVisible ? header.topAnchor : header.bottomAnchor),
    // more ...
    ]
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 14

    View Slide

  15. Example 5 (Before)
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 3
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath)
    if indexPath.row == 0 {
    cell.textLabel.text = "General"
    } else if indexPath.row == 1 {
    cell.textLabel.text = "Notifications"
    } else if indexPath.row == 2 {
    cell.textLabel.text = "Log Out"
    }
    return cell
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 15

    View Slide

  16. Example 5 (After)
    enum Row {
    case general, notifications, logout
    var title: String {
    switch self {
    case .general: return "General"
    // ...
    }
    }
    }
    let rows: [Row] = [.general, .notifications, .logout]
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return rows.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = collectionView.dequeueReusableCell(...
    cell.textLabel.text = rows[indexPath.row].title
    return cell
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 16

    View Slide

  17. Example 6 (Before)
    func showTutorial() {
    guard !UserDefaults.standard.bool(forKey: "has_seen_tutorial") else { return }
    // Show tutorial
    UserDefaults.standard.set(true, forKey: "has_seen_tutorial")
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 17

    View Slide

  18. Example 6 (After)
    var hasSeenTutorial: Bool {
    get { return UserDefaults.standard.bool(forKey: "has_seen_tutorial") }
    set { UserDefaults.standard.set(newValue, forKey: "has_seen_tutorial") }
    }
    func showTutorial() {
    guard !hasSeenTutorial else { return }
    // Show tutorial
    hasSeenTutorial = true
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 18

    View Slide

  19. Example 7 (Before)
    if #available(iOS 11.0, *) {
    constraints = [
    subview.topAnchor ≈ view.safeAreaLayoutGuide.topAnchor,
    subview.leadingAnchor ≈ view.safeAreaLayoutGuide.leadingAnchor,
    subview.bottomAnchor ≈ view.safeAreaLayoutGuide.bottomAnchor,
    subview.trailingAnchor ≈ view.safeAreaLayoutGuide.trailingAnchor,
    ]
    } else {
    constraints = [
    subview.topAnchor ≈ view.topAnchor,
    subview.leadingAnchor ≈ view.leadingAnchor,
    subview.bottomAnchor ≈ view.bottomAnchor,
    subview.trailingAnchor ≈ view.trailingAnchor,
    ]
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 19

    View Slide

  20. Example 7 (After)
    extension UIView {
    var tw_safeAreaLayoutGuide: UILayoutGuide {
    if #available(iOS 11.0, *) {
    return safeAreaLayoutGuide
    }
    if let tw_safeAreaLayoutGuide = objc_getAssociatedObject(self, associatedKey) as? UILayoutGuide {
    return tw_safeAreaLayoutGuide
    }
    let tw_safeAreaLayoutGuide = UILayoutGuide()
    addLayoutGuide(tw_safeAreaLayoutGuide)
    NSLayoutConstraint.activate(NSLayoutConstraint.anchoring(tw_safeAreaLayoutGuide, within: self))
    objc_setAssociatedObject(self, associatedKey, tw_safeAreaLayoutGuide, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    return tw_safeAreaLayoutGuide
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 20

    View Slide

  21. Example 8 (Before)
    // Set size big enough to make it easy to tap
    button.frame = CGRect(origin: .zero, size: CGSize(width: 44, height: 44))
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 21

    View Slide

  22. Example 8 (After)
    class TWMinimumHitAreaButton: UIButton {
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !isHidden && enabled && isUserInteractionEnabled && alpha >= 0.01 else {
    return nil
    }
    let lengthOfTappableAreaOutside: CGFloat = 10.0
    let minimumHitAreaSize = CGSize(width: 44, height: 44)
    // Increase the hit frame to be at least as big as `minimumHitArea`
    let buttonSize = bounds.size
    let widthToAdd = max(minimumHitAreaSize.width - buttonSize.width, lengthOfTappableAreaOutside * 2)
    let heightToAdd = max(minimumHitAreaSize.height - buttonSize.height, lengthOfTappableAreaOutside * 2)
    let largerFrame = bounds.insetBy(dx: -widthToAdd / 2, dy: -heightToAdd / 2)
    // Perform hit test on larger frame
    let hit = largerFrame.contains(point)
    return hit ? self : nil
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 22

    View Slide

  23. Example 9 (Before)
    /// [ Send ] / [ Close ] / [ Send ] - [ Close ]
    private let sendButton: UIButton
    var showSendButton: Bool {
    didSet {
    if showSendButton {
    addSubview(sendButton)
    } else {
    sendButton.removeFromSuperview()
    }
    configureConstraints()
    }
    }
    private let closeButton: UIButton
    var showCloseButton: Bool {
    didSet {
    // ...
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 23

    View Slide

  24. Example 9 (After)
    private let sendButton: UIButton
    private let closeButton: UIButton
    private let stackView = UIStackView(arrangedViews: sendButton, closeButton)
    var showSendButton: Bool {
    didSet {
    sendButton.isHidden = !showSendButton
    }
    }
    var showCloseButton: Bool {
    didSet {
    closeButton.isHidden = !showCloseButton
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 24

    View Slide

  25. Example 10
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 25

    View Slide

  26. Example 10
    — ProfileViewController
    — user details
    — children views
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 26

    View Slide

  27. Example 10
    — ProfileViewController
    — userID
    — (user details)
    — (children views)
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 27

    View Slide

  28. Example 10
    final class ProfileViewController: UIViewController {
    // Before:
    private let userInfo: UserInfo
    private let headerView: ProfileHeaderView
    // After:
    private var userInfo: UserInfo?
    private var headerView: ProfileHeaderView?
    private var spinner: UIActivityIndicatorView?
    private var retryButton: UIButton?
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 28

    View Slide

  29. Example 10
    final class ProfileViewController: UIViewController {
    private enum State {
    case pending
    case loading(spinner: UIActivityIndicatorView)
    case failed(retryButton: UIButton)
    case loaded(userInfo: UserInfo, headerView: ProfileHeaderView)
    }
    private var state: State = .pending
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 29

    View Slide

  30. Example 10
    final class ProfileViewController: UIViewController {
    var state: State = .pending
    private func loadUserDetails() {
    state = .loading(spinner: UIActivityIndicatorView())
    api.requestUserDetails(userID) { [weak self] result in
    switch result {
    case let .success(userInfo):
    self?.state = .loaded(userInfo: userInfo, headerView: createHeaderView(userInfo))
    case let .failure:
    self?.state = .failed(createRetryButton())
    }
    }
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 30

    View Slide

  31. Example 10
    final class ProfileViewController: UIViewController {
    var visibleView: UIView? {
    willSet { visibleView?.removeFromSuperview() }
    didSet { if let visibleView = visibleView { view.addSubview(visibleView) } }
    }
    var state: State = .pending {
    didSet {
    switch state {
    case .pending: visibleView = nil
    case .loading(let spinner): visibleView = spinner
    case .loaded(_, let profileHeaderView): visibleView = profileHeaderView
    case .failed(let retryButton): visibleView = retryButton
    }
    }
    }
    }
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 31

    View Slide

  32. Summary
    - Value in optimizing readability in local scopes
    ϺЄθϸφπЄϤ΄ݢ抎௔΄๋晒۸΁΅㭅㮔͘͢Ρ
    - DRY (Don't Repeat Yourself)
    - Swift enums are awesome
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 32

    View Slide

  33. Happy Swifting!
    Thank you.
    "Optimizing Swi! code for separation of concerns and simplicity." "樛ஞ΄ړ櫝;㶨奈۸΄͵Η΄Swi!πЄϖ΄๋晒۸" - @Javi 33

    View Slide