Useful Global Swift Functions
Global functions, or functions that can be accessed from anywhere without the scope of a specific type is an old concept that was popular in languages like C and Objective-C, but unrecommended in Swift as we would rather have things that are nicely typed and scoped ("swifty").
For historical reasons, the Swift Standard Library still has quite a few public global functions, and some of them are still useful to this day. Let's take a look at functions like zip()
and dump()
.
zip()
Perhaps the most known global function, the zip
function allows you to take two or more arrays and merge them into a single Sequence
of a tuple. This is extremely useful if you need to iterate two things at the same time, as without zip
you would have to manually build a for loop and access each index from each array individually. Using zip
allows you access elements from all arrays in the more useful for-in fashion.
For example, if we had a user registration form screen and we would like to update our textFields to render a list of validation results fetched from the backend, we could do something like this:
func present(validationResults: [FieldValidationResult],
inTextFields textFields: [MyTextField]) {
for i in 0..<textFields.count {
let field = textFields[i]
let result = validationResults[i]
field.render(validationResult: result)
}
}
With zip
, we can remove all the manual indexing.
func present(validationResults: [FieldValidationResult],
inTextFields textFields: [MyTextField]) {
for (field, result) in zip(textFields, validationResults) {
field.render(validationResult: result)
}
}
The return type of zip
is a Zip2Sequence
object that conforms to Sequence
, and so all other sequence-related methods are applicable to it, including transforming it to a real array.
dump()
The dump
function is a neat alternative to printing objects. While printing objects is just a syntax sugar for a type's description
or debugDescription
property, dump
is a supercharged version of Mirror(reflecting:)
that prints the contents of an object using reflection, which will usually result in considerably more info, including the hierarchy of the object.
class Foo: NSObject {
let bar: String = "bar"
}
let foo = Foo()
print(foo)
// <SwiftRocks.Foo: 0x1030b9250>
dump(foo)
// ▿ <SwiftRocks.Foo: 0x1030b9250> #0
// - super: NSObject
// - bar: "bar"
sequence()
The global sequence()
function is a bit obscure, but it's a very cool function that lets you write recursive functions in a nicer syntax.
Let's pretend that we're changing the background color of a subview and all of its parents. Perhaps you would build a while
loop like this:
var currentView: UIView? = self
while currentView != nil {
currentView?.backgroundColor = .green
currentView = currentView?.superview
}
This is the best use case for sequence()
, as the purpose of this function is to give you a Sequence
that applies a specific closure over and over. As the recursive aspect of this method (currentView = currentView?.superview
) is always the same, we can use sequence()
to turn it into a simple for
loop:
for view in sequence(first: self, next: { $0.superview } ) {
view.backgroundColor = .green
}
The way this works is that sequence()
returns a custom UnfoldFirstSequence
type, a simple wrapper for a Sequence
that keeps applying the closure over and over in its next()
function.
isKnownUniquelyReferenced()
The isKnownUniquelyReferenced
function receives a class
object and returns a boolean that indicates if the object is being reference only one time, with the purpose of enabling you to implement value semantics to reference types. Although structs are value types themselves, the contents inside of it might not be. You might know that putting a class inside a struct doesn't mean it will be copied on assignment:
class Foo: NSObject {
var bar: String = "bar"
}
struct FooHolder {
let foo: Foo = Foo()
var intValue: Int = 1
}
var fooHolder = FooHolder()
var fooHolder2 = fooHolder
fooHolder2.foo.bar = "bar2"
fooHolder2.intValue = 2
print(fooHolder.intValue)
// 1
print(fooHolder2.intValue)
// 2
print(fooHolder.foo.bar)
// bar2
print(fooHolder2.foo.bar)
// bar2
In this example, although fooHolder2
and its underlying number are separate entities from the original holder, the underlying class is still shared between them. To fix this, we can use isKnownUniquelyReferenced
to detect when this property is being accessed and create a new instance of the class if necessary:
struct FooHolder {
private var _foo: Foo = Foo()
var foo: Foo {
mutating get {
if isKnownUniquelyReferenced(&_foo) {
return _foo
} else {
let newFoo = Foo()
newFoo.bar = _foo.bar
_foo = newFoo
return _foo
}
} set {
_foo = newValue
}
}
var intValue: Int = 1
}
You might be interested to know that this is exactly how the Swift Standard Library enables copy-on-write semantics to Arrays and Strings -- something that I've mentioned in my article about the memory management of value types.
repeatElement()
The repeatElement()
does exactly what it says. Given an object and a number, the result is a Sequence
that can be iterated to give you that object that specific amount of times.
let repeated: Repeated<String> = repeatElement("SwiftRocks", count: 3)
for value in repeated {
print(value)
}
//SwiftRocks
//SwiftRocks
//SwiftRocks
Repeating elements is a common operation in Swift, especially to fill in gaps in Strings
and Arrays
. in fact, most of these types even have a specific initializer for this:
let array = [Int](repeating: 0, count: 10)
So why would you use repeatElement? The reason is performance. The return type of repeatElement()
is a Repeated<T>
Sequence
type, similar to Zip2Sequence
in terms that it does nothing besides provide this "repeating" functionality. Let's say you would like to replace a specific section of the array of numbers with another number; one way to achieve this would be to use replaceSubrange
with another array:
array.replaceSubrange(2...7, with: [Int](repeating: 1, count: 6))
print(array)
// [0, 0, 1, 1, 1, 1, 1, 1, 0, 0]
While this works, the usage of [Int](repeating:)
comes with all the overhead of having to initialize array buffers that will serve no purpose here. If you only need the repeating functionality, then using repeatElement
will perform much better.
array.replaceSubrange(2...7, with: repeatElement(1, count: 6))
stride()
Also fairly popular, the stride()
function was added to Swift as a way to create for loops that could skip certain elements, as the equivalent C-style way to do so was removed from the language:
for (int i = 0; i < 10; i += 2) { ... }
Now, you can use stride()
to achieve the same behavior:
for i in stride(from: 0, to: 10, by: 2) {
// from 0 to 9, skipping odd numbers.
}
The arguments for stride()
are arguments that conform to the Strideable
protocol, which represents objects that can represent the concept of distances. For example, here's how we could add the concept of "day difference" in Date
objects to that they can be used in stride()
:
extension Date: Strideable {
func advanced(by n: Int) -> Date {
return Calendar.current.date(byAdding: .day,
value: n,
to: self)!
}
func distance(to other: Date) -> Int {
return Calendar.current.dateComponents([.day],
from: other,
to: self).day!
}
}
let startDate = Date()
let finalDate = startDate.advanced(by: 5)
for date in stride(from: startDate, to: finalDate, by: 1) {
print(date)
}
// March 24th
// March 25th
// March 26th
// March 27th
// March 28th
(Note that Date already has an implementation of Strideable methods that strides in seconds, so copying this to a project won't work.)
Other Useful Functions
Math
max()
- Returns the maximum value of the arguments
min()
- Returns the minimum value of the arguments
abs()
- Returns the absolute value of the argument (useful in competitive programming questions)
Values
swap()
- Swaps the value of two objects. This is not mentioned in its own section in this article because if you need to swap array elements the correct method to use is Array.swapAt()
. However, you can still use swap()
in other situations where you would need to create a fake "aux" property to hold a value.
Conclusion
As we can see, although none of these methods are necessary to make things happen, using them allows you write code that is easier to maintain and some times even more performant than old school solutions.