Skip to content

Luur/SwiftEchoes-Tips

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

80 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Swift Tips

🔥 SwiftEchoes - Tips

Here's list of tips and tricks with all additional sources (playgrounds, images) that I would like to share. Also you can find them on Twitter @szubyak, where you can ask questions and respond with feedback. I will really glad to have you there! 😀

📃 Table of contents

#63 How to make UIStoryboard usage safer?
#62 "Massive" Storyboard
#61 XCTUnwrap assertion function
#60 UITableViewCell identifier
#59 AlertPresentable protocol
#58 CollectionView extension for adaptive grid layout
#57 Render HTML within a UILabel
#56 Custom Error by adopting LocalizedError protocol
#55 'Result' type without value to provide
#54 Given, When, Then
#53 sut and test lifecycle
#52 Point on circle perimeter
#51 zip() function
#50 StackView custom spacing
#49 Named UIColor
#48 Result error handling
#47 Generics: Type parameters
#46 Generics: Basics
#45 UserDefaults during testing
#44 Additional Info to #38 Protocols: Optional methods
#43 Responsible view controller for particular view
#42 Move between textfields
#41 Autogenerated allCases property for your enum (Swift 4.2)
#40 Protocols: Class-only
#39 Protocols: Inheritance and composition
#38 Protocols: Optional methods
#37 Protocols: Naming
#36 Property observers, getter/setter and lazy are mutually exclusive
#35 Prepare Alamofire standalone functions to unit-testing
#34 Sort array of objects with multiple optional criteria
#33 Remove object from array
#32 Delegate naming
#31 Run, Playground, run!
#30 DispatchGroup usage
#29 Remove duplicates
#28 Debugging: View Debugging
#27 Debugging: Breakpoints
#26 Debugging: Asserts
#25 Debugging: Log functions
#24 Update UIView content with animation
#23 Observe MOC changes
#22 Split String into words
#21 Comparing tuples
#20 How to detect that user stop typing
#19 Left/rigth text offset inside UITextField
#18 Common elements in two arrays
#17 Apply gradient to Navigation Bar
#16 Get next element of array
#15 Split array by chunks of given size
#14 Transparent/Opaque Navigation Bar
#13 Group objects by property
#12 Semicolons in Swift
#11 Fake AppDelegate
#10 Invoke didSet when property’s value is set inside init context
#9 Change type of items in array
#8 forEach and map execution order difference
#7 Testing settings
#6 Tips for writing error messages
#5 Profit to compiler
#4 Combinations of pure functions
#3 Enumerated iteration
#2 Easy way to hide Status Bar
#1 Safe way to return element at specified index

Below I want to show you a few simple ways how to make usage of string literals connected with UIStoryboard much more safer.

  • Global constant string literals
    At first it sounds like a good idea. You only need to define the constant once and it will be available everywhere. Then if you want to change its value, there is only one place in code to change for which the effects will cascade throughout the project. But global constants have some disadvantages. Not as much as global variables but still enought to stop using them. It's not the best practice. I will cover the topic of andvatages and disadavntages of global constants/variables usage in my next posts.

  • Relatable storyboard names
    Your storyboards should be named after the sections of which they cover. It's a general rule. If you have a storyboard which houses view controllers are related to Profile, then the name of that storyboard’s file should be Profile.storyboard.

  • Uniform storyboard identifiers
    When you want to use Storyboard Identifiers on your view controllers, usage of the class names as identifiers will be a good practice. For example, “ProfileViewController” would be the identifier for ProfuleViewController. Adding this to your naming convention is a good idea.

  • Enum
    Try to consider enums as uniform, centralized global string literal identifiers for 'UIStoryboard'. You can create UIStoryboard class extension which defines all the storyboard files you have in your project. You can also add the convenience initializer or class function to add more syntactic sugar.

extension UIStoryboard {
    enum Storyboard: String {
        case profile
        case login
        
        var name: String {
            return rawValue.capitalized
        }
    }
    
    convenience init(storyboard: Storyboard, bundle: Bundle? = nil) {
        self.init(name: storyboard.name, bundle: bundle)
    }
    // let storyboard = UIStoryboard(storyboard: .profile)
    
    class func storyboard(storyboard: Storyboard, bundle: Bundle? = nil) -> UIStoryboard {
        return UIStoryboard(name: storyboard.name, bundle: bundle)
    }
    // let storyboard = UIStoryboard.storyboard(.profile)
}

I hope provided solutions will really help you to maintain your storyboards.

Back to Top

I'm prety sure that almost every one has heard about "main" MVC problem known as Massive View Controller and how fashionable architectures (MVVM, VIPER, CleanArchitecture) valiantly fight it. But I want to pay attation to not less common problem - massive storyboard. Because of this many developers prefer to make layouts in code insted of storyboard. I've to admit that developers despise storyboards not without reason.

Let's list main storyboard problems:

  • Storyboard always become unwieldy and unmanageable.

  • Storyboard is slow. Extremely slow. As it grows in size, it doesn't only become unhandy for developers but also for XCode. Once you tap on a storyboard file of this size, you can leave your seat, grab a cup of coffee and come back. You’ll be lucky if XCode has loaded the storyboard.
  • Merge Conflicts in Source Control. It’s always a painful experience when multiple developers are working on a project as it almost always leads to merge conflicts in storyboard. Merging storyboards is hell because of their nature - storyboards are just hugeass XML files.

Inspite of the issues, I'm convinced there is a way to make storyboard great again 🇺🇸, resolving most of the above.

"A storyboard is meant to explain a story, not a saga."

Storyboard can be easily splitted into multiple storyboards, with each one representing an individual story. Yes, it's OK not to have one big, fat, slowloading storyboard file but have 30+ devided storyboards.

Also don't forget about Storyboard References introduced in iOS 9.

But splitting storyboard will not help with one serious problem. Storyboard usage has strong coupling with one silent killer known as String Literals. Look for the solution in #63 StoryboardIdentifiable protocol

Back to Top

