# iOS Coding Guidelines Amendments
## Overview
- **Xcode 12.4** and higher
- Coding should follow the standards found at https://github.com/raywenderlich/swift-style-guide Unless otherwise stated in this document
## Releaseable Software - Develop Branch
Code checked into the develop branch is ready for public distribution.
It is releasable software, which means
- Unit Test Coverage
- Coverage tools minimums are not the goal. If there is logic, if QA could find a bug with what the code does, the code will be tested.
- Feature fully implemented
- No dummy data
- No Tech Debt Tickets
- No TODOS:
- Expected to Pass QA, ready for QA testing
- Ready for Design Review
- Ready for Product Owner review
Releaseable doesn't take into account if what is completed is a Minimal Viable Product.
For example, if an onboarding feature is fully implemented and the App only contained that feature, it wouldn't make a lot of sense to release an App with just that feature, but that is a product, not a technical decision.
Technically yes, it could be released. The feature is done.
## Rebase vs Merge
Merges will not be used to sync branches together that are then submitted into different PR's.
When this happens the same commits are reviewed multiple, times and what was actually done in the PR becomes hard if not impossible to track.
If a PR is submitted that contains commits that are not truly part of the work, it will be rejected and a rebase will be requested.
## Squash Commits
It's benefencial to have fewer commits than more commits on a PR.
If a PR has 30 commits with very minor changes, it's harder to focus on the what was done in the PR.
Commits can be squashed to combine many commits into one.
How to Squash:
- SourceTree: https://www.atlassian.com/blog/sourcetree/interactive-rebase-sourcetree
- Git Command Line: https://levelup.gitconnected.com/how-to-squash-git-commits-9a095c1bc1fc
## Developer Specific Files
Xcode keeps some local data for the developer. This is not to be commited and should be ignored by git.
**Examples**
- xcuserdata
## SwiftLint Rules
[SwiftLint rules](https://realm.github.io/SwiftLint/rule-directory.html)
- SwiftLint is used to automate best practices.
- All rules that are on by default will be enabled.
###### Are all of these rules necessary?
- Some of the rules acheive better performance, some rules are for formatting.
- We are keeping most of formatting rules simply for consistency reasons.
They might seem pedantic but code that is consistent is easier to read than multiple developers formatting to their
own personal preference.
###### Do I have to fix all of these formatting issues myself?
Review the documentation for [SwiftLint](https://github.com/realm/SwiftLint)
```
swiftlint autocorrect
```
#### Additional Rules
| Rule | Reason |
| -------- | -------- |
| [anyobject_protocol](https://realm.github.io/SwiftLint/anyobject_protocol.html) | `class` is deprecated |
| [array_init](https://realm.github.io/SwiftLint/array_init.html) | Using map is less intuitive then the Array constructor |
| [closure_body_length](https://realm.github.io/SwiftLint/closure_body_length.html) | Closures should be short and rely of methods if the task requires more code. |
| [closure_end_indentation](https://realm.github.io/SwiftLint/closure_end_indentation.html) | More consistent code formatting |
| [closure_spacing](https://realm.github.io/SwiftLint/closure_spacing.html) | More consistent code formatting |
| [collection_alignment](https://realm.github.io/SwiftLint/collection_alignment.html) | More consistent code formatting |
| [contains_over_filter_count](https://realm.github.io/SwiftLint/contains_over_filter_count.html) | More efficient, readable |
| [contains_over_filter_is_empty](https://realm.github.io/SwiftLint/contains_over_filter_is_empty.html) | More efficient, readable |
| [contains_over_first_not_nil](https://realm.github.io/SwiftLint/contains_over_first_not_nil.html) | More efficient, readable |
| [contains_over_range_nil_comparison](https://realm.github.io/SwiftLint/contains_over_range_nil_comparison.html) | More efficient, readable |
| [discouraged_object_literal](https://realm.github.io/SwiftLint/discouraged_object_literal.html) |Makes code harder to read|
| [discouraged_optional_boolean](https://realm.github.io/SwiftLint/discouraged_optional_boolean.html) | Prefer default value over nil |
| [discouraged_optional_collection](https://realm.github.io/SwiftLint/discouraged_optional_collection.html) | Prefer default value over nil |
| [empty_collection_literal](https://realm.github.io/SwiftLint/empty_collection_literal.html) | More efficient, readable |
| [empty_count](https://realm.github.io/SwiftLint/empty_count.html) | More efficient, readable |
| [empty_string](https://realm.github.io/SwiftLint/empty_string.html) | More efficient, readable |
| [empty_xctest_method](https://realm.github.io/SwiftLint/empty_xctest_method.html) |Clean Code|
| [enum_case_associated_values_count](https://realm.github.io/SwiftLint/enum_case_associated_values_count.html) |Clean Code|
| [explicit_acl](https://realm.github.io/SwiftLint/explicit_acl.html) | Clean Code, should not just accept default (internal) |
| [explicit_init](https://realm.github.io/SwiftLint/explicit_init.html) | Clean Code, prefer standard constructor syntax |
| [extension_access_modifier](https://realm.github.io/SwiftLint/extension_access_modifier.html) | Clean Code |
| [fatal_error_message](https://realm.github.io/SwiftLint/fatal_error_message.html) | Clean Code |
| [file_types_order](https://realm.github.io/SwiftLint/file_types_order.html) | Clean Code |
| [first_where](https://realm.github.io/SwiftLint/first_where.html) | More efficient, readable |
| [flatmap_over_map_reduce](https://realm.github.io/SwiftLint/flatmap_over_map_reduce.html) | More efficient, readable |
| [force_unwrapping](https://realm.github.io/SwiftLint/force_unwrapping.html) | Code Quality |
| [function_default_parameter_at_end](https://realm.github.io/SwiftLint/function_default_parameter_at_end.html) | Code Quality |
| [implicit_return](https://realm.github.io/SwiftLint/implicit_return.html) | Verbosity |
| [implicitly_unwrapped_optional](https://realm.github.io/SwiftLint/implicitly_unwrapped_optional.html) | Stability |
| [indentation_width](https://realm.github.io/SwiftLint/indentation_width.html) | Readability |
| [joined_default_parameter](https://realm.github.io/SwiftLint/joined_default_parameter.html) | Verbosity |
| [last_where](https://realm.github.io/SwiftLint/last_where.html) |More efficient, readable|
| [legacy_random](https://realm.github.io/SwiftLint/legacy_random.html) |Legacy Code|
| [let_var_whitespace](https://realm.github.io/SwiftLint/let_var_whitespace.html) | Readability |
| [literal_expression_end_indentation](https://realm.github.io/SwiftLint/literal_expression_end_indentation.html) | Readability |
| [lower_acl_than_parent](https://realm.github.io/SwiftLint/lower_acl_than_parent.html) | Code Quality |
| [missing_docs](https://realm.github.io/SwiftLint/missing_docs.html)| Documentation |
| [modifier_order](https://realm.github.io/SwiftLint/modifier_order.html) | Readability |
| [multiline_arguments](https://realm.github.io/SwiftLint/multiline_arguments.html) | Readability |
| [multiline_function_chains](https://realm.github.io/SwiftLint/multiline_function_chains.html) | Readability |
| [multiline_literal_brackets](https://realm.github.io/SwiftLint/multiline_literal_brackets.html) | Readability |
| [multiline_parameters](https://realm.github.io/SwiftLint/multiline_parameters.html) | Readability |
| [no_grouping_extension](https://realm.github.io/SwiftLint/no_grouping_extension.html) |Misuse of language feature|
| [non_private_xctest_member](https://realm.github.io/SwiftLint/non_private_xctest_member.html) | Proper use of Access Control |
| [nslocalizedstring_key](https://realm.github.io/SwiftLint/nslocalizedstring_key.html) | Proper use of language feature |
| [number_separator](https://realm.github.io/SwiftLint/number_separator.html) | Readability |
| [object_literal](https://realm.github.io/SwiftLint/object_literal.html) | Readability |
| [operator_usage_whitespace](https://realm.github.io/SwiftLint/operator_usage_whitespace.html) |Readability|
| [optional_enum_case_matching](https://realm.github.io/SwiftLint/optional_enum_case_matching.html) |Readability|
| [overridden_super_call](https://realm.github.io/SwiftLint/overridden_super_call.html) |Code Correctness|
| [override_in_extension](https://realm.github.io/SwiftLint/override_in_extension.html) |Readability|
| [pattern_matching_keywords](https://realm.github.io/SwiftLint/pattern_matching_keywords.html) |Readability|
| [prefer_self_type_over_type_of_self](https://realm.github.io/SwiftLint/prefer_self_type_over_type_of_self.html)| Code Quality |
| [prefer_zero_over_explicit_init](https://realm.github.io/SwiftLint/prefer_zero_over_explicit_init.html) |Code Quality|
| [private_action Reference](https://realm.github.io/SwiftLint/private_action.html) |Code Quality|
| [private_outlet Reference](https://realm.github.io/SwiftLint/private_outlet.html) |Code Quality|
| [private_over_fileprivate](https://realm.github.io/SwiftLint/private_over_fileprivate.html) |Code Quality|
| [prohibited_super_call Reference](https://realm.github.io/SwiftLint/prohibited_super_call.html) |Code Quality|
| [raw_value_for_camel_cased_codable_enum](https://realm.github.io/SwiftLint/raw_value_for_camel_cased_codable_enum.html) |Proper use of language feature|
| [reduce_boolean](https://realm.github.io/SwiftLint/reduce_boolean.html) |More efficient, readable|
| [reduce_into](https://realm.github.io/SwiftLint/reduce_into.html) |More efficient, readable|
| [redundant_nil_coalescing](https://realm.github.io/SwiftLint/redundant_nil_coalescing.html) |Code Correctness|
| [redundant_type_annotation](https://realm.github.io/SwiftLint/redundant_type_annotation.html)|Verbosity|
| [single_test_class](https://realm.github.io/SwiftLint/single_test_class.html)|Code Correctness|
| [sorted_first_last](https://realm.github.io/SwiftLint/sorted_first_last.html) |More efficient, readable|
| [sorted_imports](https://realm.github.io/SwiftLint/sorted_imports.html) |Readability|
| [statement_position](https://realm.github.io/SwiftLint/statement_position.html) |Readability|
| [static_operator](https://realm.github.io/SwiftLint/static_operator.html) |Readability|
| [strict_fileprivate](https://realm.github.io/SwiftLint/strict_fileprivate.html) |Code Correctness|
| [strong_iboutlet](https://realm.github.io/SwiftLint/strong_iboutlet.html) |Code Quality|
| [switch_case_on_newline](https://realm.github.io/SwiftLint/switch_case_on_newline.html) |Readability|
| [test_case_accessibility](https://realm.github.io/SwiftLint/test_case_accessibility.html) |Code Correctness|
| [toggle_bool](https://realm.github.io/SwiftLint/toggle_bool.html) | Readability |
| [trailing_closure](https://realm.github.io/SwiftLint/trailing_closure.html) | Readability |
| [type_contents_order](https://realm.github.io/SwiftLint/type_contents_order.html) |Readability|
| [unavailable_function](https://realm.github.io/SwiftLint/unavailable_function.html) |Code Correctness|
| [unneeded_parentheses_in_closure_argument](https://realm.github.io/SwiftLint/unneeded_parentheses_in_closure_argument.html) |Verbosity|
| [unowned_variable_capture](https://realm.github.io/SwiftLint/unowned_variable_capture.html) |Stability|
| [untyped_error_in_catch](https://realm.github.io/SwiftLint/untyped_error_in_catch.html)|Code Quality|
| [unused_declaration](https://realm.github.io/SwiftLint/unused_declaration.html) |Code Quality|
| [unused_enumerated](https://realm.github.io/SwiftLint/unused_enumerated.html) |Verbosity|
| [unused_import](https://realm.github.io/SwiftLint/unused_import.html) | Code Quality |
| [unused_optional_binding](https://realm.github.io/SwiftLint/unused_optional_binding.html) |Code Quality|
| [vertical_parameter_alignment_on_call](https://realm.github.io/SwiftLint/vertical_parameter_alignment_on_call.html)
| [vertical_whitespace_between_cases](https://realm.github.io/SwiftLint/vertical_whitespace_between_cases.html) |Readability|
| [vertical_whitespace_closing_braces](https://realm.github.io/SwiftLint/vertical_whitespace_closing_braces.html) |Readability|
| [vertical_whitespace_opening_braces](https://realm.github.io/SwiftLint/vertical_whitespace_opening_braces.html) |Readability|
| [xct_specific_matcher](https://realm.github.io/SwiftLint/xct_specific_matcher.html) |Readability|
| [yoda_condition](https://realm.github.io/SwiftLint/yoda_condition.html) |Readability|
## Other formatting rules
Rules not convered by SwiftLint or the RW guidelines
### Variable declaration on each line (Purposal)
- In reading code, it takes more effort to parse multiple variables on line vs, a declaration on each line.
- It is easier to document code on seperate lines
#### Not Perfered
```swift=
let price, salePrice, quantity, onHand: Float
```
#### Perfered
```swift=
/// Price of this product
let price: Float
/// The sale price
let salePrice: Float
/// How many items in cart
let quantity: Float
/// How many items available at store
let onHand: Float
```
## Don't always fix SwiftLint rules
Make sure you understand the why of a SwiftLint rule.
### Example - Number of Lines
Splitting code into extensions to avoid the number of lines rule.
The purpose of the rule is that classes with a large number of lines is probably not following the [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single-responsibility_principle).
Splitting code into extensions, still has a class with the same number of responsibilities. It is really the same class, now fragmented.
In this situation we should
1. Think how we can split the responsibilities into smaller classes
2. Confirm that this large class is being tested. In a situation like this, we might find classes not being tested because the more a class does, the harder it might be to test.
3. If we think that the class is following single responsibility we should ignore this error, because you can consider it a false positive.
Any tool like SwiftLint is not perfect. Sometimes to cure is worse than the disease. If you are making a change because of a SwiftLint rule, make sure you understand the why, and not just blindly try to get the rule to pass.
## Unit Testing
Unit Testing is a requirement. 100% coverage is not achievable, but we should always lean towards doing it.
When reviewing a PR, use the Xcode unit testing coverage tool and see what is and what is not unit testing.
Always write up a PR comment if you believe that the code that should be tested is not.
Think of it this way, if a bug is noticed and you can't go back to a unit test to verify how it's working, then we didn't have enough unit testing.
AC should be traceable to unit test(s)
### Naming
A test should explain precisely what is being tested.
Preferably with the when then syntax.
This tells us when an object is in a given state, and then we get an expected result
We shouldn't have to dig into a unit test to try to determine what isn't working.
##### Preferred:
```swift
func testWhenLocationHasNotBeenEnabledThenErrorIsReturned()
```
##### Not Preferred:
```swift
func testLocationManager() {}
```
## How do I Unit Test?
**Must View!!!**
⭐️ **https://developer.apple.com/videos/play/wwdc2017/414/** ⭐️
https://www.raywenderlich.com/960290-ios-unit-testing-and-ui-testing-tutorial
**Mocks**
https://www.bignerdranch.com/blog/mocking-with-protocols-in-swift/
https://www.swiftbysundell.com/articles/mocking-in-swift/
https://swiftsenpai.com/testing/test-doubles-in-swift/
## UI Testing
*(Subject to review, might be a QA and not a developer task)*
UI should be tested using XCTUITest.
When placing an order we will have end to end UI testing.
### What are we looking for in unit test
Please see the following PR to understand the expectations. https://github.com/dsg-photon/ios-wla-core/pull/23
## Secure Coding Guidelines
Refer the best practices outlined in https://owasp.org/www-project-mobile-security/
## Code Documentation
All public interfaces will be documented, however internal and even private members are encouraged.
https://nshipster.com/swift-documentation/
## Third Party Libraries
Developers are not allowed to add a 3rd party libraries to the project without the approval of the EM.
We will not add 3rd party libraries for technologies where there is a system equivalent without justification.
For example Realm (CoreData) RxSwift (Combine) AlamoFire (URLSession).
### Wrapper Functions
It is discouraged to write helpers that rename built-in functions.
If every codebase renames built-in functions, this adds an unnecessary learning curve for what the developer should already be familiar.
The guideline is not discouraging, creating helper functions, just not creating functions that rename existing functions.
##### Preferred:
```swift
dismiss(animated: true)
dismiss(animated: false)
DispatchQueue.main
```
##### Not Preferred:
```swift
@objc func dismissModalAnimated() {
dismiss(animated: true, completion: .none)
}
@objc func dismissModalWithoutAnimation() {
dismiss(animated: false, completion: .none)
}
var GlobalMainQueue: DispatchQueue {
return DispatchQueue.main
}
```
#### Naming
https://github.com/raywenderlich/swift-style-guide#naming
In addition
- getter / setter syntax
Using a get in a function name adds not value to the readability it is redundent to indicate that a function returns something.
When using get syntax ask, should this be a property instead.
If the code should read more like data, and is not an expensive operation that it should be a property.
##### Preferred:
```swift
func textForFilter(filter: String) -> String
var isFilterEnabled: Bool {
return (!selectedFilters.isEmpty || sortOption != ShopConstants.defaultFilterItem)
}
```
##### Not Preferred:
```swift
func getTextForFilter(filter: String) -> String
func isFilterEnabled() -> Bool {
return (!selectedFilters.isEmpty || sortOption != ShopConstants.defaultFilterItem)
}
```
#### Verbosity
Keywords should be admitted if not needed
There are various rules discourage keywords that aren't needed.
Keywords
##### Preferred:
```swift
let myInstance = MyClass()
```
#### Network Access:
Network errors must be handled. Even though it's logical to test for network access before making a call (reachability), it's redundant and offers little value.
This is the same as checking for the same error twice. Once is all that's needed.
Example:
```swift
guard isReachable else { return }
let task = session.dataTask(with: logRequest.request) { data, response, error in
if let error = error {
//Error must be handled regardless
}
```
CFNetworking does this check and returns an error.
Reachability might still be needed when updating the UI based on network access but is redundant before making a call.
## Localization
Hard coded strings should not be used in the UI.
The language feature [NSLocalizedString](https://developer.apple.com/documentation/foundation/nslocalizedstring) will be used for any string that is to be presented.
##### Preferred:
```swift
let renameText = NSLocalizedString("Rename", comment: "")
```
##### Not Preferred:
```swift
let renameText = "Rename"
```
## Singletons
Singletons are highly discouraged. If a Singleton is used, it will be used to enforce that an instance can only be created once, not to provide easy access to an instance of an object (Global State)
Any addition of a singleton will be reviewed by Senior/Principle developers first.
If a singleton is used, it will still be passed into any dependent types to avoid coupling to that singleton.
##### Preferred:
```swift
final class MyClass {
private let someDependencny: AwsomeType
// someDependecy can still be unit tested, or replaced
// with a different implementation. Not coupled
init(someDependency: AwsomeType = AwsomeImpl.shared) {
😀
self.someDependencny = someDependencny
}
func doSomething() {
//Not coupled to an implementation 👍
someDependencny.doIt()
}
}
```
##### Not Preferred:
```swift
final class MyClass {
func doSomething() {
🤮
AwsomeImpl.shared.doIt()
}
}
```
## Optionals & Mutability
https://github.com/raywenderlich/swift-style-guide#optionals
**Do we really need to use that swift optional**
https://jasonzurita.com/do-we-really-need-to-use-that-swift-optional/
Optionals are one of the language features that make Swift shine.
Everyone has been had their App crash due to a null pointer exception.
Since in most languages, any object type can contain either a null/nil or an object reference, if code references a pointer to an object reference that is nil, nothing can be done so that the code will crash.
**Swift handles this by**
1. Having a special type for nil (Optionals). Any variable can be optional, including simple value types (int, float)
2. If a variable is not optional, then it can never be nil
3. If a variable is optional, Swift encourages you to think about what happens when the value is nil.
An essential aspect of using optionals is determining if the underlying Data Model is correct.
Is it valid for a value to ever have nil?
If a value is optional and mutable, then the risk of introducing bugs is raised.
The Data Model should always favor non-optional and immutability.
By the time that data is being displayed in the App, there should be no chance that the value has changed somewhere else or a default value has been assumed by whatever developer is working in a specific part of the code.
**What about Crashing?**
If a value is non-optional and it is missing from JSON, the App will crash.
When this happens, outputting the error should lead to the specific property that was not found.
In production, this should indicate that data failed to load (and not crash). This would mean that we are incorrect in our data model.
Data Integrity is essential in reducing bugs and the ability to reason about the code.
##### Preferred:
```swift
struct Cart {
let id: UUID
let productDescription: String
let quantity: Int
let cartTotal: Float
}
```
##### Not Preferred:
It would never be valid for any of these to be optional.
To make them optional, to not have to worry about an accurate data model is a short term gain with long term consequence.
```swift
struct Cart {
var id: UUID?
var productDescription: String?
var quantity: Int?
var cartTotal: Float?
}
```
## Storyboard / xibs
- Storyboards/xibs will be used.
- It's acceptable to build a view in code, but this is the exception.
- It is better to have fewer ViewControllers in a storyboard and use Storyboard references
## SwiftUI
- As of 12/15/2020 it is has been decided that we will be using a hybrid approach on having SwiftUI combined with UIKit. The decision on which screens will be used will be based on case by case decision.
### Observables ViewModel
All SwiftUI views with very few exceptions will have a backing ViewModel to support Unit Testing
### View Frames
Using screen bounds is assuming that the UI element will be the same since as the device. This is more likley to break.
iOS does provide **GeometryReader** to know the size of the parent, which is really what's needed.
#### Prefered
```swift
GeometryReader { geometry in
Image(uiImage: placeholder)
.resizable()
.frame(width: geometry.size.width, height: geometry.size.width, alignment: .center)
// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
.clipped()
}
```
#### Not Prefered
```swift
.position(x: UIScreen.main.bounds.width - xPos, y: 20.0)
```
## Combine (Rx)
- [Combine](https://developer.apple.com/documentation/combine) is acceptable
- Reactive Programming is to be used sparingly
- [Reference](https://heckj.github.io/swiftui-notes/)
## Dependency Injection
https://cocoacasts.com/nuts-and-bolts-of-dependency-injection-in-swift
## ViewModel
* A ViewModel is a class that is not coupled to any UI framework
* Shouln't reference UIKit, SwiftUI ext...
* Testable (Dependencies are passed in, not instanated)
#### Example
This is only to illistrate the basic concepts of a ViewModel.
You should not create a class to Add two numbers together.
https://github.com/dsg-tech/ViewModel.iOS
```swift
import Foundation
protocol Addable {
func add(x: Int, y: Int) -> String
}
final class Adder: Addable {
func add(x: Int, y: Int) -> String {
return "\(x + y)"
}
}
import Foundation
protocol AddViewModelDelegate: AnyObject {
var text1: String { get }
var text2: String { get }
func showError(_ text: String)
func updateAnswer(_ answer: String)
}
final class AddViewModel {
private let adder: Addable
private weak var delegate: AddViewModelDelegate?
init(adder: Addable = Adder(),
delegate: AddViewModelDelegate) {
self.adder = adder
self.delegate = delegate
}
func add() {
guard let delegate = self.delegate else { return }
guard let firstInt = Int(delegate.text1) else {
delegate.showError("First is not a number")
return
}
guard let secondInt = Int(delegate.text2) else {
delegate.showError("Second is not a number")
return
}
delegate.updateAnswer(adder.add(x: firstInt, y: secondInt))
}
func addAsync() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {[weak self] in
self?.add()
}
}
}
import UIKit
final class ViewController: UIViewController {
@IBOutlet weak var firstTextField: UITextField!
@IBOutlet weak var secondTextField: UITextField!
@IBOutlet weak var answerLabel: UILabel!
private var viewModel: AddViewModel?
override func viewDidLoad() {
super.viewDidLoad()
viewModel = AddViewModel(delegate: self)
}
@IBAction func addAction(_ sender: Any) {
viewModel?.add()
}
}
extension ViewController: AddViewModelDelegate {
var text1: String {
firstTextField.text ?? ""
}
var text2: String {
secondTextField.text ?? ""
}
func showError(_ text: String) {
let alert = UIAlertController(title: "Error", message: text, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
show(alert, sender: self)
}
func updateAnswer(_ answer: String) {
answerLabel.text = answer
}
}
import Foundation
@testable import ViewModelExample
final class AddMock: Addable {
var addResult = ""
private (set) var addCalled = 0
func add(x: Int, y: Int) -> String {
addCalled += 1
return addResult
}
}
import Foundation
@testable import ViewModelExample
final class AddViewModelDelegateMock: AddViewModelDelegate {
var text1Value = ""
var text1: String { text1Value }
var text2Value = ""
var text2: String { text2Value }
private (set) var showErrorCalled = 0
private (set) var showErrorText: String!
func showError(_ text: String) {
showErrorCalled += 1
showErrorText = text
}
private (set) var updateAnswerAnswer: String!
private (set) var updateAnswerCalled = 0
var answerUpdated: (() -> Void)?
func updateAnswer(_ answer: String) {
updateAnswerCalled += 1
updateAnswerAnswer = answer
answerUpdated?()
}
}
final class AddViewModelTest: XCTestCase {
// MARK: Dependencies
private var addMock: AddMock!
private var addViewModelDelegate: AddViewModelDelegateMock!
// MARK: Sut
private var sut: AddViewModel!
// MARK: Lifecycle
override func setUpWithError() throws {
addMock = AddMock()
addViewModelDelegate = AddViewModelDelegateMock()
sut = AddViewModel(adder: addMock,
delegate: addViewModelDelegate)
}
// MARK: Test
func testWhenFirstNumberIsNotANumberErrorIsShown() throws {
addViewModelDelegate.text1Value = "abcd"
sut.add()
XCTAssertEqual(1, addViewModelDelegate.showErrorCalled)
XCTAssertEqual(0, addViewModelDelegate.updateAnswerCalled)
}
func testWhenTextNotEnteredThenErrorMessageIsShown() throws {
addViewModelDelegate.text1Value = ""
sut.add()
XCTAssertEqual(1, addViewModelDelegate.showErrorCalled)
XCTAssertEqual(0, addViewModelDelegate.updateAnswerCalled)
}
func testWhenFirstNumberEnteredSecondNotNotEnteredThenErrorIsShown() {
addViewModelDelegate.text1Value = "1"
addViewModelDelegate.text2Value = ""
sut.add()
XCTAssertEqual(1, addViewModelDelegate.showErrorCalled)
XCTAssertEqual(0, addViewModelDelegate.updateAnswerCalled)
}
func testWhenFirstAndSecondNumberAreValidThenAnswerIsUpdated() {
addViewModelDelegate.text1Value = "1"
addViewModelDelegate.text2Value = "2"
addMock.addResult = "3"
sut.add()
XCTAssertEqual(0, addViewModelDelegate.showErrorCalled)
XCTAssertEqual(1, addViewModelDelegate.updateAnswerCalled)
XCTAssertEqual("3", addViewModelDelegate.updateAnswerAnswer)
}
func testWhenAddAsyncCalledThenAddCalledLater() {
addViewModelDelegate.text1Value = "1"
addViewModelDelegate.text2Value = "2"
addMock.addResult = "3"
let expect = expectation(description: "ExpectAdd`")
addViewModelDelegate.answerUpdated = {
expect.fulfill()
}
sut.addAsync()
XCTAssertEqual(.completed, XCTWaiter().wait(for: [expect], timeout: 2))
XCTAssertEqual(1, addViewModelDelegate.updateAnswerCalled)
XCTAssertEqual("3", addViewModelDelegate.updateAnswerAnswer)
}
}
```