We use swift-format to enforce the following rules. This is automatically run on the GitHub Actions.
Inside this repository, there is a swiftformat.json
configuration files that set all the swift-format rules.
- There is NO space between the declaration of a class/struct/enum and the first line of code
- There is NO space after the declaration of a function
- There is NO space after the declaration of an
if
,guard
etc.. condition - There is 1 line space between each variable
- There is 1 line space after a
guard
/if
/reduce
etc.. body - There is 1 line space before and after a
// MARK: -
The order of the code is important, and usually we stick to a single style described below, however some situations may require a tweak of the order (for instance a view with different sections) and the order can be modified accordingly.
The normal order goes as follows:
- Constants
- Stored Properties / Variables
- Computed Properties
- Body (for a SwiftUI
View
) - Functions
Each block should first contain the public ones, then internal then private.
If function A calls function B, function A should preceed function B.
func functionA() {
functionB()
}
func functionB() {}
Helpers (tho not recommended to exist) should be the last thing in a file.
Documentation is a very important part of any application regardless of its size and complexity.
Properly documented code allows other developers to jump in the codebase easily and be productive sooner.
Even if you are the only one working on a project, documenting the code helps you know precisely what to expect and does therefore makes it easier to refactor, modify and introduce new features faster and safer.
Documenting code should be done properly, or not done at all. A good documentation includes:
- A description of what a function does, what it is expected to return when applicable, and what it expects as parameters It is preferable to provide an example for more complex functions
- A function with high complexity or special implementation should have an explanation of these choices in the documentation
- What type of data a variable is storing, what values can it store (if finite values are expected), and if it changes on special cases, what those cases are
- A function with special assumptions (example: Integer input parameter is positive) should be included in the documentation
- A description of the parameters/variables. Documenting variables start with properly choosing their names (more on that later)
But even when it might seem self-explanatory, it is better to explain what a variable does or what it reflects. Use
Cmd + Option + /
to have a good start - A function with default values should document them
- Every sentence of the documentations must start with an uppercase letter, and finish with a dot (
.
) - Use
///
and not/** */
- Use metadata keywords when they lead to a better understanding of the documentation. Read more and find these keywords at Apple Documentation
Example
/// Creates the `Operation` with an execution block.
/// - Parameters:
/// - name: Operation name, useful for Queue Restoration. It must be unique. Default is `nil`.
/// - executionBlock: Execution block. Default is `nil`.
public init(name: String? = nil, executionBlock: ((_ operation: ConcurrentOperation) -> Void)? = nil) {
super.init()
self.name = name
self.executionBlock = executionBlock
}
Naming a variable, class, struct or function is an important aspect of writing code. Take your time when naming elements.
Clarity at the point of use is your most important goal. Entities such as methods and properties are declared only once but used repeatedly.
Design APIs to make those uses clear and concise. When evaluating a design, reading a declaration is seldom sufficient; always examine a use case to make sure it looks clear in context.
Clarity is more important than brevity. Although Swift code can be compact, it is a non-goal to enable the smallest possible code with the fewest characters.
Brevity in Swift code, where it occurs, is a side-effect of the strong type system and features that naturally reduce boilerplate.
Include all the words needed to avoid ambiguity for anyone reading code where the name is used.
Preferred
extension List {
public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)
Not Preferred
employees.remove(x) // unclear: are we removing x?
Remove all the words that are redundant.
Preferred
public mutating func remove(_ member: Element) -> Element
employees.remove(element)
Not Preferred
public mutating func removeElement(_ member: Element) -> Element
employees.removeElement(element)
Boolean
properties referring to a state, should be prefixed withis
. Example:isVisible
,isTurnedOn
Boolean
variables referring to a state, should be prefixed withis
and the name of element they refer to. Example:isButtonVisible
,isSettingsSwitchOn
Boolean
variables referring to a permission, should start withshould
orcan
. Example:canResetPassword
,shouldAskForPassword
UI Elements
should clearly describe the element it refers to, not its color or position for instance. Example:deleteButton
is a good name.bottomRedButton
is notTuple
elements should always be labeled. Example:let temperature: (min: Double, max: Double)
Array
s should be declared as follow:
Preferred
let dummyArray: [Int] = []
Not Preferred
let dummyArray2 = Array<Int>()
let dummyArray3 = [Int]()
Do not explicitly set the variable type when it can be easily inferred.
Preferred
let isTrue = true
Not Preferred
let isTrue: Bool = true
-
Do not name the parameter of a function the same as a class property unless it is for the initializer.
-
leverage
typealias
; It is easy to declare anemail
asString
, but is "sadsgdsag" an email? What if in the future we want to validate that email? By leveragingtypealias
, we can give a better explanation of what to expect from a field.
Preferred
typealias People = [Person]
struct Event {
let name: String
let participants: People
}
Naming a function should clearly describe what it does. Its parameters should serve as documentation. Below is a two example, one you should do, one you should not do.
Preferred
/// Return an `Array` containing the elements of `self`
/// that satisfy `predicate`.
func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]
/// Replace the given `subRange` of elements with `newElements`.
mutating func replaceRange(_ subRange: Range, with newElements: [E])
Not Preferred
/// Return an `Array` containing the elements of `self`
/// that satisfy `includedInResult`.
func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]
/// Replace the range of elements indicated by `r` with
/// the contents of `with`.
mutating func replaceRange(_ r: Range, with: [E])
Take advantage of defaulted parameters when it simplifies common uses. They improve readability by hiding irrelevant information. IMPORTANT: It is better NOT to add defaulted parameters to an already existing function. This can lead to bugs.
-
Try to keep defaulted parameters at the end of the function signature.
-
Mention the default value of defaulted parameters if any.
It is important and more efficient and cleaner to leverage Swift
's built in functions such as map
, reduce
, allSatisfy
, compactMap
, sort
, contains
etc... here is how to use the most common ones.
- When working with models, regardless of how backend data is delivered, always aim to have small reusable models. If a transaction model has user parameters such as
userName
,userID
, etc... Extract those information into a separatedUser
model/struct which can later be reused. - Leverage protocols and generics. Protocols and Generics are powerful Swift options that allows to write better reusable code. Make sure to be familiar with them.
As a best practice always leverage enum
for encapsulation/namespacing, and struct
over class
.
Unless you have to use class for obvious reasons, like subclassing a UIView
, prefer struct
. Being a value type, it helps you avoid bugs knowing that the value being modified is to that single variable.
Generally known as Managers
, choose to create objects responsible for handling specific parts of business logic. For instance, an app dealing with GitHubAPI, can have a GitHubAPIManager
which contains all the code related to that business logic.
A manager should have a configuration provider, that is a protocol backed object that contains all the necessary pieces for the proper functioning of that manager. The manager itself should contain ideally only one stored variable, that is the configuration, and computed variables. Other stored variables can exist if they exist to access a sub value faster, and are populated from the configuration variable itself.
By having a configuration provider it makes testing a lot easier. You only change the configurationProvider values, pass the new values to the Manager, and test it.
Example
protocol GitHubAPIManagerProvider {
/// The base URL for Github API calls
var baseUrl: String { get }
/// The URL session used to launch the data tasks.
var session: URLSession { get }
}
struct GitHubAPIManagerConfigurationProvider: GitHubAPIManagerProvider {
var baseUrl = "https://api.github.com/"
var session: URLSession = URLSession.shared
}
struct GitHubAPIManager {
let configuration: GitHubAPIManagerProvider
init(config: GitHubAPIManagerProvider) {
self.configuration = config
}
// Logic goes here.
}
Avoid using abbreviations when writing code. Always prefer clarity over fast. Prefer clarity over abbreviation.
-
Code should be indented by 2 white spaces, not 4. This helps with visualizing the code. It is really useful with multiple nested functions and closures. Please activate the following Xcode settings under
Text Editing
tab >While Editing
>Automatically trim trailing whitespace
and the sub-optionIncluding whitespace-only lines
. -
Code should wrap at 150 characters. This assures a proper vision on 13" inch
MacBook
s. -
Always leave a space before opening a brace
{
.
Preferred
func remove(element at: Int) {
}
Not Preferred
func remove(element at: Int){
}
The opening brace should on the same line as the function signature / class name / struct name etc.
Preferred
func remove(element at: Int) {
}
Not Preferred
func remove(element at: Int)
{
}
The :
in a Dictionary should immediately follow the key, and have a whitespace before the value.
Preferred
[key: value]
Not Preferred
[key : value]
[key :value]
[key:value]
-
When declaring a variable with an explicit type, a white space should be kept after the semicolon. Example:
var button: UIButton?
-
When writing a boolean condition, do not compare a boolean variable to a boolean value.
Preferred
let isTrue = true
if isTrue {
// Do something.
}
guard !isTrue else {
// Do something.
}
Not Preferred
let isTrue = true
if isTrue == true {
// Do something.
}
guard isTrue == false else {
// Do something.
}
Moreover, rather than using !isTrue
it it better to create a variable called isNotTrue
for better clarity.
In a conditional statement only use parenthesis when grouping elements logically together.
Preferred
if x == 3 {
// Do something.
}
if (x == 3 || y == 5) && z == 2 {
// Do something.
}
Not Preferred
if (x == 3) {
// Do something.
}
if (x == 3 || y == 5) {
// Do something.
}
Here is a list of preferable // MARK: -
names to give to sections:
Init
Constants
forstatic let
constantsStored Properties
Computed Properties
Body
(for SwiftUIView
)Functions
Helpers
File headers should contain only the app name or the SDK name.
Below is a recommended format:
//
// Mocka
//
We opted to not explicitly call self, unless necessary.
In case of a missing implementation, do NOT add a // TODO
but rather a #warning(_ message: String)
that way it is more bold and explicit that something is missing.
Before pushing code, compile it, and make sure there are no code warnings. Except for third-party libraries and missing implementation warnings. It is important to always maintain a 0 warning code base.
The only accepted commented code inside the code base are documentation, or explanation on why a certain decision was made or equivalent.
Garbage code, old code, and equivalents should NEVER be included in the codebase. If a code is commented, it might as well be deleted. If we will need that code in the future, we will use the Git History feature.
If you think you are re-writing the same code over and over again, with a lot of boiler plate, evaluate if it makes sense to dedicate some time to writing a component / extension to centralize it and cut future coding time.
GitFlow is a proven working method for collaborating on big projects. If you are not familiar with it, please read this document.
-
The main branch should mirror the currently released version.
-
The develop branch is the main working branch to which all code should finally be merged. You do not directly work on this branch. You do not modify code on this branch.
-
Each task should be on a separate branch. Branches should be named in the following manner:
- Feature:
feature/name-of-feature
- Fix:
bugfix/name-of-bugfix
- When a release should be created:
release/<number-of-the-release>
- Feature:
-
When a task is finished, a Pull Request should be opened, reviewed by 2 developers following these guidelines, and then merged into the develop branch.
-
Do not keep dead branches around. When a branch is merged, it should be automatically deleted by GitHub, but if it doesn't, please delete it.
NOTHING IS A LAW. SHARE, DISCUSS, and RESEARCH best practices. Propose it to team members and update this file accordingly.