iOS coding guidelines
Please read the following chapters before jumping in
- General
- Code style
- App architecture
- Git flow
- Localization
- Versioning
- 3rd party libraries
- Testing and CI
- Code reviews
- Deployment
- App Store
1. General
1.1 Coding language
All apps are written in Swift.
1.2 IDE
We recommend Xcode for developing iOS apps. Make sure you are running the latest version of Xcode (and Swift) that is available from the App Store.
1.3 Supported iOS versions
Each project should support current iOS SDK version and one version before. Older versions are supported on client’s request.
2. Code style
Reference to Swift style guide.
2.1 Linter
Use SwiftLint for code linting. Do not install it as dependency. Install it on a system and run it as a run script:
if [ -z "$CI" ]; then
export PATH="$PATH:/opt/homebrew/bin"
if which swiftlint >/dev/null; then
swiftlint
else
echo "error: SwiftLint not installed. Install it using `brew install swiftlint` command or download it from https://github.com/realm/SwiftLint"
exit 1
fi
fi
2.2 Best practices
- Use
DromedaryCase
for Classes, Enums, Structs, Protocols, Extensions - Use
camelCase
for functionNames, variables, constants, enumCases - Use
snake_case
ONLY when referencing JSON attributes - Never use
SCREAMING_SNAKE_CASE
- Use project-wide 2 space indentations
- As always, please write self-documenting code
- Try hard to keep classes under 500 lines of code
- A method should do what its name entails and nothing more
- Please keep names as short as possible without making strange abbreviations
- Groups, classes, and functions should be cohesively grouped and composed
- Project groups should have a backing folder on the filesystem
- Use of SwiftLint tool to ensure our code complies with standards
2.3 Optionals
Use optionals visely, but do not try to abuse them. Implicitly unwrapped optionals are forbidden.
2.4 Error handling
- Use do-catch statement for cases when error is thrown. Also throw an error instead of returning optional values.
- Create enums for each error case you are handling. Try to return explicit errors as much as you can.
public enum LoginError: LocalizedError {
case invalidUsername
case invalidPassword
public var localizedTitle: String? {
switch self {
case .invalidUsername, .invalidPassword:
return "error_title".localized()
}
}
public var errorDescription: String? {
switch self {
case .invalidUsername:
return "login_invalid_username_error".localized()
case .invalidPassword:
return "login_invalid_password_error".localized()
}
}
}
2.5 Large views
If views become too big, consider replacing them with scenes instead. This way, the whole logic is bundled within one complete and isolated bundle/scene.
2.6 Message broadcasting
The use of NotificationCenter
for dispatching messages is highly discouraged. It’s acceptable when dealing with Apple’s frameworks, but for general use, try to avoid it and use other techniques, like delegation pattern.
3. App architecture
We are using a slightly modified version of Clean Swift (VIP) architecture/design pattern on all of our projects.
Boilerplate project setup and VIP samples can be found here.
3.1 Templates
Scenes should be created using the VIP template.
Adding a template to the Xcode
mkdir -p ~/Library/Developer/Xcode/Templates/File\ Templates/Custom
cd project/File\ Templates/
cp -R ./* ~/Library/Developer/Xcode/Templates/File\ Templates/Custom/
- Creating a scene from template
- New File…
- Select
Template
and tap ‘Next’ - Give the scene a name and tap ‘Create’
3.2 App configuration
Use Xcode schemes for app configuration across dev/staging/production. Preferably use *.xcconfig
files to store configurations per scheme.
3.3 Storyboards
We are not using Storyboards at all. We use SnapKit for autolayout in code instead.
3.4 Networking
We should use AlamofireNetworkClient from PovioKit preferably.
3.4.1 Serialization
When we think about serialization we immediately think about Codable protocol that is widely used in swift development. But it can be easily implemented in a wrong way. The models that we serialize responses to, are called Data Transformation Objects (DTO). They are acutally 1:1 representation of the models defined on the backend API. They often contain some kind of violations that swift developers don’t like. To avoid that and as well have clean models that we pass around the app, we should transform them to Domain models.
An example of the DTO to Domain model transformation:
// dto model
struct UserDto: Decodable {
let id: String
let firstName: String
let lastName: String
}
// domain model
struct User {
let id: String
let firstName: String
let lastName: String
let fullName: String
}
// supporting protocol
protocol ModelMapper {
associatedtype T
associatedtype U
static func map(_ objects: [T]) -> [U]
static func transform(_ object: T) -> U?
}
extension ModelMapper {
static func map(_ objects: [T]) -> [U] {
objects.compactMap { transform($0) }
}
}
// mapper that transforms dto -> domain
struct UserMapper: ModelMapper {
static func transform(_ object: UserDto) -> User? {
.init(id: object.id,
firstName: object.firstName,
lastName: object.lastName,
fullName: "\(object.firstName) \(object.lastName)")
}
}
// call site within the networking layer
let user: Promise<User> = client
.request(method: .get, endpoint: Endpoints.getUser)
.validate()
.decode(UserDto.self, decoder: .default)
.compactMap(with: UserMapper.transform)
3.5 Project Structure
The physical files should be kept in sync with the Xcode project files in order to avoid file sprawl. Any Xcode groups created should be reflected by folders in the filesystem.
Here is a sample of the project structure you should follow
Project
|
|AppDelegate.swift
|
|- Scenes
|—— Login // each scene generated with VIP template, in a separate folder
|——— Data Source
|——— Workers
|——— Views
|——— LoginViewController.swift
|——— LoginInteractor.swift
|——— LoginPresenter.swift
|——— LoginRouter.swift
|—— ...
|
|- Common
|—— Managers
|—— Controllers
|—— Extensions
|—— Models
|—— Enums
|—— Protocols
|—— Views
|—— Utils
|—— Workers
|—— ...
|
|- Networking
|—— Client
|—— APIs
|
|— Resources
|—— Fonts
|—— Localization
|—— Plists
|—— ...
|
3.6 New Projects
To jumpstart a new project there is a Xcode template available. You can use it to generate a new Xcode project, it includes preconfigured options such as:
- Handy bash script for preparing the project
- Ready to go project structure
- Includes boilerplate code
- SPM ready to go with PovioKit as standard
- Pre-made schemes:
- prod, dev, sta, qa
- Individual Release and Debug configuration for each scheme
- Bundle IDs with suffix
- .dev, .sta, .qa
- Unit tests
- .gitignore
- Scripts
- Tag warnings, Project sort, Swift Lint
You can find the latest version of the template here.
4. Git flow
The Git guidelines are a part of the general guidelines. Be sure to read them as well.
5. Localization
- Every project should have it’s own spreadsheet named like
<ProjectName> - Localization
. Please ask your PM or lead developer to create one and give you read/write access. - Create the following Gemfile in project root:
source "https://rubygems.org"
gem 'fastlane'
gem 'babelish'
- Create folder named
Localize
and copy files from sample zip file - Look for further instructions in
Localize/README.txt
6. Versioning
We are trying to follow semantic versioning for our apps. Which is quite a standard nowadays. The details are explained in the link, so we’ll cover just some basics here.
6.1 Major release
When we do a feature or a set of features that are breaking changes, or could break the previous release, then this is the right number to change for the release. If you release a smaller feature set and it doesn’t break the previous release, look to the next section.
Keep in mind, that when we do a major release, the minor and patch parts need to be reset to 0.
Example: 1.x.x
-> 2.0.0
6.2 Minor release
Similar to a major release, a minor also includes a feature or a feature set, with the difference that it doesn’t break the previous release. It can also include bugfixes until there is a feature. If it only contains bugfixes, look next section.
Example: X.2.x
-> X.3.0
6.3 Patch release
We like to call it point release
as well. Since it should only contain bugfixes, these releases should be really small and done when we discover bugs and want to patch them asap. Any features should not be included in this release.
Example: X.X.1
-> X.X.2
7. 3rd party libraries
We should use as few 3rd party libraries as possible. If a custom solution can be implemented in a reasonable time, do it. Otherwise, you are encouraged to use top rated and evolving libraries only that support Swift Package Manager (SPM) (or Cocoapods if SPM is non-existing).
Every project should include PovioKit dependency and use it’s AlamofireNetworkClient for networking.
DO NOT
- use libraries with low star rating
- use outdated libraries
- install libraries manually bypassing Cocoapods or SPM
- add libraries without version
Here is a list of a few packages you should consider using in your projects
- Realm - Mobile database
- ReachabilitySwift - Network reachability
- Kingfisher - Lightweight image downloader
- Disk - Easy to persist data
- SnapKit - Swift Autolayout DSL
- PhoneNumberKit - Phone numbers parser and validator
- ActiveLabel - Tappable links within label
- FSCalendar - Customizable calendar
- Lottie - Vector based animations rendering
- SwiftLint - Tool to enforce Swift style and conventions
8. Testing and CI
You should make a conscious effort to write tests early and often, this holds especially true for core functionality. Every feature should have backing tests before pushing it to the git. Tests should pass locally before pushing the code.
Keep the following naming convention when writing tests: test_methodName_expectedBehaviour
Your test flow should follow the given approach:
// given - a scenario (system under test)
// when - something happens
// then - expected behaviour
To simplify testing clean arch scenes, make sure you’ve installed VIP template for tests.
Tests will need to run and pass on a dedicated CI service. Be careful of crossing boundaries, having to mock and spy too many components may indicate a flaw in your design.
9. Code reviews
- Keep branches as small and relevant as possible to avoid large pull requests.
- Every team member must do code reviews of other team members’ code.
- Each pull request should have at least one approved code review.
- Pull request cannot be merged before the code review is done.
- Use labels to mark the state of the pull request. For example, set the label
needs review
when it is ready for review orrequested changes
when you comment on a thing that needs updating. - When updating your branch from the base branch, use the git rebase command.
- The person who opens the pull request is also responsible for closing or merging it.
- We should use the
Squash and merge
option to merge the pull request.
You can find more about this topic here.
10. Deployment
Deployment needs to be done with the use of Fastlane tool with predefined configurations. Please check the documentation.
Primary testing service is TestFlight. Don’t use others unless there are requirements to do that.
11. App Store
Through the project lifecycle, we usually talk about App Store right at the project start and when we prepare builds or releases. This chapter will try to cover most cases required to accomplish app publishing.
11.1 Basics
When we are talking about app setup for the App Store, we need to have in mind two Apple portals.
App Store Connect is where pretty much everything non-tech related is going on. From managing users, permissions, builds, releases, in-app purchases, etc. This is the main portal to interact when doing a release.
On the other hand, Developer Portal is purely for technical stuff like managing certificates, devices, profiles, services, keys, etc.
11.2 New App
When you kick-off a project or you just want to create another target of existing app, you need to create a new app entity on the App Store Connect. You can read about this here.
One important thing to notice here is to select unique name for the app. It needs to be unique between all available apps in the App Store.
11.3 Users
Internal user
- is part of the project team or company
- is invited to the App Store Connect directly
- example: developer, qa, designer, etc
External user
- is not part of the project team or company, just an outside entity
- is not invited to the App Store Connect directly, but rather as a tester on the TestFlight Beta Group
- example: someone outside the company to try out and play with the app
11.4 TestFlight
In order to prepare a TestFlight build, you need to have an app entity available (see New App section) and deployment to be set (see Deployment section). Once this is done, you can deploy new build to the TestFlight, add testers to the build and start playing with it.
For more info, read the documentation.
11.5 Releases
A release is usually done in a cadence with the sprint planning. But not necessarily. Here is the list of things that we need to oversee for each release:
- create a new version
- each version needs to be unique, documentation)
- release notes or what’s new
- this is required for each release
- if possible, text should not be generic and used for each release
- metadata update
- screenshots
- app description
- keywords
- other fields as needed
- release strategy
- app can be released immediately after it was approved or on demand/manually
- make that decision based on your needs
- typically we set manual release strategy for special marketing events that are happening on a certain date
- on the other hand, automatically is preferred for all point releases or when we would like to push update asap)