In Xcode 11 new assertion function has been added for use in Swift tests. XCTUnwrap asserts that an Optional variable’s value is not nil, returning the unwrapped value of expression for subsequent use in the test. It protects you from dealing with conditional chaining for the rest of the test. Also it removes the need to use XCTAssertNotNil(_:_:file:line:) with either unwrapping the value. It’s common to unwrap optional before checking it for a particular value so that’s really where XCTUnwrap() will come into its own.

struct Note {
    var id: Int
    var title: String
    var body: String
}

class NotesViewModel {

    static func all() -> [Note] {
        return [
            Note(id: 0, title: "first_note_title", body: "first_note_body"),
            Note(id: 1, title: "second_note_title", body: "second_note_body"),
        ]
    }
}


func testGetFirstNote() throws {
    let notes = NotesViewModel.all()
    let firstNote =  try XCTUnwrap(notes.first)
    XCTAssertEqual(firstNote.title, "first_note_title")
}

This approach is cleaner than what we might have written previously:

func testGetFirstNote() {
    let notes = NotesViewModel.all()
    if let firstNote = notes.first {
        XCTAssertEqual(firstNote.title, "first_note_title")
    } else {
        XCTFail("Failed to get first note.")
    }
}

Back to Top

To register or dequeue UITableViewCell object we need to provide its string type identifier. Typing string by hand is wasting time and could couse you typos. In this case I would recomend to use extension which declares static identifier property inside UITableViewCell to avoid these problems.

extension UITableViewCell {
   static var identifier: String {
       return String(describing: self)
   }
}

let cell = tableView.dequeueReusableCell(withIdentifier: ExampleTableViewCell.identifier)
tableView.register(ExampleTableViewCell.self, forCellReuseIdentifier: ExampleTableViewCell.identifier)

print(ExampleTableViewCell.identifier) //ExampleTableViewCell

Back to Top

In my current project I work on I present alerts almost on every view controller. To reduce lines of codes and time spent on duplicate code I created AlertPresentable layer and want to share it with you. Any ViewController which implements AlertPresentable protocol receive opportunity to present any type of alerts discribed in this layer just by one line of code.

protocol AlertPresentable {
    func presentErrorAlert(with message: String)
    func presentSuccessAlert(with message: String, action: ((UIAlertAction) -> Void)?)
    func presentConfirmationAlert(with message: String, action: ((UIAlertAction) -> Void)?)
}

extension AlertPresentable where Self: UIViewController {

    func presentErrorAlert(with message: String) {
        presentAlert(message: message, actions: [UIAlertAction(title: "OK", style: .cancel, handler: nil)])
    }

    func presentSuccessAlert(with message: String, action: ((UIAlertAction) -> Void)?) {
        presentAlert(message: message, actions: [UIAlertAction(title: "OK", style: .cancel, handler: action)])
    }

    func presentConfirmationAlert(with message: String, action: ((UIAlertAction) -> Void)?) {
        presentAlert(message: message, actions: [UIAlertAction(title: "Yes", style: .default, handler: action), UIAlertAction(title: "No", style: .cancel, handler: nil)])
    }

    private func presentAlert(title: String? = "", message: String? = nil, actions: [UIAlertAction] = []) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
        actions.forEach { (action) in
            alertController.addAction(action)
        }
        present(alertController, animated: true, completion: nil)
    }
}

Usage:

class ViewController: UIViewController, AlertPresentable {

    override func viewDidLoad() {
        super.viewDidLoad()

        presentErrorAlert(with: "User not found")

        presentSuccessAlert(with: "File downloaded") { _ in
            // use downloaded file
        }

        presentConfirmationAlert(with: "Are you sure you would like to sign out?") { _ in
            // sign out user
        }
    }
}

Back to Top

Implementation of grid CollectionView layout is a commont task. But I found that calculation of cell width when you dont know how many cells can fit in one row of CollectionView is not a common task.

Here is my extension for calculation width of cell in grid CollectionView to make your layot adaptive.

extension UICollectionView {

    func flexibleCellWidth(minCellWidth: CGFloat, minimumInteritemSpacing: CGFloat) -> CGFloat {
        let contentWidth = frame.size.width - contentInset.left - contentInset.right
        let numberOfItemsInRow = Int((contentWidth + minimumInteritemSpacing) / (minCellWidth + minimumInteritemSpacing))
        let spacesWidth = CGFloat(numberOfItemsInRow - 1) * minimumInteritemSpacing
        let availableContentWidth = contentWidth - spacesWidth
        return availableContentWidth / CGFloat(numberOfItemsInRow)
    }
}

Based on minimal cell width and minimal interitem spacing it autocalculates number of cells in row and return cell width for the best placement.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let cellWidth = collectionView.flexibleCellWidth(minCellWidth: 72, minimumInteritemSpacing: 10)
    return CGSize(width: cellWidth, height: cellWidth)
}

iPhoneSE

iPhoneX

Back to Top

You can render HTML strings within a UILabel using a special initializer of NSAttributedString and passing in NSAttributedString.DocumentType.html for .documentType. But in most cases it is not enough to display it as is. If we want to add custom font or color we need to use CSS (add CSS header to our HTML string).

extension String {

    func htmlAttributedString(with fontName: String, fontSize: Int, colorHex: String) -> NSAttributedString? {
        do {
            let cssPrefix = "<style>* { font-family: \(fontName); color: #\(colorHex); font-size: \(fontSize); }</style>"
            let html = cssPrefix + self
            guard let data = html.data(using: String.Encoding.utf8) else {  return nil }
            return try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
        } catch {
            return nil
        }
    }
}

Usage example

Back to Top

Custom errors are an integral parts of you work. Swift-defined error types can provide localized error descriptions by adopting the new LocalizedError protocol.

enum UserError: Error {
    case credentialsNotMatch
    case invalidEmail
    case invalidName
}

extension UserError: LocalizedError {
    public var errorDescription: String? {
        switch self {
            case .credentialsNotMatch:
                return NSLocalizedString("Your username and/or password do not match", comment: "Credentials do not match")
            case .invalidEmail:
                return NSLocalizedString("Please enter email address in format: [email protected]", comment: "Invalid email format")
            case .invalidName:
                return NSLocalizedString("Please enter you name", comment: "Name field is blank")
        }
    }
}

