Swift tips and tricks
Here's list of Swift tips & tricks with all additional sources (playgrounds, images) that I would like to share. Also you can find them on Twitter @szubyak and Facebook @szubyakdev, where you can ask questions and respond with feedback. I will really glad to have you there! ๐
Table of contents
#1 Safe way to return element at specified index
#2 Easy way to hide Status Bar
#3 Enumerated iteration
#4 Combinations of pure functions
#5 Profit to compiler
#6 Tips for writing error messages
#7 Testing settings
#8 forEach
and map
execution order difference
#9 Change type of items in array
#10 Invoke didSet
when propertyโs value is set inside init
context
#11 Fake AppDelegate
#12 Semicolons in Swift
#13 Group objects by property
#14 Transparent/Opaque Navigation Bar
#15 Split array by chunks of given size
#16 Get next element of array
#17 Apply gradient to Navigation Bar
#18 Common elements in two arraysr
#19 Left/rigth text offset inside UITextField
#20 How to detect that user stop typing
#21 Comparing tuples
#22 Split String
into words
#23 Observe MOC changes
#24 Update UIView
content with animation
#1 Safe way to return element at specified index
You can extend collections to return the element at the specified index if it is within bounds, otherwise nil.
extension Collection {
subscript (safe index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
}
let cars = ["Lexus", "Ford", "Volvo", "Toyota", "Opel"]
let selectedCar1 = cars[safe: 3] // Toyota
let selectedCar2 = cars[safe: 6] // not crash, but nil
Back to Top
#2 Easy way to hide Status Bar
Ever faced the problem that u can't hide status bar because of prefersStatusBarHidden
is get-only
? The simplest solution is to override
it ๐ง๐จโ๐ป
let vc = UIViewController()
vc.prefersStatusBarHidden = true // error
print("statusBarHidded \(vc.prefersStatusBarHidden)") // false
class TestViewController: UIViewController {
override var prefersStatusBarHidden: Bool {
return true
}
}
let testVC = TestViewController()
print("statusBarHidded \(testVC.prefersStatusBarHidden)") // true
Back to Top
#3 Enumerated iteration
Use enumerated
when you iterate over the collection to return a sequence of pairs (n, c)
, where n
- index for each element and c
- its value ๐จโ๐ป๐ป
for (n, c) in "Swift".enumerated() {
print("\(n): \(c)")
}
Result:
0: S
1: w
2: i
3: f
4: t
Also be careful with this tricky thing, enumerated
on collection will not provide actual indices, but monotonically increasing integer, which happens to be the same as the index for Array but not for anything else, especially slices.
Back to Top
#4 Combinations of pure functions
compactMap
func is effectively the combination of using map
and joined
in a single call, in that order. It maps items in array A into array B using a func you provide, then joins the results using concatenation.
Functions min
and max
could be also combinations of sorted.first
and sorted.last
in single call.
let colors = ["red", "blue", "black", "white"]
let min = colors.min() // black
let first = colors.sorted().first // black
let max = colors.max() // white
let last = colors.sorted().last // white
Back to Top
#5 Profit to compiler
Do you know that using map
gives profit to the compiler: it's now clear we want to apply some code to every item in an array, then like in for
loop we could have break
on halfway through.
Back to Top
#6 Tips for writing error messages
- Say what happened and why
- Suggest a next step
- Find the right tone (If itโs a stressful or serious issue, then a silly tone would be inappropriate)
Commonโ โTypesโ โofโ โErrorโ โMessages
Back to Top
#7 Testing settings
- Even if you don't write UI Tests, they still take considerable amount of time to run. Just skip it.
- Enable code coverage stats in Xcode, it helps to find which method was tested, not tested, partly tested. But donโt pay too much attention to the percentage ๐.
Back to Top
#8 forEach
and map
execution order difference
Execution order is interesting difference between forEach
and map
: forEach
is guaranteed to go through array elements in its sequence, while map
is free to go in any order.
Back to Top
#9 Change type of items in array
Two ways of changing type of items in array and obvious difference between them ๐ง๐จโ๐ป
let numbers = ["1", "2", "3", "4", "notInt"]
let mapNumbers = numbers.map { Int($0) } // [Optional(1), Optional(2), Optional(3), Optional(4), nil]
let compactNumbers = numbers.compactMap { Int($0) } // [1, 2, 3, 4]
Back to Top
#10 Invoke didSet
when propertyโs value is set inside init
context
Apple's docs specify that: "Property observers are only called when the propertyโs value is set outside of initialization context."
defer
can change situation ๐
class AA {
var propertyAA: String! {
didSet {
print("Function: \(#function)")
}
}
init(propertyAA: String) {
self.propertyAA = propertyAA
}
}
class BB {
var propertyBB: String! {
didSet {
print("Function: \(#function)")
}
}
init(propertyBB: String) {
defer {
self.propertyBB = propertyBB
}
}
}
let aa = AA(propertyAA: "aa")
let bb = BB(propertyBB: "bb")
Result:
Function: propertyBB
Back to Top
#11 Fake AppDelegate
Unit testing shouldnโt have any side effects. While running tests, Xcode firstly launches app and thus having the side effect of executing any code we may have in our App Delegate and initial View Controller. Fake AppDelegate in your main.swift
to prevent it.
You can find main.swift
file here
Back to Top
#12 Semicolons in Swift
Do you need semicolons in Swift ? Short answer is NO, but you can use them and it will give you interesting opportunity. Semicolons enable you to join related components into a single line.
func sum(a: Int, b: Int) -> Int {
let sum = a + b; return sum
}
Back to Top
#13 Group objects by property
One more useful extension ๐จ๐ป Gives you opportunity to group objects by property ๐จโ๐ป๐ง
extension Sequence {
func group<GroupingType: Hashable>(by key: (Iterator.Element) -> GroupingType) -> [[Iterator.Element]] {
var groups: [GroupingType: [Iterator.Element]] = [:]
var groupsOrder: [GroupingType] = []
forEach { element in
let key = key(element)
if case nil = groups[key]?.append(element) {
groups[key] = [element]
groupsOrder.append(key)
}
}
return groupsOrder.map { groups[$0]! }
}
}
Usage:
struct Person {
var name: String
var age: Int
}
let mike = Person(name: "Mike", age: 18)
let john = Person(name: "John", age: 18)
let bob = Person(name: "Bob", age: 56)
let jake = Person(name: "Jake", age: 56)
let roman = Person(name: "Roman", age: 25)
let persons = [mike, john, bob, jake, roman]
let groupedPersons = persons.group { $0.age }
for persons in groupedPersons {
print(persons.map { $0.name })
}
Result:
["Mike", "John"]
["Bob", "Jake"]
["Roman"]
Also in-box alternative
Back to Top
#14 Transparent/Opaque Navigation Bar
Scene with UIImageView
on top looks stylish if navigation bar is transparent.
Easy way how to make navigation bar transparent or opaque.
func transparentNavigationBar() {
self.setBackgroundImage(UIImage(), for: .default)
self.shadowImage = UIImage()
}
func opaqueNavigationBar() {
self.shadowImage = nil
self.setBackgroundImage(nil, for: .default)
}
Back to Top
#15 Split array by chunks of given size
Great extension to split array by chunks of given size
extension Array {
func chunk(_ chunkSize: Int) -> [[Element]] {
return stride(from: 0, to: self.count, by: chunkSize).map({ (startIndex) -> [Element] in
let endIndex = (startIndex.advanced(by: chunkSize) > self.count) ? self.count-startIndex : chunkSize
return Array(self[startIndex..<startIndex.advanced(by: endIndex)])
})
}
}
Back to Top
#16 Get next element of array
Easy way how to get next element of array
extension Array where Element: Hashable {
func after(item: Element) -> Element? {
if let index = self.index(of: item), index + 1 < self.count {
return self[index + 1]
}
return nil
}
}
Back to Top
#17 Apply gradient to Navigation Bar
Gradient ๐ณ๏ธโ๐ on Navigation Bar is really good looking, but not very easy to implement ๐ง๐จ๐จโ๐ป Works with iOS 11 largeTitle navigation bar too ๐
struct GradientComponents {
var colors: [CGColor]
var locations: [NSNumber]
var startPoint: CGPoint
var endPoint: CGPoint
}
extension UINavigationBar {
func applyNavigationBarGradient(with components: GradientComponents) {
let size = CGSize(width: UIScreen.main.bounds.size.width, height: 1)
let gradient = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
gradient.colors = components.colors
gradient.locations = components.locations
gradient.startPoint = components.startPoint
gradient.endPoint = components.endPoint
UIGraphicsBeginImageContext(gradient.bounds.size)
gradient.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.barTintColor = UIColor(patternImage: image!)
}
}
Back to Top
#18 Common elements in two arrays
I'm not huge fan of custom operators ๐ because they are intuitively obvious only to their authors, but I've created one which gives you opportunity to get common elements in two arrays whos elements implement Equatable
protocol ๐จ๐ง๐ป
infix operator &
func &<T : Equatable>(lhs: [T], rhs: [T]) -> [T] {
return lhs.filter { rhs.contains($0) }
}
Back to Top
#19 Left/rigth text offset inside UITextField
Clear way of adding left\right text offset inside UItextField
๐จ๐ง๐ป Also, because of @IBInspectable
it could be easily editable in Interface Builderโs inspector panel.
@IBDesignable
extension UITextField {
@IBInspectable var leftPaddingWidth: CGFloat {
get {
return leftView!.frame.size.width
}
set {
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: newValue, height: frame.size.height))
leftView = paddingView
leftViewMode = .always
}
}
@IBInspectable var rigthPaddingWidth: CGFloat {
get {
return rightView!.frame.size.width
}
set {
let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: newValue, height: frame.size.height))
rightView = paddingView
rightViewMode = .always
}
}
}
Back to Top
#20 How to detect that user stop typing
Painless way ( NO to timers from now โ๏ธ ) how to detect that user stop typing text in text field โจ๏ธ Could be usefull for lifetime search ๐
class TestViewController: UIViewController {
@objc func searchBarDidEndTyping(_ textField: UISearchBar) {
print("User finsihed typing text in search bar")
}
@objc func textFieldDidEndTyping(_ textField: UITextField) {
print("User finished typing text in text field")
}
}
extension TestViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(searchBarDidEndTyping), object: searchBar)
self.perform(#selector(searchBarDidEndTyping), with: searchBar, afterDelay: 0.5)
return true
}
}
extension TestViewController: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(textFieldDidEndTyping), object: textField)
self.perform(#selector(textFieldDidEndTyping), with: textField, afterDelay: 0.5)
return true
}
}
Back to Top
#21 Comparing tuples
I discovered strange behavior of tuples during comparing ๐คช. Comparison cares only about types and ignores labels ๐ฆ. So result can be unexpected. Be careful
let car = (model: "Tesla", producer: "USA")
let company = (name: "Tesla", country: "USA")
if car == company {
print("Equal")
} else {
print("Not equal")
}
Printed result will be: Equal
Back to Top
#22 Split String
into words
Default ways of splitting โ๏ธ String
don't work perfect sometimes, because of punctuation characters and other "wreckers" ๐. Here is extension for splitting โ๏ธ String
into words ๐ป๐ง๐.
extension String {
var words: [String] {
return components(separatedBy: .punctuationCharacters)
.joined()
.components(separatedBy: .whitespaces)
.filter{!$0.isEmpty}
}
}
Back to Top
#23 Observe MOC changes
Next code snippet ๐ I use to keep eye on changes that take place in the managed object context. Useful thing to know what's going on, what was added, updated ( what specific values were changed ) or deleted ๐ฅ๐๐ค
func changeNotification(_ notification: Notification) {
guard let userInfo = notification.userInfo else { return }
if let inserts = userInfo[NSInsertedObjectsKey] as? Set<NSManagedObject>, inserts.count > 0 {
print("--- INSERTS ---")
print(inserts)
print("+++++++++++++++")
}
if let updates = userInfo[NSUpdatedObjectsKey] as? Set<NSManagedObject>, updates.count > 0 {
print("--- UPDATES ---")
for update in updates {
print(update.changedValues())
}
print("+++++++++++++++")
}
if let deletes = userInfo[NSDeletedObjectsKey] as? Set<NSManagedObject>, deletes.count > 0 {
print("--- DELETES ---")
print(deletes)
print("+++++++++++++++")
}
}
NotificationCenter.default.addObserver(self, selector: #selector(self.changeNotification(_:)), name: .NSManagedObjectContextObjectsDidChange, object: moc)
Back to Top
#24 Update UIView
content with animation
Really lightweight way ๐ How to add content changing animation to UIView and it subclasses.
extension UIView {
func fadeTransition(_ duration: CFTimeInterval) {
let animation = CATransition()
animation.timingFunction = CAMediaTimingFunction(name:
kCAMediaTimingFunctionEaseInEaseOut)
animation.type = kCATransitionFade
animation.duration = duration
layer.add(animation, forKey: kCATransitionFade)
}
}
Just invoke ๐งโโ๏ธ fadeTransition(_ duration: CFTimeInterval)
by your view before you will apply a change.
label.fadeTransition(1)
label.text = "Updated test content with animation"
Back to Top