├── Branching Strategy ├── Images │ └── git-flow.png └── Branching Strategy.md ├── Code Style ├── Images │ └── method-navigator-separators.png ├── Code Style.md └── Swift Style Guide.md ├── Accessibility ├── Images │ ├── accessibility-inspector-audit.png │ ├── accessibility-shortcut-options.png │ ├── settings-general-accessibility.png │ ├── accessibility-inspector-settings.png │ ├── accessibility-inspector-inspection.png │ └── accessibility-shortcut-menu-activated.png └── Accessibility.md ├── Interface Building ├── Images │ └── interface-builder-object.png └── Interface Building.md ├── Localization and Internationalization ├── Images │ ├── localizable-stringsdict-example.png │ └── testing-localization-scheme-language.png └── Localization and Internationalization.md ├── CODEOWNERS ├── NOTICE.txt ├── CONTRIBUTING.md ├── Development Environment ├── Ruby Environment Setup.md └── Development Environment.md ├── README.md ├── Tools and Utilities └── Tools and Utilities.md ├── Architecture └── Architecture.md ├── Build Warnings and Errors └── Build Warnings and Errors.md ├── Code Review Standards └── Code Review Standards.md ├── Dependency Management └── Dependency Management.md ├── Project Structure └── Project Structure.md ├── LICENSE.txt └── Testing └── Testing.md /Branching Strategy/Images/git-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Branching Strategy/Images/git-flow.png -------------------------------------------------------------------------------- /Code Style/Images/method-navigator-separators.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Code Style/Images/method-navigator-separators.png -------------------------------------------------------------------------------- /Accessibility/Images/accessibility-inspector-audit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Accessibility/Images/accessibility-inspector-audit.png -------------------------------------------------------------------------------- /Accessibility/Images/accessibility-shortcut-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Accessibility/Images/accessibility-shortcut-options.png -------------------------------------------------------------------------------- /Accessibility/Images/settings-general-accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Accessibility/Images/settings-general-accessibility.png -------------------------------------------------------------------------------- /Interface Building/Images/interface-builder-object.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Interface Building/Images/interface-builder-object.png -------------------------------------------------------------------------------- /Accessibility/Images/accessibility-inspector-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Accessibility/Images/accessibility-inspector-settings.png -------------------------------------------------------------------------------- /Accessibility/Images/accessibility-inspector-inspection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Accessibility/Images/accessibility-inspector-inspection.png -------------------------------------------------------------------------------- /Accessibility/Images/accessibility-shortcut-menu-activated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Accessibility/Images/accessibility-shortcut-menu-activated.png -------------------------------------------------------------------------------- /Localization and Internationalization/Images/localizable-stringsdict-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Localization and Internationalization/Images/localizable-stringsdict-example.png -------------------------------------------------------------------------------- /Localization and Internationalization/Images/testing-localization-scheme-language.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BottleRocketStudios/iOS-Project-Standards/HEAD/Localization and Internationalization/Images/testing-localization-scheme-language.png -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | # Unless a later match takes precedence, these owners will be requested for review when someone opens a pull request. 3 | * @BottleRocketStudios/team-ios-open-source-w 4 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | ============================================================================= 2 | = NOTICE file corresponding to section 4d of the Apache License Version 2.0 = 3 | ============================================================================= 4 | This product includes software developed by 5 | Bottle Rocket LLC (http://www.bottlerocketstudios.com/). -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | If you would like to contribute code to this project you can do so through GitHub by 5 | forking the repository and sending a pull request. 6 | 7 | When submitting code follow the existing conventions and code style. Ensure that your code changes build and unit tests pass. 8 | 9 | Before your code can be accepted into the project you must also sign the 10 | [Individual Contributor License Agreement (CLA)][1]. 11 | 12 | 13 | [1]: https://cla-assistant.io/BottleRocketStudios/iOS-Project-Standards 14 | -------------------------------------------------------------------------------- /Development Environment/Ruby Environment Setup.md: -------------------------------------------------------------------------------- 1 | # Ruby Environment Setup 2 | 3 | How to properly setup your Ruby environment using `rbenv`. 4 | 5 | ## Purpose 6 | 7 | Using some sort of Ruby version management is highly recommended as opposed to using the system version of Ruby (which often requires `sudo` access to install dependencies). 8 | 9 | ## Installing `rbenv` 10 | 11 | ### 1. Install [Homebrew](https://brew.sh) if it's not already installed: 12 | 13 | ```bash 14 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"" 15 | ``` 16 | 17 | ### 2. Install `rbenv`: 18 | 19 | ```bash 20 | brew install rbenv 21 | ``` 22 | 23 | ### 3. Initilaize `rbenv`: 24 | 25 | ```bash 26 | rbenv init 27 | ``` 28 | 29 | **NOTE:** Don't forget to also add `eval "$(rbenv init -)"` to your `~/.bash_profile`, as suggested by the output of the above command. 30 | 31 | ### 4. Install a current version of Ruby: 32 | 33 | ```bash 34 | rbenv install 2.6.3 35 | ``` 36 | 37 | **NOTE:** You can use the `rbenv install --list` command to list available Ruby versions. 38 | 39 | ### 5. Configure `rbenv` to use the newer version of Ruby: 40 | 41 | ```bash 42 | rbenv global 2.6.3 43 | ``` 44 | 45 | ### 6. Reload your `~/.bash_profile` (or restart Terminal): 46 | 47 | ```bash 48 | source ~/.bash_profile 49 | ``` 50 | 51 | ### 7. If on macOS Catalina and using Zsh (Z shell), you may encounter a write permissions error. Here's how to fix it. 52 | 53 | Add the following to `.zshenv`: 54 | 55 | ``` 56 | export PATH="$HOME/.rbenv/bin:$PATH" 57 | ``` 58 | 59 | Add the following lines to `.zshrc`: 60 | 61 | ``` 62 | source $HOME/.zshenv 63 | eval "$(rbenv init - zsh)" 64 | ``` 65 | 66 | Restart terminal. 67 | 68 | ## References 69 | 70 | The information in this guide is based on [this excellent article](https://jasoncharnes.com/install-ruby/) by Jason Charnes. 71 | 72 | The information for installing rbenv on Zsh [is this article](https://programmingzen.com/installing-rbenv-on-zsh-on-macos/) by Antonio Cangiano. 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bottle Rocket iOS Project Standards 2 | 3 | ## Introduction 4 | 5 | This document provides Bottle Rocket's point-of-view on several topics regarding iOS application development. Having served the smartphone app market since its inception in 2008, we consider the information here to be a set of "best practices" that we try to apply when building iOS apps. 6 | 7 | Although we generally expect our engineers to follow these standards, we recognize that there are many ways to build iOS apps. If you have a differing opinion on how something should be done (employee or not), have additional tips or ideas to share, or see something that seems incorrect, feel free to open an issue or submit a pull request to start a conversation. We welcome community discussion and look forward to learning from one another! 8 | 9 | ## Topics 10 | 11 | ### Accessibility 12 | 13 | See [Accessibility.md](./Accessibility/Accessibility.md). 14 | 15 | ### Architecture 16 | 17 | See [Architecture.md](./Architecture/Architecture.md). 18 | 19 | ### Branching Strategy 20 | 21 | See [Branching Strategy.md](./Branching%20Strategy/Branching%20Strategy.md). 22 | 23 | ### Build Warnings and Errors 24 | 25 | See [Build Warnings and Errors.md](./Build%20Warnings%20and%20Errors/Build%20Warnings%20and%20Errors.md). 26 | 27 | ### Code Review Standards 28 | 29 | See [Code Review Standards.md](./Code%20Review%20Standards/Code%20Review%20Standards.md). 30 | 31 | ### Code Style 32 | 33 | See [Code Style.md](./Code%20Style/Code%20Style.md). 34 | 35 | ### Dependency Management 36 | 37 | See [Dependency Management.md](./Dependency%20Management/Dependency%20Management.md). 38 | 39 | ### Development Environment 40 | 41 | See [Development Environment.md](./Development%20Environment/Development%20Environment.md). 42 | 43 | ### Interface Building 44 | 45 | See [Interface Building.md](./Interface%20Building/Interface%20Building.md). 46 | 47 | ### Localization and Internationalization 48 | 49 | See [Localization and Internationalization.md](./Localization%20and%20Internationalization/Localization%20and%20Internationalization.md). 50 | 51 | ### Project Structure 52 | 53 | See [Project Structure.md](./Project%20Structure/Project%20Structure.md). 54 | 55 | ### Testing 56 | 57 | See [Testing.md](./Testing/Testing.md). 58 | 59 | ### Tools and Utilities 60 | 61 | See [Tools and Utilities.md](./Tools%20and%20Utilities/Tools%20and%20Utilities.md). 62 | -------------------------------------------------------------------------------- /Development Environment/Development Environment.md: -------------------------------------------------------------------------------- 1 | # Development Environment 2 | 3 | ## Xcode 4 | 5 | ### Using the Latest Version 6 | 7 | We make a conscious effort to keep our projects on the latest version of [Xcode](https://developer.apple.com/xcode/). When new Xcode versions are released every fall, some of our largest projects might take a few months to upgrade, but most of our projects are using the latest version within a matter of days/weeks. During the summer, some projects also become early adopters of the latest beta versions of Xcode 8 | 9 | ### Embracing the Defaults 10 | 11 | We prefer to keep our development environment as close to "stock" as possible. This means using the Xcode defaults for most everything, which: 12 | 13 | * Negates the need to manage scripts that setup/configure the development environment. 14 | * Reduces maintenance resulting from custom settings being blown away by Xcode updates/reinstalls. 15 | * Ensures developers can contribute with as little friction as possible. 16 | 17 | ## Gitignore 18 | 19 | We make use of [GitHub's Swift gitignore](https://github.com/github/gitignore/blob/master/Swift.gitignore) as the default gitignore for our projects. The only change that we typically make is uncommenting the `Pods/` line so that the [`Pods` folder isn't included in source control](../Dependency%20Management/Dependency%20Management.md#checking-in-the-pods-folder). 20 | 21 | ## Cocoapods 22 | 23 | Many developers naively use the built-in system Ruby to install Cocoapods using the `sudo gem install cocoapods` command. Unfortunately, this can lead to headaches due to the use of `sudo`. 24 | 25 | The better way to install Cocoapods is to get off the system Ruby and use a Ruby version manager to negate the need for `sudo` access when installing or executing gems. Follow the instructions in [Ruby Environment Setup](./Ruby%20Environment%20Setup.md) to install and setup [`rbenv`](https://github.com/rbenv/rbenv). 26 | 27 | Once your Ruby is properly setup, installing Cocoapods is as simple as: 28 | 29 | ```bash 30 | gem install cocoapods 31 | ``` 32 | 33 | ## Fastlane 34 | 35 | [Fastlane](https://fastlane.tools/) is our main build tool. Again, a Ruby version manager should be used so that a `sudo`-less installation of Fastlane can be achieved. 36 | 37 | ## Build Servers 38 | 39 | As a prerequisite for running our Fastlane scripts, we have custom build scripts that manage keychains, signing certificates, and provisioning profiles for our projects. We store certificates and provisioning profiles in a separate repository from the source code for the project in order to provide an additional layer of security. 40 | -------------------------------------------------------------------------------- /Tools and Utilities/Tools and Utilities.md: -------------------------------------------------------------------------------- 1 | # Tools and Utilities 2 | 3 | This is a collection of useful tools and utilities we typically use when developing apps at Bottle Rocket. 4 | 5 | ## Linters 6 | 7 | We use linters to help enforce code consistency and quality between projects. 8 | 9 | ### SwiftLint 10 | 11 | [SwiftLint](https://github.com/realm/SwiftLint) helps identify and enforce community-driven style and conventions in Swift code. Check out the [Linting section of our Code Style document](../Code%20Style/Code%20Style.md#linting) for more information on how we utilize SwiftLint in our projects. 12 | 13 | ### OCLint 14 | 15 | [OCLint](http://oclint.org/) is a static analysis tool that can help to identify code smells in Objective-C code. It can also identify dead code like unused methods and parameters. 16 | 17 | ### IBLinter 18 | 19 | [IBLinter](https://github.com/IBDecodable/IBLinter) helps identify issues in Interface Builder documents. It's a rather new tool so it doesn't seem ready for prime time just yet, but it's something we're watching as it evolves. 20 | 21 | ## Code Generation 22 | 23 | ### Sourcery 24 | 25 | [Sourcery](https://github.com/krzysztofzablocki/Sourcery) is a code generation tool for Swift, allowing you to generate boilerplate code automatically. 26 | 27 | ## Code Cleanup 28 | 29 | ### Fui 30 | 31 | [Fui](https://github.com/dblock/fui) allows you to find unused Objective-C imports (which can help to identify unused Obj-C classes). 32 | 33 | ### Fus 34 | 35 | [Fus](https://github.com/tsabend/fus) finds unused Swift classes in your project. 36 | 37 | ### Periphery 38 | 39 | [Periphery](https://peripheryapp.com/) finds unused Swift code in your project. 40 | 41 | ## Project Health 42 | 43 | ### Synx 44 | 45 | [Synx](https://github.com/venmo/synx) reorganizes your project's file structure to match how your Xcode groups are organized. It can also identify unused image resources and source files that are not part of your Xcode project. 46 | 47 | ## Provisioning and Signing 48 | 49 | ### ProvisionQL 50 | 51 | [ProvisionQL](https://github.com/ealeksandrov/ProvisionQL) is a Quick Look plugin for showing provisioning and signing information for provisioning profiles and IPAs. 52 | 53 | ## Build Automation 54 | 55 | ### Fastlane 56 | 57 | [Fastlane](https://fastlane.tools/) is a suite of tools to make iOS development easier. It can handle everything from building your app to distributing it to the App Store. 58 | 59 | ## API Communication 60 | 61 | ### Postman 62 | 63 | [Postman](https://www.getpostman.com/) is an app that allows you to save and execute HTTP requests. We use Postman to validate and test API functionality before we implement it in the app. 64 | 65 | ### Charles 66 | 67 | [Charles](https://www.charlesproxy.com/) is an HTTP proxy that allows you to intercept and read all of the HTTP traffic that passes through a device. It's incredibly useful for debugging communication issues between the app and external APIs. 68 | 69 | ## Git 70 | 71 | ### SourceTree 72 | 73 | [SourceTree](https://www.sourcetreeapp.com/) is a Git GUI app that provides great visualization and management tools. 74 | 75 | ### GitKraken 76 | 77 | [GitKraken](https://www.gitkraken.com/) is another Git GUI that some of our devs prefer over SourceTree. 78 | 79 | ## Merging Code 80 | 81 | ### Kaleidoscope 82 | 83 | [Kaleidoscope](https://www.kaleidoscopeapp.com/) is an advanced file comparison app. It performs many of the same functions as Apple's FileMerge, but can handle diffing many more file types. 84 | 85 | ## UI Specifications 86 | 87 | ### Zeplin 88 | 89 | [Zeplin](https://zeplin.io/) is an app that allows for designers to easily communicate UI specifications to developers. Before using Zeplin, our designers would need to manually "blueprint" specs for app screens like spacing between elements, font attributes, colors, etc. Zeplin integrates with Sketch to automate much of that process. 90 | 91 | ### Lottie 92 | 93 | [Lottie](https://airbnb.design/lottie/) is a library that allows mobile apps to easily render After Effects animations. Under the hood, Lottie uses JSON to encode the vector animations, which keeps the animation file sizes small and even allows animations to be downloaded at run-time. 94 | 95 | ### Lottie Player 96 | 97 | [Lottie Player](https://github.com/willowtreeapps/lottie-player) is a standalone viewer app for Lottie animations. It allows designers to test their Lottie animations before exporting them to developers. 98 | -------------------------------------------------------------------------------- /Architecture/Architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Model View Controller (MVC) 4 | 5 | ### Keeping It Simple 6 | 7 | We feel that Apple got it right with this one. All of our projects follow [Apple's MVC architecture](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html). We've found this beneficial for many reasons: 8 | 9 | * Keeps the barrier to entry for understanding the code low, minimizing the amount of ramp-up time required to onboard new devs. 10 | * Is well-documented. 11 | * Avoids the need to rely on third party frameworks for app architecture. Less dependencies is always better (see [Dependency Management](../Dependency%20Management/Dependency%20Management.md) for more information). 12 | 13 | ### Avoiding Massive View Controller 14 | 15 | Massive view controllers only happen when proper separation of concerns isn't implemented. By simply following [SOLID design principles](https://en.wikipedia.org/wiki/SOLID), you can make even a 300 line view controller a rare occurrence. 16 | 17 | ## Table and Collection Views 18 | 19 | ### Built-In Table/Collection View Controllers 20 | 21 | We **rarely** use the Apple-provided view controller templates (`UITableViewController` & `UICollectionViewController`). While they can be useful for beginners just starting out in iOS development, they reduce your ability to properly apply separation of concerns and can be challenging to build upon whenever you need to do non-default behavior. 22 | 23 | ### Data Sources and Delegates 24 | 25 | The data source and delegate methods for table and collection views should not be implemented by the view controller. Instead, separate "table/collection controller" objects should be created for the task. This prevents table/collection view details from polluting your view controller, which leads to better separation of concerns and greatly simplifies its responsibilities. 26 | 27 | ## Model Objects 28 | 29 | ### JSON Deserialization 30 | 31 | Data received from external APIs is almost always JSON. We prefer to to convert the raw JSON into native model objects **as soon as possible** rather than work with raw JSON dictionaries. With the release of Swift 4, we now make heavy use of the [`Decodable` protocol](https://developer.apple.com/documentation/swift/decodable) rather than relying on third party libraries like SwiftyJSON or Argo. 32 | 33 | ### Classes vs Structs 34 | 35 | We generally prefer pure Swift structs due to their value-typed nature and ability of the Swift compiler to automatically generate the [memberwise initializer](https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID214). 36 | 37 | ## Protocols 38 | 39 | ### Prefer Protocols Over Inheritance 40 | 41 | The [Protocol-Oriented Programming in Swift](https://developer.apple.com/videos/play/wwdc2015/408/) session from WWDC 2015 is a great introduction to the power of protocols. It's worth a watch if you've never seen it or a re-watch for a quick refresher. 42 | 43 | You should prefer using composition over inheritance when writing Swift code. Specifically, you should prefer __protocol based composition__ over inheritance. 44 | 45 | Protocol based composition allows a struct or class to fulfill the desired behaviors of an object without requiring that direct inheritance - which is not available for a struct in any case. 46 | 47 | Inheritance should be used as a last resort. There are two generally palatable situations where you may use inheritance: 48 | 49 | * Base view controllers 50 | * Inheritance required by mandated third party libraries 51 | 52 | ## UI Responsiveness 53 | 54 | It's imperative that the app's UI remain responsive during all aspects of the app's lifecycle. In general, any delay greater than 200 ms will be immediately noticeable by the user and could cause them to question whether or not the system registered their tap. 55 | 56 | * Reserve the main thread for UI work. Move as much work to background threads as possible. 57 | * Avoid purposely ignoring user interactions. There should generally never be a need to globally ignore user interaction by calling [`beginIgnoringInteractionEvents()`](https://developer.apple.com/documentation/uikit/uiapplication/1623047-beginignoringinteractionevents) on `UIApplication`. 58 | 59 | ## Architectural Guidelines 60 | 61 | When working on a new project it is imperative that all developers understand the scope and significance of any technical decision they make. In general, the more junior the developer, the more guidance and the less architectural decision making power the developer has. This is primarily to protect the customer as well as Bottle Rocket from “rogue” developers making a poor technology or other architecturally significant decision that would impact our ability to meet customer expectations. 62 | 63 | In general, if a developer has to make a decision between development technologies (e.g. languages, tools, 3rd party components, complex implementations, etc.) then this is an architecturally significant decision that requires director level approval and notification. The sole intent of this guideline is to protect the client and Bottle Rocket, not to create an artificial bottleneck. 64 | -------------------------------------------------------------------------------- /Build Warnings and Errors/Build Warnings and Errors.md: -------------------------------------------------------------------------------- 1 | # Build Warnings and Errors 2 | 3 | ## Build-time 4 | 5 | ### Project Warnings 6 | 7 | Project warnings add clutter and make it difficult to identify potential issues in your codebase. 8 | 9 | * Aim for **0** warnings during build-time. If not obtainable due to third party libraries, aim for 0 warnings in all of the code that *you* own. 10 | * If using Cocoapods, use the `inhibit_all_warnings!` flag to [silence warnings from pods](https://stackoverflow.com/a/13209057/4343618). 11 | 12 | ## Run-time 13 | 14 | ### Console Noise 15 | 16 | Excessive console output makes it difficult to see useful information when running/testing/debugging your app. The worst offences involve console output that is a constant stream of incomprehensible text. The console log is a valuable tool. Don't put your project in a situation where developers are forced to ignore it! 17 | 18 | * Keep logging to a minimum if possible. When you need to have a robust logging mechanism, consider using a logging library that allows for configurable logging levels (e.g. debug, info, warn, error, etc.). You can also use Apple's [unified logging system](https://developer.apple.com/documentation/os/logging), os_log, to accomplish this. 19 | * If you do need to output log information to the console, consider using the capability of a breakpoint to output console output, so that logging code does not get committed to the code repository. 20 | * Keep an eye out for system warnings that only manifest themselves in console output. Most often, these are things like unsatisfiable constraints or situations where UIKit methods are called on a background thread. 21 | * Scrub all logging from production builds of your application. Excessive logging in a production app can make it easier for others to reverse engineer your app. 22 | 23 | ### Project Breakpoints 24 | 25 | There are 3 types of breakpoints that can be used within Xcode: 26 | 27 | * Code breakpoints 28 | * You're probably most familiar with these. You click on a line number to create a breakpoint for that line of code. The next time the app executes that line of code, it will interrupt the app just before execution to give you a chance to inspect the state of the app. From here, you can step over a line of code to execute the app line-by-line, step into/out of functions, or resume normal execution. 29 | * Symbolic breakpoints 30 | * These breakpoints aren't set on any specific line of code, but are used to pause app execution when a specific method or function is called. These can be useful if you want to pause execution when a specific method is called, but are not sure where the method is being called from. 31 | * Exception breakpoints 32 | * These breakpoints pause app execution when an exception or crash occurs. Enabling these breakpoints typically gives you a head start on fixing crashes since you'll get context about what triggered the failure instead of execution halting on `main()` with a generic error like `SIGABRT`. 33 | 34 | In addition to making debugging easier, breakpoints can provide a useful safety net against latent errors that occur at run-time. Ensure that all developers have these enabled as "User Breakpoints" so that they persist across any project that's opened up in Xcode. Better yet, promote them to "Shared Breakpoints" so that they live in source control with the rest of the project files. For more information, check out our article on the [top 5 iOS breakpoints](https://medium.com/rocket-fuel/ios-breakpoint-secret-sauce-for-better-debugging-c0009f116ca1): 35 | 36 | * All Objective-C Exceptions 37 | * `-[UIApplication main]` (importing UIKit to enhance code completion in the debugger) 38 | * `UIViewAlertForUnsatisfiableConstraints` 39 | * `-[UIView(UIConstraintBasedLayout) _viewHierarchyUnpreparedForConstraint:]` 40 | * `UICollectionViewFlowLayoutBreakForInvalidSizes` 41 | 42 | ### Sanitizers 43 | 44 | Sanitizers also exist to help you catch run-time issues. 45 | 46 | #### Address Sanitizer 47 | 48 | The [Address Sanitizer](https://developer.apple.com/documentation/code_diagnostics/address_sanitizer) (ASan) is an LLVM based tool for detecting memory corruptions and other memory errors at run-time. [Enable ASan](https://developer.apple.com/documentation/code_diagnostics/address_sanitizer/enabling_the_address_sanitizer) in your app's scheme. 49 | 50 | #### Thread Sanitizer 51 | 52 | The [Thread Sanitizer](https://developer.apple.com/documentation/code_diagnostics/thread_sanitizer) (TSan) is an LLVM based tool for detecting race conditions at run-time. [Enable TSan](https://developer.apple.com/documentation/code_diagnostics/thread_sanitizer/enabling_the_thread_sanitizer) in your app's scheme. 53 | 54 | #### Undefined Behavior Sanitizer 55 | 56 | The [Undefined Behavior Sanitizer](https://developer.apple.com/documentation/code_diagnostics/undefined_behavior_sanitizer) (UBSan) is an LLVM tool for detecting undefined behavior at run-time. These are things like dividing by zero, loading memory from a misaligned pointer, or dereferencing a null pointer. [Enable UBSan](https://developer.apple.com/documentation/code_diagnostics/undefined_behavior_sanitizer/enabling_the_undefined_behavior_sanitizer) in your app's scheme. 57 | 58 | #### Main Thread Checker 59 | 60 | The [Main Thread Checker](https://developer.apple.com/documentation/code_diagnostics/main_thread_checker) is a tool that detects when AppKit/UIKit APIs are called on a background thread (which often leads to unexpected behavior). By default, the Main Thread Checker is enabled in your app's scheme when you debug your app using Xcode. 61 | -------------------------------------------------------------------------------- /Code Style/Code Style.md: -------------------------------------------------------------------------------- 1 | # Code Style 2 | 3 | ## Linting 4 | 5 | We make heavy use of [SwiftLint](https://github.com/realm/SwiftLint) to inform our code style decisions. Each project starts out with a copy of our [standardized .swiftlint.yml configuration file](https://gist.github.com/tylermilner/f33e33e3b4f23d8c6b2fdd4f87af98a1), which keeps things pretty close to default, but cranks the warnings up to the max by enabling almost all of the opt-in rules. This helps to ensure a consistent baseline structure of projects across teams. 6 | 7 | Anecdotally, we have found that when we use SwiftLint in a pedantic manner, we end up with a code base with zero or near-zero crashes, and a significantly lower number of abnormal behavior bugs. Most of these can be attributed to not allowing for force-unwrapped variables, but there are other elements that SwiftLint has helped us discover early in development. 8 | 9 | Rather than require all developers to install SwiftLint locally onto their machine, we [use Cocoapods to install SwiftLint](https://github.com/realm/swiftlint#using-cocoapods) into projects. This allows SwiftLint to be managed in the `Podfile`, just like any other dependency, and ensures all developers are using the same version. 10 | 11 | ## Readability 12 | 13 | * Code is written for humans, not computers. Less clever, more verbose code is preferred to more clever, concise code. 14 | * Aim for one class or idea per file. Long files that span several concepts are more difficult to parse compared to concise files that each focus on a particular idea. 15 | 16 | ## Consistency 17 | 18 | > When in Rome, code as the Romans do. 19 | 20 | Much of the code that we maintain wasn't initially written by us (or was written before these guidelines were in place). As such, do your best to adopt the style present in your particular project. The idea is that a consistent style (even if not ideal) is better than a mishmash of different styles. Ideally, some time should be devoted to modernizing the project's style, but that's not always possible depending on deadlines and where you are in the project lifecycle. 21 | 22 | ## Comments 23 | 24 | * Because most iOS code is fairly verbose and "self documenting", comments should focus on details around intention, rather than implementation (i.e. explaining *why* vs explaining *what*/*how*). 25 | * Make use of the Xcode `⌥` + `⌘` + `/` (option + command + slash) shortcut to automatically generate system-style documentation comments. 26 | * Be liberal with these for `public` methods and properties in framework targets. 27 | * Document complex business logic and algorithms, ideally pointing to "living" documentation that might exist in some other system as well (e.g. Confluence, wiki, etc.). 28 | * Document any "magic numbers". An arbitrarily hard-coded number should have a comment explaining what the number does or how it came to be. 29 | * Hacks and workarounds should be documented. If there is any supporting information online like a post on Stack Overflow or a blog, include a summary of the important content along with the URL. 30 | * Make use of the `///` comment above class/struct/enum/etc. definitions to provide a brief description of the purpose of the object. The `///` comment allows Xcode to show the content in the info popup that appears when `⌥`-clicking (option + click) a type. 31 | * Deliberate deviations from standards should include a comment explaining why it was necessary. 32 | 33 | Comments are very important when working on teams, since your solutions may not be obvious to everyone. A general rule of thumb is that if you think you may have difficulty remembering how something works or why you decided upon a specific solution a year from now, **comment it**! 34 | 35 | ## Tabs vs Spaces 36 | 37 | We [embrace the Xcode defaults](../Development%20Environment/Development%20Environment.md#embracing-the-defaults) for just about everything, which means we use **spaces** since the Xcode default is 4 spaces for every "tab". 38 | 39 | ## Naming 40 | 41 | Following industry standards, we use `camelCase` as the main naming convention for everything: 42 | 43 | * Classes and protocols should always start with an uppercase letter. 44 | * Methods, variables, and functions should start with a lowercase letter. This includes [acronyms](./Swift%20Style%20Guide.md#acronyms-in-variable-names). 45 | 46 | ### Naming Classes 47 | 48 | There is an age-old saying in the software industry: 49 | 50 | > There are only two hard things in Computer Science: cache invalidation and naming things. 51 | 52 | Indeed, the problem of naming things hasn't gotten any easier. With generic names like manager, provider, coordinator, service, controller, etc., how do you know when to use the right one? We prefer the following convention: 53 | 54 | * **Service** - an object that performs a task without persisting any state (i.e. a `NetworkService` executes a request and calls a completion block). 55 | * **Controller** - an object that performs a task while persisting state (i.e. user object, authentication object, etc.). 56 | * **Coordinator** - an object that manages the flow between view controllers/screens (i.e. `JobsCoordinator` manages the navigation between a jobs list, details, etc.). 57 | * **Provider** - an object which creates and vends other objects when passed the required dependencies. 58 | * **Manager** - anything that doesn’t really fit the other four options. Although there is probably still a more descriptive name you can use (see Ben Sandofsky's [The Trouble with Manager Objects](https://sandofsky.com/blog/manager-classes.html)). 59 | 60 | ## Swift Style Guide 61 | 62 | For any styling concerns not covered by our SwiftLint configuration, refer to our [Swift Style Guide](./Swift%20Style%20Guide.md). 63 | -------------------------------------------------------------------------------- /Branching Strategy/Branching Strategy.md: -------------------------------------------------------------------------------- 1 | # Branching Strategy 2 | 3 | Most projects at Bottle Rocket follow the [Gitflow](https://nvie.com/posts/a-successful-git-branching-model/) pattern for branching. More recently, some projects have experimented [trunk-based development](https://trunkbaseddevelopment.com/), but Gitflow is still the standard for all new projects. 4 | 5 | It's also worth noting that our open source repos more closely follow [GitHub Flow](https://guides.github.com/introduction/flow/). This is essentially "Gitflow lite" where `master` is used as the primary development branch. We found that trying to follow true Gitflow with a separate `develop` branch added an additional layer of redundant pull requests when preparing a release. [This article](https://hackernoon.com/a-branching-and-releasing-strategy-that-fits-github-flow-be1b6c48eca2#98c4) aligns with our current approach for [our open source projects](https://github.com/BottleRocketStudios). 6 | 7 | ## Branching Models 8 | 9 | ### Gitflow 10 | 11 | Gitflow is a branching model that organizes a repository into distinct branches, each with a unique purpose. It facilitates the ability to generate quality software via a stable release and development process. 12 | 13 | ![Gitflow branching strategy diagram](Images/git-flow.png) 14 | *[Diagram created by Vincent Driessen](https://nvie.com/posts/a-successful-git-branching-model/)* 15 | 16 | #### Branches 17 | 18 | ##### master 19 | 20 | * Each commit in `master` should reflect a stable release and be tagged with that release's version number. 21 | * The latest commit in `master` reflects the build currently available in the App Store. 22 | * Until the first version of the app is released, `master` will usually reflect the initial state of the repository. 23 | 24 | ##### develop 25 | 26 | * The main development branch, which reflects the most current non-shipped version of the app. 27 | * Serves as the main integration point for new features/fixes as they are developed. 28 | * The CI system points to `develop` by default, with QA performing the bulk of their testing on builds created from this branch. 29 | 30 | ##### feature/* 31 | 32 | * Represents a feature or fix actively in development (e.g. `feature/loginScreen`). 33 | * Initially branched from `develop` to start feature work. 34 | * As the feature is developed, `develop` should be merged into `feature/*` periodically to keep the feature branch up-to-date with the latest changes. 35 | * Once the feature is complete, the `feature/*` branch is merged into `develop` (via pull request) and then removed. 36 | 37 | ##### release/* 38 | 39 | * Represents a release candidate (e.g. `release/2.1.0`). 40 | * Initially branched from `develop` to start a code freeze process. 41 | * Only bugfixes should be merged into the `release/*` branch (i.e. no merging from `feature/*` branches). 42 | * Merged into `develop` periodically so that fixes get incorporated into new feature development. 43 | * Merged into `master` and tagged using [semantic versioning](https://semver.org/) once a release is complete (i.e. the app/update is live). 44 | * Merged into `develop` one last time before being removed. 45 | 46 | ##### hotfix/* 47 | 48 | * Represents a "hotfix" update used to quickly address issues with the live app. 49 | * Initially created from the corresponding version number tag on the `master` branch (e.g. `2.1.0` --> `hotfix/2.1.1`). 50 | * Allows for production issues to be resolved quickly without interrupting active feature development or having to manually cherry-pick or re-apply fixes from `develop`. 51 | * Follows the same process as a `release/*` branch to keep `develop` up-to-date with any fixes and finalize the hotfix release. 52 | 53 | #### Key Stages 54 | 55 | 1. The repo is created with only a `master` branch, by default. 56 | 2. A `develop` branch is created from `master`. 57 | 3. `feature/*` branches are created from `develop`. 58 | 4. When a feature is complete, it's merged into `develop` (via PR) and then removed. 59 | 5. To initiate a release, a `release/*` branch is created from `develop`. 60 | 6. When a release is complete, `release/*` is merged into `develop` and `master`, tagged, and then removed. 61 | 7. If an issue in `master` needs to be resolved, a `hotfix/*` branch is created from `master`. 62 | 8. When the hotfix release is complete, `hotfix/*` is merged into `develop` and `master`, tagged, and then removed. 63 | 64 | ### Trunk-Based Development 65 | 66 | Trunk-based development is a branching model that revolves around a single main "trunk" branch. The main idea is for all developers to integrate their in-progress feature work as quickly as possible to avoid complex merges and ensure success of the continuous integration and continuous delivery mechanisms. Because feature work merged into `trunk` may not be 100% complete, a "feature toggle" configuration system is often employed to ensure that only completed features are accessible in releases. 67 | 68 | ## Commits 69 | 70 | **Commit early, commit often!** Try to remember to push any pending commits to the remote repo at the end of every day so that all in-progress work is backed up. 71 | 72 | Commit messages should be detailed and helpful - avoid anything that's not a complete sentence. You should aim to tell a story in the commit message (i.e. what was broken and how it was fixed). A good rule of thumb to follow is to begin commit messages with a verb so that the message completes the phrase "This commit...". 73 | 74 | ## Merging 75 | 76 | All merges into `develop`/`trunk`, `release/*`, and `hotfix/*` should happen via pull requests. This ensures that all code gets reviewed at some point before it's shipped. For more information, see [Code Review Standards](../Code%20Review%20Standards/Code%20Review%20Standards.md). 77 | 78 | ## Deleting Branches 79 | 80 | Branches should be deleted after they've been merged into `develop` or `master`. This keeps the repository clean and makes it clear where active development is occurring. 81 | -------------------------------------------------------------------------------- /Code Review Standards/Code Review Standards.md: -------------------------------------------------------------------------------- 1 | # Code Review Standards 2 | 3 | ## Introduction 4 | 5 | Code reviews are critical to ensuring quality and efficiency in our software development process. We aim for 100% code contribution review coverage by utilizing pre-merge pull requests (PRs) to review code as it is integrated into the main development branches of the repository. 6 | 7 | ## Goals 8 | 9 | There are two main goals with respect to code reviews: 10 | 11 | * Facilitate a shared understanding of the codebase and foster a collective ownership of the project. 12 | * Improve the quality of the code as much as possible (i.e. 2/3/4 brains are better than 1). 13 | 14 | As you probably know, software issues cost more to fix later in the software development lifecycle. Code reviews serve as the first line of defense, helping to find and correct problems as early as possible. 15 | 16 | ## Guidelines and Considerations 17 | 18 | * Reviewing code is of equal priority to writing code. 19 | * Be thorough - read, understand, and think about the code you're reviewing. Are there logic errors? Did they follow the coding style guidelines? Could this be simplified? 20 | * **DO NOT** skim through and blindly "approve" PRs. 21 | * Code reviews should be small and cohesive. Having many small, focused reviews instead of one large review helps to ensure quality reviews. Keep this in mind when developing large features. It's perfectly acceptable to create PRs on your feature branch as you go rather than waiting until the end and doing a large PR into the main development branch. 22 | * Provide context/instructions as necessary. If there is a GIF or screenshot that will help the reviewer understand what they're looking at, include it. 23 | * When suggesting improvements, be generous with code examples to ensure that your feedback is as clear as possible. Checking out the author's branch and experimenting is encouraged! 24 | * Aim to respond to all code review requests within 24 hours. 25 | * All members of the team should all be required to review each PR before it's merged. This greatly improves project visibility, provides training opportunities, and fosters a shared ownership of the codebase. 26 | * If not possible due to team size, at the very least aim for 75% of the team to have completed a review before allowing merging. A PR should always require review from all of the lead developers as well. 27 | * Be respectful. 28 | * Criticism is valuable, but only if it's constructive. Suggest improvements instead of only pointing out flaws. 29 | * Along the same line of thinking, give the reviewer the benefit of the doubt. If something sounds harsh, they most likely didn't mean it that way. Embrace offline brainstorming sessions or pair programming when necessary. 30 | * Be vocal. 31 | * If you see a better way of doing something, then speak up! It should be **very rare** that you can't think of at least one small improvement that could be made to the code. 32 | * Ask for clarification when needed - either as the reviewer trying to understand the code or the author trying to understand a reviewer's feedback. 33 | 34 | ### Quality of Code 35 | 36 | Overall, reviews should focus on the quality of the code itself (i.e. logic errors, force unwrapping, etc.) over trivial issues (i.e. tabs vs spaces, etc.). The development process should enable this though things like linters, static analysis, continuous integration, etc. so that the code is as clean as possible before humans start looking at it. 37 | 38 | ### Commented Out Code 39 | 40 | PRs should not contain commented out code. It adds clutter and often doesn't get cleaned up once it's merged. As long as the commented out code existed in at least one commit, we can always use source control to restore it if necessary. 41 | 42 | ## Pull Request Process 43 | 44 | Most of our projects utilize the PR functionality of [Bitbucket Cloud](https://bitbucket.org/) so this PR process is tailored to that experience. However, other PR mechanisms like [GitHub](https://github.com/) or [Bitbucket Server](https://www.atlassian.com/software/bitbucket/server) will generally follow the same process. 45 | 46 | ### Pre-PR Checklist 47 | 48 | Before creating the pull request, make sure to do the following: 49 | 50 | 1. Review your own code. Ideally, you're doing this with each commit that you make in addition to a final look when previewing the PR. 51 | 2. Integrate the latest changes by merging `develop` into your `feature/*` branch. 52 | 3. Make sure to address any outstanding `TODO`s or commented out code that you intended to clean up. 53 | 4. Make sure there are no new compilation warnings or errors. Don't break the build! 54 | 5. Make sure all of the unit tests pass. 55 | 56 | ### Creating the PR 57 | 58 | 1. Author creates a PR from their `feature/*` branch into `develop`. Most of the time the default title and review description based on commit messages should be sufficient, but any additional context is welcome. 59 | 2. The repository should be configured to automatically add default reviewers to each PR, but if there is anyone missing, the author should make sure to add them manually. 60 | 3. If not previewed during the PR creation process, the author should immediately perform a self-review after the PR is created to spot and fix any final details before others start reviewing. 61 | 62 | ### PR Feedback Process 63 | 64 | 1. Reviewers review the code, adding comments and suggestions for improvements. 65 | 2. The author implements the suggested feedback or starts a discussion in order to get clarification or provide an alternate opinion. 66 | 3. Once satisfied with the implemented improvements (if any), the reviewer "approves" the PR. 67 | 68 | ### Merging the PR 69 | 70 | 1. Once all required reviewers have approved the PR, the author merges the PR. 71 | 2. Once merged, the `feature/*` branch can be removed. 72 | 3. After merging, the author should build and run the app to verify functionality one last time. This is especially important if there were merge conflicts that had to be resolved before the merge. 73 | 4. After merging, the author is also responsible for updating the relevant ticket(s) in the sprint board as well. 74 | -------------------------------------------------------------------------------- /Dependency Management/Dependency Management.md: -------------------------------------------------------------------------------- 1 | # Dependency Management 2 | 3 | External dependencies should be added to your project only after careful consideration. Always attempt to build the solution yourself by building on top of Apple's excellent frameworks, rather than immediately resorting to third party libraries. 4 | 5 | Software built with many dependencies starts to feel a bit like "software engineering with duct tape" and will become a maintenance nightmare down the road. Simply put, fewer dependencies leads to a better understanding of the code and gives you more control over its evolution over time. 6 | 7 | ## CocoaPods 8 | 9 | [CocoaPods](https://cocoapods.org/) is the preferred mechanism for managing dependencies in our iOS apps. 10 | 11 | ### Checking in the `Pods` folder 12 | 13 | In order to keep repository size as minimal as possible, most of our apps do not add the `Pods` folder to source control (i.e. `Pods/` is an entry in the project's `.gitignore`). An exception is made for apps that: 14 | 15 | * Primarily deal with sensitive data or financial transactions. For these apps, we will check in the `Pods` folder so that we can more easily audit the changes made to third party libraries as they are updated over time. 16 | * Require a non-BR build environment. Some client build environments don't allow network connections during the build process so all pod sources need to be present in the repository at build-time. 17 | 18 | ### Versioning Pods 19 | 20 | The `Podfile.lock` should always be checked in to source control, as it contains the versioning information for the dependencies specified in the `Podfile`. In general, it's not necessary to define explicit versions in your `Podfile` because the `Podfile.lock` will take care of this for you. In many cases, locking down your `Podfile` with explicit version numbers will make it more difficult for you to quickly update the dependencies using the `pod update` command. 21 | 22 | In summary, we prefer `Podfile` entries like this: 23 | 24 | ```ruby 25 | pod 'Hyperspace' 26 | ``` 27 | 28 | We discourage `Podfile` entries like this: 29 | 30 | ```ruby 31 | pod 'Hyperspace', '2.0.0' 32 | ``` 33 | 34 | If you really do feel you have a need to lock down your dependencies in the `Podfile`, the `~>` operator can be a good middle ground, since it will still allow `pod update` to work until the next major/minor version is released. In the example below, `pod update` would continue to update the dependency up until version `3.0`: 35 | 36 | ```ruby 37 | pod 'Hyperspace', '~> 2.0' 38 | ``` 39 | 40 | Though `pod update` can make updating dependencies a breeze, you should always take an extra few minutes to read the release notes for the dependencies being updated. Even if there are no obvious syntax changes, it is generally a good idea to know how the dependencies you rely on change over time. 41 | 42 | ### Understanding `pod` Commands 43 | 44 | Knowing when to use `pod install` vs `pod update`, or even what the differences between the commands are can be tricky. In summary: 45 | 46 | * `pod install` 47 | * Fetches and installs the dependencies listed in the `Podfile` according to the versions listed in the `Podfile.lock`. This is the main command you'll use to install dependencies when you initially check out a repository as well as when you add or remove dependencies from the `Podfile`. 48 | * If no `Podfile.lock` is present, then the latest available versions of each dependency will be used (and a new `Podfile.lock` generated). 49 | * `pod update` 50 | * Updates all dependencies, respecting any version locks present in the `Podfile`. For example, if you lock a dependency down to version `2.0.0`, but version `3.1.0` is available, then Cocoapods will continue to use version `2.0.0`. This is the reason we recommend letting the `Podfile.lock` handle versioning rather than manually defining version locks in the `Podfile`. 51 | * `pod update ` 52 | * Updates a specific dependency, again respecting any version locks present in the `Podfile`. 53 | * `pod repo update` 54 | * Updates your local copy of the [Cocoapods master repo](https://github.com/CocoaPods/Specs). You need to run this command periodically to make sure your local Cocoapods installation is aware of the latest versions of pods available during `pod install` and `pod update` commands. 55 | * `pod deintegrate` 56 | * Completely removes all traces of Cocoapods from your project. This really only needs to be run when there is a problem since it will rework your project's structure, which can lead to nasty merge conflicts. 57 | 58 | ## Carthage 59 | 60 | [Carthage](https://github.com/Carthage/Carthage) is another popular dependency management solution, but we rarely make use of it at Bottle Rocket. It's typically only used when required by a third party integration. 61 | 62 | ## Manual Integration 63 | 64 | Manually integrating third party libraries and SDKs into the Xcode project is only done as a last resort. Some potential challenges you might face when manually integrating libraries include: 65 | 66 | * Updating Xcode project settings, which can become a source of technical debt when the library is removed or its integration method altered. 67 | * Figuring out library version numbers. There's no standard for how pre-baked SDKs and frameworks document their version number - sometimes it's in the file name, sometimes it's a constant inside a header file, sometimes it's a method called at runtime, and sometimes it's not included at all. 68 | * Temptations to modify the library code directly (assuming raw source code is available). We try to avoid ever modifying third party code as it can lead to maintenance headaches as newer versions of the library are released. 69 | 70 | ## Use of Open Source Software (OSS) 71 | 72 | **Never use any GPL licensed component**! The GNU General Public License (GPL) is incompatible with the iOS App Store's terms and service agreement. Use of GPL components can, and most likely will, result in your app's rejection from the App Store. 73 | 74 | In general any OSS that uses the MIT or BSD license is fine for our use. But before actually using it in a project, we require approval from the tech lead or the director of software development. 75 | -------------------------------------------------------------------------------- /Interface Building/Interface Building.md: -------------------------------------------------------------------------------- 1 | # Interface Building 2 | 3 | ## Interface Builder vs Code 4 | 5 | Interface Builder is the preferred mechanism for creating UI components, rather than creating UI from scratch in code. There are several reasons for this: 6 | 7 | * Less code to write and maintain. 8 | * No need to bring in a third party solution to make manually writing constraints tolerable. 9 | * Easier to get a visual sense of the component, especially when utilizing size classes. This also allows you to iterate faster. 10 | 11 | ### Objects in Interface Builder 12 | 13 | It's also possible to include objects inside of storyboards or xibs: 14 | 15 | ![Interface Builder object](Images/interface-builder-object.png) 16 | 17 | This does potentially reduce the need for some boilerplate code in your view controller, but we **do not** recommend instantiating objects in Interface Builder for the following reasons: 18 | 19 | * It's not a standard practice in the iOS community. 20 | * Interface Builder is primarily UI-focused. Using it to instantiate non-UI objects doesn't make much sense. 21 | * It makes proper dependency injection almost impossible, often negating the code reduction benefits of instantiating the object in Interface Builder in the first place. 22 | 23 | ## Storyboards vs Xibs 24 | 25 | ### When to use storyboard vs xib 26 | 27 | Storyboards should be preferred to xibs for things like: 28 | 29 | * Creating view controllers 30 | * Creating table/collection view prototype cells 31 | * Extra views that are programmatically inserted/managed in the view hierarchy (via the [scene dock](https://medium.com/if-let-swift-programming/interface-builder-scene-dock-extra-views-26cef5fe363b)) 32 | 33 | For the most part, xib use should be restricted to views that are common to multiple screens within an app. Generic table or collection view cells are a good example of this. 34 | 35 | ### Dealing with merge conflicts 36 | 37 | In general, large storyboard/xib merge conflicts can be avoided if a little bit of thought is put into the distribution of work: 38 | 39 | * Don't allow multiple people to modify UI on the same screen in the same storyboard at the same time. 40 | * Don't be afraid to have multiple storyboard files. Group related screens together by storyboard and make use of [storyboard references](https://useyourloaf.com/blog/refactoring-with-storyboard-references/) when necessary. 41 | 42 | ## Auto Layout 43 | 44 | ### vs Springs and Struts 45 | 46 | Prefer Auto Layout to springs and structs or manual frame math. With the amount of variation in screen sizes today, there's a good reason Apple moved us away from this a long time ago! This also applies to performing animations. Many UI animations can be implemented by manipulating layout constraints, and these will be much safer than with direct frame math. 47 | 48 | ### Creating Constraints Programmatically vs Interface Builder 49 | 50 | Auto Layout constraints should be configured in Interface Builder whenever possible. Creating constraints in code should be an absolute last resort. And when necessary, prefer using the highest level of abstraction available to you: 51 | 52 | * [Stack Views](https://developer.apple.com/documentation/uikit/uistackview) - you should start here since it handles the constraint creation for you. 53 | * [UtiliKit](https://github.com/BottleRocketStudios/iOS-UtiliKit/blob/master/Sources/UtiliKit/General/UIView%2BExtensions.swift) - we have several convenience methods via an extension on `UIView`. If you feel there is something missing, please open a pull request! 54 | * [Layout Anchors](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html#//apple_ref/doc/uid/TP40010853-CH16-SW5) - this gives you a lot of control, but still keeps things relatively high-level. 55 | * [`NSLayoutConstraint`](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/ProgrammaticallyCreatingConstraints.html#//apple_ref/doc/uid/TP40010853-CH16-SW8) - more verbose than layout anchors, but can be necessary sometimes. 56 | * [Visual Format Language](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/VisualFormatLanguage.html) - there shouldn't be a need to write constraints this way. 57 | 58 | Note the lack of recommendation for using third party constraint libraries. While some of them do manage to get the constraint syntax to be quite concise, we find that it's best to just stick with Apple's technologies and avoid adding another dependency. 59 | 60 | Also make sure to keep constraint creation and configuration code tucked away into its own method(s). Avoid polluting high-level methods like `viewDidLoad()` with a bunch of constraint setup or management. 61 | 62 | ### Auto Layout Warnings and Errors 63 | 64 | These should be resolved as soon as they appear in your debugger window. If your layout is causing these kinds of warnings and errors, realize that they may start a cascading effect that will prohibit your app from rendering properly even if everything looks OK at first. Along those same lines, even if everything is working properly today, know that future iOS SDK updates could cause your app to have problems down the road. 65 | 66 | The reduction of [run-time console noise](../Build%20Warnings%20and%20Errors/Build%20Warnings%20and%20Errors.md#console-noise) is also a huge benefit to taking care of these issues before they become real problems. Consider setting up some [project-wide breakpoints](../Build%20Warnings%20and%20Errors/Build%20Warnings%20and%20Errors.md#project-breakpoints) to help your team identify these errors early. 67 | 68 | ### Named Constraints 69 | 70 | Whenever possible, provide a name for your layout constraint; this will make locating it in the debugger much simpler. 71 | 72 | ## Sensible Defaults 73 | 74 | When creating interfaces, either programmatically or in Interface Builder, it is good to give fields and controls sensible default values. It is possible that when there, in the event of an error or a lost network connection, a user might see values that were meant to be replaced by live data, so the contents of those fields need to make sense. 75 | 76 | More often than not, the best default value is blank. If things go completely wrong the worst case scenario is an empty screen as opposed to nonsense values or meaningless stub text. 77 | 78 | When blank doesn’t work, simple descriptions or “loading...” will often do, but bear in mind that changing some text from “loading...” to it’s proper contents doesn’t animate well. It would be better to just use an activity indicator and go from blank to populated. 79 | 80 | For collection view and table view cells, it is a good idea to blank out any labels or image views in `prepareForReuse()`. 81 | -------------------------------------------------------------------------------- /Project Structure/Project Structure.md: -------------------------------------------------------------------------------- 1 | # Project Structure 2 | 3 | ## Xcode Folder Structure 4 | 5 | There are typically two schools of thought when it comes to folder structure - "is a"-based and feature-based. 6 | 7 | ### "Is-a"-based 8 | 9 | "Is-a"-based refers to grouping classes according to their type (e.g. Models, Views, View Controllers, etc.). 10 | 11 | ### Feature-based 12 | 13 | Feature-based refers to grouping classes according to the feature or screen they belong to. 14 | 15 | ### Hybrid structure 16 | 17 | We prefer a hybrid, but predominantly feature-based folder structure. 18 | 19 | #### Keep top-level folders feature-based 20 | 21 | The top-level folders typically follow a feature-based structure for the following reasons: 22 | 23 | * Folder structure grows organically over time instead of needing to pre-define several folder types at project creation. 24 | * Easier to onboard new devs. Related feature code is grouped together. 25 | * Works better with Xcode's project navigator. You don't have to open/expand several sets of nested folders to see all of the building blocks that make up a screen or feature. 26 | 27 | #### Use "is-a"-based structure for subfolders within a feature 28 | 29 | Within a given feature or screen, "is-a"-based structure can be used to provide some additional organization (e.g. cells, controllers, models that are within a certain screen). You certainly don't want to have dozens of files sitting loosely in a single folder. Use subfolders to keep things organized at the feature-level. 30 | 31 | #### Use a `Shared` folder for features used on multiple screens 32 | 33 | For classes that are shared across multiple screens, a top-level `Shared` folder is used with "is-a"-based organization for its subfolders. 34 | 35 | #### Utilize Xcode's default structure 36 | 37 | In general, leave the files that Xcode generates with new project creation where they are. This can be helpful for the following reasons: 38 | 39 | * Keeps the `AppDelegate`, `SceneDelegate`, and `App` (SwiftUI) at the top-level 40 | * As the entry point for your application, it's nice to be able to quickly dive in without having to expand a lot of folders. 41 | * Keeps the `Assets.xcassets` (Asset Catalog) at the top-level 42 | * This makes it easy to get to your image resources from anywhere. 43 | * Keeps the `Info.plist` at the top-level 44 | * Again, this makes it easy to get to from wherever you are in the project. Moving this file also requires updating the project's build settings. 45 | 46 | ### Keeping the file system in-sync 47 | 48 | It's important to keep things organized at the file-level as well. Ideally your Xcode project structure should match the folder structure on the file system. This makes it easy to find things when browsing your repository in a web browser or file browser. Luckily, Xcode 9+ now does this by default. 49 | 50 | #### Synx 51 | 52 | Although old, Venmo's [Synx](https://github.com/venmo/synx) can help to automatically reorganize your folder structure to match your Xcode project structure. Another neat feature is that you can use the `--prune` flag to be alerted to orphaned files on your file system that are not referenced in the Xcode project. This makes Synx quite useful when inheriting an unorganized codebase or as a general maintenance/cleanup tool for older projects. Beware that extra care must be taken for [projects with localization](https://github.com/venmo/synx/issues/68). 53 | 54 | ## Target Structure 55 | 56 | When creating a new Xcode project, you typically get the following targets automatically: 57 | 58 | * `
` 59 | * `
`Tests 60 | * `
`UITests 61 | 62 | In order to facilitate the adoption of app extensions, we will usually create a "Kit" framework target that contains shared business logic, models, screens, views, etc. that are, or could conceivably be, used in an extension target. 63 | 64 | * `
`Kit 65 | * `
`KitTests 66 | 67 | Of course, any extensions developed will also have their corresponding targets and test targets: 68 | 69 | * `` 70 | * ``Tests 71 | 72 | Additional framework targets can be created for things like: 73 | 74 | * Custom networking layer 75 | * Complex data/model layer 76 | * etc. 77 | 78 | ## Typical Project Structure Example 79 | 80 | Starting with Xcode project file as the root, your folder structure should look something like the following: 81 | 82 | ``` 83 | 84 |
85 | Resources 86 | Fonts 87 | ... 88 | Screens 89 | 90 | 91 | ... 92 | Shared 93 | Alerts 94 | Analytics 95 | Constants 96 | Controllers 97 | Environment 98 | Extensions 99 | Logging 100 | Managers 101 | Models 102 | Storyboards 103 | Views 104 | ... 105 | AppDelegate.swift 106 | Assets.xcassets 107 | Info.plist 108 |
109 |
110 | 111 | 112 | ``` 113 | 114 | ![Xcode project structure](https://github.com/tylermilner/ProjectStructureExample/raw/master/Images/project-structure-example.png) 115 | 116 | You can also reference our [example Xcode project](https://github.com/tylermilner/ProjectStructureExample). 117 | 118 | ## Colors, Fonts, and Images 119 | 120 | ### Colors 121 | 122 | #### `UIColor` (UIKit) or `Color` (SwiftUI) Extension 123 | 124 | In order to keep all color information contained in a single spot, all of your app's colors should live as `static let` properties inside an extension on `UIColor` or `Color`. You shouldn't be manually instantiating `UIColor` or `Color` objects elsewhere in your codebase. 125 | 126 | It can also be helpful to add a `///` documentation comment above each property describing the red, green, blue, and alpha values for the color. This allows you to option-click a color's name throughout your codebase to quickly get a sense for a color's RGBA values. Keep in mind that these comments will need to be updated over time, as you update the colors themselves. 127 | 128 | #### Color Assets 129 | 130 | Xcode 9 introduced named colors in asset catalogs. This is our preferred approach to setting up colors in a project since it allows you to define a palette of colors once and use them both programmatically and in Interface Builder. 131 | 132 | Again, we make these colors available in code via the `UIColor` or `Color` extension. Keep in mind that the `UIColor(named:)` initializer is only available in iOS 11+ so you may need to make use of a `#available(iOS 11.0, *)` attribute to use the new API while falling back to another initializer for older OS versions. The `Color(_:bundle:)` initializer is available in iOS 13+. 133 | 134 | #### Color Literals 135 | 136 | If supporting versions of iOS prior to iOS 11, we prefer color literals to the older `UIColor(red:green:blue:alpha:)` initializer. 137 | 138 | ### Fonts 139 | 140 | #### `UIFont` (UIKit) or `Font` (SwiftUI) Extension 141 | 142 | In order to keep all font information contained in a single spot, all of your app's fonts should live as `static let` properties inside an extension on `UIFont` or `Font`. You shouldn't be manually instantiating `UIFont` or `Font` objects elsewhere in your codebase. 143 | 144 | ### Images 145 | 146 | #### Image Resources 147 | 148 | * All images should live inside of the main `Assets.xcassets` asset catalog. Each Target may have its own asset catalog. 149 | * Images should be organized and grouped into subfolders according to some criteria (category/screen/feature/etc.). 150 | * Images should have a consistent naming scheme (e.g. icons prefixed with `ic_`, etc.). Work with your designer to implement a scheme if they aren't already doing this by default. 151 | 152 | #### Image Literals 153 | 154 | While they don't read that great in code, we prefer using image literals because it helps to remove some of the "stringly-typedness" of the `UIImage(named:)` initializer. Another option is to create `static let` properties inside an extension of `UImage` (UIKit) or `Image` (SwiftUI). 155 | 156 | #### SF Symbols 157 | 158 | We prefer to use SF Symbols over bringing in a similar image asset. [SF Symbols](https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/) provides a set of over 3,100 consistent, highly configurable symbols provided by Apple. They automatically align with text in all weights and sizes. They are great for representing a variety of UI elements such as navigation bars, toolbars, tab bars, context menus, and widgets. It is even possible to create custom symbols. SF Symbols are available in iOS 13+, macOS 11+, watchOS 6+, and tvOS 13+. 159 | -------------------------------------------------------------------------------- /Localization and Internationalization/Localization and Internationalization.md: -------------------------------------------------------------------------------- 1 | # Internationalization and Localization 2 | 3 | ## Internationalization vs Localization 4 | 5 | There is a slight distinction between internationalization and localization: 6 | 7 | * Internationalization - the process of making your app able to adapt to different languages, regions, and cultures. 8 | * Localization - the process of translating your app into multiple languages. 9 | 10 | You must first internationalize your app before you can take advantage of localization. Together, internationalization and localization allow you to better serve users around the globe by making your app appear as though it was built natively for whatever language and region it's being used in. 11 | 12 | ## Localized Strings 13 | 14 | ### Use `NSLocalizedString` or `String(localized:)` (iOS 15+) for all user-facing copy 15 | 16 | Using [`NSLocalizedString()`](https://developer.apple.com/documentation/foundation/nslocalizedstring) or [`String(localized:)`](https://developer.apple.com/documentation/swift/string/3867985-init?changes=latest_minor) from the start of a project will ensure that supporting additional languages can be done in the future with minimal effort. 17 | 18 | ### Use quality comments when invoking `NSLocalizedString()` of `String(localized:)` 19 | 20 | The `comment` parameter of `NSLocalizedString()` or `String(localized:)` is used by translators when you add additional languages to your app. Adding some context here can make the job of the translators easier and help the overall quality of the translations. You should describe the interface element, the context, and what each variable is. Simply echoing the string itself is not recommended. The translator may not have access to the UI so it is important to provide good comments. 21 | 22 | #### Do 23 | 24 | ```swift 25 | NSLocalizedString("Order", comment: "Button: confirms purchase of a book") 26 | String(localized: "Orders", comment: "Title: a list of current book orders") 27 | ``` 28 | 29 | #### Do Not 30 | 31 | ```swift 32 | NSLocalizedString("Order", comment: "Order") 33 | String(localized: "Orders", comment: "Orders") 34 | ``` 35 | 36 | ### Avoid manually editing `.strings` files 37 | 38 | Once you import translations into your app, Xcode will automatically generate a `Localizable.strings` file for the various languages you support. Avoid editing this file by hand since your edits might get blown away the next time you go through the localization export/import process. 39 | 40 | ### Consolidate all user-facing copy into a single enum/file 41 | 42 | Consolidating all user-facing copy into a `LocalizedStrings` enum (inside of a `LocalizedStrings.swift` file) makes it easier to change and audit user-facing copy. 43 | 44 | #### Example `LocalizedStrings.swift` 45 | 46 | ```swift 47 | enum LocalizedStrings { 48 | 49 | // MARK: - General 50 | 51 | enum General { 52 | static let back = NSLocalizedString("Back", comment: "Button: generic back button used throughout the app.") 53 | static let next = NSLocalizedString("Next", comment: "Button: generic next button used throughout the app.") 54 | // ... 55 | } 56 | 57 | // MARK: - Account 58 | 59 | enum Account { 60 | // ... 61 | } 62 | 63 | // MARK: - Home 64 | 65 | enum Home { 66 | static let welcome = NSLocalizedString("Welcome, %@", comment: "Message: home screen welcome. Translations should preserve the '%@' characters.") 67 | // ... 68 | } 69 | 70 | // MARK: - Login 71 | 72 | enum Login { 73 | // ... 74 | } 75 | } 76 | ``` 77 | 78 | #### Example `LocalizedStrings.swift` usage 79 | 80 | ```swift 81 | nextButton.setTitle(LocalizedStrings.General.next, for: .normal) 82 | ``` 83 | 84 | ### Placeholders in localized strings 85 | 86 | When using placeholders in localized strings, it can be helpful to remind translators that the `%@` placeholder should be included in the translations (see the `comment` parameter for `LocalizedStrings.Home.welcome` in the example above). From here, you can use the `String(format:)` initializer to populate the string appropriately: 87 | 88 | ```swift 89 | welcomeLabel.text = String(format: LocalizedStrings.Home.welcome, user.name) 90 | ``` 91 | 92 | ### Utilize `.stringsdict` for pluralization 93 | 94 | Not all languages handle pluralization of words in the same way. **Do not** hard-code pluralization logic into your app to programmatically select a word's plurality. Instead, you should use a [`.stringsdict` file](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/StringsdictFileFormat/StringsdictFileFormat.html) to handle pluralization. 95 | 96 | #### Do 97 | 98 | ```swift 99 | return NSLocalizedString("Purchased %d days ago", comment: "Message: describes when items were purchased") 100 | ``` 101 | 102 | With corresponding `Localizable.stringsdict`: 103 | 104 | ![Localizable.stringsdict example](./Images/localizable-stringsdict-example.png) 105 | 106 | #### Do Not 107 | 108 | ```swift 109 | if daysSincePurchase < 1 { 110 | return NSLocalizedString("Purchased today", comment: "Purchase made today.") 111 | } else if daysSincePurchase < 2 { 112 | return NSLocalizedString("Purchased yesterday", comment: "Purchase made yesterday.") 113 | } else { 114 | return NSLocalizedString("Purchased %d days ago", comment: "Purchase made '%d' days ago.") 115 | } 116 | ``` 117 | 118 | ## Localized Resources 119 | 120 | Asset catalogs are great to organize and manage different types of assets. There are several asset types that support localization. They include: color sets, image sets, symbol sets, watch complications, Apple TV image stacks, and Sprite Atlases. In the asset catalog, click the asset you want to localize. Next, click Localize in the Attributes inspector and select the localizations you want to add. Other resources not supported by asset catalogs can be localized by selecting them in the Project navigator and clicking Localize in the inspector. 121 | 122 | ## Respect the device's locale 123 | 124 | Use the [formatters built-in to Foundation](https://developer.apple.com/documentation/foundation/formatter#overview) to format values according to the device's current locale. Some of the more common ones include: 125 | 126 | * [`DateFormatter`](https://developer.apple.com/documentation/foundation/dateformatter) - localized representations of dates and times. 127 | * [`DateComponentsFormatter`](https://developer.apple.com/documentation/foundation/datecomponentsformatter) - localized representations of quantities of time. 128 | * [`DateIntervalFormatter`](https://developer.apple.com/documentation/foundation/dateintervalformatter) - localized representations of time intervals. 129 | * [`LengthFormatter`](https://developer.apple.com/documentation/foundation/lengthformatter) - localized representations of linear distances. 130 | * [`NumberFormatter`](https://developer.apple.com/documentation/foundation/numberformatter) - localized representations of numeric values (including currency). 131 | * [`PersonNameComponentsFormatter`](https://developer.apple.com/documentation/foundation/personnamecomponentsformatter) - localized representations of a person's name. 132 | 133 | ## Use Auto Layout 134 | 135 | Make use of Auto Layout to ensure that views will automatically adjust for different languages. Follow [Apple's tips](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPInternational/InternationalizingYourUserInterface/InternationalizingYourUserInterface.html#//apple_ref/doc/uid/10000171i-CH3-SW10): 136 | 137 | * **Avoid fixed width constraints.** Text elements with a fixed width will likely be truncated or cropped in some languages. 138 | * **Use intrinsic content size.** This allows built-in elements like `UILabel` and `UITextView` to automatically size themselves according to their content. You can also use this technique for [your own custom views](https://developer.apple.com/documentation/uikit/uiview/1622600-intrinsiccontentsize). 139 | * **Use leading and trailing constraints.** This will allow your views to automatically adjust for right-to-left languages when appropriate. 140 | * **Pin adjacent views together.** This allows views to maintain their spacing between each other when resizing to fit localized text. 141 | 142 | ## Testing Localization 143 | 144 | During development, you can modify your app's scheme to get a sense for how your app will support localization. The "Application Language" drop down has several options for testing a variety of languages as well as pseudolanguages: 145 | 146 | ![Changing app scheme language](Images/testing-localization-scheme-language.png) 147 | 148 | ## Additional Resources 149 | 150 | As always, [Apple's Localization guide](https://developer.apple.com/localization/) is a good hub for learning more about internationalization and localization. 151 | -------------------------------------------------------------------------------- /Accessibility/Accessibility.md: -------------------------------------------------------------------------------- 1 | # Accessibility 2 | 3 | Making your app accessible is all about making your app as user-friendly as possible to users that have different needs. Most commonly, this refers to making your app usable to blind or low vision users. The goal is to make an interface that is predictable and easy to navigate without depending on (at least not completely) visual queues. 4 | 5 | There are [four principles](https://www.w3.org/WAI/WCAG21/Understanding/intro#understanding-the-four-principles-of-accessibility) that contribute to a good accessibility experience: 6 | 7 | * **Perceivability**: information and user interface components must be presentable to users in ways they can perceive. 8 | * **Operability**: user interface components and navigation must be operable. 9 | * **Understandability**: information and the operation of the user interface must be understandable. 10 | * **Robustness**: content must be robust enough to be interpreted reliably by a wide variety of user agents, including assistive technologies. 11 | 12 | ## Accessibility Guidelines 13 | 14 | ### WCAG 15 | 16 | The Web Content Accessibility Guidelines (WCAG) cover a wide range of recommendations for making web content more accessible. While there is no official standard for native mobile apps, we can still use WCAG to inform us on how to make app content more accessible. WCAG categorizes each guideline into one of three levels based on the impact they have on the accessibility experience - A, AA, or AAA. Conforming to the AAA guidelines will provide the most beneficial accessible experience to the most users, but also has the most impact on the visual design of the application. In general, attempting to meet the level A criteria is a step in the right direction for making sure your app provides an acceptable experience. 17 | 18 | #### WCAG 2.0 19 | 20 | WCAG 2.0 was established in 2008 and, until recently, was the standard websites were measured against to determine their accessibility level. There are many [checklists available](https://www.wuhcag.com/wcag-checklist/) which explain what each guideline means and provide tips for adhering to the guideline. 21 | 22 | #### WCAG 2.1 23 | 24 | [WCAG 2.1](https://www.w3.org/TR/WCAG21/) was recently released in June of 2018 and builds upon WCAG 2.0, adding additional criteria that modern applications must take into account when implementing accessibility. 25 | 26 | ## Exploring Accessibility on iOS 27 | 28 | Before diving straight into making your app more accessible, take some time to get familiar with the accessibility settings that iOS provides: 29 | 30 | 1. Open up your Settings app. 31 | 2. Navigate to General --> Accessibility. 32 | 3. Explore and play around. 33 | 34 | ![Some of the accessibility options in iOS](Images/settings-general-accessibility.png) 35 | *Some of the accessibility options in iOS* 36 | 37 | ### Accessibility Shortcut 38 | 39 | In order to try things out on-the-fly, it can be helpful to configure an "Accessibility Shortcut" that you can activate by triple-clicking the home button. The option can be found at the very bottom of the Accessibility screen. Simply tap on the various accessibility options to enable them on the shortcut menu: 40 | 41 | ![Accessibility Shortcut configuration](Images/accessibility-shortcut-options.png) 42 | *Accessibility Shortcut configuration* 43 | 44 | Then, you can triple-click the home button to turn on/off the various accessibility settings without having to constantly navigate into the Settings app: 45 | 46 | ![Accessibility Shortcut configuration](Images/accessibility-shortcut-menu-activated.png) 47 | *Accessibility Shortcut menu activated* 48 | 49 | ### Apple's Accessibility Resources 50 | 51 | Apple provides extensive [developer documentation on accessibility](https://developer.apple.com/accessibility/) that can be helpful to review before you dive into making your app accessible. 52 | 53 | ## Tips and Tricks 54 | 55 | In order to minimize the amount of custom code you need to write and maintain related to accessibility, it is extremely helpful to be aware of decisions that can affect accessibility while you're building your app. 56 | 57 | ### Use standard UIKit components where possible 58 | 59 | Apple's built-in UI elements are accessible out-of-the box. Avoid creating custom UI when the defaults will work (e.g. don't create a "button" from a `UIView` when a standard `UIButton` will do). 60 | 61 | Pay special attention to built-in navigational elements like `UITabBar`, `UINavigationBar`, and `UIToolBar`. New in iOS 11, you can enable your app to allow users to long-press these elements to see a [larger version of the icon and tab name](https://medium.com/bbc-design-engineering/improving-your-apps-accessibility-with-ios-11-db8bb4ee7c9f#ba89). 62 | 63 | ### Implement `UIAccessibilityContainer` for custom UI components 64 | 65 | Custom UI components should implement [UIAccessibilityContainer](https://developer.apple.com/documentation/uikit/accessibility/uiaccessibilitycontainer) methods to make their subviews available to VoiceOver. As of iOS 8, you just need to implement the [`accessibilityElements` property](https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements), populating the array with instances of [`UIAccessibilityElement`](https://developer.apple.com/documentation/uikit/uiaccessibilityelement). 66 | 67 | ### Set titles for images 68 | 69 | By default, VoiceOver will read the name of the image file itself. Make sure to give any `UIButton` that uses an image a proper title and annotate each `UIImageView` appropriately. 70 | 71 | ### Remember to localize accessibility labels 72 | 73 | While accessibility labels aren't visually displayed to the user, they should still be considered a component of your app's overall user-facing copy. Make sure to include them as part of your localization process if your app supports multiple languages. 74 | 75 | ### Support Smart Color Invert 76 | 77 | By default, the "Smart Invert" Display Accommodation setting will invert all of the colors in your app. New in iOS 11, you can now tell the system to skip color inversion for components of your app that it doesn't make sense to invert (such as a profile picture). Simply set the [`accessibilityIgnoresInvertColors` property](https://developer.apple.com/documentation/uikit/uiview/2865843-accessibilityignoresinvertcolors) on your views. See [this article](https://duan.ca/2017/12/20/smart-invert-support-for-you-app/) for more information. 78 | 79 | ### Look at Apple's apps for inspiration 80 | 81 | Apple's apps that ship with iOS are generally made to be very accessible. It can be helpful to reference their apps when you need insight into how to solve a particular accessibility problem. 82 | 83 | ## VoiceOver 84 | 85 | The bulk of your accessibility work will likely be interacting with iOS's built-in screen reader called [VoiceOver](https://www.apple.com/accessibility/iphone/vision/#vision-panel). 86 | 87 | ### Testing with VoiceOver 88 | 89 | Keep the following points in mind when testing your app's VoiceOver support: 90 | 91 | #### VoiceOver reads any text it can find 92 | 93 | Tap everything to see what gets read and what doesn't. 94 | 95 | #### VoiceOver changes how taps work 96 | 97 | Single taps select an element and double taps perform an action. Scrolling is performed using a three finger drag gesture. 98 | 99 | #### VoiceOver reads subviews in order from front to back 100 | 101 | That is, subviews higher in the view hierarchy (i.e. ones at the bottom of the list in Interface Builder) get read first. Make sure the order that things are being read makes sense and adjust subview ordering accordingly. 102 | 103 | #### VoiceOver might read things differently than you would expect 104 | 105 | VoiceOver uses spacing, line breaks, capitalization, and punctuation to try to say things the way they're intended, but it won't always get you the result you want. Adjust accessibility labels accordingly. 106 | 107 | ### Interacting with VoiceOver 108 | 109 | If a feature can't be made accessible without a lot of work, consider adopting an alternate UI when VoiceOver is enabled. This might mean adding: 110 | 111 | * Extra navigation 112 | * Extra or alternate messaging to the user 113 | * A different layout 114 | 115 | #### Checking the state of VoiceOver 116 | 117 | You can query the [`isVoiceOverRunning` property](https://developer.apple.com/documentation/uikit/uiaccessibility/1615187-isvoiceoverrunning) on `UIAccessibility` to check if VoiceOver is currently enabled. 118 | 119 | You can also observe the [`voiceOverStatusDidChangeNotification` notification](https://developer.apple.com/documentation/uikit/uiaccessibility/2865862-voiceoverstatusdidchangenotifica) to be notified when the user turns VoiceOver on or off and adjust your UI accordingly. 120 | 121 | #### Post UIAccessibilityNotifications when appropriate 122 | 123 | If you are implementing a UI that changes frequently or has items that appear and disappear, you can make use of the [`post(notification:argument)` method](https://developer.apple.com/documentation/uikit/uiaccessibility/1615194-post) on `UIAccessibility` to have VoiceOver announce the change. There are many different [accessibility notification types](https://developer.apple.com/documentation/uikit/accessibility/notification_names) so make sure to use the appropriate one for your situation. 124 | 125 | ## Dynamic Type 126 | 127 | Supporting Dynamic Type is another great way to make your app more accessible to low vision users. If you're not using the San Francisco system font, then you'll likely need to work with a designer to define a [Dynamic Type size table](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/typography/#dynamic-type-sizes) for your font styles. From there, getting a [scaled version of your font](https://developer.apple.com/documentation/uikit/uifont/getting_a_scaled_font) according to the user's Dynamic Type preferences is relatively straightforward. WWDC 2017 Session 245 - [Building Apps with Dynamic Type](https://developer.apple.com/videos/play/wwdc2017/245/) is also another great resource to learn more about Dynamic Type. 128 | 129 | If you're targeting iOS 11 and above, Apple also makes it easy for your fonts to [scale automatically](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically). 130 | 131 | ## Testing Accessibility 132 | 133 | For the most part, testing accessibility is a very manual and time consuming process. Luckily, Apple does provide some tools to make testing and debugging accessibility issues easier. 134 | 135 | ### The Accessibility Inspector 136 | 137 | macOS ships with a tool called the [Accessibility Inspector](https://developer.apple.com/library/archive/documentation/Accessibility/Conceptual/AccessibilityMacOSX/OSXAXTestingApps.html) which exposes the information that your app exposes to tools like VoiceOver. Run your app in the simulator and use the Accessibility Inspector to gain insight into the accessibility of your app. 138 | 139 | ![Accessibility Inspector inspection](Images/accessibility-inspector-inspection.png) 140 | *Accessibility Inspector inspection* 141 | 142 | The Accessibility Inspector also provides a built-in auditing tool to help you identify potential accessibility issues. 143 | 144 | ![Accessibility Inspector audit tool](Images/accessibility-inspector-audit.png) 145 | *Accessibility Inspector audit tool* 146 | 147 | Another neat feature of the Accessibility Inspector is the ability to toggle common accessibility options on-the-fly. This can be a huge time saver for testing things like Dynamic Type. 148 | 149 | ![Accessibility Inspector settings](Images/accessibility-inspector-settings.png) 150 | *Accessibility Inspector settings* 151 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Bottle Rocket LLC 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Testing/Testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Testing is a vital aspect of any software development process. At Bottle Rocket, we utilize many testing techniques to ensure we ship extremely high quality software. 4 | 5 | ## Unit Testing 6 | 7 | In its simplest form, a unit test verifies whether a piece of code (a "unit") works as expected. 8 | 9 | ### Types of Unit Tests 10 | 11 | A unit test will usually fit into one of 3 categories, with varying levels of difficulty to write. 12 | 13 | #### Return Value Verification 14 | 15 | This is the simplest type of test to write and lends itself to functional programming techniques. 16 | 17 | * Setup the system under test (SUT). 18 | * Send the SUT a message. 19 | * Verify the returned value is what you expect. 20 | 21 | ##### Example 22 | 23 | Suppose we have a simple `Rectangle` class with a method for checking if a given width and height create a square: 24 | 25 | ```swift 26 | struct Rectangle { 27 | 28 | static func isSquare(width: Int, height: Int) -> Bool { 29 | return width == height 30 | } 31 | } 32 | ``` 33 | 34 | The `static` keyword gives us a clue that this is a good candidate for Return Value Verification. We can reasonably assume that this function is stateless, and any output directly depends on its input rather than some internal state (though that's not always the case). One of our tests might look like this: 35 | 36 | ```swift 37 | func test_5x5rect_isSquare() { 38 | // Arrange 39 | let width = 5 40 | let height = 5 41 | 42 | // Act 43 | let isSquare = Rectangle.isSquare(width: width, height: height) 44 | 45 | // Assert 46 | XCTAssertTrue(isSquare) 47 | } 48 | ``` 49 | 50 | Notice how we are simply passing data into the `isSquare(width:height)` function and observing the output. Another test would pass different values for `width` and `height` into the function and assert on that output. 51 | 52 | #### State Verification 53 | 54 | This is similar to Return Value Verification, but deals with objects that manage some sort of internal "state". 55 | 56 | * Setup the SUT with an initial state. 57 | * Send the SUT a message that should modify that state. 58 | * Call an accessor method to get the state back and verify that it was mutated correctly. 59 | 60 | ##### Example 61 | 62 | Let's say we're working on the authentication flow for our app. We might have `SignInViewController` (just pretend it's a bit more interesting): 63 | 64 | ```swift 65 | class SignInViewController: UIViewController { 66 | // ... 67 | } 68 | ``` 69 | 70 | If our authentication flow is complex, we'd probably like to factor some of the presentation flow logic out into its own class: 71 | 72 | ```swift 73 | class SignInFlowCoordinator { 74 | 75 | private let navigationController: UINavigationController 76 | var currentViewController: UIViewController? { 77 | return navigationController.topViewController 78 | } 79 | 80 | init(navigationController: UINavigationController) { 81 | self.navigationController = navigationController 82 | } 83 | 84 | func startSignInFlow() { 85 | guard navigationController.topViewController == nil else { return } 86 | 87 | let signInViewController = SignInViewController() 88 | 89 | navigationController.pushViewController(signInViewController, animated: true) 90 | } 91 | } 92 | ``` 93 | 94 | From here, we can employ State Verification to make sure the `currentViewController` property behaves as we expect: 95 | 96 | ```swift 97 | func test_startSignInFlow_presentsSignInViewController() { 98 | // Arrange 99 | let navigationController = UINavigationController() 100 | let flowCoordinator = SignInFlowCoordinator(navigationController: navigationController) 101 | 102 | // Act 103 | flowCoordinator.startSignInFlow() 104 | 105 | // Assert 106 | XCTAssertNotNil(flowCoordinator.currentViewController as? SignInViewController) 107 | } 108 | ``` 109 | 110 | #### Behavior Verification 111 | 112 | This is the most difficult and involved type of test to write. Use this technique when you need to test something that performs side effects (i.e. interacts with another underlying object). In this technique, you'll make use of a mock object to verify functionality. 113 | 114 | * Setup the SUT with an initial state, injecting mock object(s) via something like [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection). 115 | * Send the SUT a message. 116 | * Verify the mock object was called correctly. 117 | 118 | ##### Example 119 | 120 | Network requests are a prime example of side effects that you typically don't want to be performed as part of your core unit testing infrastructure. Suppose you're building a networking layer and have created `NetworkService` class as a thin wrapper around `URLSession`. In order to perform proper dependency injection, let's first declare a `NetworkSession` protocol so that we can mock the call to `URLSessions`'s `dataTask(with url:)` method: 121 | 122 | ```swift 123 | protocol NetworkSession { 124 | func dataTask(with url: URL) -> URLSessionDataTask 125 | } 126 | 127 | extension URLSession: NetworkSession { } 128 | ``` 129 | 130 | Notice how we made sure the function name and signature matches with the one declared in `URLSession`. This allows us to create a zero-code extension declaring `URLSession` as conforming to our `NetworkSession` protocol. 131 | 132 | From here, our `NetworkService` class might look something like this: 133 | 134 | ```swift 135 | class NetworkService { 136 | private let session: NetworkSession 137 | 138 | init(session: NetworkSession = URLSession.shared) { 139 | self.session = session 140 | } 141 | 142 | func executeURL(_ url: URL) { 143 | let task = session.dataTask(with: url) 144 | task.resume() 145 | } 146 | } 147 | ``` 148 | 149 | Notice how we require an instance of the `NetworkSession` protocol in the initializer. Because `URLSession` now conforms to the protocol, we can also use `URLSession.shared` as the default value to allow our actual app code to be a little cleaner. 150 | 151 | Since we won't be using an actual instance of `URLSession` in our test, we'll also need to create a fake version of it called `MockNetworkSession`: 152 | 153 | ```swift 154 | class MockNetworkSession: NetworkSession { 155 | private(set) var dataTaskWithURLCallCount = 0 156 | private(set) var dataTaskWithURLLastURL: URL? 157 | 158 | func dataTask(with url: URL) -> URLSessionDataTask { 159 | dataTaskWithURLCallCount += 1 160 | dataTaskWithURLLastURL = url 161 | 162 | return URLSessionDataTask() 163 | } 164 | } 165 | ``` 166 | 167 | Notice how the mock captures information about how it was called. We're capturing both the number of times `dataTask(with:)` is called as well as the value of the `URL` parameter that was passed into it. 168 | 169 | Now it's time to write the test. We want to make sure calling `executeURL()` on our `NetworkService` class will create the `URLSessionDataTask` from the `NetworkSession` provided at initialization time (i.e. we want to make sure our mock's `dataTask(with:)` method gets called in the correct manner): 170 | 171 | ```swift 172 | func test_executingNetworkService_generatesDataTaskFromUnderlyingNetworkSession() { 173 | // Arrange 174 | let mockSession = MockNetworkSession() 175 | let service = NetworkService(session: mockSession) 176 | let url = URL(string: "http://apple.com")! 177 | 178 | // Act 179 | service.executeURL(url) 180 | 181 | // Assert 182 | XCTAssertEqual(mockSession.dataTaskWithURLCallCount, 1) 183 | XCTAssertEqual(mockSession.dataTaskWithURLLastURL, url) 184 | } 185 | ``` 186 | 187 | The test is pretty self-explanatory - we create an instance of our `NetworkService` class using a `MockNetworkSession`, call `executeURL()` on it, and then make sure the `MockNetworkSession` was called the correct number of times and with the `URL` we provided. 188 | 189 | This basic example shows how a mock object is used to verify the interaction between the `NetworkService` class and its underlying `NetworkSession` dependency. To complete the testing of our `NetworkService` class, we'd also want to create a mock version of the underlying `URLSessionDataTask` so that we can verify `resume()` is called to actually execute the request. As it stands now, we're calling `resume()` on a live `URLSessionDataTask`, which technically executes the request on the network. A more complete version of this example can be found in our [Hyperspace networking library](https://github.com/BottleRocketStudios/iOS-Hyperspace/blob/master/Tests/Helper/Mocks/MockNetworkSessionDataTask.swift). 190 | 191 | ### FIRST 192 | 193 | Unit tests should be [FIRST](https://pragprog.com/magazines/2012-01/unit-tests-are-first): 194 | 195 | * Fast - slow tests are not likely to be run as often. 196 | * Isolated - tests should not depend upon external factors. Following the [single responsibility principle](https://en.wikipedia.org/wiki/Single_responsibility_principle), tests should have only one reason to fail. 197 | * Repeatable - re-running a test should produce consistent results. 198 | * Self-Verifying - make use of test assertion functions to verify conditions (as opposed to relying on a human to interpret results from something like console output). 199 | * Timely - write your tests when you write your feature. You don't have to practice strict Test Driven Development (TDD) by writing your tests beforehand, but you should make an effort to test your code before you move on to the next feature. 200 | 201 | ### Quality of Test Code 202 | 203 | Treat test code with the same amount of care as you would for your app code. This: 204 | 205 | * Allows the test code to scale in the same manner as the app code. 206 | * Prevents low quality test code from becoming a burden to maintain as the app changes. 207 | 208 | Another guard against poor quality test code can be implemented at the source control level: 209 | 210 | * As always, concentrate on small, cohesive commits. This means ensuring your commits that involve tests are *separate* from the ones that introduce or modify features. 211 | * Along the same lines, create *independent* code reviews for features and functionality vs the tests of those features. This allows reviewers to better focus on improving the quality of the tests. 212 | 213 | ### Arrange, Act, Assert 214 | 215 | To improve clarity of your tests, you should aim to structure your tests into 3 distinct sections: 216 | 217 | * Arrange - this is where you setup your system under test (SUT). 218 | * Act - this is where you actually perform your test (calling a method on the SUT). 219 | * Assert - this is where you should make sure your SUT behaved in an expected manner. 220 | 221 | See the examples above for an illustration of this. 222 | 223 | ### Documenting Tests 224 | 225 | Take some time to ensure your tests are as self-documenting as possible. This will help greatly with test maintenance over long periods of time. 226 | 227 | #### Use a naming convention for test names 228 | 229 | Careful consideration of test method names can greatly increase clarity in your unit test classes. We like to use the following naming convention for unit test methods: 230 | 231 | ```swift 232 | func test_condition_expectedResult() { 233 | // ... 234 | } 235 | ``` 236 | 237 | #### Use the right assertion for the job 238 | 239 | Prefer specific assertion macros (i.e. `XCTAssertEqual()`, `XCTAssertTrue()`, etc.) as opposed to the general ones (i.e. `XCTAssert()`) to improve context around the expected results. Also make sure to provide custom failure messages when making use of the general `XCTAssert()` function. 240 | 241 | ### Bad Unit Tests 242 | 243 | Avoid some common pitfalls associated with bad unit tests: 244 | 245 | * No actual assertions (i.e. trying to game the code coverage by calling functions and classes, but not checking for expected output). 246 | * Indiscrete tests (i.e. long test methods that attempt to test many aspects of the SUT at once as opposed to creating several test methods that each test a specific piece of functionality). 247 | 248 | ### Testing Private Methods 249 | 250 | A common question when first beginning to write unit tests is "how do I test the private methods?". Your unit tests should actually just focus on testing public methods. Any underlying private methods should implicitly get tested as a result. If you find yourself needing to test something that's only privately accessible, that probably indicates some refactoring is necessary. A common solution can be to extract the private business logic into a structure that's injected into the SUT at creation time. 251 | 252 | ### Testing the Tests 253 | 254 | Always take a few extra minutes to test your tests! Don't trust a "green" test on the first try. 255 | 256 | * Make the simplest change in the source code of the SUT that will trigger a test failure. 257 | * Validate your change by seeing the test fail. 258 | * Undo the change and re-validate that the test passes again. 259 | 260 | ### Removing Tests 261 | 262 | In general, avoid temptations to remove tests. A well-tested codebase is a safety net to avoiding bugs in the future. However, there are a few valid reasons why you might need to remove a test: 263 | 264 | * When the feature or functionality the test was testing is removed. 265 | * When the test is non-deterministic (i.e. relies on external conditions/network, relies on randomness, etc.). 266 | 267 | ### Architecting for Testability 268 | 269 | There are several tips for making the code you write easier to test: 270 | 271 | * Make use of abstractions where it makes sense. 272 | * Favor protocols over concrete classes. 273 | * Keep methods highly cohesive. 274 | * Create several short/succinct/specialized methods as opposed to a few large methods. 275 | * Separate logic from effects. 276 | * Get a list of inputs and outputs and identify dependencies on global state (e.g. file system, time/date, etc.). 277 | * Factor out logic that affects the global state (e.g. creating a `CleanupPolicy` struct as opposed to hard-coding the logic into a `cleanCache()` method). 278 | * Turn side effects into the output of functions. This makes the outputs testable rather than having to use a mock object. 279 | 280 | In the end, testable code exhibits these characteristics: 281 | 282 | * Provides a way for the client to have control over the inputs. 283 | * Provides a way for the client to have control over the outputs. 284 | * Avoids relying on internal state that may affect later output. 285 | 286 | [WWDC 2017 Session 414 - Engineering for Testability](https://developer.apple.com/videos/play/wwdc2017/414/) is a great resource to see some of these techniques in action. 287 | 288 | ### Unit Testing Frameworks 289 | 290 | There are several frameworks out there like [Quick](https://github.com/Quick/Quick), [Nimble](https://github.com/Quick/Nimble), [EarlGrey](https://github.com/google/EarlGrey), and [OCMock](http://ocmock.org/) designed to make writing unit tests easier. In general, we avoid using such frameworks in order to keep our dependencies to a minimum and keep the barrier to entry for writing unit tests low. We want to make it as quick and easy for every engineer on the team to be able to contribute tests to the project. 291 | 292 | ## UI Testing 293 | 294 | ### UI Automation Testing (`XCUITest`) 295 | 296 | UI automation tests typically require more time to develop and maintain, but can be useful for verifying full system integration. These types of tests are typically the lowest priority for us since there is some potential for overlap with QA's automation efforts using [Appium](https://appium.io/). 297 | 298 | ### Snapshot Testing 299 | 300 | Snapshot tests are like unit tests for individual UI components (or entire screens). We use the popular [iOSSnapshotTestCase](https://github.com/uber/ios-snapshot-test-case) library. 301 | 302 | ## Continuous Integration 303 | 304 | Regular execution of the test suite should be part of your continuous integration process. Our open source projects use [Travis CI](https://travis-ci.org/BottleRocketStudios) to ensure that all tests are executed every time a pull request is created or updated. Other tools like [Fastlane scan](https://docs.fastlane.tools/actions/scan/) can be used to easily create test suite execution workflows. 305 | -------------------------------------------------------------------------------- /Code Style/Swift Style Guide.md: -------------------------------------------------------------------------------- 1 | # Swift Style Guide 2 | 3 | This iOS Swift Style Guide is intended to define the styles and guidelines used by Bottle Rocket iOS developers when writing code in Swift. The main goal is to promote a clean, consistent style of writing code that ensures the maintainability, consistency and ease of developer collaboration within our organization. All code written at Bottle Rocket should adhere to the standards defined herein, unless a specific need to violate these guidelines arises. 4 | 5 | Where any ambiguity exists in the standards, we default to the Apple standards and guidelines. Apple conventions are strongly encouraged. 6 | 7 | For any styling concerns not covered by our [SwiftLint configuration](https://gist.github.com/tylermilner/f33e33e3b4f23d8c6b2fdd4f87af98a1), refer to the notes below. If you see something missing, then please open an issue or pull request! 8 | 9 | ## Architecture 10 | 11 | ### Class Structure 12 | 13 | * Classes should follow a consistent structure. Properties should be declared first, followed by initializers, subclass overrides, public methods, and finally private methods. 14 | * Extensions on a custom type should generally be defined in the same source file below the definition of the type itself. 15 | * Extensions on Apple's types should be placed in their own file, typically inside of an "Extensions" folder in your project. 16 | 17 | #### Do 18 | 19 | ```swift 20 | class SomeClass: BaseClass { 21 | private static let privateStaticProperty = // ... 22 | static let staticProperty = // ... 23 | 24 | private let privateProperty = // ... 25 | let publicProperty = // ... 26 | 27 | init() { 28 | // ... 29 | } 30 | 31 | override func someFunc() { 32 | super.someFunc() 33 | // ... 34 | } 35 | 36 | func publicMethod() { 37 | // ... 38 | } 39 | 40 | private func privateMethod() { 41 | // ... 42 | } 43 | } 44 | 45 | extension SomeClass: SomeProtocol { 46 | // ... 47 | } 48 | ``` 49 | 50 | #### Do Not 51 | 52 | ```swift 53 | extension SomeClass: SomeProtocol { 54 | // ... 55 | } 56 | 57 | class SomeClass: BaseClass { 58 | let publicProperty = // ... 59 | 60 | override func someFunc() { 61 | super.someFunc() 62 | // ... 63 | } 64 | 65 | init() { 66 | // ... 67 | } 68 | 69 | private let privateProperty = // ... 70 | 71 | private func privateMethod() { 72 | // ... 73 | } 74 | 75 | func publicMethod() { 76 | // ... 77 | } 78 | } 79 | ``` 80 | 81 | ### Access Control 82 | 83 | * Be restrictive. Always default to the most restrictive access control level possible - `private`. Not only is this a good encapsulation practice in general, but it also allows the Swift compiler to [automatically perform additional optimizations](https://github.com/apple/swift/blob/master/docs/OptimizationTips.rst#advice-use-private-and-fileprivate-when-declaration-does-not-need-to-be-accessed-outside-of-file). 84 | * Avoid overspecification. Prefer relying on the implicit `internal` access control to explicitly declaring something as `internal`. 85 | 86 | #### Do 87 | 88 | ```swift 89 | class SomeClass { 90 | private var somePrivateProperty: String? 91 | var someInternalProperty: String? 92 | 93 | func someInternalMethod() { 94 | // ... 95 | } 96 | 97 | private func somePrivateMethod() { 98 | // ... 99 | } 100 | } 101 | ``` 102 | 103 | #### Do Not 104 | 105 | ```swift 106 | public class SomeClass { 107 | var somePrivateProperty: String? 108 | var someInternalProperty: String? 109 | 110 | public func someInternalMethod() { 111 | // ... 112 | } 113 | 114 | func somePrivateMethod() { 115 | // ... 116 | } 117 | } 118 | ``` 119 | 120 | ### High-level Methods 121 | 122 | * Keep high-level entry points clean ( `init()`, `viewDidLoad()`, `application(_:didFinishLaunchingWithOptions:)`, etc.). These methods should primarily consist of calls into other methods. This forces you to group related code together and helps to prevent the pollution of high-level methods with implementation details. 123 | 124 | #### Do 125 | 126 | ```swift 127 | class ViewController: UIViewController { 128 | @IBOutlet private var stackView: UIStackView! 129 | @IBOutlet private var textField: UITextField! 130 | 131 | override func viewDidLoad() { 132 | super.viewDidLoad() 133 | 134 | setupView() 135 | setupTextField() 136 | setupSubmitButton() 137 | } 138 | 139 | private func setupView() { 140 | view.backgroundColor = .gray 141 | } 142 | 143 | private func setupTextField() { 144 | textField.placeholder = "Placeholder" 145 | textField.layer.cornerRadius = 5 146 | textField.delegate = self 147 | } 148 | 149 | private func setupSubmitButton() { 150 | let submitButton = UIButton(type: .system) 151 | submitButton.setTitle("Submit", for: .normal) 152 | submitButton.layer.cornerRadius = 5 153 | submitButton.layer.masksToBounds = true 154 | submitButton.isEnabled = false 155 | stackView.addArrangedSubview(submitButton) 156 | } 157 | } 158 | ``` 159 | 160 | #### Do Not 161 | 162 | ```swift 163 | class ViewController: UIViewController { 164 | @IBOutlet private var stackView: UIStackView! 165 | @IBOutlet private var textField: UITextField! 166 | 167 | override func viewDidLoad() { 168 | super.viewDidLoad() 169 | 170 | view.backgroundColor = .gray 171 | 172 | textField.placeholder = "Placeholder" 173 | textField.layer.cornerRadius = 5 174 | textField.delegate = self 175 | 176 | let submitButton = UIButton(type: .system) 177 | submitButton.setTitle("Submit", for: .normal) 178 | submitButton.layer.cornerRadius = 5 179 | submitButton.layer.masksToBounds = true 180 | submitButton.isEnabled = false 181 | stackView.addArrangedSubview(submitButton) 182 | } 183 | } 184 | ``` 185 | 186 | ### Forced Unwrapping 187 | 188 | * Optionals are what inherently allow apps written in Swift to achieve a much lower crash rate than their counterpart written in Objective-C. 189 | * [Forced unwrapping](https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html#ID332) of optionals is not permitted. Instead, proper error handling should be employed through the use of a safe unwrapping mechanism like `guard`, `if let`, [optional chaining](https://docs.swift.org/swift-book/LanguageGuide/OptionalChaining.html), or [nil-coalescing](https://docs.swift.org/swift-book/LanguageGuide/BasicOperators.html#ID72). 190 | 191 | #### Do 192 | 193 | ```swift 194 | var someOptional: String? 195 | 196 | guard let text = someOptional else { return } 197 | titleLabel.text = text 198 | ``` 199 | 200 | ```swift 201 | var someOptional: String? 202 | 203 | if let text = someOptional { 204 | titleLabel.text = text 205 | } 206 | ``` 207 | 208 | ```swift 209 | var someOptional: String? 210 | 211 | titleLabel.text = someOptional ?? "default value" 212 | ``` 213 | 214 | #### Do Not 215 | 216 | ```swift 217 | var someOptional: String? 218 | 219 | titleLabel.text = someOptional! 220 | ``` 221 | 222 | ### Forced Casting 223 | 224 | * For the same reasons outlined above regarding forced unwrapping, [forced casting](https://docs.swift.org/swift-book/LanguageGuide/TypeCasting.html#ID341) is not permitted. 225 | 226 | #### Do 227 | 228 | ```swift 229 | func parseParameters(from dictionary: [String: Any]) { 230 | guard let firstName = dictionary["first_name"] as? String, 231 | let lastName = dictionary["last_name"] as? String else { return } 232 | // ... 233 | } 234 | ``` 235 | 236 | #### Do Not 237 | 238 | ```swift 239 | func parseParameters(from dictionary: [String: Any]) { 240 | let firstName = dictionary["first_name"] as! String 241 | let lastName = dictionary["last_name"] as! String 242 | // ... 243 | } 244 | ``` 245 | 246 | ### Implicitly Unwrapped Optionals 247 | 248 | * [Implicitly unwrapped optionals](https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html#ID334) should be avoided as well. In general, there are only certain situations where their use is acceptable: 249 | * IBOutlets - Unconnected outlets are development errors which, once correct, will never fail due to dynamic circumstances. 250 | * Unit testing - The system under test (or its dependencies) when it's created/destroyed as part of the `XCTestCase` lifecycle (i.e. `setUp()` and `tearDown()`). 251 | 252 | #### Do 253 | 254 | ```swift 255 | class ViewController: UIViewController { 256 | 257 | var requiredData: [String: Any]? 258 | 259 | override func viewDidLoad() { 260 | super.viewDidLoad() 261 | 262 | guard let requiredData = requiredData else { 263 | assertionFailure("Required data was not set before presenting \(self)") 264 | return 265 | } 266 | 267 | processData(requiredData) 268 | } 269 | 270 | private func processData(_ data: [String: Any]) { 271 | // ... 272 | } 273 | } 274 | ``` 275 | 276 | #### Do Not 277 | 278 | ```swift 279 | class ViewController: UIViewController { 280 | 281 | var requiredData: [String: Any]! 282 | 283 | override func viewDidLoad() { 284 | super.viewDidLoad() 285 | 286 | processData(requiredData) 287 | } 288 | 289 | private func processData(_ data: [String: Any]) { 290 | // ... 291 | } 292 | } 293 | ``` 294 | 295 | ### Swift Types 296 | 297 | * Prefer Swift's native types (`Int`, `Float`, `String`, etc.) to the older Objective-C types (`NSInteger`, `CGFloat`, `NSString`, etc.). You can always bridge Swift's native types into Objective-C types whenever necessary. 298 | 299 | #### Do 300 | 301 | ```swift 302 | var height: Float = 25.0 303 | let numberOfRows: Int = 5 304 | ``` 305 | 306 | ```swift 307 | let basePath = "https://bottlerocketstudios.com" // String 308 | let fullPath = (basePath as NSString).appendingPathComponent("contact") // String 309 | ``` 310 | 311 | #### Do Not 312 | 313 | ```swift 314 | var height: CGFloat = 25.0 315 | let numberOfRows: NSInteger = 5 316 | ``` 317 | 318 | ```swift 319 | let basePath = NSString(string: "https://bottlerocketstudios.com") // NSString 320 | let fullPath = basePath.appendingPathComponent("contact") // String 321 | ``` 322 | 323 | ### Extensions and Protocol Conformance 324 | 325 | * Prefer using extensions to make classes conform to protocols rather than directly implementing them at the class definition. 326 | * Avoid using extensions to create separations between various parts of your code (e.g. private methods, lifecycle methods, etc.). Use `MARK: -` comments instead. 327 | 328 | #### Do 329 | 330 | ```swift 331 | class ViewController: UIViewController { 332 | 333 | // MARK: - Lifecycle 334 | 335 | override func viewDidLoad() { 336 | super.viewDidLoad() 337 | // ... 338 | } 339 | 340 | override func viewWillAppear(_ animated: Bool) { 341 | super.viewWillAppear(animated) 342 | // ... 343 | } 344 | 345 | override func viewWillDisappear(_ animated: Bool) { 346 | super.viewWillDisappear(animated) 347 | // ... 348 | } 349 | 350 | // MARK: - Private 351 | 352 | private func somePrivateMethod() { 353 | // ... 354 | } 355 | 356 | private func someOtherPrivateMethod() { 357 | // ... 358 | } 359 | } 360 | 361 | extension ViewController: UITableViewDataSource { 362 | // ... 363 | } 364 | 365 | extension ViewController: UITableViewDelegate { 366 | // ... 367 | } 368 | ``` 369 | 370 | #### Do Not 371 | 372 | ```swift 373 | class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { 374 | 375 | override func viewDidLoad() { 376 | super.viewDidLoad() 377 | // ... 378 | } 379 | 380 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 381 | // ... 382 | } 383 | 384 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 385 | // ... 386 | } 387 | } 388 | 389 | extension ViewController { 390 | 391 | override func viewWillAppear(_ animated: Bool) { 392 | super.viewWillAppear(animated) 393 | // ... 394 | } 395 | 396 | override func viewWillDisappear(_ animated: Bool) { 397 | super.viewWillDisappear(animated) 398 | // ... 399 | } 400 | } 401 | 402 | extension ViewController { 403 | 404 | private func somePrivateMethod() { 405 | // ... 406 | } 407 | 408 | private func someOtherPrivateMethod() { 409 | // ... 410 | } 411 | } 412 | ``` 413 | 414 | ### IBOutlets 415 | 416 | * Default to `private`. This keeps things better encapsulated, but still allows you to access the outlets in Interface Builder. 417 | * Default to a **strong** reference. There is [no need](https://stackoverflow.com/a/31395938) to make IBOutlets `weak` in modern iOS code. 418 | * Suffix the name with they type of object (e.g. `title**Label**`, `logo**ImageView**`). 419 | * Use the most specific type suffix (e.g. `profileImageView` over `profileView`). 420 | 421 | #### Do 422 | 423 | ```swift 424 | @IBOutlet private var logoImageView: UIImageView! 425 | ``` 426 | 427 | #### Do Not 428 | 429 | ```swift 430 | @IBOutlet var logo: UIImageView! 431 | ``` 432 | 433 | ```swift 434 | @IBOutlet var logoView: UIImageView! 435 | ``` 436 | 437 | ### Lazy Loading 438 | 439 | * Make use of Swift's `lazy` keyword to lazy load properties rather than performing the setup logic in hooks like `init()` or `viewDidLoad()`. 440 | * Take extra care to use `lazy` for objects that are expensive to create (e.g. `NSDateFormatter`). 441 | 442 | #### Do 443 | 444 | ```swift 445 | class ViewController: UIViewController { 446 | 447 | private lazy var activityIndicator: UIActivityIndicatorView = { 448 | let activityIndicator = UIActivityIndicatorView(style: .whiteLarge) 449 | view.addSubview(activityIndicator) 450 | return activityIndicator 451 | }() 452 | 453 | override func viewDidLoad() { 454 | super.viewDidLoad() 455 | 456 | showSpinner() 457 | loadDataFromNetwork() 458 | } 459 | 460 | private func showSpinner() { 461 | activityIndicator.startAnimating() 462 | } 463 | 464 | private func loadDataFromNetwork() { 465 | // ... 466 | } 467 | } 468 | ``` 469 | 470 | ```swift 471 | class SomeTableViewCell: UITableViewCell { 472 | private lazy var dateFormatter: DateFormatter = { 473 | let formatter = DateFormatter() 474 | // Configure formatter 475 | return formatter 476 | }() 477 | 478 | func configureWithDate(_ date: Date) { 479 | textLabel?.text = dateFormatter.string(from: date) 480 | // ... 481 | } 482 | } 483 | ``` 484 | 485 | #### Do Not 486 | 487 | ```swift 488 | class ViewController: UIViewController { 489 | 490 | private var activityIndicator: UIActivityIndicatorView? 491 | 492 | override func viewDidLoad() { 493 | super.viewDidLoad() 494 | 495 | if activityIndicator == nil { 496 | let activityIndicator = UIActivityIndicatorView(style: .whiteLarge) 497 | view.addSubview(activityIndicator) 498 | self.activityIndicator = activityIndicator 499 | } 500 | activityIndicator?.startAnimating() 501 | 502 | loadDataFromNetwork() 503 | } 504 | 505 | private func loadDataFromNetwork() { 506 | // ... 507 | } 508 | } 509 | ``` 510 | 511 | ```swift 512 | class SomeTableViewCell: UITableViewCell { 513 | 514 | func configureWithDate(_ date: Date) { 515 | let dateFormatter = DateFormatter() 516 | // Configure formatter 517 | 518 | textLabel?.text = dateFormatter.string(from: date) 519 | // ... 520 | } 521 | } 522 | ``` 523 | 524 | ### Constants 525 | 526 | * Keep the scope of constants as small as possible. 527 | * If a constant is only needed for a class, it should live in that class. Another option would be to make it a `static let` property of a `fileprivate` enum inside of the same source file. 528 | * App-wide constants should be kept outside of the global namespace. Instead, make them `static let` properties on an enum. Using a struct would generally be okay as well, but an enum offers an additional protection against accidental instantiation. 529 | 530 | #### Do 531 | 532 | ```swift 533 | enum Constants { 534 | static let globalConstant = // ... 535 | } 536 | ``` 537 | 538 | ```swift 539 | class ViewController: UIViewController { 540 | private let someConstant = // ... 541 | 542 | // ... 543 | } 544 | ``` 545 | 546 | ```swift 547 | class SomeObject { 548 | enum ObjectConstants { 549 | static let someConstant = // ... 550 | } 551 | } 552 | ``` 553 | 554 | #### Do Not 555 | 556 | ```swift 557 | import UIKit 558 | 559 | let globalConstant = // ... 560 | let otherGlobalConstant = // ... 561 | 562 | @UIApplicationMain 563 | class AppDelegate: UIResponder, UIApplicationDelegate { 564 | // ... 565 | } 566 | ``` 567 | 568 | ```swift 569 | import UIKit 570 | 571 | let someConstant = // ... 572 | 573 | class ViewController: UIViewController { 574 | // ... 575 | } 576 | ``` 577 | 578 | ### Enum Cases 579 | 580 | * Enum cases should be declared on separate lines instead of declaring multiple cases on a single line. 581 | 582 | #### Do 583 | 584 | ```swift 585 | enum MyEnum { 586 | case caseOne 587 | case caseTwo 588 | case caseThree 589 | } 590 | ``` 591 | 592 | #### Do Not 593 | 594 | ```swift 595 | enum MyEnum { 596 | case caseOne, caseTwo, caseThree 597 | } 598 | ``` 599 | 600 | ### Inter-function Spacing 601 | 602 | * There should be exactly one line of whitespace between function declarations. 603 | 604 | #### Do 605 | 606 | ```swift 607 | open func cancel(with error: Error) { 608 | // ... 609 | } 610 | 611 | open func cancel(withErrors errors: [Error]) { 612 | // ... 613 | } 614 | ``` 615 | 616 | #### Do Not 617 | 618 | ```swift 619 | open func cancel(with error: Error) { 620 | // ... 621 | } 622 | open func cancel(withErrors errors: [Error]) { 623 | // ... 624 | } 625 | ``` 626 | 627 | ### Extension spacing 628 | 629 | * There should be exactly one line of whitespace after the opening declaration for an extension. 630 | 631 | #### Do 632 | 633 | ```swift 634 | extension Operation { 635 | 636 | open func add(condition: Condition) { 637 | // ... 638 | } 639 | 640 | open func add(observer: Observer) { 641 | // ... 642 | } 643 | } 644 | ``` 645 | 646 | #### Do Not 647 | 648 | ```swift 649 | extension Operation { 650 | open func add(condition: Condition) { 651 | // ... 652 | } 653 | 654 | open func add(observer: Observer) { 655 | // ... 656 | } 657 | } 658 | ``` 659 | 660 | ### Immutability 661 | 662 | * Prefer immutable variable declarations over mutable declarations whenever possible. This not only allows us to make reasonable assumptions about the use of that variable in code, but also assists the complier. 663 | 664 | #### Do 665 | 666 | ```swift 667 | let foo = ... 668 | ``` 669 | 670 | #### Do Not 671 | 672 | ```swift 673 | var foo = ... 674 | ``` 675 | 676 | ### Singletons 677 | 678 | * Make sure the class you are using this for actually needs to be a singleton. Odds are, it doesn't. 679 | * Prefer the name `shared` for the singleton instance as opposed to `sharedInstance`, `sharedClassName`, etc. 680 | * While there are several ways to create a singleton in Swift, the most concise method described below should be preferred on all Bottle Rocket projects. 681 | 682 | #### Do 683 | 684 | ```swift 685 | class MyClass() { 686 | static let shared = MyClass() 687 | 688 | init() { 689 | // ... 690 | } 691 | } 692 | ``` 693 | 694 | #### Do Not 695 | 696 | ```swift 697 | class MyClass() { 698 | private static let shared: MyClass = { 699 | // ... 700 | } 701 | 702 | init() { 703 | // ... 704 | } 705 | 706 | static func shared() -> MyClass { 707 | return shared 708 | } 709 | } 710 | ``` 711 | 712 | ### Parameter Validation 713 | 714 | * Use optionals to enforce validation of input parameters. Prefer using `guard` statements to verify parameters and return early instead of nesting `if let` statements. 715 | 716 | #### Do 717 | 718 | ```swift 719 | func cachedImage(url: URL?) -> UIImage? { 720 | guard let absoluteString = url?.absoluteString, let data = imageCache.object(forKey: absoluteString) as? Data else { return nil } 721 | 722 | return UIImage(data: data) 723 | } 724 | ``` 725 | 726 | #### Do Not 727 | 728 | ```swift 729 | func cachedImage(url: URL?) -> UIImage? { 730 | if let absoluteString = url?.absoluteString { 731 | if let data = imageCache.object(forKey: absoluteString) as? Data { 732 | return turnImageIntoDeliciousSandwich(UIImage(data: data)) 733 | } 734 | } 735 | 736 | return nil 737 | } 738 | ``` 739 | 740 | ### Array and Dictionary Accessors 741 | 742 | * When you're accessing an element in a dictionary or array, prefer the square bracket operators over `objectAtIndex` (and similar) methods. These operators make for readable, straightforward code. 743 | * In addition, prefer the use of the `first`, `last`, and `isEmpty` properties on arrays. 744 | 745 | #### Do 746 | 747 | ```swift 748 | let theElement = array[14] 749 | 750 | guard !array.isEmpty else { return } 751 | guard let theFirstOne = array.first else { return } 752 | guard let theLastOne = array.last else { return } 753 | ``` 754 | 755 | #### Do Not 756 | 757 | ```swift 758 | let theElement = array.object(at: 14) 759 | 760 | guard array.count > 0 else { return } 761 | let theFirstOne = array[0] 762 | let theLastOne = array[array.count - 1] 763 | ``` 764 | 765 | ### Magic Numbers 766 | 767 | * Avoid hard-coding constants for values that can be calculated for easier readability. 768 | * Document any other hard-coded values with a comment. 769 | 770 | #### Do 771 | 772 | ```swift 773 | let myColor = UIColor(red: 230 / 255, green: 220 / 255, blue: 225 / 255, alpha: 1) 774 | ``` 775 | 776 | ```swift 777 | let timeUntilRefresh = 60 * 60 * 24 * 2 // 2 days 778 | ``` 779 | 780 | #### Do Not 781 | 782 | ```swift 783 | let myColor = UIColor(red: 0.901961, green: 0.862745, blue: 0.882353, alpha: 1) 784 | ``` 785 | 786 | ```swift 787 | let timeUntilRefresh = 172800 // 2 days 788 | ``` 789 | 790 | ## Syntactic Sugar 791 | 792 | ### Type Inference 793 | 794 | * Omit type information whenever possible. Let the compiler infer the type of a constant, variable, parameter, etc. This not only applies to enums, but also the majority of static properties (e.g. `UIColor.black` becomes simply `.black`). 795 | 796 | #### Do 797 | 798 | ```swift 799 | UIView.animate(withDuration: 0.1, delay: 0.0, options: .curveEaseInOut, animations: { 800 | // ... 801 | }) { _ in 802 | // ... 803 | } 804 | ``` 805 | 806 | ```swift 807 | let someBool = true 808 | ``` 809 | 810 | ```swift 811 | let music: Genre = .metal 812 | ``` 813 | 814 | #### Do Not 815 | 816 | ```swift 817 | UIView.animate(withDuration: 0.1, delay: 0.0, options: UIView.AnimationOptions.curveEaseInOut, animations: { 818 | // ... 819 | }) { _ in 820 | // ... 821 | } 822 | ``` 823 | 824 | ```swift 825 | let someBool: Bool = true 826 | ``` 827 | 828 | ```swift 829 | let music: Genre = Genre.metal 830 | ``` 831 | 832 | ### Swift's Generic Types 833 | 834 | * Prefer the syntactic sugar variants of Swift's collection and optional types as opposed to the full generics syntax. 835 | 836 | #### Do 837 | 838 | ```swift 839 | var shoppingList: [String] 840 | ``` 841 | 842 | ```swift 843 | var itemCount: [String: Int] 844 | ``` 845 | 846 | ```swift 847 | var phoneNumber: String? 848 | ``` 849 | 850 | #### Do Not 851 | 852 | ```swift 853 | var shoppingList: Array 854 | ``` 855 | 856 | ```swift 857 | var itemCount: Dictionary 858 | ``` 859 | 860 | ```swift 861 | var phoneNumber: Optional 862 | ``` 863 | 864 | ### `$0` Syntax 865 | 866 | * The `$0` shorthand syntax that's available on collection operations is generally fine to use, but you might consider using a named variable if implementing a very complicated operation. Always make sure that other people (including your future self) can easily understand what the `$0` represents when they read your code. 867 | 868 | #### Do 869 | 870 | ```swift 871 | static func generateRawQueryParametersString(from queryParameters: [URLQueryItem]) -> String { 872 | return queryParameters.reduce("") { (partialResult, queryItem) -> String in 873 | let nextPartialResult = (partialResult.isEmpty ? "" : "\(partialResult)&") 874 | 875 | guard let queryValue = queryItem.value else { 876 | return nextPartialResult + "\(queryItem.name)" 877 | } 878 | 879 | return nextPartialResult + "\(queryItem.name)=\(queryValue)" 880 | } 881 | } 882 | ``` 883 | 884 | #### Do Not 885 | 886 | ```swift 887 | static func generateRawQueryParametersString(from queryParameters: [URLQueryItem]) -> String { 888 | return queryParameters.reduce("") { 889 | return ($0.isEmpty ? "" : "\($0)&").appending($1.value != nil ? "\($1.name)=\($1.value ?? "")" : $1.name) 890 | } 891 | } 892 | ``` 893 | 894 | ## Naming 895 | 896 | ### Acronyms in Variable Names 897 | 898 | * Acronyms should always appear in all caps, unless the acronym starts at the beginning of the variable name. 899 | 900 | #### Do 901 | 902 | ```swift 903 | let resultFromAPI = parseResponse() 904 | ``` 905 | 906 | ```swift 907 | let apiResult = parseResponse() 908 | ``` 909 | 910 | #### Do Not 911 | 912 | ```swift 913 | let resultFromApi = parseResponse() 914 | ``` 915 | 916 | ```swift 917 | let APIResult = parseResponse() 918 | ``` 919 | 920 | ### Class Prefixes 921 | 922 | * Do not prefix Swift types. Due to Swift offering proper namespacing, this convention from Objective-C is no longer necessary. 923 | * If you do need to expose a class to Objective-C and want to avoid namespace collisions, you can use the `@objc(...)` attribute to specify a class name that includes the prefix, but will only be visible to Objective-C code. 924 | 925 | #### Do 926 | 927 | ```swift 928 | class MyAwesomeSwiftClass { 929 | // ... 930 | } 931 | ``` 932 | 933 | ```swift 934 | @objc(BRSMyAwesomeSwiftClass) class MyAwesomeSwiftClass { 935 | // ... 936 | } 937 | ``` 938 | 939 | #### Do Not 940 | 941 | ```swift 942 | class BRSMyAwesomeSwiftClass { 943 | // ... 944 | } 945 | ``` 946 | 947 | ### Underscores in Variable Names 948 | 949 | * Variable/property names should not be prefixed with an `_`, even if used privately. Variables should use headless camel case and not begin with a symbol or number. 950 | 951 | #### Do 952 | 953 | ```swift 954 | struct MyStruct { 955 | private var backingInteger = 0 956 | var mutableInteger: Int { 957 | get { 958 | return backingInteger 959 | } set { 960 | backingInteger = newValue 961 | } 962 | } 963 | } 964 | ``` 965 | 966 | #### Do Not 967 | 968 | ```swift 969 | struct MyStruct { 970 | private var _mutableInteger = 0 971 | var mutableInteger: Int { 972 | get { 973 | return _mutableInteger 974 | } set { 975 | _mutableInteger = newValue 976 | } 977 | } 978 | } 979 | ``` 980 | 981 | ## Comments 982 | 983 | ### `MARK:` Comments 984 | 985 | * Make extensive use of `MARK:` comments ( `#pragma mark` in Obj-C) to break your class up into sections. This forces you to group related code together, making it easier to browse through files. 986 | * Add a dash after the `MARK:` so that Xcode's method browser will include nice visual separators: 987 | 988 | ![MARK separators in Xcode's method browser](Images/method-navigator-separators.png) 989 | 990 | #### Do 991 | 992 | ```swift 993 | class ViewController: UIViewController { 994 | 995 | // MARK: - IBOutlets 996 | 997 | @IBOutlet private var someButton: UIButton! 998 | 999 | // MARK: - Properties 1000 | 1001 | private var someData: Data? 1002 | 1003 | // MARK: - Lifecycle 1004 | 1005 | override func viewDidLoad() { 1006 | super.viewDidLoad() 1007 | // ... 1008 | } 1009 | 1010 | // MARK: - IBActions 1011 | 1012 | @IBAction private func someButtonTapped(_ button: UIButton) { 1013 | // ... 1014 | } 1015 | 1016 | // MARK: - Private 1017 | 1018 | private func somePrivateMethod() { 1019 | // ... 1020 | } 1021 | } 1022 | ``` 1023 | 1024 | #### Do Not 1025 | 1026 | ```swift 1027 | class ViewController: UIViewController { 1028 | 1029 | @IBOutlet private var someButton: UIButton! 1030 | private var someData: Data? 1031 | 1032 | override func viewDidLoad() { 1033 | super.viewDidLoad() 1034 | // ... 1035 | } 1036 | 1037 | @IBAction private func someButtonTapped(_ button: UIButton) { 1038 | // ... 1039 | } 1040 | 1041 | // MARK: Private 1042 | 1043 | private func somePrivateMethod() { 1044 | // ... 1045 | } 1046 | } 1047 | ``` 1048 | --------------------------------------------------------------------------------