Example:

func validate(email: String?, password: String?) throws {
    throw UserError.credentialsNotMatch
}

do {
    try validate(email: "email", password: "password")
} catch UserError.credentialsNotMatch {
    print(UserError.credentialsNotMatch.localizedDescription)
}

You can provide even more information. It's common decency to show user except error description also failure reasons and recovery suggestions. Sorry for ambiguous error messages :)

enum ProfileError: Error {
    case invalidSettings
}

extension ProfileError: LocalizedError {
    public var errorDescription: String? {
        switch self {
            case .invalidSettings:
                return NSLocalizedString("Your profile settings are incorrect", comment: "")
        }
    }

    public var failureReason: String? {
        switch self {
            case .invalidSettings:
                return NSLocalizedString("I don't know why", comment: "")
        }
    }
    
    public var recoverySuggestion: String? {
        switch self {
            case .invalidSettings:
                return NSLocalizedString("Please provide correct profile settings", comment: "")
        }
    }
}

let error = ProfileError.invalidSettings
print(error.errorDescription)
print(error.failureReason)
print(error.recoverySuggestion)

Back to Top

Result type usage is really popular nowadays.

enum Result<T> {
    case success(result: T)
    case failure(error: Error)
}

func login(with credentials: Credentials, handler: @escaping (_ result: Result<User>) -> Void) {
    // Two possible options:
    handler(Result.success(result: user))
    handler(Result.failure(error: UserError.notFound))
}

login(with:) operation has user value to provide and default Result type fits perfectly here. But let’s imagine that your operation hasn’t got value to provide or you don’t care about it. Default Result type makes you to provide the result value any way.

To fix this inconvenience you need to add extension and instantiate a generic with an associated value of type Void.

func login(with credentials: Credentials, handler: @escaping (_ result: Result<Void>) -> Void)

extension Result where T == Void {
    static var success: Result {
        return .success(result: ())
    }
}

Now we can change our func login(with:) a bit, to ignore result success value if we don’t care about it.

func login(with credentials: Credentials, handler: @escaping (_ result: Result<Void>) -> Void) {
    // Two possible options:
    handler(Result.success)
    handler(Result.failure(error: UserError.notFound))
}

Back to Top

In unit testing terms, there are three phases to a unit test:

  • Given prepares the preconditions for the test. These preconditions include arguments to the function call, states of the subject, and any test doubles that are used to inspect the test subject in subsequent assertions.
  • The When phase can be as simple as making the call to the function on the test subject. In the when section an operation to be tested is performed.
  • The Then phase verifies the actual results against your expected results. These are done with assertions statements. An assertion can check if something is true or false, nil or non-nil, two objects match or not.
func testSum() {
    // Given
    let firstValue = 4
    let secondValue = 3
    // When
    let sum = sut.sum(firstValue: firstValue, secondValue: secondValue)
    // Then
    XCTAssertEqual(sum, 7)
}

Note: When comparing two values using XCTAssertEqual you should always put the actual value first before the expected value.

Back to Top

The subject under test is always named sut . It refers to a subject that is being tested for correct operation. It is short for "whatever thing we are testing" and is always defined from the perspective of the test. So when you look at your tests, there is no ambiguity as to what is being tested. All other objects are named after their types. Clearly identifying the subject under test in the sut variable makes it stand out from the rest.

The test lifecycle for each test is surrounded by a pair of functions - setUp() and tearDown(). The setUp() function is called before the execution of each test function in the test class. The tearDown() function is called after the execution of each test function in the test class. They provide you a place to prepare the sut ready before the test starts, and clean up the sut and any states after the test finishes. It’s important that each test starts with the desired initial state.

import XCTest

class YourClassTests: XCTestCase {

    // MARK: - Subject under test
    
    var sut: YourClass!
    
    // MARK: - Test lifecycle
    
    override func setUp() {
        super.setUp()
        sut = YourClass()
    }
    
    override func tearDown() {
        sut = nil
        super.tearDown()
    }
}

Back to Top

Could be useful solution for positioning nodes in your game or views in ordinary business application on the perimeter of circle.

func pointOnCircle(radius: Float, center: CGPoint) -> CGPoint {
    // Random angle between 0 and 2*pi
    let theta = Float(arc4random_uniform(UInt32.max)) / Float(UInt32.max-1) * .pi * 2.0
    // Convert polar to cartesian
    let x = radius * cos(theta)
    let y = radius * sin(theta)
    return CGPoint(x: CGFloat(x) + center.x, y: CGFloat(y) + center.y)
}

Back to Top

Let’s imagine you have an array of LOTR heroes, and an array of weapons that match those heros. How could I create a list with the heros and weapons combined at each index? zip() function is designed to combine two sequences into a single sequence of tuples.

Note: I would recomend to wrap the output from zip() into array to make its output easier to read.

One of the useful features of zip() is that if your two arrays arent equal in size it will choose the shorter one. Yes, it will not crash your application, just ignore element which doesn’t have a match in second sequence.

Back to Top

StackView, introduced in iOS 9, made Auto Layout usage much easier, because of reducing the amount of constraints needed to create manually for common layouts. But we faced with the problem, property spacing applies equally to the arranged subviews of StackView. Before iOS 11, there were 2 ways how to overcome this problem. One way is to create views with desired height and use them as spacer views or we could do it by nesting stack views but these two ways always seemed to be an unnecessary complication.

In iOS 11 you can create stack views with custom spacing between views. But there is no way to specify this custom spacing in Storyboard, only in code.

stackView.setCustomSpacing(32.0, after: label)

Also, in iOS 11 Apple introduced default and system spacing properties on the UIStackView class. You can use these to set or reset the custom spacing after a view.

stackview.setCustomSpacing(UIStackView.spacingUseSystem, after: label)

Back to Top

XCode 9.0 gives us opportunity to create named colors. We can do it directly inside assets catalog and use it in code and storyboards. Named colors contain 3 parts: name ("FerrariRed"), color specified as range and device compatibility.

Created named color you can use like this:

view.backgroundColor = UIColor(named: "FerrariRed")

Inside storyboards you can use created named color by selecting it from the color dropdown menu.

Back to Top

Error handling is realy common functionality. But if you want to handle errors across asynchronous boundaries or store value/error results for later processing, then ordinary Swift error handling won’t help. The best alternative is a common pattern called a Result type: it stores a value/error “sum” type (either one or the other) and can be used between any two arbitrary execution contexts.

Example of Result type:

enum Result<T> {
    case success(result: T)
    case failure(error: String)
}

Usage of Result type:

protocol TaskStoreProtocol {
    func fetchTasks(handler: @escaping TaskStoreFetchTasksResult)
}

typealias TaskStoreFetchTasksResult = (_ result: Result<[Task]>) -> Void

class TasksStore: TasksStoreProtocol {
    
    func fetchTasks(handler: @escaping TaskStoreFetchTasksResult) {
        // Some useful code, network request or something like this goes here
        if success {
            handler(Result.success(result: fetchedTasks))
        } else {
            handler(Result.failure(error: error))
        }
    }
}

Back to Top

Here I want to introduce you with general info about generic type parameters and their naming style. Type parameters specify and name a placeholder type, and are written immediately after the function’s name, between a pair of angle brackets (such as <T>). Once you specify a type parameter, you can use it to define the type of a function’s parameters, or as the function’s return type, or as a type annotation within the body of the function. In each case, the type parameter is replaced with an actual type whenever the function is called.

You can provide more than one type parameter by writing multiple names within the angle brackets, separated by commas.

In most cases, type parameters have descriptive names, such as Key and Value in Dictionary<Key, Value> and Element in Array<Element>, which tells about the relationship between the type parameter and the generic type or function it’s used in. However, when there isn’t a meaningful relationship between them, it’s traditional to name them using single letters such as T, U, and V.

Back to Top

You can find usage of generics in several of my tips. So here I want to show you some basics of generics. They are one of the most powerful features of Swift. It gives you opportinity to write flexible, reusable functions. Types that can work with any type, subject to requirements that you define. You can write code that avoids duplication and expresses its intent in a clear, abstracted way.

At first, here is nongeneric function which swaps two Int values:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

As you can see, this func is useful but a bit limited, because you can swap only Int values. If you want to swap two String values, or two Double values, you have to write more functions.

Generic functions can work with any type. Here’s a generic representation of our function.

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

The body of our two functions is the same. But the first line of is slightly different. The generic version of the function uses a placeholder type name (called T) instead of an actual type name (such as Int, String, or Double). The placeholder type name doesn’t say anything about what T must be, but it does say that both a and b must be of the same type T. The actual type to use in place of T is determined each time the function is called.

Back to Top

As with all dependencies, we should reduce UserDefaults to an abstract protocol, inject it into the classes that require it, and thereby decouple ourselves from implementation details and increase the testability of our code by introducing seams. But I'll be honest, the UserDefaults API is so simple and pervasive that standard dependency management feels like overkill. If so, we've probably encountered the same problem: without the ability to inject mocks, testing code that makes use of UserDefaults can be a pain. Any time we use our test device (including running tests on it!) we potentially change the state of its persisted settings.

Here is extension UserDefaults that gives your test method an opportunity to run in clean state and not jinx real persisted settings.

extension UserDefaults {
    static func blankDefaultsWhile(handler:()->Void){
        guard let name = Bundle.main.bundleIdentifier else {
            fatalError("Couldn't find bundle ID.")
        }
        let old = UserDefaults.standard.persistentDomain(forName: name)
        defer {
            UserDefaults.standard.setPersistentDomain( old ?? [:], forName: name)
        }
        UserDefaults.standard.removePersistentDomain(forName: name)
        handler()
    }
}

Usage:

class MyTests: XCTestCase {
    func testSomething() {
        // Defaults can be full of junk.
        UserDefaults.blankDefaultsWhile {
            // Some tests that expect clean defaults.
            // They can also scribble all over defaults with test-specific values.
        }
        // Defaults are back to their pre-test state.
    }
}

Back to Top

Here is one more trick how to make protocol method to be optional. Let’s remember why we would like to declare a protocol method as optional? It is because we we don’t want to write the implementation if that method will not be used. So Swift has a feature called extension that allow us to provide a default implementation for those methods that we want to be optional.

protocol CarEngineStatusDelegate {
    func engineWillStop()
    func engineDidStop()
}

extension CarEngineStatusDelegate {
    func engineWillStop() {}
}

This way the class/struct that will use our protocol will only need to implement func engineDidStop().

Back to Top

How to find the view controller that is responsible for a particular view? This is as easy as walking the responder chain looking for the first UIViewController you find.

extension UIView {
    func findViewController() -> UIViewController? {
        if let nextResponder = self.next as? UIViewController {
            return nextResponder
        } else if let nextResponder = self.next as? UIView {
            return nextResponder.findViewController()
        } else {
            return nil 
        }
    }
}

You should only use it when you really need it – if you’re able to call methods directly using a delegate or indirectly by posting notifications then do it in first place.

Back to Top

Lets imagine full of texfields scene (e.g. UserProfile scene). Moving between textfields using Next/Return on on-screen keyboard is integral functionality.

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    let nextTag = textField.tag + 1
    if let nextResponder = textField.superview?.viewWithTag(nextTag) {
        nextResponder.becomeFirstResponder()
    } else {
        textField.resignFirstResponder()
    }
    return true 
}

If you need to force the first responder to resign itself and aren’t sure which textfield is in control, it’s easier to use view.endEditing(true).

Back to Top

Retrieving all cases of enum is really common task, but it's implementation is not so clean. We were forced to create static variable of array with all cases of our enum manually.

enum Cars {
    case BMW
    case Audi
    case Volkswagen
    case Mercedes

    static let allCases = [BMW, Audi, Volkswagen, Mercedes]
}

In Swift 4.2 we got really useful property allCases, which is autogenerated (all you need to do is make your enum conform to the CaseIterable protocol), so it releases us from additional steps.

enum Cars: CaseIterable {
    case BMW
    case Audi
    case Volkswagen
    case Mercedes
}

