My personal collections of things, tips & tricks I've learned during iOS development so far and do not want to forget.
I'm happy for any feedback, so feel free to write me on twitter.
#65 β Get the size of a child view in SwiftUI
#64 β Check for enabled state in ButtonStyle
#63 β Animate text-color with SwiftUI
#62 β Custom localized date format
#61 β Animate isHidden
on a UIStackView
#60 β Making types expressible by literals
#59 β SwiftUI ToggleStyle
Protocol
#58 β Getting the size of a view as defined by Auto Layout
#57 β Decode Array while filtering invalid entries
#56 β Codable cheat sheet
#55 β SwiftUI make a child view respect the safe area
#54 β Convert string with basic HTML tags to SwiftUI's Text
#53 β Concatenate two Texts in SwiftUI
#52 β Animated reload of a UITableView
#51 β Redux & SwiftUI Example
#50 β Basic Combine Examples
#49 β Convert units using Measurement<UnitType>
#48 β FloatingPoint
Protocol
#47 β Wait for multiple async tasks to complete
#46 β Snapshot testing
#45 β Span subview to superview
#44 β Animate a view using a custom timing function
#43 β How to test a delegate protocol
#42 β Xcode multi-cursor editing
#41 β Create a dynamic color for light- and dark mode
#40 β UITableViewCell
extension that declares a static identifier
#39 β Prefer "for .. in .. where"-loop over filter()
and forach {}
#38 β Lightweight observable implementation
#37 β Run test cases in a playground
#36 β Show progress of a WKWebView
in a UIProgressBar
#35 β Destructure tuples
#34 β Avoid huge if statements
#33 β Compare dates in test cases
#32 β Be aware of the strong reference to the target of a timer
#31 β Initialize DateFormatter
with formatting options
#30 β Map latitude and longitude to X and Y on a coordinate system
#29 β Encapsulation
#28 β Remove UITextView
default padding
#27 β Name that color
#26 β Structure classes using // MARK: -
#25 β Structure test cases
#24 β Avoid forced unwrapping
#23 β Always check for possible dividing through zero
#22 β Animate alpha
and update isHidden
accordingly
#21 β Create custom notification
#20 β Override UIStatusBarStyle
the elegant way
#19 β Log extension on String
using swift literal expressions
#18 β Use gitmoji for commit messages
#17 β Initialize a constant based on a condition
#16 β Why viewDidLoad
might be called before init
has finished
#15 β Capture iOS Simulator video
#14 β Xcode open file in focused editor
#13 β Handle optionals in test cases
#12 β Safe access to an element at index
#11 β Check whether a value is part of a given range
#10 β Use compactMap
to filter nil
values
#09 β Prefer Set
instead of array for unordered lists without duplicates
#08 β Remove all sub-views from UIView
#07 β Animate image change on UIImageView
#06 β Change CALayer
without animation
#05 β Override layerClass
to reduce the total amount of layers
#04 β Handle notifications in test cases
#03 β Use didSet
on outlets to setup components
#02 β Most readable way to check whether an array contains a value (isAny(of:)
)
#01 β Override self
in escaping closure, to get a strong reference to self
\
π Using a PreferenceKey
it's possible to get the size of a child view in SwiftUI.
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct SizeModifier: ViewModifier {
func body(content: Content) -> some View {
content.background(
GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
}
)
}
}
In the following example the property textSize
will contain the size of the Text
view.
struct ContentView: View {
@State
private var textSize: CGSize = .zero
var body: some View {
Text("Hello World")
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) { textSize in
self.textSize = textSize
}
}
}
Further information on PreferenceKey
can be found here: The magic of view preferences in SwiftUI
π¨ The ButtonStyle
protocol allows us to customise buttons through our application without copy-pasting the styling code.
Unfortunately it's not possible to get the environment property isEnabled
inside ButtonStyle
. But it's possible to get it inside a View
as a workaround.
struct PrimaryButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
PrimaryButtonStyleView(configuration: configuration)
}
}
private struct PrimaryButtonStyleView: View {
// MARK: - Public properties
let configuration: ButtonStyle.Configuration
// MARK: - Private properties
@SwiftUI .Environment(\.isEnabled)
private var isEnabled: Bool
private var foregroundColor: Color {
guard isEnabled else {
return .gray
}
return configuration.isPressed
? .white.opacity(0.5)
: .white
}
// MARK: - Render
var body: some View {
configuration.label
.foregroundColor(foregroundColor)
}
}
π¨ Unfortunately in SwiftUI the property foregroundColor
can't be animated. But it's possible to animate colorMultiply
instead.
Therefore we set foregroundColor
to white
and use colorMultiply
to set the actual color we want. This color is then animatable.
struct AnimateTextColor: View {
// MARK: - Private properties
@State
private var textColor: Color = .red
// MARK: - Render
var body: some View {
Text("Lorem Ipsum Dolor Sit Amet.")
.foregroundColor(.white)
.colorMultiply(textColor)
.onTapGesture {
withAnimation(.easeInOut) {
textColor = .blue
}
}
}
}
π Using the method dateFormat(fromTemplate:options:locale:)
we can further customise a date-format (e.g. MMMd
) to a specific locale.
extension Date {
/// Returns a localized string from the current instance for the given `template` and `locale`.
///
/// - Parameters:
/// - template: A string containing date format patterns (such as βMMβ or βhβ).
/// - locale: The locale for which the template is required.
///
/// - SeeAlso: [dateFormat(fromTemplate:options:locale:)](https://developer.apple.com/documentation/foundation/dateformatter/1408112-dateformat)
func localizedString(from template: String, for locale: Locale) -> String {
let dateFormatter = DateFormatter()
dateFormatter.locale = locale
let localizedDateFormat = DateFormatter.dateFormat(fromTemplate: template, options: 0, locale: locale)
dateFormatter.dateFormat = localizedDateFormat
return dateFormatter.string(from: self)
}
}
let template = "MMMd"
let now: Date = .now
let usLocale = Locale(identifier: "en_US")
print("United States:", now.localizedString(from: template, for: usLocale))
// United States: Oct 1
let deLocale = Locale(identifier: "de")
print("Germany:", now.localizedString(from: template, for: deLocale))
// Germany: 1. Okt.
#61 β Animate isHidden
on a UIStackView
π§ββοΈ It's easily possible to animate the visibility of an arranged subview inside a UIStackView
. In this example the corresponding view will slide out when setting the property isHidden
to true
.
UIView.animateWithDuration(0.3) {
viewInsideStackView.isHidden = true
stackView.layoutIfNeeded()
}
π Swift provides protocols which enable you to initialize a type using literals, e.g.:
let int = 0 // ExpressibleByIntegerLiteral
let string = "Hello World!" // ExpressibleByStringLiteral
let array = [0, 1, 2, 3, 4, 5] // ExpressibleByArrayLiteral
let dictionary = ["Key": "Value"] // ExpressibleByDictionaryLiteral
let boolean = true // ExpressibleByBooleanLiteral
A complete list of these protocols can be found in the documentation: Initialization with Literals
Here we focus on ExpressibleByStringLiteral
and ExpressibleByStringInterpolation
for initialising a custom type.
struct StorageKey {
let path: String
}
extension StorageKey: ExpressibleByStringLiteral, ExpressibleByStringInterpolation {
init(stringLiteral path: String) {
self.init(path: path)
}
}
Build an instance of StorageKey
using ExpressibleByStringLiteral
:
let storageKey: StorageKey = "/cache/"
Build an instance of StorageKey
using ExpressibleByStringInterpolation
:
let username = "f.mau"
let storageKey: StorageKey = "/users/\(username)/cache"
This pattern is especially handy when creating an URL instance from a string:
extension URL: ExpressibleByStringLiteral {
/// Initializes an URL instance from a string literal, e.g.:
/// ```
/// let url: URL = "https://felix.hamburg"
/// ```
public init(stringLiteral value: StaticString) {
guard let url = URL(string: "\(value)") else {
fatalError("β οΈ β Failed to create a valid URL instance from `\(value)`.")
}
self = url
}
}
For safety reason we only conform to ExpressibleByStringLiteral
and thereof use StaticString
, as we don't want any dynamic string interpolation to crash our app.
Based on
- Defining static URLs using string literals
- Making types expressible by string interpolation
- Expressible literals in Swift explained by 3 useful examples
π¨ SwiftUI provides a ToggleStyle protocol to completely customize the appearance of a Toggle.
Important: When customizing a Toggle
using this protocol, itβs down to you to visualize the state! Therefore the method makeBody(configuration:)
is passed with a parameter configuration
that contains the current state and allows toggling it by calling configuration.isOn.toggle()
.
To demonstrate custom Toggle styles I've added two gists with screenshots in the comments:
- A fully configurable toggle style for SwiftUI.
- A toggle style for SwiftUI, making the Toggle look like a checkbox.
systemLayoutSizeFitting(targetSize:)
method on UIView
, we can obtain the size of a view as defined by Auto Layout.
For example we could ask for the height of a view, using a given width:
let size = view.systemLayoutSizeFitting(
CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height),
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
πͺ Usually an API should have a clear interface and the App should know which data to receive. But there are cases when you can't be 100% sure about a response.
Imagine fetching a list of flights for an airport. You don't want the entire decoding to fail in case one flight has a malformed departure date.
As a workaround we define a helper type, that wraps the actual data-model, in our case a Flight
data-model.
/// Helper to filter-out invalid array entries when parsing a JSON response.
///
/// This way we prevent the encoding-failure of an entire array, if the decoding of a single element fails.
///
/// Source: https://stackoverflow.com/a/46369152/3532505
private struct FailableDecodable<Base: Decodable>: Decodable {
let base: Base?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
base = try? container.decode(Base.self)
}
}
In our service we decode the array of Flight
s to FailableDecodable<Flight>
, to filter out invalid array elements, but don't let the entire decoding fail (only the property base
will be nil
on failure).
Afterwards we use compactMap { $0.base }
to filter out array-values where the property base
is nil
.
/// Data-model
struct Flight {
let number: String
let departure: Date
}
/// Service-method
func fetchDepartures(for url: URL) -> AnyPublisher<[Flight], Error> {
URLSession.shared
.dataTaskPublisher(for: url)
.map { $0.data }
// We explicitly use `FailableDecodable<T>` here, to filter out invalid array elements afterwards.
.decode(type: [FailableDecodable<Flight>].self, decoder: JsonDecoder())
.map {
// Map the array of type `FailableDecodable<Flight>` to `Flight`, while filtering invalid (`nil`) elements.
$0.compactMap { $0.base }
}
.eraseToAnyPublisher()
}
}
π Paul Hudson has written a great cheat sheet about converting between JSON and Swift data types.
π² Neat trick for having the content of a View
respect the safe-area, while having the background covering the entire device.
struct FullScreenBackgroundView: View {
var body: some View {
Text("Hello, World!")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.background(Color.red.edgesIgnoringSafeArea(.all))
}
}
struct FullScreenBackgroundViewPreviews: PreviewProvider {
static var previews: some View {
FullScreenBackgroundView()
.previewDevice(PreviewDevice(rawValue: "iPhone 11 Pro"))
}
}
π Using the underneath shown +
operator we can build an extension on SwiftUI's Text, that allows us to parse basic HTML tags (like <strong>
, β<em>
etc).
Please have a look at the comments for some usage examples.
Update 28.05.2021
iOS 15.0 brings AttributedString
to SwiftUI
including Markdown support.
Converting basic HTML formatting tags to Markdown is not too difficult, so I added a second gist showing exactly that and further adds support for hyperlinks: SwiftUI+HTML.swift
π§ββοΈ The +
operator can concatenate two Text
in SwiftUI.
Text("Note:")
.bold() +
Text(" Lorem Ipsum Dolor Sit Amet.")
This will render: "Note: Lorem Ipsum Dolor Sit Amet."
π Calling tableView.reloadData()
inside the animation block of UIView.transition(with:duration:options:animations:completion:) will result in an animated reload of the table view cells.
UIView.transition(with: tableView,
duration: 0.3,
options: .transitionCrossDissolve,
animations: { self.tableView.reloadData() })
You can pass any UIView.AnimationOptions
mentioned here.
Source: https://stackoverflow.com/a/13261683
π The following gist shows you how to integrate basic Redux functionality in SwiftUI (without using any additional frameworks): Redux.swift
Feel free to copy the code into a Xcode Playground and give it a try π
π§ͺ Here are two Gists regarding Apple's new Combine framework:
- Combine-PassthroughSubject-CurrentValueSubject.swift
This gist explains the difference between aPassthroughSubject
and aCurrentValueSubject
. - Combine-CLLocationManagerDelegate.swift
This gists shows how to convert a delegate pattern to combine publishers, in this case theCLLocationManagerDelegate
.
Feel free to copy the code a playground and get your hands dirty with Combine π
π Starting from iOS 10 we can use Measurement
to convert units like e.g. angles, areas, durations, speeds, temperature, volume and many many more.
Using e.g. Measurement<UnitAngle>
we can refactor the computed property shown in note #48 to a method, that allows us to convert between any UnitAngle
:
extension BinaryFloatingPoint {
func converted(from fromUnit: UnitAngle, to toUnit: UnitAngle) -> Self {
let selfAsDouble = Double(self)
let convertedValueAsDouble = Measurement(value: selfAsDouble, unit: fromUnit)
.converted(to: toUnit)
.value
return type(of: self).init(convertedValueAsDouble)
}
}
Furthermore this approach leads to a very clean call side:
let cameraBearing: CLLocationDegrees = 180
cameraBearing.converted(from: .degrees, to: .radians)
π² By extending the protocol FloatingPoint
we can define a method / computed property on all floating point datatypes, e.g. Double
, Float
or CGFloat
:
extension FloatingPoint {
var degToRad: Self {
self * .pi / 180
}
}
let double: Double = 90
let float: Float = 180
let cgFloat: CGFloat = 270
print("Double as radians", double.degToRad)
print("Float as radians", float.degToRad)
print("CGFloat as radians", cgFloat.degToRad)
β° Using a DispatchGroup
we can wait for multiple async tasks to finish.
let dispatchGroup = DispatchGroup()
var profile: Profile?
dispatchGroup.enter()
profileService.fetchProfile {
profile = $0
dispatchGroup.leave()
}
var friends: Friends?
dispatchGroup.enter()
profileService.fetchFriends {
friends = $0
dispatchGroup.leave()
}
// We need to define the completion handler of our `DispatchGroup` with an unbalanced call to `enter()` and `leave()`,
// as otherwise it will be called immediately!
dispatchGroup.notify(queue: .main) {
guard let profile = profile, let friends = friends else { return }
print("We've downloaded the user profile together with all friends!")
}
Update for Projects targeting iOS >= 13.0
Starting from iOS 13 we can use CombineLatest
to wait for multiple publishers to at least fire publish one message.
let fetchProfileFuture = profileService.fetchProfile()
let fetchFriendsFuture = profileService.fetchFriends()
cancellable = Publishers.CombineLatest(fetchProfileFuture, fetchFriendsFuture)
.sink { result in
let (profile, friends) = result
print("We've downloaded the user profile together with all friends!")
}
Starting from iOS 15 iOS 13 we can also use async let
to wait for multiple async values.
Task {
async let profileTask = profileService.fetchProfile()
async let friendsTask = profileService.fetchFriends()
let (profile, friends) = await(profileTask, friendsTask)
print("We've downloaded the user profile together with all friends!")
}
πΈ Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly.
Using the library SnapshotTesting from Point-Free you can easily start testing snapshots of your UIView
, UIViewController
, UIImage
or even URLRequest
.
βοΈ A small extension to span a subview to the anchors of its superview.
extension UIView {
/// Adds layout constraints to top, bottom, leading and trailing anchors equal to superview.
func fillToSuperview(spacing: CGFloat = 0) {
guard let superview = superview else { return }
translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topAnchor.constraint(equalTo: superview.topAnchor, constant: spacing),
leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: spacing),
superview.bottomAnchor.constraint(equalTo: bottomAnchor, constant: spacing),
superview.trailingAnchor.constraint(equalTo: trailingAnchor, constant: spacing)
])
}
}
π Starting from iOS 10 we can use a UIViewPropertyAnimator
to animate changes on views.
Using the initializer init(duration:timingParameters:)
we can pass a UITimingCurveProvider
, which allows us to provide a custom timing function. You can find lots of these functions on Easings.net.
Using e.g. "easeInBack" your animation code could look like this:
extension UICubicTimingParameters {
static let easeInBack = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.6, y: -0.28),
controlPoint2: CGPoint(x: 0.735, y: 0.045))
}
class CustomTimingAnimationViewController: UIViewController {
// ...
func userDidTapButton() {
let animator = UIViewPropertyAnimator(duration: 1.0,
timingParameters: UICubicTimingParameters.easeInBack)
animator.addAnimations {
// Add your animation code here. E.g.:
// `self.someConstraint?.isActive = false`
// `self.someOtherConstraint?.isActive = true`
}
animator.startAnimation()
}
}
π§ͺ Delegation is a common pattern whenever one object needs to communicate to another object (1:1 communication).
The following gist shows you how to test a delegate-protocol from a view-model, by creating a mock and validate the invoked method(s) using an enum: Example on how to elegantly test a delegate protocol
πβ Since Xcode 10 the Source Editor supports multi-cursor editing, allowing you to quickly edit multiple ranges of code at once. You can place additional cursors with the mouse via:
shift + control + click
shift + control + β
shift + control + β
π¨ Using the gist UIColor+MakeDynamicColor.swift we can create a custom UIColor
that generates its color data dynamically based on the current userInterfaceStyle
.
Furthermore this method falls back to the lightVariant
color for iOS versions prior to iOS 13.
π§ββοΈ Using the extension below we can automatically register and dequeue table view cells. It prevents typos and declaring a static string on each cell.
extension UITableViewCell {
static var identifier: String {
return String(describing: self)
}
}
Register a cell:
tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
Dequeue a cell:
let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier)
π’ For iterating over a large array using a "for .. in .. where" loop is two times faster than combing filter()
and forach {}
, as it saves one iteration.
So instead of writing:
scooterList
.filter({ !$0.isBatteryEmpty })
.forEach({ scooter in
// Do something with each scooter, that still has some battery left.
})
it is more efficient to write:
for scooter in scooterList where !scooter.isBatteryEmpty {
// Do something with each scooter, that still has some battery left.
}
π΅οΈββοΈ If you need a simple and lightweight observable implementation for e.g. UI bindings check out the following gist: Observable.swift
For re-usability reasons I've moved the code into a framework and released it as a CocoaPod. Please check out https://github.com/fxm90/LightweightObservable π
π§ͺ Playgrounds are an easy way to try out simple ideas. It is a good approach to directly think about the corresponding test-cases for the idea or even start the implementation test driven.
By calling MyTestCase.defaultTestSuite.run()
inside the playground we can run a test-case and later copy it into our "real" project.
import Foundation
import XCTest
class MyTestCase: XCTestCase {
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testFooBarShouldNotBeEqual() {
XCTAssertNotEqual("Foo", "Bar")
}
}
MyTestCase.defaultTestSuite.run()
You can see the result of each test inside the debug area of the playground.
For running asynchronous test cases you have to add the following line:
PlaygroundPage.current.needsIndefiniteExecution = true
π€ For showing the loading-progress of a WKWebView
on a UIProgressBar
, please have a look at the following gist: WebViewExampleViewController.swift
In the example code, the UIProgressBar
is attached to the bottom anchor of an UINavigationBar
(see method setupProgressView()
for further layout details).
π§β Image having a tuple with the following properties: (firstName: String, lastName: String)
. We can destructure the tuple into two properties in just one line:
let (firstName, lastName) = accountService.fullName()
print(firstName)
print(lastName)
β¨ Instead of writing long "if statements" like this:
struct HugeDataObject {
let category: Int
let subCategory: Int
// Imagine lots of other properties, so we can't simply conform to `Equatable` ...
}
if hugeDataObject.category != previousDataObject.category || hugeDataObject.subCategory != previousDataObject.subCategory {
// ...
}
We can split the long statement into several properties beforehand, to increase readability:
let isDifferentCategory = hugeDataObject.category != previousDataObject.category
let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory
if isDifferentCategory || isDifferentSubCategory {
// ...
}
Or use guard
to do an early return:
let isDifferentCategory = hugeDataObject.category != previousDataObject.category
let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory
let didChange = isDifferentCategory || isDifferentSubCategory
guard didChange else { return }
Notice: By using that pattern we do not skip further checks on failure (e.g. if we use OR
in the statement and one condition returns true
/ we use AND
in the statement and one condition returns false
). So if you're having a load intensive method, it might be better to keep it as a single statement. Or, first check the "lighter" condition and then use an early return to prevent the load intensive method from being executed.
π Small example on how to compare dates in tests.
func testDatesAreEqual() {
// Given
let dateA = Date()
let dateB = Date()
// When
// ...
// Then
XCTAssertEqual(dateA.timeIntervalSince1970,
dateB.timeIntervalSince1970,
accuracy: 0.01)
}
π Creating a timer with the method scheduledTimer(timeInterval:target:selector:userInfo:repeats:)
always creates a strong reference to the target until the timer is invalidated. Therefore, an instance of the following class will never be deallocated:
class ClockViewModel {
// MARK: - Private properties
weak var timer: Timer?
// MARK: - Initializer
init(interval: TimeInterval = 1.0) {
timer = Timer.scheduledTimer(timeInterval: interval,
target: self,
selector: #selector(timerDidFire),
userInfo: nil,
repeats: true)
}
deinit {
print("This will never be called π")
timer?.invalidate()
timer = nil
}
// MARK: - Private methods
@objc private func timerDidFire() {
// Do something every x seconds here.
}
}
But didn't we declare the variable timer
as weak
? So even though we have a strong reference from the timer to the view-model (via the target and selector), we should not have a retain cycle? Well, that's true. The solution is mentioned in the documentation for the class "Timer"
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you donβt have to maintain your own strong reference to a timer after you have added it to a run loop.
and the documentation for the method "timerWithTimeInterval"
target: The timer maintains a strong reference to this object until it (the timer) is invalidated.
Therefore the run loop contains a strong reference to the view-model, as long as the timer is not invalidated. As we call invalidate
inside the deinit
of the view-model method, the timer gets never invalidated.
From iOS 10.0 we can use the method scheduledTimer(withTimeInterval:repeats:block:)
instead and pass a weak
reference to self
in the closure, in order to prevent a retain cycle.
init(interval: TimeInterval = 1.0) {
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [weak self] _ in
self?.timerDidFire()
})
}
For iOS version below 10.0, we can use DispatchSourceTimer
instead. There is a great article from Daniel Galasko on how to do that: A Background Repeating Timer in Swift
Notice: Even for non repeating timers, you should be aware of that strong reference, cause the corresponding object won't get deallocated until the timer has fired.
π Basic formatting, which requires only setting dateStyle
and timeStyle
, can be achieved with the class function localizedString(from:dateStyle:timeStyle:).
In case you need further formatting options, the following extension allows you to directly initialize a DateFormatter
with all available options:
extension DateFormatter {
convenience init(configure: (DateFormatter) -> Void) {
self.init()
configure(self)
}
}
Use it like this:
let dateFormatter = DateFormatter {
$0.locale = .current
$0.dateStyle = .long
$0.timeStyle = .short
}
let dateFormatter = DateFormatter {
$0.dateFormat = "E, d. MMMM"
}
Feel free to bring this extension to other formatters, like e.g. DateComponentsFormatter or DateIntervalFormatter, as well.
Update: Starting with Swift 4 we can use key-paths instead of closures:
protocol Builder {}
extension Builder {
func set<T>(_ keyPath: WritableKeyPath<Self, T>, to value: T) -> Self {
var mutableCopy = self
mutableCopy[keyPath: keyPath] = value
return mutableCopy
}
}
extension Formatter: Builder {}
Use it like this:
let dateFormatter = DateFormatter()
.set(\.locale, to: .current)
.set(\.dateStyle, to: .long)
.set(\.timeStyle, to: .short)
let numberFormatter = NumberFormatter()
.set(\.locale, to: .current)
.set(\.numberStyle, to: .currency)
Based on: Vadim Bulavin β KeyPath Based Builder
π Not really an iOS specific topic but something to keep in mind π
On a standard north facing map, latitude is represented by horizontal lines, which go up and down (North and South) the Y axis. It's easy to think that since they are horizontal lines, they would be on the x axis, but they are not. So similarly, the X axis is Longitude, as the values shift left to right (East and West) along the X axis. Confusing for the same reason since on a north facing map, these lines are vertical.
https://gis.stackexchange.com/a/68856
The following graphics illustrate the quote above:
Latitude | Longitude |
---|---|
πͺ When working on a continuously evolving code base, one of the biggest challenges is to keep things nicely encapsulated. Having clear defined APIs avoids sharing implementation details with other types and therefore prevent unwanted side-effects.
Even notification receivers or outlets can be marked as private.
class KeyboardViewModel {
// MARK: - Public properties
/// Boolean flag, whether the keyboard is currently visible.
/// We assume that this property has to be accessed from the view controller, therefore we allow public read-access.
private(set) var isKeyboardVisible = false
// MARK: - Initializer
init(notificationCenter: NotificationCenter = .default) {
notificationCenter.addObserver(self,
selector: #selector(didReceiveUIKeyboardWillShowNotification),
name: UIResponder.keyboardWillShowNotification,
object: nil)
notificationCenter.addObserver(self,
selector: #selector(didReceiveUIKeyboardDidHideNotification),
name: UIResponder.keyboardDidHideNotification,
object: nil)
}
// MARK: - Private methods
@objc private func didReceiveUIKeyboardWillShowNotification(_: Notification) {
isKeyboardVisible = true
}
@objc private func didReceiveUIKeyboardDidHideNotification(_: Notification) {
isKeyboardVisible = false
}
}
β With the following code the default padding from an UITextView
can be removed:
// This brings the left edge of the text to the left edge of the container
textView.textContainer.lineFragmentPadding = 0
// This causes the top of the text to align with the top of the container
textView.textContainerInset = .zero
Source: https://stackoverflow.com/a/18987810/3532505
The above code can also be applied inside the interface builder within the "User Defined Runtime Attributes" section. Just add the following lines there:
Key Path | Type | Value |
---|---|---|
textContainer.lineFragmentPadding | Number | 0 |
textContainerInset | Rect | {{0, 0}, {0, 0}} |
π¨ Not an iOS specific topic, but if your designer comes up with the 9th gray tone and you somehow need to find a proper name inside your code, check out this site: Name That Color. It automatically generates a name for the given color π§β
π Using // MARK:
we can add some additional information that is shown in the quick jump bar. Adding a dash at the end (// MARK: -
) causes a separation line to show up. Using this technique we can structure classes and make them easier to read.
class StructuredViewController: UIViewController {
// MARK: - Types
typealias CompletionHandler = (Bool) -> Void
// MARK: - Outlets
@IBOutlet private var submitButton: UIButton!
// MARK: - Public properties
var completionHandler: CompletionHandler?
// MARK: - Private properties
private let viewModel: StructuredViewModel
// MARK: - Initializer
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
viewModel = StructuredViewModel()
// ...
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
}
deinit {
// ...
}
// MARK: - Public methods
override func viewDidLoad() {
// ...
}
// MARK: - Private methods
private func setupSubmitButton() {
// ...
}
}
Given
, When
, Then
increases the readability and helps understanding complex tests.
- In the
Given
phase we setup all preconditions for the test, e.g. configuring mock objects. - In the
When
phase we call the function we want to test. - In the
Then
phase we verify the actual results against our expected results usingXCTAssert
methods.
class MapViewModelTestCase: XCTestCase {
var locationServiceMock: LocationServiceMock!
var viewModel: MapViewModel!
var delegateMock: MapViewModelDelegateMock!
override func setUp() {
super.setUp()
// ...
}
override func tearDown() {
// ...
super.tearDown()
}
func testLocateUser() {
// Given
let userLocation = CLLocationCoordinate2D(latitude: 12.34,
longitude: 56.78)
locationServiceMock.userLocation = userLocation
// When
viewModel.locateUser()
// Then
XCTAssertEqual(delegateMock.focusedUserLocation.latitude, userLocation.latitude)
XCTAssertEqual(delegateMock.focusedUserLocation.longitude, userLocation.longitude)
}
}
The only time you should be using implicitly unwrapped optionals is with @IBOutlets. In every other case, it is better to use a non-optional or regular optional property. Yes, there are cases in which you can probably "guarantee" that the property will never be nil when used, but it is better to be safe and consistent. Similarly, don't use force unwraps.
Source: https://github.com/linkedin/swift-style-guide
Using the patterns shown underneath, we can easily unwrap optionals or use early return to stop further code executing, if an optional is nil
.
if let value = value {
// Do something with value here..
}
guard let value = value else {
// Write a comment, why to exit here.
return
}
// Do something with value here..
π₯ We should always make sure that a certain value is NOT zero before dividing through it.
class ImageViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var imageView: UIImageView!
// MARK: - Private methods
func someMethod() {
let bounds = imageView.bounds
guard bounds.height > 0 else {
// Avoid diving through zero for calculating aspect ratio below.
return
}
let aspectRatio = bounds.width / bounds.height
}
}
#22 β Animate alpha
and update isHidden
accordingly
π¦ Using the following gist we can animate the alpha
property and update the isHidden
flag accordingly: fxm90/UIView+AnimateAlpha.swift
π For creating custom notifications we first should have a look on how to name them properly:
[Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification
Source: Coding Guidelines for Cocoa
We create the new notification by extending the corresponding class:
extension Notification.Name {
static let AccountServiceDidLoginUser = Notification.Name("AccountServiceDidLoginUserNotification")
}
And afterwards post it like this:
class AccountService {
func login() {
NotificationCenter.default.post(name: .AccountServiceDidLoginUser,
object: self)
}
}
For Objective-C support we further need to extend NSNotification
:
@objc extension NSNotification {
static let AccountServiceDidLoginUser = Notification.Name.AccountServiceDidLoginUser
}
Then, we can post it like this:
[NSNotificationCenter.defaultCenter post:NSNotification.AccountServiceDidLoginUser
object:self];
By extending Notification.Name
we make sure our notification names are unique.
Notice: The object parameter should always contain the object, that is triggering the notification. If you need to pass custom data, use the userInfo
parameter.
βοΈ Using a custom property, combined with the observer didSet
we can call setNeedsStatusBarAppearanceUpdate()
to apply a new status-bar style:
class SomeViewController: UIViewController {
// MARK: - Public properties
override var preferredStatusBarStyle: UIStatusBarStyle {
return customBarStyle
}
// MARK: - Private properties
private var customBarStyle: UIStatusBarStyle = .default {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
}
π Swift contains some special literals:
Literal | Type | Value |
---|---|---|
#file | String | The name of the file in which it appears. |
#line | Int | The line number on which it appears. |
#column | Int | The column number in which it begins. |
#function | String | The name of the declaration in which it appears. |
Source: Swift.org β Expressions |
Especially with default parameters those expressions are really useful, as in that case the expression is evaluated at the call site. We could use a simple extension on String to create a basic logger:
"Lorem Ipsum Dolor Sit Amet π".log(level: .info)
That would create the following output:
βΉοΈ β 2018/09/16 19:46:45.189 - ViewController.swift - viewDidLoad():15
> Lorem Ipsum Dolor Sit Amet π
π Not an iOS specific topic, but I'd like to use gitmoji for my commit messages, e.g. TICKET-NUMBER - β»οΈ :: Description
(Credits go to Martin Knabbe for that pattern).
To easily create the corresponding emojis for the type of commit, you can use this alfred workflow.
π A very readable way of initializing a constant after the declaration.
let startCoordinate: CLLocationCoordinate2D
if let userCoordinate = userLocationService.userCoordinate, CLLocationCoordinate2DIsValid(userCoordinate) {
startCoordinate = userCoordinate
} else {
// We don't have a valid user location, so we fallback to Hamburg.
startCoordinate = CLLocationCoordinate2D(latitude: 53.5582447,
longitude: 9.647645)
}
This way we can avoid using a variable and therefore prevent any mutation of startCoordinate
in further code.
β‘οΈ Be aware that the method viewDidLoad
is being called immediately on accessing self.view
in the initializer.
This happens because the view is not loaded yet, but the property self.view
shouldn't return nil
.
Therefore the view controller will load the view immediately and call the corresponding method viewDidLoad
afterwards.
class ViewDidLoadBeforeInitViewController: UIViewController {
// MARK: - Initializer
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
view.isHidden = true
print("π :: `\(#function)` did finish!")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
view.isHidden = true
print("π :: `\(#function)` did finish!")
}
// MARK: - Public methods
override func viewDidLoad() {
super.viewDidLoad()
print("π :: `\(#function)` did finish!")
}
}
The code will output log statements in the following order:
π :: `viewDidLoad()` did finish.
π :: `init(nibName:bundle:)` did finish.
Source: https://stackoverflow.com/a/5808477
More on view life cycle: Work with View Controllers
πΉ A small tutorial on how create a video of what's happening in the simulator.
- Run your App in the simulator
- Open terminal
- Run one of the following commands:
- To take a screenshot:
xcrun simctl io booted screenshot
- To take a video:
xcrun simctl io booted recordVideo <filename>.<file extension>
- Press ctrl + c to stop recording the video.
For example:
xcrun simctl io booted recordVideo ~/appVideo.mp4
Source: https://stackoverflow.com/a/41141801
In case you want to further customise the simulator, e.g. by setting a custom battery level, check out this amazing tool by Paul Hudson: ControlRoom
πββοΈ Shortcuts are a great way to increase productivity. I often use CMD[β] + Shift[β§] + O
to quickly open a file or CMD[β] + Shift[β§] + J
to focus the current file in the project navigator etc.
But when you βQuick Openβ a file via cmd-shift-O, it opens in the βPrimary Editorβ on the left β even if the right editor pane is currently focused.
By going to Settings Β» Navigation Β» Navigation
and there checking Uses Focused Editor
, we can tell Xcode to always open files in the currently focused pane.
Source: Jesse Squires β Improving the assistant editor
β
Using XCTUnwrap
we can safely unwrap optionals in test-cases. If the optional is nil
, only the current test-case will fail, but the app won't crash and all other test-cases will continue to be executed.
In the example below, we initialize a view model with a list of bookings. Using the method findBooking(byIdentifier:)
we search for a given booking. But as we might pass an invalid identifier, the response of the method is an optional booking object. Using XCTUnwrap
we can easily unwrap the response.
class BookingViewModelTestCase: XCTestCase {
func testFindBookingByIdentifierShouldReturnMockedBooking() throws {
// Given
let mockedBooking = Booking(identifier: 1)
let viewModel = BookingViewModel(bookings: [mockedBooking])
// When
let fetchedBooking = try XCTUnwrap(
viewModel.findBooking(byIdentifier: 1)
)
// Then
XCTAssertEqual(fetchedBooking, mockedBooking)
}
}
Require is a simple, yet really useful framework for handling optionals in test cases (by John Sundell again π). He also wrote a great blog post explaining the use-case for this framework: Avoiding force unwrapping in Swift unit tests
β Using the range operator, we can easily create an extension to safely return an array element at the specified index, or nil
if the index is outside the bounds.
extension Array {
subscript(safe index: Index) -> Element? {
let isValidIndex = (0 ..< count).contains(index)
guard isValidIndex else {
return nil
}
return self[index]
}
}
let fruits = ["Apple", "Banana", "Cherries", "Kiwifruit", "Orange", "Pineapple"]
let banana = fruits[safe: 2]
let pineapple = fruits[safe: 6]
// Does not crash, but contains nil
let invalid = fruits[safe: 7]
π‘ Instead of writing x >= 10 && x <= 100
, we can write 10 ... 100 ~= x
.
let statusCode = 200
let isSuccessStatusCode = 200 ... 299 ~= statusCode
let isRedirectStatusCode = 300 ... 399 ~= statusCode
let isClientErrorStatusCode = 400 ... 499 ~= statusCode
let isServerErrorStatusCode = 500 ... 599 ~= statusCode
Another (more readable way) for checking whether a value is part of a given range can be achieved using the contains
method:
let statusCode = 200
let isSuccessStatusCode = (200 ... 299).contains(statusCode)
let isRedirectStatusCode = (300 ... 399).contains(statusCode)
let isClientErrorStatusCode = (400 ... 499).contains(statusCode)
let isServerErrorStatusCode = (500 ... 599).contains(statusCode)
π Using compactMap
we can filter out any nil
values of an array.
struct ItemDataModel {
let title: String?
}
struct ItemViewModel {
let title: String
}
extension ItemViewModel {
/// Convenience initializer, that maps the data-model from the server to our view-model
/// if all required properties are available.
init?(dataModel: ItemDataModel) {
guard let title = dataModel.title else {
return nil
}
self.init(title: title)
}
}
class ListViewModel {
/// ...
func mapToItemViewModel(response: [ItemDataModel]) -> ([ItemViewModel]) {
// Using `compactMap` we filter out invalid data-models automatically.
response.compactMap { ItemViewModel(dataModel: $0) }
}
}
π« Advantage over Array
:
- Constant Lookup time O(1), as a
Set
stores its members based on hash value.
Disadvantage compared to Array
:
- No guaranteed order.
- Can't contain duplicate values.
- All items we want to store must conform to
Hashable
protocol.
For further examples and use-cases please have a look at "The power of sets in Swift" (by John Sundell).
π A small extension to remove all sub-views.
extension UIView {
func removeAllSubviews() {
subviews.forEach { $0.removeFromSuperview() }
}
}
βοΈ Easily (ex)change an image with using a transition (note that the .transitionCrossDissolve
is the key to get this working).
extension UIImageView {
func updateImageWithTransition(_ image: UIImage?, duration: TimeInterval) {
UIView.transition(with: self, duration: duration, options: .transitionCrossDissolve, animations: { () -> Void in
self.image = image
})
}
}
π¨βπ¨ CALayer has a default implicit animation duration of 0.25 seconds. Using the following extension we can do changes without an animation:
extension CALayer {
class func performWithoutAnimation(_ runWithoutAnimation: () -> Void) {
CATransaction.begin()
CATransaction.setAnimationDuration(0.0)
runWithoutAnimation()
CATransaction.commit()
}
}
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
By overriding 'layerClass' you can tell UIKit what CALayer class to use for a UIView's backing layer. That way you can reduce the amount of layers, and don't have to do any manual layout. John Sundell
This is useful to e.g. add a linear gradient behind an image. Furthermore we could change the gradient-color based on the time of the day, without having to add multiple images to our app.
You can see the full code for the example in my gist for the Vertical Gradient Image View.
π¬ Examples on how to test notifications in test cases:
- XCTest β Assert notification (not) triggered
- XCTest β Use custom notification center in test case and assert notification (not) triggered
π By using didSet
on outlets we can setup our view components (declared in a storyboard or xib) in a very readable way:
class FooBarViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var button: UIButton! {
didSet {
button.setTitle(viewModel.normalTitle, for: .normal)
button.setTitle(viewModel.disabledTitle, for: .disabled)
}
}
// MARK: - Private properties
private var viewModel = FooBarViewModel()
}
β¨ A small extension to check whether a value is part of a list of candidates, in a very readable way (by John Sundell)
extension Equatable {
func isAny(of candidates: Self...) -> Bool {
return candidates.contains(self)
}
}
enum Device {
case iPhone7
case iPhone8
case iPhoneX
case iPhone11
}
let device: Device = .iPhoneX
// Before
let hasSafeAreas = [.iPhoneX, .iPhone11].contains(device)
// After
let hasSafeAreas = device.isAny(of: .iPhoneX, .iPhone11)
πΈ To avoid retain cycles we often have to pass a weak
reference to self
into closures. By using the following pattern, we can get a strong reference to self
for the lifetime of the closure.
someService.request() { [weak self] response in
guard let self = self else { return }
self.doSomething(with: response)
}
Notice: The above works as of Swift 4.2. Before you have to use:
guard let `self` = self else { return }
There is a great article about when to use weak self
and why it's needed.