Useful Global Swift Functions

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.

Bruno Rocha
1951 followers

About

Bruno Rocha is a Software Engineer at Spotify and is the developer of open sources libraries like SwiftInfo and SwiftShield.
Contact Me

Newsletter

Want to be notified of new posts? Click here to subscribe to my newsletter.

RSS / Social

Info

This website's static HTML pages are generated by WriteIt.