for car in Cars.allCases {
    print(car)
}

Back to Top

One more thing left to say about protocols. Sometimes protocols should be adopted only by classes. To achieve this behaviour you should define you protocol with class keyword or by inheriting from AnyObject. This is commonly happens because you have a delegate property that needs to use weak storage to avoid the risk of a strong reference cycle (formerly known as a retain cycle).

weak var delegate: CarDelegate?

The weak keyword can't be used with structs and enums, because they are value types.

Back to Top

In previous tip you can observe how to split your protocol to smaller pieces. Here I will show you how to combine your protocols to make bigger one using inheritance and composition. To merge protocols together use & for protocol composition.

typealias Codable = Decodable & Encodable

You can also use protocol inheritance to build larger protocols.

protocol CarEngineStatusDelegate { }
protocol CarMovingStatusDelegate { }
protocol CarDelegate: CarMovingStatusDelegate, CarEngineStatusDelegate { }

Back to Top

If you implement a protocol in Swift you must implement all its requirements. Optional methods are not allowed.

Example:

protocol CarDelegate {
    func engineDidStart()
    func carShouldStartMoving() -> Bool
    func carDidStartMoving()
    func engineWillStop()
    func engineDidStop()
}

But there are a few tricks how to make some methods to be optionals:

  • You can split them in two protocols, and adopt only needed one.
protocol CarMovingStatusDelegate {
    func carShouldStartMoving() -> Bool
    func carDidStartMoving()
}

protocol CarEngineStatusDelegate {
    func engineDidStart()
    func engineWillStop()
    func engineDidStop()
}
  • Use the @objc attribute on a Swift protocol to receive opportunity to mark methods as being optional, but you may no longer use Swift structs and enums with that protocol, and you may no longer use protocol extensions.
@objc protocol CarDelegate {
    @objc optional func engineDidStart()
    func carShouldStartMoving() -> Bool
    func carDidStartMoving()
    @objc optional func engineWillStop()
    @objc optional func engineDidStop()
}

Here is the optional method usage:

delegate?.engineWillStop?()

The delegate property is optional because there might not be a delegate assigned. And in the CarDelegate protocol we made engineWillStop() an optional requirement, so even if a delegate is present it might not implement that method. As a result, we need to use optional chaining.

Back to Top

If we are talking about naming the protocol itself:

  • Protocols that describe what something is should read as nouns (e.g. Collection).
  • Protocols that describe a capability should be named using the suffixes able, ible, or ing (e.g. Equatable, ProgressReporting).

Briefly talking about methods names inside protocol, you can use whatever names you want. But, if your protocol is created to represent delegates please look: #32 Delegate naming

Back to Top

Little thing that surprisingly became a discovery for me, because it's rare case in practice, so I'm not ashamed of it 🤣. Property observers, getter/setter and lazy cant be used together, they are mutually exclusive. So you should choose what pattern makes most profit to you in particaulr situation.

Back to Top

Methods are functions associated with a type. Mock the type, and you can intercept the method. In Swift, we prefer to do this with protocols. But a standalone function lives on its own, without any associated data. In other words, it has no self. During unit-testing we want to intercept these calls, for two main reasons:

  • To spy on the arguments a function receives.
  • To stub any return values.

In popular networking 3party library Almofire all request, upload or download functions implemented as standalone functions. So, we have dificulties with intercepting during unit testing. We can't mock associated type, because there is no type. Also responseJSON function is implemented in extension, so we can't make any changes or override it.

I defined a new class with 2 methods (request and responseJSON) with the same method signature as Alamofire's request and responseJSON methods. I called the Alamofire version of these methods inside. And then used this class as a dependency in my sut.

import Alamofire

class AlamofireWrapper {
    @discardableResult
    public func request(_ url: URLConvertible, method: HTTPMethod = .get, parameters: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, headers: HTTPHeaders? = nil) -> DataRequest {
        return Alamofire.request(url, method: method, parameters: parameters, encoding: encoding, headers: headers)
    }

    public func responseJSON(request: DataRequest, queue: DispatchQueue? = nil, options: JSONSerialization.ReadingOptions = .allowFragments, completionHandler: @escaping (DataResponse<Any>) -> Void) {
        request.responseJSON(queue: queue, options: options, completionHandler: completionHandler)
    }
}

My request part starts looking like this:

var alamofireWrapper = AlamofireWrapper()

let request = alamofireWrapper.request(url, parameters: params, encoding: URLEncoding(destination: .queryString))
alamofireWrapper.responseJSON(request: request) { response in
    if response.result.isSuccess {
        success(response.result.value)
    } else if let error = response.result.error {
        failure(error)
    }
}

And here is my test double finaly:

// MARK: - Test doubles

class AlamofireWrapperSpy: AlamofireWrapper {

    enum RequestStatus {
        case success
        case failure
    }

    var status: RequestStatus = .success

    // MARK: Method call expectations

    var requestCalled = false
    var responseJSONCalled = false

    // MARK: Spied methods

    override func request(_ url: URLConvertible, method: HTTPMethod, parameters: Parameters?, encoding: ParameterEncoding, headers: HTTPHeaders?) -> DataRequest {
        requestCalled = true
        return  SessionManager.default.request("dummy_url")
    }

    override func responseJSON(request: DataRequest, queue: DispatchQueue?, options: JSONSerialization.ReadingOptions, completionHandler: @escaping (DataResponse<Any>) -> Void) {
        responseJSONCalled = true
        switch status {
        case .success:
            let value = ["result": "best_response_ever"]
            completionHandler(DataResponse(request: nil, response: nil, data: nil, result: Result.success(value)))
        case .failure:
            let error = NSError(domain: "", code: 500, userInfo: nil)
            completionHandler(DataResponse(request: nil, response: nil, data: nil, result: Result.failure(error)))
        }
    }
}

Back to Top

Few days ago I faced a sorting task. I needed to sort array of object by more then one criteria that is also optional. My object was Place with rating and distance properties. rating was main criteria for sorting and distance was secondary. So, here is my workouts.

