We use SwiftLint to enforce Swift style and conventions. Recommended config file .swiftlint.yml.
You can add it to your project as a build phase script and a pre-commit git hook to abort commits if SwiftLint checks are failing. Recommended build phase script: run-swiftlint.sh. Recommended hook: pre-commit.
We use SwiftGen to generate Swift wrappers for images, colors and localized strings.
- Make sure you are familiar with Apple's API Design Guidelines.
- Don't put opening braces on new lines (1TBS style):
class TestClass { func testFunc(value: Int) { guard value != .zero else { // code return } if value.isMultiple(of: 2) { // code } else { // code } } }
- Nested types should be located at the top of a parent type:
struct Foo { struct Bar {} let bar: Bar let name: String } class ViewController: UIViewController { struct Props {} enum Constants {} // code }
- When declaring a function that doesn't fit in 1 line - put its parameters on separate lines. Each argument and the return value should be on its line:
func testFunc( firstArgument: Int, secondArgument: Int, thirdArgument: Int ) -> Int { // code }
- When calling a function that doesn't fit in 1 line - put each argument on a separate line with a single extra indentation:
testFunc( firstArgument: 1, secondArgument: 2, thirdArgument: 3 )
- Put spaces after comma:
let array = [1, 2, 3, 4, 5]
- Put an extra line break before the next
case
in aswitch
for visual logic separation:switch task { case .doWork: // code case .ignoreThis, .ignoreThatToo: break }
- Do not put an empty line at the beginning and end of a type:
// Preferred class MyView: UIView { private let someLabel = UILabel() } // Not Preferred class MyView: UIView { private let someLabel = UILabel() }
- Prefer using commas to the
&&
operator inif
andguard
predicates. Also, moving predicates logic to local constants dramatically improves code readability:// Preferred let firstCondition = x == firstPredicateFunction() let secondCondition = y <= secondPredicateFunction() + Constants.minimumBarrier let thirdCondition = z == thirdPredicateFunction() ?? fallbackFunction() if firstCondition, secondCondition, thirdCondition { // code } // Not Preferred if x == firstPredicateFunction() && y <= secondPredicateFunction() + Constants.minimumBarrier && z == thirdPredicateFunction() ?? fallbackFunction() { // code }
- Add a line break after
if
andguard
statement:// Preferred if condition { // code } guard condition else { return } // Not Preffered if condition { // code } guard condition else { return }
- Put an empty line after
if
andguard
closing bracket:// Preferred if condition { // code } // code guard condition else { return } // code // Not Preferred if condition { // code } // code guard condition else { return } // code
- For multi-conditioned
guard
statements, each subsequent condition should be positioned on the new line and be horizontally aligned with the preceding one:guard let description = config.description, let someCustomTypeValue = config.someCustomTypeValue else { return nil }
-
We use
PascalCase
forstruct
,enum
,class
,protocol
,associatedtype
andtypealias
names. -
We use
camelCase
for functions, properties, variables, argument names and enum cases. -
Names of all types for a given screen usually contain the same prefix (e.g.
TemplatesPageListViewModel
,TemplatesPageListViewController
,TemplatesPageListView
etc). Sometimes the names are too long, so we use prefix abbreviations to name them (e.g.TPLViewModel
,TPLViewController
,TPLView
).An alternative approach uses namespacing enums (e.g.
TemplatesPageList
), but this approach has a significant downside. When we put a controller in the namespacing extension, we cannot see enclosing type information in the memory debugger, only a bunch ofViewController
objects.// Preferred class TPLViewModel {} class TPLViewController: UIViewController {} // Not Preferred enum TemplatesPageList {} extension TemplatesPageList { class ViewModel {} class ViewController: UIViewController {} }
-
Do not use non-descriptive, shortened or single-letter names:
// Preferred final class RoundAnimatingButton: UIButton { private let animationDuration: NSTimeInterval func startAnimating() { let firstSubview = subviews.first // code } } // Not Preferred final class RoundAnimating: UIButton { private let aniDur: NSTimeInterval func anim() { let a = subviews.first // code } }
-
Include type information in constant/variable names when working with a subclass of
UIViewController
orUIView
:// Preferred final class TestViewController: UIViewController { private let popupTableViewController: UITableViewController private let submitButton = UIButton() private let emailTextField = UITextField() private let nameLabel = UILabel() } // Not Preferred final class TestViewController: UIViewController { private let popup: UITableViewController private let submit = UIButton() private let email = UITextField() private let name = UILabel() }
-
Provide labels for tuple members when it's hard to infer their purpose otherwise:
// Preferred func analyze(numbers: [Int]) -> (average: Double, containsDuplicates: Bool) { // code } // Not Preferred func analyze(numbers: [Int]) -> (Double, Bool) { // code }
-
Avoid using multiple shorthand argument names in a single closure - provide argument names for clarity:
// Preferred applyChanges { currentValue, positiveDelta in currentValue + positiveDelta } // Not Preferred applyChanges { $0 + $1 }
-
When dealing with an acronym or other name usually written in all caps: use
Uppercase
if an abbreviation is in the middle of a name andlowercase
if acamelCase
name starts with it:final class URLFinder {} let htmlBodyContent = "<p>Hello, World!</p>" func setupUI() {}
- Prefer
let
tovar
whenever possible. - Do not explicitly declare types for constants or variables if they can be inferred (but be aware that inferring a chain of closures will result in slower compilation time):
// Preferred let age = user.age let name = "John" // Not Preferred let age: Int = user.age let name: String = "John"
- Avoid writing out an enum type or nesting type of static variables where possible - use shorthand notation instead:
// Preferred tableView.contentInset = .zero let textBounds = attributedString.boundingRect( with: size, options: .usesLineFragmentOrigin, context: nil ) // Not Preferred tableView.contentInset = UIEdgeInsets.zero let textBounds = attributedString.boundingRect( with: size, options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil )
- Prefer the composition of
map
,filter
,reduce
, etc over iterating when transforming collections:// Preferred let evenNumbersSum = [4, 7, 10, 11, 13, 14, 18, 26] .filter { number in number.isMultiple(of: 2) } .reduce(.zero, +) // Not Preferred var evenNumbersSum: Int = .zero let numbers = [4, 7, 10, 11, 13, 14, 18, 26] for number in numbers { if number % 2 == .zero { evenNumbersSum += number } }
- Use
defer
to avoid code duplication in early return scenarios:// Preferred let group = DispatchGroup() group.enter() networkLayer.getItem(with: id) { data in defer { group.leave() } guard let data = data else { return } // code } // Not Preferred let group = DispatchGroup() group.enter() networkLayer.getItem(with: id) { data in guard let data = data else { group.leave() return } // code group.leave() }
- Omit parentheses around HOF arguments:
// Preferred let evenNumbersSum = [4, 7, 10, 11, 13, 14, 18, 26] .filter { number in number.isMultiple(of: 2) } .reduce(.zero, +) // Not Preferred let evenNumbersSum = [4, 7, 10, 11, 13, 14, 18, 26] .filter({ number in number.isMultiple(of: 2) }) .reduce(.zero, +)
- Do not put parentheses around control flow predicates:
// Preferred if x == y { // code } // Not Preferred if (x == y) { // code }
- Do not use
return
in single-line functions and computed properties:func canExchangeAssets(for exchangeValue: Double) -> Bool { self.availableAssetsValue > exchangeValue } var availableAssetsValue: Double { self.price * self.count }
- Avoid making tuples with more than 2 arguments. Instead, create a struct to store named values:
// Preferred struct NumbersInfo { let average: Double let sum: Int let containsDuplicates: Bool let containsPrimes: Bool } func analyze(numbers: [Int]) -> NumbersInfo { // code } // Not Preferred func analyze( numbers: [Int] ) -> (average: Double, sum: Int, containsDuplicates: Bool, containsPrimes: Bool) { // code }
- If a variable or class is not intended to be overridden, apply
final
to it. - When writing public methods, keep in mind whether the user should override the method or not. If not, mark it as
final
. In general,final
methods improve compilation time, so it is beneficial to use this when applicable. - Constants used two or more times should be
static
and stored in anenum
namedConstants
.
- When defining a case with an associated value that isn't obvious, make sure that this value is appropriately labeled:
// Preferred enum ViewState { case question(isUserActive: Bool) } // Not Preferred enum ViewState { case question(Bool) }
- When extracting an associated value from an enum case, skip its label:
// Preferred switch viewState { case let .alert(message): // code } // Not Preferred switch value { case .alert(message: let alertMessage): // code }
- When extracting multiple values from the enum case, prefer placing a single
var
orlet
annotation before the case name. You can also stick to this positioning for single associated value cases to keep thecase let
pattern consistent across the app:// Preferred switch value { case let .multiple(first, second, third): // code case let .single(first): // code } // Not Preferred switch value { case .multiple(let first, let second, let third): // code case .single(let first): // code }
- Avoid creating huge
case
statements. In general, if it takes more than 5 lines percase
, consider grouping it into the method to keep theswitch
size reasonable:// Preferred switch viewState { case let .transitioning(props): renderTransitioningState(with: props) // code } // Not Preferred switch viewState { case let .transitioning(props): loadingIndicator.startAnimating() view.backgroundColor = props.backgroundColor view.alpha = props.alpha view.transform = props.transform view.headerView.render(with: props.headerProps) view.footerView.render(with: props.footerProps) // code }
- Do not include a
default
case forswitch
statements with a finite set of cases. Instead, place unused cases at the bottom of it and addbreak
:// Preferred switch task { case .doWork: // code case .ignoreThis, .ignoreThatToo: break } // Not Preferred switch task { case .doWork: // code default: break }
- Prefer lists of cases to using the
fallthrough
keyword:// Preferred switch order { case .first, .second, .third: // code } // Not Preferred switch order { case .first: fallthrough case .second: fallthrough case .third: // code }
- The only time you should be using implicitly unwrapped optionals is when the resulting crash is a programmer's error (e.g., when resolving dependencies using Dip or creating regular expressions).
- If you don't plan to use the value stored in an optional but need to check whether it is
nil
or not - explicitly check this value againstnil
instead of using theif let
syntax:// Preferred if optionalValue != nil { // code } // Not Preferred if let _ = optionalValue { // code }
- When unwrapping optionals, prefer
guard let
statements toif let
ones to avoid unnecessary nesting in your code. - Avoid unwrapping optionals where optional chaining use is sufficient:
// Preferred UIView.animate(withDuration: 0.3) { [weak animatingView] in animatingView?.alpha = .zero animatingView?.transform = .identity } // Not Preferred UIView.animate(withDuration: 0.3) { [weak animatingView] in guard let animatingView = animatingView else { return } animatingView.alpha = .zero animatingView.transform = .identity }
- If a
protocol
only has one possible conforming class (e.g., if theprotocol
acts as a wrapper for a uniqueViewModel
), it should be stored in the same source file with its concrete class implementation. Otherwise, keep it in a separate dedicated file. - Keep all protocol conformance methods in a dedicated extensions (1 extension per 1 protocol). Also, you can add
// MARK: - [Protocol name]
comments for improved readability and easier file navigation:// MARK: - UIScrollViewDelegate extension MyClass: UIScrollViewDelegate { // code }
- Be careful when calling
self
from an escaping closure as this can cause a retain cycle - use a capture list when this might be the case:self.firstClosure = { [weak self] in // code } self.secondClosure = { [unowned self] in // code }
- Prefer capturing individual variables to capturing
self
where applicable:// Preferred UIView.animate(withDuration: 0.3) { [weak animatingView] in animatingView?.alpha = .zero animatingView?.transform = .identity } // Not Preferred UIView.animate(withDuration: 0.3) { [weak self] in self?.animatingView.alpha = .zero self?.animatingView.transform = .identity }
- Use trailing closure syntax unless the purpose of the closure is not clear without the parameter name:
// Preferred UIView.animate(withDuration: 0.3) { [weak animatingView] in animatingView?.alpha = .zero animatingView?.transform = .identity } completion: { _ in // code } removeUnlinkedEntries(completion: { // code }) // Not Preferred UIView.animate( withDuration: 0.3, animations: { [weak animatingView] in animatingView?.alpha = .zero animatingView?.transform = .identity }, completion: { _ in // code } ) removeUnlinkedEntries { // code }
- If the types of the parameters are obvious - do not specify them explicitly. Sometimes readability is enhanced by adding clarifying detail and sometimes by taking repetitive parts away:
// Preferred delegate.rowHeight { indexPath in dataSource[indexPath].expectedHeight } // Not Preferred delegate.rowHeight { (indexPath: IndexPath) -> CGFloat in dataSource[indexPath].expectedHeight }
- We prefer to use an early return strategy where applicable instead of nesting code in
if
statements:struct FailableConfig { let value: Int let description: String? let someCustomTypeValue: CustomType? } init?(config: FailableConfig) { guard let description = config.description, let someCustomTypeValue = config.someCustomTypeValue else { return nil } // code }
- When you need to check if a condition is true and a failure should exit the current context - use
guard
:// Preferred guard users.indices.contains(index) else { return } // Not Preffered if users.indices.contains(index) { // code }
Tagged
is a wrapper for an additional layer of type-safety.
The main benefit that you will get is more clear code. And you don't have a chance accidentally set incorrect tagged value.
There are cases when you want to distinguish IDs or any other primitive types like String
or Int
etc. And you're faced with a question of what particular value it represents.eg.
You have a usual struct like this.
struct User {
let phone: String
let email: String
let advertisingID: UUID
let vendorID: UUID
}
You run into API like this where you have several options what arguments you should pass.
func authenticateUser(credential: String)
func registerUser(id: UUID)
And you have to spend time looking for API documentation than makes comments or change functions.
Tagged can prevent bugs at compile time.
We can improve User
struct with Tagged types.
struct User {
let phone: PhoneNumber
let email: Email
let advertisingID: AdvertisingID
let vendorID: VendorID
}
When you have specifically indicated type Email
and AdvertisingID
you have no doubt what you should pass.
func authenticateUser(credential: Email)
func registerUser(id: AdvertisingID)
If rawValue conforms to Codadble
, Equatable
, Hashable
, Comparable
and ExpressibleBy-Literal
family of protocols that mean Tagged
conditionally conformed to them too.
Tagged is convenient in usage the only thing you should care about is to create a unique tag.
You can create typealiases in a separate file with a dedicated tags.
enum EmailTag {}
typealias Email = Tagged<EmailTag, String>
Or you can do it directly inside the model.
struct User {
typealias ID = Tagged<User, Int>
let id: ID // User.ID
}
There are several ways to add it to the project.
Carthage
github "pointfreeco/swift-tagged" ~> 0.4
CocoaPods
pod 'Tagged', '~> 0.4'
SwiftPM
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-tagged.git", from: "0.4.0")
]
References: