├── Assets ├── latitude--thumbnail.jpg ├── latitude.jpg ├── longitude--thumbnail.jpg ├── longitude.jpg └── overwrite-layer-class.jpg └── README.md /Assets/latitude--thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fxm90/dev-notes/b7cada4986c235d45d603ae65df99ed2f6fc6f8a/Assets/latitude--thumbnail.jpg -------------------------------------------------------------------------------- /Assets/latitude.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fxm90/dev-notes/b7cada4986c235d45d603ae65df99ed2f6fc6f8a/Assets/latitude.jpg -------------------------------------------------------------------------------- /Assets/longitude--thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fxm90/dev-notes/b7cada4986c235d45d603ae65df99ed2f6fc6f8a/Assets/longitude--thumbnail.jpg -------------------------------------------------------------------------------- /Assets/longitude.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fxm90/dev-notes/b7cada4986c235d45d603ae65df99ed2f6fc6f8a/Assets/longitude.jpg -------------------------------------------------------------------------------- /Assets/overwrite-layer-class.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fxm90/dev-notes/b7cada4986c235d45d603ae65df99ed2f6fc6f8a/Assets/overwrite-layer-class.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iOS Dev-Notes 🗒 🚀 2 | My personal collections of things, tips & tricks I've learned during iOS development so far and do not want to forget. 3 | 4 | I'm happy for any feedback, so feel free to write me on [twitter](https://twitter.com/_fxm90). 5 | 6 | 7 | ## Table of contents 8 | [\#65 – Get the size of a child view in SwiftUI](#65--get-the-size-of-a-child-view-in-swiftui)\ 9 | [\#64 – Check for enabled state in `ButtonStyle`](#64--check-for-enabled-state-in-buttonstyle)\ 10 | [\#63 – Animate text-color with SwiftUI](#63--animate-text-color-with-swiftui)\ 11 | [\#62 – Custom localized date format](#62--custom-localized-date-format)\ 12 | [\#61 – Animate `isHidden` on a `UIStackView`](#61--animate-ishidden-on-a-uistackview)\ 13 | [\#60 – Making types expressible by literals](#60--making-types-expressible-by-literals)\ 14 | [\#59 – SwiftUI `ToggleStyle` Protocol](#59--swiftui-togglestyle-protocol)\ 15 | [\#58 – Getting the size of a view as defined by Auto Layout](#58--getting-the-size-of-a-view-as-defined-by-auto-layout)\ 16 | [\#57 – Decode Array while filtering invalid entries](#57--decode-array-while-filtering-invalid-entries)\ 17 | [\#56 – Codable cheat sheet](#56--codable-cheat-sheet)\ 18 | [\#55 – SwiftUI make a child view respect the safe area](#55--swiftui-make-a-child-view-respect-the-safe-area)\ 19 | [\#54 – Convert string with basic HTML tags to SwiftUI's Text](#54--convert-string-with-basic-html-tags-to-swiftuis-text)\ 20 | [\#53 – Concatenate two Texts in SwiftUI](#53--concatenate-two-texts-in-swiftui)\ 21 | [\#52 – Animated reload of a `UITableView`](#52--animated-reload-of-a-uitableview)\ 22 | [\#51 – Redux & SwiftUI Example](#51--redux--swiftui-example)\ 23 | [\#50 – Basic Combine Examples](#50--basic-combine-examples)\ 24 | [\#49 – Convert units using `Measurement`](#49--convert-units-using-measurementunittype)\ 25 | [\#48 – `FloatingPoint` Protocol](#48--floatingpoint-protocol)\ 26 | [\#47 – Wait for multiple async tasks to complete](#47--wait-for-multiple-async-tasks-to-complete)\ 27 | [\#46 – Snapshot testing](#46--snapshot-testing)\ 28 | [\#45 – Span subview to superview](#45--span-subview-to-superview)\ 29 | [\#44 – Animate a view using a custom timing function](#44--animate-a-view-using-a-custom-timing-function)\ 30 | [\#43 – How to test a delegate protocol](#43--how-to-test-a-delegate-protocol)\ 31 | [\#42 – Xcode multi-cursor editing](#42--xcode-multi-cursor-editing)\ 32 | [\#41 – Create a dynamic color for light- and dark mode](#41--create-a-dynamic-color-for-light--and-dark-mode)\ 33 | [\#40 – `UITableViewCell` extension that declares a static identifier](#40--uitableviewcell-extension-that-declares-a-static-identifier)\ 34 | [\#39 – Prefer "for .. in .. where"-loop over `filter()` and `forach {}`](#39--prefer-for--in--where-loop-over-filter-and-forach-)\ 35 | [\#38 – Lightweight observable implementation](#38--lightweight-observable-implementation)\ 36 | [\#37 – Run test cases in a playground](#37--run-test-cases-in-playground)\ 37 | [\#36 – Show progress of a `WKWebView` in a `UIProgressBar`](#36--show-progress-of-wkwebview-in-uiprogressbar)\ 38 | [\#35 – Destructure tuples](#35--destructure-tuples)\ 39 | [\#34 – Avoid huge if statements](#34--avoid-huge-if-statements)\ 40 | [\#33 – Compare dates in test cases](#33--compare-dates-in-test-cases)\ 41 | [\#32 – Be aware of the strong reference to the target of a timer](#32--be-aware-of-the-strong-reference-to-the-target-of-a-timer)\ 42 | [\#31 – Initialize `DateFormatter` with formatting options](#31--initialize-dateformatter-with-formatting-options)\ 43 | [\#30 – Map latitude and longitude to X and Y on a coordinate system](#30--map-latitude-and-longitude-to-x-and-y-on-a-coordinate-system)\ 44 | [\#29 – Encapsulation](#29--encapsulation)\ 45 | [\#28 – Remove `UITextView` default padding](#28--remove-uitextview-default-padding)\ 46 | [\#27 – Name that color](#27--name-that-color)\ 47 | [\#26 – Structure classes using `// MARK: - `](#26--structure-classes-using--mark--)\ 48 | [\#25 – Structure test cases](#25--structure-test-cases)\ 49 | [\#24 – Avoid forced unwrapping](#24--avoid-forced-unwrapping)\ 50 | [\#23 – Always check for possible dividing through zero](#23--always-check-for-possible-dividing-through-zero)\ 51 | [\#22 – Animate `alpha` and update `isHidden` accordingly](#22--animate-alpha-and-update-ishidden-accordingly)\ 52 | [\#21 – Create custom notification](#21--create-custom-notification)\ 53 | [\#20 – Override `UIStatusBarStyle` the elegant way](#20--override-uistatusbarstyle-the-elegant-way)\ 54 | [\#19 – Log extension on `String` using swift literal expressions](#19--log-extension-on-string-using-swift-literal-expressions)\ 55 | [\#18 – Use gitmoji for commit messages](#18--use-gitmoji)\ 56 | [\#17 – Initialize a constant based on a condition](#17--initialize-a-constant-based-on-a-condition)\ 57 | [\#16 – Why `viewDidLoad` might be called before `init` has finished](#16--why-viewdidload-might-be-called-before-init-has-finished)\ 58 | [\#15 – Capture iOS Simulator video](#15--capture-ios-simulator-video)\ 59 | [\#14 – Xcode open file in focused editor](#14--xcode-open-file-in-focused-editor)\ 60 | [\#13 – Handle optionals in test cases](#13--handle-optionals-in-test-cases)\ 61 | [\#12 – Safe access to an element at index](#12--safe-access-to-an-element-at-index)\ 62 | [\#11 – Check whether a value is part of a given range](#11--check-whether-a-value-is-part-of-a-given-range)\ 63 | [\#10 – Use `compactMap` to filter `nil` values](#10--use-compactmap-to-filter-nil-values)\ 64 | [\#09 – Prefer `Set` instead of array for unordered lists without duplicates](#09--prefer-set-instead-of-array-for-unordered-lists-without-duplicates)\ 65 | [\#08 – Remove all sub-views from `UIView`](#08--remove-all-sub-views-from-uiview)\ 66 | [\#07 – Animate image change on `UIImageView`](#07--animate-image-change-on-uiimageview)\ 67 | [\#06 – Change `CALayer` without animation](#06--change-calayer-without-animation)\ 68 | [\#05 – Override `layerClass` to reduce the total amount of layers](#05--override-layerclass-to-reduce-the-total-amount-of-layers)\ 69 | [\#04 – Handle notifications in test cases](#04--handle-notifications-in-test-cases)\ 70 | [\#03 – Use `didSet` on outlets to setup components](#03--use-didset-on-outlets-to-setup-components)\ 71 | [\#02 – Most readable way to check whether an array contains a value (`isAny(of:)`)](#02--most-readable-way-to-check-whether-an-array-contains-a-value-isanyof)\ 72 | [\#01 – Override `self` in escaping closure, to get a strong reference to `self`](#01--override-self-in-escaping-closure-to-get-a-strong-reference-to-self)\ 73 | 74 | 75 | ## #65 – Get the size of a child view in SwiftUI 76 | 📏 Using a `PreferenceKey` it's possible to get the size of a child view in SwiftUI. 77 | 78 | ```swift 79 | struct SizePreferenceKey: PreferenceKey { 80 | static var defaultValue: CGSize = .zero 81 | 82 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) { 83 | value = nextValue() 84 | } 85 | } 86 | 87 | struct SizeModifier: ViewModifier { 88 | func body(content: Content) -> some View { 89 | content.background( 90 | GeometryReader { geometry in 91 | Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size) 92 | } 93 | ) 94 | } 95 | } 96 | ``` 97 | 98 | In the following example the property `textSize` will contain the size of the `Text` view. 99 | 100 | ```swift 101 | struct ContentView: View { 102 | 103 | @State 104 | private var textSize: CGSize = .zero 105 | 106 | var body: some View { 107 | Text("Hello World") 108 | .modifier(SizeModifier()) 109 | .onPreferenceChange(SizePreferenceKey.self) { textSize in 110 | self.textSize = textSize 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | Further information on `PreferenceKey` can be found here: [The magic of view preferences in SwiftUI](https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/) 117 | 118 | ## #64 – Check for enabled state in `ButtonStyle` 119 | 🎨 The `ButtonStyle` protocol allows us to customise buttons through our application without copy-pasting the styling code. 120 | 121 | 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. 122 | 123 | ```swift 124 | struct PrimaryButtonStyle: ButtonStyle { 125 | func makeBody(configuration: Self.Configuration) -> some View { 126 | PrimaryButtonStyleView(configuration: configuration) 127 | } 128 | } 129 | 130 | private struct PrimaryButtonStyleView: View { 131 | 132 | // MARK: - Public properties 133 | 134 | let configuration: ButtonStyle.Configuration 135 | 136 | // MARK: - Private properties 137 | 138 | @SwiftUI .Environment(\.isEnabled) 139 | private var isEnabled: Bool 140 | 141 | private var foregroundColor: Color { 142 | guard isEnabled else { 143 | return .gray 144 | } 145 | 146 | return configuration.isPressed 147 | ? .white.opacity(0.5) 148 | : .white 149 | } 150 | 151 | // MARK: - Render 152 | 153 | var body: some View { 154 | configuration.label 155 | .foregroundColor(foregroundColor) 156 | } 157 | } 158 | ``` 159 | 160 | ## #63 – Animate text-color with SwiftUI 161 | 🎨 Unfortunately in SwiftUI the property `foregroundColor` can't be animated. But it's possible to animate `colorMultiply` instead. 162 | 163 | Therefore we set `foregroundColor` to `white` and use `colorMultiply` to set the actual color we want. This color is then animatable. 164 | 165 | ```swift 166 | struct AnimateTextColor: View { 167 | 168 | // MARK: - Private properties 169 | 170 | @State 171 | private var textColor: Color = .red 172 | 173 | // MARK: - Render 174 | 175 | var body: some View { 176 | Text("Lorem Ipsum Dolor Sit Amet.") 177 | .foregroundColor(.white) 178 | .colorMultiply(textColor) 179 | .onTapGesture { 180 | withAnimation(.easeInOut) { 181 | textColor = .blue 182 | } 183 | } 184 | } 185 | } 186 | ``` 187 | 188 | ## #62 – Custom localized date format 189 | 📝 Using the method [`dateFormat(fromTemplate:options:locale:)`](https://developer.apple.com/documentation/foundation/dateformatter/1408112-dateformat) we can further customise a date-format (e.g. `MMMd`) to a specific locale. 190 | 191 | ```swift 192 | extension Date { 193 | 194 | /// Returns a localized string from the current instance for the given `template` and `locale`. 195 | /// 196 | /// - Parameters: 197 | /// - template: A string containing date format patterns (such as “MM” or “h”). 198 | /// - locale: The locale for which the template is required. 199 | /// 200 | /// - SeeAlso: [dateFormat(fromTemplate:options:locale:)](https://developer.apple.com/documentation/foundation/dateformatter/1408112-dateformat) 201 | func localizedString(from template: String, for locale: Locale) -> String { 202 | let dateFormatter = DateFormatter() 203 | dateFormatter.locale = locale 204 | 205 | let localizedDateFormat = DateFormatter.dateFormat(fromTemplate: template, options: 0, locale: locale) 206 | dateFormatter.dateFormat = localizedDateFormat 207 | 208 | return dateFormatter.string(from: self) 209 | } 210 | } 211 | ``` 212 | 213 | ```swift 214 | let template = "MMMd" 215 | let now: Date = .now 216 | 217 | let usLocale = Locale(identifier: "en_US") 218 | print("United States:", now.localizedString(from: template, for: usLocale)) 219 | // United States: Oct 1 220 | 221 | let deLocale = Locale(identifier: "de") 222 | print("Germany:", now.localizedString(from: template, for: deLocale)) 223 | // Germany: 1. Okt. 224 | ``` 225 | 226 | 227 | ## #61 – Animate `isHidden` on a `UIStackView` 228 | 🧙‍♀️ 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`. 229 | 230 | ```swift 231 | UIView.animateWithDuration(0.3) { 232 | viewInsideStackView.isHidden = true 233 | stackView.layoutIfNeeded() 234 | } 235 | ``` 236 | 237 | ## #60 – Making types expressible by literals 238 | 🖌 Swift provides protocols which enable you to initialize a type using literals, e.g.: 239 | 240 | ```swift 241 | let int = 0 // ExpressibleByIntegerLiteral 242 | let string = "Hello World!" // ExpressibleByStringLiteral 243 | let array = [0, 1, 2, 3, 4, 5] // ExpressibleByArrayLiteral 244 | let dictionary = ["Key": "Value"] // ExpressibleByDictionaryLiteral 245 | let boolean = true // ExpressibleByBooleanLiteral 246 | ``` 247 | 248 | A complete list of these protocols can be found in the documentation: [Initialization with Literals 249 | ](https://developer.apple.com/documentation/swift/swift_standard_library/initialization_with_literals) 250 | 251 | Here we focus on `ExpressibleByStringLiteral` and `ExpressibleByStringInterpolation` for initialising a custom type. 252 | 253 | ```swift 254 | struct StorageKey { 255 | let path: String 256 | } 257 | 258 | extension StorageKey: ExpressibleByStringLiteral, ExpressibleByStringInterpolation { 259 | init(stringLiteral path: String) { 260 | self.init(path: path) 261 | } 262 | } 263 | ``` 264 | 265 | Build an instance of `StorageKey` using `ExpressibleByStringLiteral`: 266 | 267 | ```swift 268 | let storageKey: StorageKey = "/cache/" 269 | ``` 270 | 271 | Build an instance of `StorageKey` using `ExpressibleByStringInterpolation`: 272 | 273 | ```swift 274 | let username = "f.mau" 275 | let storageKey: StorageKey = "/users/\(username)/cache" 276 | ``` 277 | 278 | This pattern is especially handy when creating an URL instance from a string: 279 | 280 | ```swift 281 | extension URL: ExpressibleByStringLiteral { 282 | /// Initializes an URL instance from a string literal, e.g.: 283 | /// ``` 284 | /// let url: URL = "https://felix.hamburg" 285 | /// ``` 286 | public init(stringLiteral value: StaticString) { 287 | guard let url = URL(string: "\(value)") else { 288 | fatalError("⚠️ – Failed to create a valid URL instance from `\(value)`.") 289 | } 290 | 291 | self = url 292 | } 293 | } 294 | ``` 295 | 296 | 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. 297 | 298 | Based on 299 | - [Defining static URLs using string literals](https://www.swiftbysundell.com/tips/defining-static-urls-using-string-literals/) 300 | - [Making types expressible by string interpolation](https://www.swiftbysundell.com/tips/making-types-expressible-by-string-interpolation/) 301 | - [Expressible literals in Swift explained by 3 useful examples](https://www.avanderlee.com/swift/expressible-literals/) 302 | 303 | ## #59 – SwiftUI `ToggleStyle` Protocol 304 | 🎨 SwiftUI provides a [ToggleStyle](https://developer.apple.com/documentation/swiftui/togglestyle) protocol to completely customize the appearance of a [Toggle](https://developer.apple.com/documentation/swiftui/toggle). 305 | 306 | **Important:** When customizing a `Toggle` using this protocol, it’s down to you to visualize the state! Therefore the method [`makeBody(configuration:)`](https://developer.apple.com/documentation/swiftui/togglestyle/makebody(configuration:)) is passed with a parameter `configuration` that contains the current state and allows toggling it by calling `configuration.isOn.toggle()`. 307 | 308 | To demonstrate custom Toggle styles I've added two gists with screenshots in the comments: 309 | - [A fully configurable toggle style for SwiftUI. 310 | ](https://gist.github.com/fxm90/6afe050ac331d8f719029d7fec87e961) 311 | - [A toggle style for SwiftUI, making the Toggle look like a checkbox. 312 | ](https://gist.github.com/fxm90/b56d537d9fb8bf20d573a45367e18c4f) 313 | 314 | ## #58 – Getting the size of a view as defined by Auto Layout 315 | ↔️ Using the [`systemLayoutSizeFitting(targetSize:)`](https://developer.apple.com/documentation/uikit/uiview/1622624-systemlayoutsizefitting) method on `UIView`, we can obtain the size of a view as defined by Auto Layout. 316 | 317 | For example we could ask for the height of a view, using a given width: 318 | 319 | ```swift 320 | let size = view.systemLayoutSizeFitting( 321 | CGSize(width: view.bounds.width, height: UIView.layoutFittingCompressedSize.height), 322 | withHorizontalFittingPriority: .required, 323 | verticalFittingPriority: .fittingSizeLevel 324 | ) 325 | ``` 326 | 327 | ## #57 – Decode Array while filtering invalid entries 328 | 🪄 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. 329 | 330 | 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. 331 | 332 | As a workaround we define a helper type, that wraps the actual data-model, in our case a `Flight` data-model. 333 | 334 | ```swift 335 | /// Helper to filter-out invalid array entries when parsing a JSON response. 336 | /// 337 | /// This way we prevent the encoding-failure of an entire array, if the decoding of a single element fails. 338 | /// 339 | /// Source: https://stackoverflow.com/a/46369152/3532505 340 | private struct FailableDecodable: Decodable { 341 | 342 | let base: Base? 343 | 344 | init(from decoder: Decoder) throws { 345 | let container = try decoder.singleValueContainer() 346 | base = try? container.decode(Base.self) 347 | } 348 | } 349 | ``` 350 | 351 | In our service we decode the array of `Flight`s to `FailableDecodable`, to filter out invalid array elements, but don't let the entire decoding fail (only the property `base` will be `nil` on failure). 352 | 353 | Afterwards we use `compactMap { $0.base }` to filter out array-values where the property `base` is `nil`. 354 | 355 | ```swift 356 | /// Data-model 357 | struct Flight { 358 | let number: String 359 | let departure: Date 360 | } 361 | 362 | /// Service-method 363 | func fetchDepartures(for url: URL) -> AnyPublisher<[Flight], Error> { 364 | URLSession.shared 365 | .dataTaskPublisher(for: url) 366 | .map { $0.data } 367 | // We explicitly use `FailableDecodable` here, to filter out invalid array elements afterwards. 368 | .decode(type: [FailableDecodable].self, decoder: JsonDecoder()) 369 | .map { 370 | // Map the array of type `FailableDecodable` to `Flight`, while filtering invalid (`nil`) elements. 371 | $0.compactMap { $0.base } 372 | } 373 | .eraseToAnyPublisher() 374 | } 375 | } 376 | ``` 377 | 378 | 379 | ## #56 – Codable cheat sheet 380 | 📝 [Paul Hudson](https://twitter.com/twostraws) has written a great [cheat sheet](https://www.hackingwithswift.com/articles/119/codable-cheat-sheet) about converting between JSON and Swift data types. 381 | 382 | ## #55 – SwiftUI make a child view respect the safe area 383 | 📲 Neat trick for having the content of a `View` respect the safe-area, while having the background covering the entire device. 384 | 385 | ```swift 386 | struct FullScreenBackgroundView: View { 387 | var body: some View { 388 | Text("Hello, World!") 389 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) 390 | .background(Color.red.edgesIgnoringSafeArea(.all)) 391 | } 392 | } 393 | 394 | struct FullScreenBackgroundViewPreviews: PreviewProvider { 395 | static var previews: some View { 396 | FullScreenBackgroundView() 397 | .previewDevice(PreviewDevice(rawValue: "iPhone 11 Pro")) 398 | } 399 | } 400 | ``` 401 | 402 | ## #54 – Convert string with basic HTML tags to SwiftUI's Text 403 | 🖌 Using the underneath shown `+` operator we can build an [extension on SwiftUI's Text](https://gist.github.com/fxm90/fc977d346d2372cfdad11bc822b69a82), that allows us to parse basic HTML tags (like ``, `‌` etc). 404 | 405 | Please have a look at the comments for some usage examples. 406 | 407 | **Update 28.05.2021** 408 | 409 | iOS 15.0 brings `AttributedString` to `SwiftUI` including [Markdown support](https://developer.apple.com/documentation/foundation/attributedstring#3829760). 410 | 411 | 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](https://gist.github.com/fxm90/abd949e4258050f2f3cd80118024e5bd) 412 | 413 | ## #53 – Concatenate two Texts in SwiftUI 414 | 🧙‍♀️ The `+` operator can concatenate two `Text` in SwiftUI. 415 | 416 | ```swift 417 | Text("Note:") 418 | .bold() + 419 | Text(" Lorem Ipsum Dolor Sit Amet.") 420 | ``` 421 | 422 | This will render: "**Note:** Lorem Ipsum Dolor Sit Amet." 423 | 424 | ## #52 – Animated reload of a `UITableView` 425 | 🚀 Calling [`tableView.reloadData()`](https://developer.apple.com/documentation/uikit/uitableview/1614862-reloaddata) inside the animation block of [UIView.transition(with:duration:options:animations:completion:)](https://developer.apple.com/documentation/uikit/uiview/1622574-transition) will result in an animated reload of the table view cells. 426 | 427 | ```swift 428 | UIView.transition(with: tableView, 429 | duration: 0.3, 430 | options: .transitionCrossDissolve, 431 | animations: { self.tableView.reloadData() }) 432 | ``` 433 | 434 | You can pass any [`UIView.AnimationOptions`](https://developer.apple.com/documentation/uikit/uiview/animationoptions) mentioned here. 435 | 436 | Source: 437 | 438 | ## #51 – Redux & SwiftUI Example 439 | 🔄 The following gist shows you how to integrate basic Redux functionality in SwiftUI (without using any additional frameworks): [Redux.swift](https://gist.github.com/fxm90/c3f74f2c695377b17b1f80cf96a31114) 440 | 441 | Feel free to copy the code into a Xcode Playground and give it a try 😃 442 | 443 | ## #50 – Basic Combine Examples 444 | 🧪 Here are two Gists regarding Apple's new Combine framework: 445 | - [Combine-PassthroughSubject-CurrentValueSubject.swift](https://gist.github.com/fxm90/fcb2eb9d92655889d549e7f57168a0fb)\ 446 | This gist explains the difference between a [`PassthroughSubject`](https://developer.apple.com/documentation/combine/passthroughsubject) and a [`CurrentValueSubject`](https://developer.apple.com/documentation/combine/currentvaluesubject). 447 | - [Combine-CLLocationManagerDelegate.swift](https://gist.github.com/fxm90/8b6c9753f12fcf19991f6c3f0cd635d3)\ 448 | This gists shows how to convert a delegate pattern to combine publishers, in this case the `CLLocationManagerDelegate`. 449 | 450 | Feel free to copy the code a playground and get your hands dirty with Combine 😃 451 | 452 | ## #49 – Convert units using `Measurement` 453 | 🔁 Starting from iOS 10 we can use [`Measurement`](https://developer.apple.com/documentation/foundation/measurement) to convert units like e.g. angles, areas, durations, speeds, temperature, volume and [many many more](https://developer.apple.com/documentation/foundation/dimension). 454 | 455 | Using e.g. `Measurement` we can refactor the computed property shown in note #48 to a method, that allows us to convert between any [`UnitAngle`](https://developer.apple.com/documentation/foundation/unitangle): 456 | 457 | ```swift 458 | extension BinaryFloatingPoint { 459 | func converted(from fromUnit: UnitAngle, to toUnit: UnitAngle) -> Self { 460 | let selfAsDouble = Double(self) 461 | let convertedValueAsDouble = Measurement(value: selfAsDouble, unit: fromUnit) 462 | .converted(to: toUnit) 463 | .value 464 | 465 | return type(of: self).init(convertedValueAsDouble) 466 | } 467 | } 468 | ``` 469 | 470 | Furthermore this approach leads to a very clean call side: 471 | 472 | ```swift 473 | let cameraBearing: CLLocationDegrees = 180 474 | cameraBearing.converted(from: .degrees, to: .radians) 475 | ``` 476 | 477 | ## #48 – `FloatingPoint` Protocol 478 | 🎲 By extending the protocol `FloatingPoint` we can define a method / computed property on all floating point datatypes, e.g. `Double`, `Float` or `CGFloat`: 479 | 480 | ```swift 481 | extension FloatingPoint { 482 | var degToRad: Self { 483 | self * .pi / 180 484 | } 485 | } 486 | 487 | let double: Double = 90 488 | let float: Float = 180 489 | let cgFloat: CGFloat = 270 490 | 491 | print("Double as radians", double.degToRad) 492 | print("Float as radians", float.degToRad) 493 | print("CGFloat as radians", cgFloat.degToRad) 494 | ``` 495 | 496 | ## #47 – Wait for multiple async tasks to complete 497 | ⏰ Using a `DispatchGroup` we can wait for multiple async tasks to finish. 498 | 499 | ```swift 500 | let dispatchGroup = DispatchGroup() 501 | 502 | var profile: Profile? 503 | dispatchGroup.enter() 504 | profileService.fetchProfile { 505 | profile = $0 506 | dispatchGroup.leave() 507 | } 508 | 509 | var friends: Friends? 510 | dispatchGroup.enter() 511 | profileService.fetchFriends { 512 | friends = $0 513 | dispatchGroup.leave() 514 | } 515 | 516 | // We need to define the completion handler of our `DispatchGroup` with an unbalanced call to `enter()` and `leave()`, 517 | // as otherwise it will be called immediately! 518 | dispatchGroup.notify(queue: .main) { 519 | guard let profile = profile, let friends = friends else { return } 520 | 521 | print("We've downloaded the user profile together with all friends!") 522 | } 523 | ``` 524 | 525 | **Update for Projects targeting iOS >= 13.0** 526 | Starting from iOS 13 we can use `CombineLatest` to wait for multiple publishers to at least fire publish one message. 527 | 528 | ```swift 529 | let fetchProfileFuture = profileService.fetchProfile() 530 | let fetchFriendsFuture = profileService.fetchFriends() 531 | 532 | cancellable = Publishers.CombineLatest(fetchProfileFuture, fetchFriendsFuture) 533 | .sink { result in 534 | let (profile, friends) = result 535 | print("We've downloaded the user profile together with all friends!") 536 | } 537 | ``` 538 | 539 | 540 | Starting from ~~iOS 15~~ iOS 13 we can also use `async let` to wait for multiple async values. 541 | 542 | ```swift 543 | Task { 544 | async let profileTask = profileService.fetchProfile() 545 | async let friendsTask = profileService.fetchFriends() 546 | 547 | let (profile, friends) = await(profileTask, friendsTask) 548 | print("We've downloaded the user profile together with all friends!") 549 | } 550 | ``` 551 | 552 | ## 46 – Snapshot testing 553 | 📸 Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. 554 | 555 | Using the library [SnapshotTesting](https://github.com/pointfreeco/swift-snapshot-testing) from [Point-Free](https://github.com/pointfreeco) you can easily start testing snapshots of your `UIView`, `UIViewController`, `UIImage` or even `URLRequest`. 556 | 557 | ## 45 – Span subview to superview 558 | ⚓️ A small extension to span a subview to the anchors of its superview. 559 | 560 | ```swift 561 | extension UIView { 562 | /// Adds layout constraints to top, bottom, leading and trailing anchors equal to superview. 563 | func fillToSuperview(spacing: CGFloat = 0) { 564 | guard let superview = superview else { return } 565 | 566 | translatesAutoresizingMaskIntoConstraints = false 567 | NSLayoutConstraint.activate([ 568 | topAnchor.constraint(equalTo: superview.topAnchor, constant: spacing), 569 | leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: spacing), 570 | 571 | superview.bottomAnchor.constraint(equalTo: bottomAnchor, constant: spacing), 572 | superview.trailingAnchor.constraint(equalTo: trailingAnchor, constant: spacing) 573 | ]) 574 | } 575 | } 576 | ``` 577 | 578 | ## 44 – Animate a view using a custom timing function 579 | 🚀 Starting from iOS 10 we can use a `UIViewPropertyAnimator` to animate changes on views. 580 | 581 | Using the initializer [`init(duration:timingParameters:)`](https://developer.apple.com/documentation/uikit/uiviewpropertyanimator/1648362-init) we can pass a [`UITimingCurveProvider`](https://developer.apple.com/documentation/uikit/uitimingcurveprovider), which allows us to provide a custom timing function. You can find lots of these functions on [Easings.net](https://easings.net/). 582 | 583 | Using e.g. "[easeInBack](https://easings.net/#easeInBack)" your animation code could look like this: 584 | 585 | ```swift 586 | extension UICubicTimingParameters { 587 | static let easeInBack = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.6, y: -0.28), 588 | controlPoint2: CGPoint(x: 0.735, y: 0.045)) 589 | } 590 | 591 | class CustomTimingAnimationViewController: UIViewController { 592 | 593 | // ... 594 | 595 | func userDidTapButton() { 596 | let animator = UIViewPropertyAnimator(duration: 1.0, 597 | timingParameters: UICubicTimingParameters.easeInBack) 598 | 599 | animator.addAnimations { 600 | // Add your animation code here. E.g.: 601 | // `self.someConstraint?.isActive = false` 602 | // `self.someOtherConstraint?.isActive = true` 603 | } 604 | 605 | animator.startAnimation() 606 | } 607 | } 608 | ``` 609 | 610 | 611 | ## 43 – How to test a delegate protocol 612 | 🧪 Delegation is a common pattern whenever one object needs to communicate to another object (1:1 communication). 613 | 614 | 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: 615 | [Example on how to elegantly test a delegate protocol](https://gist.github.com/fxm90/106fd802f869d3d259d672d0416b66fa) 616 | 617 | 618 | ## 42 – Xcode multi-cursor editing 619 | 🏃‍ [Since Xcode 10](https://developer.apple.com/documentation/xcode_release_notes/xcode_10_release_notes/source_editor_release_notes_for_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: 620 | ``` 621 | shift + control + click 622 | shift + control + ↑ 623 | shift + control + ↓ 624 | ``` 625 | 626 | 627 | ## 41 – Create a dynamic color for light- and dark mode 628 | 🎨 Using the gist [UIColor+MakeDynamicColor.swift](https://gist.github.com/fxm90/fd217b463222afd6eabcb006fb26d92e) we can create a custom `UIColor` that generates its color data dynamically based on the current `userInterfaceStyle`. 629 | 630 | Furthermore this method falls back to the `lightVariant` color for iOS versions prior to iOS 13. 631 | 632 | 633 | ## #40 – `UITableViewCell` extension that declares a static identifier 634 | 🧙‍♀️ Using the extension below we can automatically register and dequeue table view cells. It prevents typos and declaring a static string on each cell. 635 | 636 | ```swift 637 | extension UITableViewCell { 638 | static var identifier: String { 639 | return String(describing: self) 640 | } 641 | } 642 | ``` 643 | 644 | Register a cell: 645 | ```swift 646 | tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier) 647 | ``` 648 | 649 | Dequeue a cell: 650 | ```swift 651 | let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier) 652 | ``` 653 | 654 | 655 | ## #39 – Prefer "for .. in .. where"-loop over `filter()` and `forach {}` 656 | 🎢 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. 657 | 658 | So instead of writing: 659 | 660 | ```swift 661 | scooterList 662 | .filter({ !$0.isBatteryEmpty }) 663 | .forEach({ scooter in 664 | // Do something with each scooter, that still has some battery left. 665 | }) 666 | ``` 667 | 668 | it is more efficient to write: 669 | 670 | ```swift 671 | for scooter in scooterList where !scooter.isBatteryEmpty { 672 | // Do something with each scooter, that still has some battery left. 673 | } 674 | ``` 675 | 676 | 677 | ## #38 – Lightweight observable implementation 678 | 🕵️‍♂️ ~~If you need a simple and lightweight observable implementation for e.g. UI bindings check out the following gist: [Observable.swift](https://gist.github.com/fxm90/26357043cfe174fabdeedd07d0f25314)~~ 679 | 680 | 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 🙂 681 | 682 | 683 | ## #37 – Run test cases in playground 684 | 🧪 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. 685 | 686 | By calling `MyTestCase.defaultTestSuite.run()` inside the playground we can run a test-case and later copy it into our "real" project. 687 | 688 | ```swift 689 | import Foundation 690 | import XCTest 691 | 692 | class MyTestCase: XCTestCase { 693 | 694 | override func setUp() { 695 | super.setUp() 696 | } 697 | 698 | override func tearDown() { 699 | super.tearDown() 700 | } 701 | 702 | func testFooBarShouldNotBeEqual() { 703 | XCTAssertNotEqual("Foo", "Bar") 704 | } 705 | } 706 | 707 | MyTestCase.defaultTestSuite.run() 708 | ``` 709 | 710 | You can see the result of each test inside the debug area of the playground. 711 | 712 | For running asynchronous test cases you have to add the following line: 713 | ```swift 714 | PlaygroundPage.current.needsIndefiniteExecution = true 715 | ``` 716 | 717 | 718 | ## #36 – Show progress of WKWebView in UIProgressBar 719 | 🤖 For showing the loading-progress of a `WKWebView` on a `UIProgressBar`, please have a look at the following gist: [WebViewExampleViewController.swift](https://gist.github.com/fxm90/50d6c73d07c4d9755981b9bb4c5ab931) 720 | 721 | In the example code, the `UIProgressBar` is attached to the bottom anchor of an `UINavigationBar` (see method `setupProgressView()` for further layout details). 722 | 723 | 724 | ## #35 – Destructure tuples 725 | 🧙‍ Image having a tuple with the following properties: `(firstName: String, lastName: String)`. We can destructure the tuple into two properties in just one line: 726 | 727 | ```swift 728 | let (firstName, lastName) = accountService.fullName() 729 | 730 | print(firstName) 731 | print(lastName) 732 | ``` 733 | 734 | ## #34 – Avoid huge if statements 735 | ✨ Instead of writing long "if statements" like this: 736 | 737 | ```swift 738 | struct HugeDataObject { 739 | let category: Int 740 | let subCategory: Int 741 | 742 | // Imagine lots of other properties, so we can't simply conform to `Equatable` ... 743 | } 744 | 745 | if hugeDataObject.category != previousDataObject.category || hugeDataObject.subCategory != previousDataObject.subCategory { 746 | // ... 747 | } 748 | 749 | ``` 750 | 751 | We can split the long statement into several properties beforehand, to increase readability: 752 | 753 | ```swift 754 | let isDifferentCategory = hugeDataObject.category != previousDataObject.category 755 | let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory 756 | 757 | if isDifferentCategory || isDifferentSubCategory { 758 | // ... 759 | } 760 | ``` 761 | Or use `guard` to do an early return: 762 | 763 | ```swift 764 | let isDifferentCategory = hugeDataObject.category != previousDataObject.category 765 | let isDifferentSubCategory = hugeDataObject.subCategory != previousDataObject.subCategory 766 | 767 | let didChange = isDifferentCategory || isDifferentSubCategory 768 | guard didChange else { return } 769 | ``` 770 | 771 | **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. 772 | 773 | ## #33 – Compare dates in test cases 774 | 📆 Small example on how to compare dates in tests. 775 | 776 | ```swift 777 | func testDatesAreEqual() { 778 | // Given 779 | let dateA = Date() 780 | let dateB = Date() 781 | 782 | // When 783 | // ... 784 | 785 | // Then 786 | XCTAssertEqual(dateA.timeIntervalSince1970, 787 | dateB.timeIntervalSince1970, 788 | accuracy: 0.01) 789 | } 790 | ``` 791 | 792 | ## #32 – Be aware of the strong reference to the target of a timer 793 | 🔁 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: 794 | 795 | ```swift 796 | class ClockViewModel { 797 | // MARK: - Private properties 798 | 799 | weak var timer: Timer? 800 | 801 | // MARK: - Initializer 802 | 803 | init(interval: TimeInterval = 1.0) { 804 | timer = Timer.scheduledTimer(timeInterval: interval, 805 | target: self, 806 | selector: #selector(timerDidFire), 807 | userInfo: nil, 808 | repeats: true) 809 | } 810 | 811 | deinit { 812 | print("This will never be called 🙈") 813 | 814 | timer?.invalidate() 815 | timer = nil 816 | } 817 | 818 | // MARK: - Private methods 819 | 820 | @objc private func timerDidFire() { 821 | // Do something every x seconds here. 822 | } 823 | } 824 | ``` 825 | 826 | 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"](https://apple.co/2yY9B1M) 827 | > 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. 828 | 829 | and the [documentation for the method "timerWithTimeInterval"](https://apple.co/2CyIHB7) 830 | > target: The timer maintains a strong reference to this object until it (the timer) is invalidated. 831 | 832 | 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. 833 | 834 | #### Workaround: 835 | 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. 836 | 837 | ```swift 838 | init(interval: TimeInterval = 1.0) { 839 | timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [weak self] _ in 840 | self?.timerDidFire() 841 | }) 842 | } 843 | ``` 844 | 845 | For iOS version below 10.0, we can use `DispatchSourceTimer` instead. There is a great article from [Daniel Galasko](https://twitter.com/danielgalasko) on how to do that: [A Background Repeating Timer in Swift](https://medium.com/@danielgalasko/a-background-repeating-timer-in-swift-412cecfd2ef9) 846 | 847 | **Notice:** 848 | 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. 849 | 850 | 851 | ## #31 – Initialize `DateFormatter` with formatting options 852 | 🚀 Basic formatting, which requires only setting `dateStyle` and `timeStyle`, can be achieved with the class function [localizedString(from:dateStyle:timeStyle:)](https://developer.apple.com/documentation/foundation/dateformatter/1415241-localizedstring). 853 | 854 | In case you need further formatting options, the following extension allows you to directly initialize a `DateFormatter` with all available options: 855 | 856 | ```swift 857 | extension DateFormatter { 858 | convenience init(configure: (DateFormatter) -> Void) { 859 | self.init() 860 | 861 | configure(self) 862 | } 863 | } 864 | ``` 865 | 866 | Use it like this: 867 | 868 | ```swift 869 | let dateFormatter = DateFormatter { 870 | $0.locale = .current 871 | $0.dateStyle = .long 872 | $0.timeStyle = .short 873 | } 874 | ``` 875 | ```swift 876 | let dateFormatter = DateFormatter { 877 | $0.dateFormat = "E, d. MMMM" 878 | } 879 | ``` 880 | 881 | Feel free to bring this extension to other formatters, like e.g. [DateComponentsFormatter](https://developer.apple.com/documentation/foundation/datecomponentsformatter) or [DateIntervalFormatter](https://developer.apple.com/documentation/foundation/dateintervalformatter), as well. 882 | 883 | **Update:** Starting with Swift 4 we can use key-paths instead of closures: 884 | 885 | ```swift 886 | protocol Builder {} 887 | 888 | extension Builder { 889 | func set(_ keyPath: WritableKeyPath, to value: T) -> Self { 890 | var mutableCopy = self 891 | mutableCopy[keyPath: keyPath] = value 892 | 893 | return mutableCopy 894 | } 895 | } 896 | 897 | extension Formatter: Builder {} 898 | ``` 899 | 900 | Use it like this: 901 | 902 | ```swift 903 | let dateFormatter = DateFormatter() 904 | .set(\.locale, to: .current) 905 | .set(\.dateStyle, to: .long) 906 | .set(\.timeStyle, to: .short) 907 | ``` 908 | ```swift 909 | let numberFormatter = NumberFormatter() 910 | .set(\.locale, to: .current) 911 | .set(\.numberStyle, to: .currency) 912 | ``` 913 | 914 | Based on: [Vadim Bulavin – KeyPath Based Builder](https://twitter.com/V8tr/status/1242846971188183047) 915 | 916 | ## #30 – Map latitude and longitude to X and Y on a coordinate system 917 | 🌍 Not really an iOS specific topic but something to keep in mind 😃 918 | > 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. 919 | > 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. 920 | 921 | https://gis.stackexchange.com/a/68856 922 | 923 | The following graphics illustrate the quote above: 924 | 925 | | Latitude | Longitude | 926 | | :--------------------------------------------|:------------------------------------------------| 927 | | [![Latitude][latitude--thumbnail]][latitude] | [![Longitude][longitude--thumbnail]][longitude] | 928 | 929 | #### Further iOS related information: 930 | - [Displaying Maps 931 | ](https://apple.co/2q61aNU) 932 | - [CLLocationCoordinate2D](https://apple.co/2O1bIYn) 933 | 934 | 935 | ## #29 - Encapsulation 936 | 🚪 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. 937 | 938 | Even notification receivers or outlets can be marked as private. 939 | 940 | ```swift 941 | class KeyboardViewModel { 942 | // MARK: - Public properties 943 | 944 | /// Boolean flag, whether the keyboard is currently visible. 945 | /// We assume that this property has to be accessed from the view controller, therefore we allow public read-access. 946 | private(set) var isKeyboardVisible = false 947 | 948 | // MARK: - Initializer 949 | 950 | init(notificationCenter: NotificationCenter = .default) { 951 | notificationCenter.addObserver(self, 952 | selector: #selector(didReceiveUIKeyboardWillShowNotification), 953 | name: UIResponder.keyboardWillShowNotification, 954 | object: nil) 955 | 956 | notificationCenter.addObserver(self, 957 | selector: #selector(didReceiveUIKeyboardDidHideNotification), 958 | name: UIResponder.keyboardDidHideNotification, 959 | object: nil) 960 | } 961 | 962 | // MARK: - Private methods 963 | 964 | @objc private func didReceiveUIKeyboardWillShowNotification(_: Notification) { 965 | isKeyboardVisible = true 966 | } 967 | 968 | @objc private func didReceiveUIKeyboardDidHideNotification(_: Notification) { 969 | isKeyboardVisible = false 970 | } 971 | } 972 | ``` 973 | 974 | 975 | ## #28 – Remove `UITextView` default padding 976 | ↔ With the following code the default padding from an `UITextView` can be removed: 977 | 978 | ```swift 979 | // This brings the left edge of the text to the left edge of the container 980 | textView.textContainer.lineFragmentPadding = 0 981 | 982 | // This causes the top of the text to align with the top of the container 983 | textView.textContainerInset = .zero 984 | ``` 985 | Source: https://stackoverflow.com/a/18987810/3532505 986 | 987 | The above code can also be applied inside the interface builder within the "User Defined Runtime Attributes" section. Just add the following lines there: 988 | 989 | | Key Path | Type | Value | 990 | | :---------------------------------|:-------| :-----------------------------------------------| 991 | | textContainer.lineFragmentPadding | Number | 0 | 992 | | textContainerInset | Rect | {{0, 0}, {0, 0}} | 993 | 994 | 995 | ## #27 – Name that color 996 | 🎨 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](http://chir.ag/projects/name-that-color/). It automatically generates a name for the given color 🧙‍ 997 | 998 | 999 | ## #26 – Structure classes using `// MARK: -` 1000 | 🔖 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. 1001 | 1002 | ```swift 1003 | class StructuredViewController: UIViewController { 1004 | 1005 | // MARK: - Types 1006 | 1007 | typealias CompletionHandler = (Bool) -> Void 1008 | 1009 | // MARK: - Outlets 1010 | 1011 | @IBOutlet private var submitButton: UIButton! 1012 | 1013 | // MARK: - Public properties 1014 | 1015 | var completionHandler: CompletionHandler? 1016 | 1017 | // MARK: - Private properties 1018 | 1019 | private let viewModel: StructuredViewModel 1020 | 1021 | // MARK: - Initializer 1022 | 1023 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 1024 | viewModel = StructuredViewModel() 1025 | 1026 | // ... 1027 | 1028 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 1029 | } 1030 | 1031 | deinit { 1032 | // ... 1033 | } 1034 | 1035 | // MARK: - Public methods 1036 | 1037 | override func viewDidLoad() { 1038 | // ... 1039 | } 1040 | 1041 | // MARK: - Private methods 1042 | 1043 | private func setupSubmitButton() { 1044 | // ... 1045 | } 1046 | } 1047 | ``` 1048 | 1049 | ## #25 – Structure test cases 1050 | ⚠️ Splitting test cases into `Given`, `When`, `Then` increases the readability and helps understanding complex tests. 1051 | 1052 | - In the `Given` phase we setup all preconditions for the test, e.g. configuring mock objects. 1053 | - In the `When` phase we call the function we want to test. 1054 | - In the `Then` phase we verify the actual results against our expected results using `XCTAssert` methods. 1055 | 1056 | #### Example: 1057 | ```swift 1058 | class MapViewModelTestCase: XCTestCase { 1059 | var locationServiceMock: LocationServiceMock! 1060 | 1061 | var viewModel: MapViewModel! 1062 | var delegateMock: MapViewModelDelegateMock! 1063 | 1064 | override func setUp() { 1065 | super.setUp() 1066 | 1067 | // ... 1068 | } 1069 | 1070 | override func tearDown() { 1071 | // ... 1072 | 1073 | super.tearDown() 1074 | } 1075 | 1076 | func testLocateUser() { 1077 | // Given 1078 | let userLocation = CLLocationCoordinate2D(latitude: 12.34, 1079 | longitude: 56.78) 1080 | 1081 | locationServiceMock.userLocation = userLocation 1082 | 1083 | // When 1084 | viewModel.locateUser() 1085 | 1086 | // Then 1087 | XCTAssertEqual(delegateMock.focusedUserLocation.latitude, userLocation.latitude) 1088 | XCTAssertEqual(delegateMock.focusedUserLocation.longitude, userLocation.longitude) 1089 | } 1090 | } 1091 | ``` 1092 | 1093 | ## #24 – Avoid forced unwrapping 1094 | > The only time you should be using implicitly unwrapped optionals is with @IBOutlets. 1095 | > In every other case, it is better to use a non-optional or regular optional property. 1096 | > Yes, there are cases in which you can probably "guarantee" that the property will never be nil when used, 1097 | > but it is better to be safe and consistent. Similarly, don't use force unwraps. 1098 | 1099 | Source: https://github.com/linkedin/swift-style-guide 1100 | 1101 | Using the patterns shown underneath, we can easily unwrap optionals or use early return to stop further code executing, if an optional is `nil`. 1102 | 1103 | ```swift 1104 | if let value = value { 1105 | // Do something with value here.. 1106 | } 1107 | ``` 1108 | 1109 | ```swift 1110 | guard let value = value else { 1111 | // Write a comment, why to exit here. 1112 | return 1113 | } 1114 | 1115 | // Do something with value here.. 1116 | ``` 1117 | 1118 | 1119 | ## #23 – Always check for possible dividing through zero 1120 | 💥 We should always make sure that a certain value is **NOT** zero before dividing through it. 1121 | 1122 | ```swift 1123 | class ImageViewController: UIViewController { 1124 | 1125 | // MARK: - Outlets 1126 | 1127 | @IBOutlet private var imageView: UIImageView! 1128 | 1129 | // MARK: - Private methods 1130 | 1131 | func someMethod() { 1132 | let bounds = imageView.bounds 1133 | guard bounds.height > 0 else { 1134 | // Avoid diving through zero for calculating aspect ratio below. 1135 | return 1136 | } 1137 | 1138 | let aspectRatio = bounds.width / bounds.height 1139 | } 1140 | } 1141 | ``` 1142 | 1143 | 1144 | ## #22 – Animate `alpha` and update `isHidden` accordingly 1145 | 🦋 Using the following gist we can animate the `alpha` property and update the `isHidden` flag accordingly: [fxm90/UIView+AnimateAlpha.swift](https://gist.github.com/fxm90/723b5def31b46035cd92a641e3b184f6) 1146 | 1147 | 1148 | ## #21 – Create custom notification 1149 | 📚 For creating custom notifications we first should have a look on how to name them properly: 1150 | 1151 | > [Name of associated class] + [Did | Will] + [UniquePartOfName] + Notification 1152 | 1153 | Source: [Coding Guidelines for Cocoa](https://apple.co/2PPywfa) 1154 | 1155 | We create the new notification by extending the corresponding class: 1156 | 1157 | ```swift 1158 | extension Notification.Name { 1159 | static let AccountServiceDidLoginUser = Notification.Name("AccountServiceDidLoginUserNotification") 1160 | } 1161 | ``` 1162 | 1163 | And afterwards post it like this: 1164 | 1165 | ```swift 1166 | class AccountService { 1167 | func login() { 1168 | NotificationCenter.default.post(name: .AccountServiceDidLoginUser, 1169 | object: self) 1170 | } 1171 | } 1172 | ``` 1173 | 1174 | For Objective-C support we further need to extend `NSNotification`: 1175 | 1176 | ```swift 1177 | @objc extension NSNotification { 1178 | static let AccountServiceDidLoginUser = Notification.Name.AccountServiceDidLoginUser 1179 | } 1180 | ``` 1181 | 1182 | Then, we can post it like this: 1183 | 1184 | ``` 1185 | [NSNotificationCenter.defaultCenter post:NSNotification.AccountServiceDidLoginUser 1186 | object:self]; 1187 | ``` 1188 | 1189 | By extending `Notification.Name` we make sure our notification names are unique. 1190 | 1191 | **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. 1192 | 1193 | 1194 | ## #20 – Override `UIStatusBarStyle` the elegant way 1195 | ✌️ Using a custom property, combined with the observer `didSet` we can call `setNeedsStatusBarAppearanceUpdate()` to apply a new status-bar style: 1196 | 1197 | ```swift 1198 | class SomeViewController: UIViewController { 1199 | 1200 | // MARK: - Public properties 1201 | 1202 | override var preferredStatusBarStyle: UIStatusBarStyle { 1203 | return customBarStyle 1204 | } 1205 | 1206 | // MARK: - Private properties 1207 | 1208 | private var customBarStyle: UIStatusBarStyle = .default { 1209 | didSet { 1210 | setNeedsStatusBarAppearanceUpdate() 1211 | } 1212 | } 1213 | } 1214 | ``` 1215 | 1216 | ## 19 – Log extension on `String` using swift literal expressions 1217 | 👌 Swift contains some special literals: 1218 | 1219 | | Literal | Type | Value | 1220 | | :---------|:-------| :-----------------------------------------------| 1221 | | #file | String | The name of the file in which it appears. | 1222 | | #line | Int | The line number on which it appears. | 1223 | | #column | Int | The column number in which it begins. | 1224 | | #function | String | The name of the declaration in which it appears.| 1225 | Source: [Swift.org – Expressions](https://docs.swift.org/swift-book/ReferenceManual/Expressions.html) 1226 | 1227 | Especially with default parameters those expressions are really useful, as in that case the expression is evaluated at the call site. 1228 | We could use a [simple extension on String](https://gist.github.com/fxm90/08a187c5d6b365ce2305c194905e61c2) to create a basic logger: 1229 | 1230 | ```swift 1231 | "Lorem Ipsum Dolor Sit Amet 👋".log(level: .info) 1232 | ``` 1233 | 1234 | That would create the following output: 1235 | 1236 | ``` 1237 | ℹ️ – 2018/09/16 19:46:45.189 - ViewController.swift - viewDidLoad():15 1238 | > Lorem Ipsum Dolor Sit Amet 👋 1239 | ``` 1240 | 1241 | 1242 | ## 18 – Use gitmoji 1243 | 😃 Not an iOS specific topic, but I'd like to use [gitmoji](https://gitmoji.carloscuesta.me/) for my commit messages, e.g. `TICKET-NUMBER - ♻️ :: Description` (Credits go to [Martin Knabbe](https://twitter.com/martin_knabbe) for that pattern). 1244 | To easily create the corresponding emojis for the type of commit, you can use this [alfred workflow](https://github.com/ai0/alfred-gitmoji-workflow). 1245 | 1246 | 1247 | ## #17 – Initialize a constant based on a condition 1248 | 👏 A very readable way of initializing a constant after the declaration. 1249 | 1250 | ```swift 1251 | let startCoordinate: CLLocationCoordinate2D 1252 | if let userCoordinate = userLocationService.userCoordinate, CLLocationCoordinate2DIsValid(userCoordinate) { 1253 | startCoordinate = userCoordinate 1254 | } else { 1255 | // We don't have a valid user location, so we fallback to Hamburg. 1256 | startCoordinate = CLLocationCoordinate2D(latitude: 53.5582447, 1257 | longitude: 9.647645) 1258 | } 1259 | ``` 1260 | 1261 | This way we can avoid using a variable and therefore prevent any mutation of `startCoordinate` in further code. 1262 | 1263 | 1264 | ## #16 – Why `viewDidLoad` might be called before `init` has finished 1265 | ⚡️ Be aware that the method `viewDidLoad` is being called immediately on accessing `self.view` in the initializer. 1266 | 1267 | This happens because the view is not loaded yet, but the property `self.view` shouldn't return `nil`. 1268 | 1269 | Therefore the view controller will load the view immediately and call the corresponding method `viewDidLoad` afterwards. 1270 | 1271 | #### Example: 1272 | ```swift 1273 | class ViewDidLoadBeforeInitViewController: UIViewController { 1274 | // MARK: - Initializer 1275 | 1276 | override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 1277 | super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) 1278 | 1279 | view.isHidden = true 1280 | 1281 | print("📝 :: `\(#function)` did finish!") 1282 | } 1283 | 1284 | required init?(coder aDecoder: NSCoder) { 1285 | super.init(coder: aDecoder) 1286 | 1287 | view.isHidden = true 1288 | 1289 | print("📝 :: `\(#function)` did finish!") 1290 | } 1291 | 1292 | // MARK: - Public methods 1293 | 1294 | override func viewDidLoad() { 1295 | super.viewDidLoad() 1296 | 1297 | print("📝 :: `\(#function)` did finish!") 1298 | } 1299 | } 1300 | ``` 1301 | 1302 | The code will output log statements in the following order: 1303 | 1304 | ``` 1305 | 📝 :: `viewDidLoad()` did finish. 1306 | 📝 :: `init(nibName:bundle:)` did finish. 1307 | ``` 1308 | 1309 | Source: https://stackoverflow.com/a/5808477 1310 | 1311 | More on view life cycle: [Work with View Controllers](https://apple.co/2q8Jf9y) 1312 | 1313 | 1314 | ## #15 – Capture iOS Simulator video 1315 | 📹 A small tutorial on how create a video of what's happening in the simulator. 1316 | 1317 | 1. Run your App in the simulator 1318 | 2. Open terminal 1319 | 3. Run one of the following commands: 1320 | * To take a screenshot: `xcrun simctl io booted screenshot` 1321 | * To take a video: `xcrun simctl io booted recordVideo .` 1322 | 4. Press ctrl + c to stop recording the video. 1323 | 1324 | For example: 1325 | ``` 1326 | xcrun simctl io booted recordVideo ~/appVideo.mp4 1327 | ``` 1328 | 1329 | Source: https://stackoverflow.com/a/41141801 1330 | 1331 | 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](https://twitter.com/twostraws): **[ControlRoom](https://github.com/twostraws/ControlRoom)** 1332 | 1333 | 1334 | ## #14 – Xcode open file in focused editor 1335 | 🏃‍♂️ 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. 1336 | 1337 | 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. 1338 | 1339 | 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. 1340 | 1341 | Source: [Jesse Squires – Improving the assistant editor](https://www.jessesquires.com/blog/xcode-tip-improving-assistant-editor/) 1342 | 1343 | 1344 | ## #13 – Handle optionals in test cases 1345 | ✅ 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. 1346 | 1347 | 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. 1348 | ```swift 1349 | class BookingViewModelTestCase: XCTestCase { 1350 | func testFindBookingByIdentifierShouldReturnMockedBooking() throws { 1351 | // Given 1352 | let mockedBooking = Booking(identifier: 1) 1353 | let viewModel = BookingViewModel(bookings: [mockedBooking]) 1354 | 1355 | // When 1356 | let fetchedBooking = try XCTUnwrap( 1357 | viewModel.findBooking(byIdentifier: 1) 1358 | ) 1359 | 1360 | // Then 1361 | XCTAssertEqual(fetchedBooking, mockedBooking) 1362 | } 1363 | } 1364 | ``` 1365 | 1366 | #### Prior to Xcode 11 1367 | [Require](https://github.com/JohnSundell/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](https://www.swiftbysundell.com/posts/avoiding-force-unwrapping-in-swift-unit-tests) 1368 | 1369 | 1370 | ## #12 – Safe access to an element at index 1371 | ⛑ 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. 1372 | 1373 | ```swift 1374 | extension Array { 1375 | subscript(safe index: Index) -> Element? { 1376 | let isValidIndex = (0 ..< count).contains(index) 1377 | guard isValidIndex else { 1378 | return nil 1379 | } 1380 | 1381 | return self[index] 1382 | } 1383 | } 1384 | 1385 | let fruits = ["Apple", "Banana", "Cherries", "Kiwifruit", "Orange", "Pineapple"] 1386 | 1387 | let banana = fruits[safe: 2] 1388 | let pineapple = fruits[safe: 6] 1389 | 1390 | // Does not crash, but contains nil 1391 | let invalid = fruits[safe: 7] 1392 | ``` 1393 | 1394 | 1395 | ## #11 – Check whether a value is part of a given range 1396 | 💡 Instead of writing `x >= 10 && x <= 100`, we can write `10 ... 100 ~= x`. 1397 | #### Example: 1398 | ```swift 1399 | let statusCode = 200 1400 | 1401 | let isSuccessStatusCode = 200 ... 299 ~= statusCode 1402 | let isRedirectStatusCode = 300 ... 399 ~= statusCode 1403 | let isClientErrorStatusCode = 400 ... 499 ~= statusCode 1404 | let isServerErrorStatusCode = 500 ... 599 ~= statusCode 1405 | ``` 1406 | 1407 | Another (more readable way) for checking whether a value is part of a given range can be achieved using the `contains` method: 1408 | 1409 | ```swift 1410 | let statusCode = 200 1411 | 1412 | let isSuccessStatusCode = (200 ... 299).contains(statusCode) 1413 | let isRedirectStatusCode = (300 ... 399).contains(statusCode) 1414 | let isClientErrorStatusCode = (400 ... 499).contains(statusCode) 1415 | let isServerErrorStatusCode = (500 ... 599).contains(statusCode) 1416 | ``` 1417 | 1418 | ## #10 – Use `compactMap` to filter `nil` values 1419 | 🎛 Using `compactMap` we can filter out any `nil` values of an array. 1420 | 1421 | ```swift 1422 | struct ItemDataModel { 1423 | let title: String? 1424 | } 1425 | 1426 | struct ItemViewModel { 1427 | let title: String 1428 | } 1429 | 1430 | extension ItemViewModel { 1431 | /// Convenience initializer, that maps the data-model from the server to our view-model 1432 | /// if all required properties are available. 1433 | init?(dataModel: ItemDataModel) { 1434 | guard let title = dataModel.title else { 1435 | return nil 1436 | } 1437 | 1438 | self.init(title: title) 1439 | } 1440 | } 1441 | 1442 | class ListViewModel { 1443 | /// ... 1444 | 1445 | func mapToItemViewModel(response: [ItemDataModel]) -> ([ItemViewModel]) { 1446 | // Using `compactMap` we filter out invalid data-models automatically. 1447 | response.compactMap { ItemViewModel(dataModel: $0) } 1448 | } 1449 | } 1450 | ``` 1451 | 1452 | 1453 | ## #09 – Prefer `Set` instead of array for unordered lists without duplicates 1454 | 👫 **Advantage over `Array`:** 1455 | 1456 | - Constant Lookup time O(1), as a `Set` stores its members based on hash value. 1457 | 1458 | **Disadvantage compared to `Array`:** 1459 | 1460 | - No guaranteed order. 1461 | - Can't contain duplicate values. 1462 | - All items we want to store must conform to `Hashable` protocol. 1463 | 1464 | For further examples and use-cases please have a look at ["The power of sets in Swift" (by John Sundell)](https://medium.com/@johnsundell/the-power-of-sets-in-swift-57be8b223da0). 1465 | 1466 | 1467 | ## #08 – Remove all sub-views from `UIView` 1468 | 📭 A small extension to remove all sub-views. 1469 | 1470 | ```swift 1471 | extension UIView { 1472 | func removeAllSubviews() { 1473 | subviews.forEach { $0.removeFromSuperview() } 1474 | } 1475 | } 1476 | ``` 1477 | 1478 | 1479 | ## #07 – Animate image change on `UIImageView` 1480 | ✍️ Easily (ex)change an image with using a transition (note that the `.transitionCrossDissolve` is the key to get this working). 1481 | 1482 | ```swift 1483 | extension UIImageView { 1484 | func updateImageWithTransition(_ image: UIImage?, duration: TimeInterval) { 1485 | UIView.transition(with: self, duration: duration, options: .transitionCrossDissolve, animations: { () -> Void in 1486 | self.image = image 1487 | }) 1488 | } 1489 | } 1490 | ``` 1491 | 1492 | 1493 | ## #06 – Change `CALayer` without animation 1494 | 👨‍🎨 CALayer has a default implicit animation duration of [0.25 seconds](https://apple.co/2PVTCsB). Using the following extension we can do changes without an animation: 1495 | 1496 | ```swift 1497 | extension CALayer { 1498 | class func performWithoutAnimation(_ runWithoutAnimation: () -> Void) { 1499 | CATransaction.begin() 1500 | CATransaction.setAnimationDuration(0.0) 1501 | 1502 | runWithoutAnimation() 1503 | 1504 | CATransaction.commit() 1505 | } 1506 | } 1507 | ``` 1508 | 1509 | 1510 | ## #05 – Override `layerClass` to reduce the total amount of layers 1511 | ```swift 1512 | override class var layerClass: AnyClass { 1513 | return CAGradientLayer.self 1514 | } 1515 | ``` 1516 | 1517 | > By overriding 'layerClass' you can tell UIKit what CALayer class to use for a UIView's backing layer. 1518 | > That way you can reduce the amount of layers, and don't have to do any manual layout. 1519 | [John Sundell](https://twitter.com/johnsundell/status/1000099872580816897) 1520 | 1521 | 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. 1522 | ![Example][overwrite-layer-class] 1523 | 1524 | You can see the full code for the example in my gist for the [Vertical Gradient Image View](https://gist.github.com/fxm90/9604b0a067af46f68b80c6968736558d). 1525 | 1526 | 1527 | ## #04 – Handle notifications in test cases 1528 | 📬 Examples on how to test notifications in test cases: 1529 | 1530 | - [XCTest – Assert notification (not) triggered](https://gist.github.com/fxm90/23dc7debc5ee8245237c08e5af8679bc) 1531 | - [XCTest – Use custom notification center in test case and assert notification (not) triggered](https://gist.github.com/fxm90/3c6f146ed977100d21f0a1f3e7bb37a2) 1532 | 1533 | 1534 | ## #03 – Use `didSet` on outlets to setup components 1535 | 👏 By using `didSet` on outlets we can setup our view components (declared in a storyboard or xib) in a very readable way: 1536 | 1537 | ```swift 1538 | class FooBarViewController: UIViewController { 1539 | 1540 | // MARK: - Outlets 1541 | 1542 | @IBOutlet private var button: UIButton! { 1543 | didSet { 1544 | button.setTitle(viewModel.normalTitle, for: .normal) 1545 | button.setTitle(viewModel.disabledTitle, for: .disabled) 1546 | } 1547 | } 1548 | 1549 | // MARK: - Private properties 1550 | 1551 | private var viewModel = FooBarViewModel() 1552 | } 1553 | ``` 1554 | 1555 | 1556 | ## #02 – Most readable way to check whether an array contains a value (`isAny(of:)`) 1557 | ✨ A small extension to check whether a value is part of a list of candidates, in a very readable way (by [John Sundell](https://twitter.com/johnsundell/status/943510426586959873)) 1558 | 1559 | ```swift 1560 | extension Equatable { 1561 | func isAny(of candidates: Self...) -> Bool { 1562 | return candidates.contains(self) 1563 | } 1564 | } 1565 | ``` 1566 | 1567 | #### Example: 1568 | ```swift 1569 | enum Device { 1570 | case iPhone7 1571 | case iPhone8 1572 | case iPhoneX 1573 | case iPhone11 1574 | } 1575 | 1576 | let device: Device = .iPhoneX 1577 | 1578 | // Before 1579 | let hasSafeAreas = [.iPhoneX, .iPhone11].contains(device) 1580 | 1581 | // After 1582 | let hasSafeAreas = device.isAny(of: .iPhoneX, .iPhone11) 1583 | ``` 1584 | 1585 | 1586 | ## #01 – Override `self` in escaping closure, to get a strong reference to `self` 1587 | 🚸 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. 1588 | 1589 | ```swift 1590 | someService.request() { [weak self] response in 1591 | guard let self = self else { return } 1592 | 1593 | self.doSomething(with: response) 1594 | } 1595 | ``` 1596 | 1597 | **Notice:** The above works as of Swift 4.2. Before you have to use: 1598 | 1599 | ``` 1600 | guard let `self` = self else { return } 1601 | ``` 1602 | 1603 | There is a great article about [when to use `weak self` and why it's needed](https://matteomanferdini.com/swift-weak-self/). 1604 | 1605 | [overwrite-layer-class]: Assets/overwrite-layer-class.jpg 1606 | 1607 | [latitude]: Assets/latitude.jpg 1608 | [latitude--thumbnail]: Assets/latitude--thumbnail.jpg 1609 | 1610 | [longitude]: Assets/longitude.jpg 1611 | [longitude--thumbnail]: Assets/longitude--thumbnail.jpg 1612 | --------------------------------------------------------------------------------