struct Place {
    var rating: Int?
    var distance: Double?
}

func sortPlacesByRatingAndDistance(_ places: [Place]) -> [Place] {
    return places.sorted { t1, t2 in
        if t1.rating == t2.rating {
            guard let distance1 = t1.distance, let distance2 = t2.distance else {
                return false
            }
            return distance1 < distance2
        }
        guard let rating1 = t1.rating, let rating2 = t2.rating else {
            return false
        }
        return rating1 > rating2
    }
}

let places = [Place(rating: 3, distance: 127), Place(rating: 4, distance: 423), Place(rating: 5, distance: nil), Place(rating: nil, distance: 100), Place(rating: nil, distance: 34), Place(rating: nil, distance: nil)]

let sortedPlaces = sortPlacesByRatingAndDistance(places) // [{rating 5, nil}, {rating 4, distance 423}, {rating 3, distance 127}, {nil, distance 34}, {nil, distance 100}, {nil, nil}]

Back to Top

Super lightweight extension 🎈. How to remove particular object from array. Remove first collection element that is equal to the given object.

extension Array where Element: Equatable {
    mutating func remove(_ object: Element) {
        if let index = index(of: object) {
            remove(at: index)
        }
    }
}

Back to Top

When creating custom delegate methods take into consideration the following list items:

  • You can give the name to your delegate property delegate, if it is just one of them, or put delegate at the end, when it is more than one. Here is the example, WKWebView has uiDelegate and navigationDelegate properties that can point to two different objects. 

  • You should use Apple’s standard approach to avoid verb conjugation. It says that many of your method names will use will, did, and should.
  • Start the name by identifying the class of the object that’s sending the message func tableView(_ tableView: UITableView,.... It helps in situations where there is more than one object that can send a message .

As a result, Apple delegates have a very specific naming convention:

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)

Back to Top

Asynchronous work in Playground.

Tell the playground it should continue running forever, otherwise it will terminate before the asynchronous work has time to hppen.

PlaygroundPage.current.needsIndefiniteExecution = true

Back to Top

Let’s say you’ve got several long running tasks to perform. After all of them have finished, you’d like to run some further logic. You could run each task in a sequential fashion, but that isn’t so efficient - you’d really like the former tasks to run concurrently. DispatchGroup enables you to do exactly this.

let dispatchGroup = DispatchGroup()

for i in 1...5 {
    dispatchGroup.enter()
    Alamofire.request(url, parameters: params).responseJSON { response in
        //work with response
        dispatchGroup.leave() 
    }
}

dispatchGroup.notify(queue: .main) {
    print("All requests complete")
}

In the above, all long running functions will perform concurrently, followed by the print statement, which will execute on the main thread.

Each call to enter() must be matched later on with a call to leave(), after which the group will call the closure provided to notify().

DispatchGroup has a few other tricks:

  • Instead of notify(), we can call wait(). This blocks the current thread until the group’s tasks have completed.
  • A further option is wait(timeout:). This blocks the current thread, but after the timeout specified, continues anyway. To create a timeout object of type DispatchTime, the syntax .now() + 1 will create a timeout one second from now.
  • wait(timeout:) returns an enum that can be used to determine whether the group completed, or timed out.

Back to Top

Clear way 🛣️ to return the unique list of objects based on a given key 🔑. It has the advantage of not requiring the Hashable and being able to return an unique list based on any field or combination.

extension Array {
    func unique<T:Hashable>(map: ((Element) -> (T)))  -> [Element] {
        var set: Set<T> = []
        var arrayOrdered: [Element] = []
        for value in self {
            if !set.contains(map(value)) {
                set.insert(map(value))
                arrayOrdered.append(value)
            }   
        }
        return arrayOrdered
    }
}

Back to Top

Using View Debugging you’re now able to inspect an entire view hierarchy visually – right from within Xcode, instead of printing frames to the console and trying to visualize layouts in your head.

You can invoke the view debugger by choosing View UI Hierarchy from the process view options menu in the debug navigator, or by choosing Debug > View Debugging > Capture View Hierarchy.

You'll see a 3D representation of your view, which means you can look behind the layers to see what else is there. The hierarchy automatically puts some depth between each of its views, so they appear to pop off the canvas as you rotate them. If you have a complicated view layout, View Debugging > Show View Frames option will draw lines around all your views so you can see exactly where they are.

More about view debugging here

Back to Top

A breakpoint is a debugging tool that allows you to pause the execution of your program up to a certain moment. Creating pause points in your code can help you investigate your code. While your app is paused, light green arrow that shows your current execution position can be moved. Just click and drag it somewhere else to have execution pick up from there – although Xcode will warn you that it might have unexpected results, so tread carefully!

Right-click on the breakpoint (the blue arrow marker) and choose Edit Breakpoint.

In the popup that appears you can set the condition. Execution will now pause only when your condition is true. You can use conditional breakpoints to execute debugger commands automatically – the Automatically continue checkbox is perfect for making your program continue uninterrupted while breakpoints silently trigger actions. Also you can set Ignore times before stoping and actions like Debuger command, Log message, Sound, etc.

Shortcuts:

F6- Step Over. Ctrl+Cmd+Y - Continue (continue executing my program until you hit another breakpoint)

In Xcode debug console you can use po to print what you need during pause.

Back to Top

assert() is debug-only check that will force your app to crash if specific condition is false.

assert(4 == 4, "Maths error") //OK
assert(3 == 2, "Maths error") //Crash

As you can see assert() takes two parameters:

  • Something to check.
  • Message to print out of the check fails.

If the check evaluates to false, your app will be forced to crash because you know it's not in a safe state, and you'll see the error message in the debug console. If you don’t have a condition to evaluate, or don’t need to evaluate one, you can use assertionFailure() function.

precondition() is not debug-only check. It will crash your app even in release mode.

precondition(4 == 4, "Maths error") //OK
precondition(3 == 2, "Maths error") //Crash

preconditionFailure() works the same as assertionFailure(). With the same difference as above, it works for release builds.

fatalError(), like assertionFailure() and preconditionFailure() works for all optimisation levels in all build configurations.

fatalError("ERROR")

More about asserts and optimisation levels you can find here

Back to Top

Debuging 👨‍🔧 is one of the most importent aspects of programing👨‍💻. It should be in your skillbox anyway. It contains log functions, asserts, breakpoints and view debuging. Let's observe everyone of them just one by one. And here are log functions in the crosshairs 🔍.

We know print() as a variadic function. Function that accepts any number of parameters.

print("one", "two", "three", "four") //one two three four

But it's variadic nature becomes much more useful when you use separator and terminator, its optional extra parameters. separator gives you opportunity to provide a string that should be placed between every item. It's "space" by default.

print("one", "two", "three", "four", separator: "-") //one-two-three-four

Meanwhile terminator is what should be placed after the last item. It’s \n by default, which means "line break".

print("one", "two", "three", "four", terminator: " five") //one two three four five

Back to Top

Really lightweight way 🎈 How to add content changing animation to UIView and it subclasses.

extension UIView {
    func fadeTransition(_ duration: CFTimeInterval) {
        let animation = CATransition()
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        animation.type = kCATransitionFade
        animation.duration = duration
        layer.add(animation, forKey: kCATransitionFade)
    }
}

Just invoke 🧙‍♂️ fadeTransition(_ duration: CFTimeInterval) by your view before you will apply a change.

label.fadeTransition(1)
label.text = "Updated test content with animation"

Back to Top

Next code snippet 📃 I use to keep eye on changes that take place in the managed object context. Useful thing to know what's going on, what was added, updated ( what specific values were changed ) or deleted 📥📝📤

func changeNotification(_ notification: Notification) {
    guard let userInfo = notification.userInfo else { return }

    if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 {
        print("--- INSERTS ---")
        print(inserts)
        print("+++++++++++++++")
    }

    if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 {
        print("--- UPDATES ---")
        for update in updates {
            print(update.changedValues())
        }
        print("+++++++++++++++")
    }

    if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 {
        print("--- DELETES ---")
        print(deletes)
        print("+++++++++++++++")
    }
}

NotificationCenter.default.addObserver(self, selector: #selector(self.changeNotification(_:)), name: .NSManagedObjectContextObjectsDidChange, object: moc)

Back to Top

Default ways of splitting ✂️ String don't work perfect sometimes, because of punctuation characters and other "wreckers" 🐛. Here is extension for splitting ✂️ String into words 💻🧐👌.

extension String {
    var words: [String] {
        return components(separatedBy: .punctuationCharacters)
            .joined()
            .components(separatedBy: .whitespaces)
            .filter{!$0.isEmpty}
    }
}

Back to Top

I discovered strange behavior of tuples during comparing 🤪. Comparison cares only about types and ignores labels 😦. So result can be unexpected. Be careful ⚠️.

let car = (model: "Tesla", producer: "USA")
let company = (name: "Tesla", country: "USA")
if car == company {
    print("Equal")
} else {
    print("Not equal")
}

Printed result will be: Equal

Back to Top

Painless way ( NO to timers from now ⛔️ ) how to detect that user stop typing text in text field ⌨️ Could be usefull for lifetime search 🔍

class TestViewController: UIViewController {

    @objc func searchBarDidEndTyping(_ textField: UISearchBar) {
        print("User finsihed typing text in search bar")
    }

    @objc func textFieldDidEndTyping(_ textField: UITextField) {
        print("User finished typing text in text field")
    }
}

extension TestViewController: UISearchBarDelegate {
    func searchBar(_ searchBar: UISearchBar, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(searchBarDidEndTyping), object: searchBar)
        self.perform(#selector(searchBarDidEndTyping), with: searchBar, afterDelay: 0.5)
        return true
    }
}

extension TestViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(textFieldDidEndTyping), object: textField)
        self.perform(#selector(textFieldDidEndTyping), with: textField, afterDelay: 0.5)
        return true
    }
}

Back to Top

Clear way of adding left\right text offset inside UItextField 🔨🧐💻 Also, because of @IBInspectable it could be easily editable in Interface Builder’s inspector panel.

@IBDesignable
extension UITextField {

    @IBInspectable var leftPaddingWidth: CGFloat {
        get {
            return leftView!.frame.size.width
        }
        set {
            let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: newValue, height: frame.size.height))
            leftView = paddingView
            leftViewMode = .always
        }
    }

    @IBInspectable var rigthPaddingWidth: CGFloat {
        get {
            return rightView!.frame.size.width
        }
        set {
            let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: newValue, height: frame.size.height))
            rightView = paddingView
            rightViewMode = .always
        }
    }
}

Back to Top

I'm not huge fan of custom operators 😐 because they are intuitively obvious only to their authors, but I've created one which gives you opportunity to get common elements in two arrays whos elements implement Equatable protocol 🔨🧐💻

infix operator &
func  &<T : Equatable>(lhs: [T], rhs: [T]) -> [T] {
    return lhs.filter { rhs.contains($0) }
}

Back to Top

Gradient 🏳️‍🌈 on Navigation Bar is really good looking, but not very easy to implement 🧐🔨👨‍💻 Works with iOS 11 largeTitle navigation bar too 👌

struct GradientComponents {
    var colors: [CGColor]
    var locations: [NSNumber]
    var startPoint: CGPoint
    var endPoint: CGPoint
}

extension UINavigationBar {

    func applyNavigationBarGradient(with components: GradientComponents) {

        let size = CGSize(width: UIScreen.main.bounds.size.width, height: 1)
        let gradient = CAGradientLayer()
        gradient.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)

        gradient.colors = components.colors
        gradient.locations = components.locations
        gradient.startPoint = components.startPoint
        gradient.endPoint = components.endPoint

        UIGraphicsBeginImageContext(gradient.bounds.size)
        gradient.render(in: UIGraphicsGetCurrentContext()!)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        self.barTintColor = UIColor(patternImage: image!)
    }
}

Back to Top

Easy way how to get next element of array

extension Array where Element: Hashable {
    func after(item: Element) -> Element? {
        if let index = self.index(of: item), index + 1 < self.count {
            return self[index + 1]
        }
        return nil
    }
}

Back to Top

Great extension to split array by chunks of given size

extension Array {
    func chunk(_ chunkSize: Int) -> [[Element]] {
        return stride(from: 0, to: self.count, by: chunkSize).map({ (startIndex) -> [Element] in
            let endIndex = (startIndex.advanced(by: chunkSize) > self.count) ? self.count-startIndex : chunkSize
            return Array(self[startIndex..<startIndex.advanced(by: endIndex)])
        })
    }
}

Back to Top

Scene with UIImageView on top looks stylish if navigation bar is transparent. Easy way how to make navigation bar transparent or opaque.

func transparentNavigationBar() {
    self.setBackgroundImage(UIImage(), for: .default)
    self.shadowImage = UIImage()
}

func opaqueNavigationBar() {
    self.shadowImage = nil
    self.setBackgroundImage(nil, for: .default)
}

Back to Top

One more useful extension 🔨💻 Gives you opportunity to group objects by property 👨‍💻🧐

extension Sequence {
    func group<GroupingType: Hashable>(by key: (Iterator.Element) -> GroupingType) -> [[Iterator.Element]] {
        var groups: [GroupingType: [Iterator.Element]] = [:]
        var groupsOrder: [GroupingType] = []
        forEach { element in
            let key = key(element)
            if case nil = groups[key]?.append(element) {
                groups[key] = [element]
                groupsOrder.append(key)
            }
        }
        return groupsOrder.map { groups[$0]! }
    }
}

Usage:

struct Person {
    var name: String
    var age: Int
}

let mike = Person(name: "Mike", age: 18)
let john = Person(name: "John", age: 18)
let bob = Person(name: "Bob", age: 56)
let jake = Person(name: "Jake", age: 56)
let roman = Person(name: "Roman", age: 25)

let persons = [mike, john, bob, jake, roman]

let groupedPersons = persons.group { $0.age }

for persons in groupedPersons {
    print(persons.map { $0.name })
}

Result:

["Mike", "John"]
["Bob", "Jake"]
["Roman"]

Also in-box alternative

Back to Top

Do you need semicolons in Swift ? Short answer is NO, but you can use them and it will give you interesting opportunity. Semicolons enable you to join related components into a single line.

func sum(a: Int, b: Int) -> Int {
    let sum = a + b; return sum
}

Back to Top

Unit testing shouldn’t have any side effects. While running tests, Xcode firstly launches app and thus having the side effect of executing any code we may have in our App Delegate and initial View Controller. Fake AppDelegate in your main.swift to prevent it.

You can find main.swift file here

Back to Top

Apple's docs specify that: "Property observers are only called when the property’s value is set outside of initialization context."

defer can change situation 😊

class AA {
    var propertyAA: String! {
        didSet {
            print("Function: \(#function)")
        }
    }

    init(propertyAA: String) {
        self.propertyAA = propertyAA
    }
}

class BB {
    var propertyBB: String! {
        didSet {
            print("Function: \(#function)")
        }
    }

    init(propertyBB: String) {
        defer {
            self.propertyBB = propertyBB
        }
    }
}

let aa = AA(propertyAA: "aa")
let bb = BB(propertyBB: "bb")

Result:

Function: propertyBB

Back to Top

Two ways of changing type of items in array and obvious difference between them 🧐👨‍💻

let numbers = ["1", "2", "3", "4", "notInt"]
let mapNumbers = numbers.map { Int($0) }  // [Optional(1), Optional(2), Optional(3), Optional(4), nil]
let compactNumbers = numbers.compactMap { Int($0) } // [1, 2, 3, 4]

Back to Top

Execution order is interesting difference between forEach and map: forEach is guaranteed to go through array elements in its sequence, while map is free to go in any order.

Back to Top

  1. Even if you don't write UI Tests, they still take considerable amount of time to run. Just skip it.
  2. Enable code coverage stats in Xcode, it helps to find which method was tested, not tested, partly tested. But don’t pay too much attention to the percentage 😊.

Back to Top

  1. Say what happened and why
  2. Suggest a next step
  3. Find the right tone (If it’s a stressful or serious issue, then a silly tone would be inappropriate)

Common​ ​Types​ ​of​ ​Error​ ​Messages

Back to Top

Do you know that using map gives profit to the compiler: it's now clear we want to apply some code to every item in an array, then like in for loop we could have break on halfway through.

Back to Top

compactMap func is effectively the combination of using map and joined in a single call, in that order. It maps items in array A into array B using a func you provide, then joins the results using concatenation.

Functions min and max could be also combinations of sorted.first and sorted.last in single call.

let colors = ["red", "blue", "black", "white"]

let min = colors.min() // black
let first = colors.sorted().first // black

let max = colors.max() // white
let last = colors.sorted().last // white

Back to Top

Use enumerated when you iterate over the collection to return a sequence of pairs (n, c), where n - index for each element and c - its value 👨‍💻💻

for (n, c) in "Swift".enumerated() {
    print("\(n): \(c)")
}

Result:

0: S
1: w
2: i
3: f
4: t

Also be careful with this tricky thing, enumerated on collection will not provide actual indices, but monotonically increasing integer, which happens to be the same as the index for Array but not for anything else, especially slices.

Back to Top

Ever faced the problem that u can't hide status bar because of prefersStatusBarHidden is get-only? The simplest solution is to override it 🧐👨‍💻

let vc = UIViewController()
vc.prefersStatusBarHidden = true // error
print("statusBarHidded \(vc.prefersStatusBarHidden)") // false

class TestViewController: UIViewController {
    override var prefersStatusBarHidden: Bool {
        return true
    }
}

let testVC = TestViewController()
print("statusBarHidded \(testVC.prefersStatusBarHidden)") // true

Back to Top

You can extend collections to return the element at the specified index if it is within bounds, otherwise nil.

extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

let cars = ["Lexus", "Ford", "Volvo", "Toyota", "Opel"]
let selectedCar1 = cars[safe: 3] // Toyota
let selectedCar2 = cars[safe: 6] // not crash, but nil

Back to Top

Releases

No releases published

Packages

No packages published

Languages