Swift
Function
Array
Predicate
Algorithm

In Swift, an efficient function that separates an array into 2 arrays based on a predicate

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Splitting an array into two arrays based on a predicate is a common task, and the naive solution is often wasteful. In Swift, the most practical implementation is a single pass that appends each element to either the matching bucket or the non-matching bucket while preserving the original order.

Use A Single Pass Partition Function

The simplest correct solution is also efficient. Walk the array once, evaluate the predicate once per element, and push the element into one of two result arrays.

This avoids the most common alternative, which is calling filter twice. Two filter calls are readable, but they perform two full passes and evaluate the predicate twice for every element.

Here is a reusable generic helper:

swift
1extension Array {
2    func split(by predicate: (Element) -> Bool) -> (matching: [Element], nonMatching: [Element]) {
3        var matching: [Element] = []
4        var nonMatching: [Element] = []
5
6        matching.reserveCapacity(count)
7        nonMatching.reserveCapacity(count)
8
9        for element in self {
10            if predicate(element) {
11                matching.append(element)
12            } else {
13                nonMatching.append(element)
14            }
15        }
16
17        return (matching, nonMatching)
18    }
19}
20
21let numbers = Array(1...10)
22let result = numbers.split { $0.isMultiple(of: 2) }
23
24print(result.matching)     // [2, 4, 6, 8, 10]
25print(result.nonMatching)  // [1, 3, 5, 7, 9]

This solution has linear time complexity and preserves order inside each output array. Order preservation is often the deciding factor in real applications, especially when the input array already has semantic ordering.

Why Not Use partition(by:)

Swift also offers partition(by:), but it solves a slightly different problem. It rearranges the elements of a mutable collection in place and returns the boundary index between the two partitions.

That can be useful when mutation is acceptable and you want to avoid allocating two fresh arrays immediately. It does not preserve order, and the predicate semantics can surprise people because the method groups elements according to where the predicate returns false.

Example:

swift
1var values = [1, 2, 3, 4, 5, 6]
2let pivot = values.partition { $0.isMultiple(of: 2) }
3
4let firstPart = Array(values[..<pivot])
5let secondPart = Array(values[pivot...])
6
7print(values)
8print(firstPart)
9print(secondPart)

That code is valid, but it is usually not the best answer when the goal is "give me two arrays that preserve the original relative order." For that requirement, the manual loop is clearer and safer.

A Functional Variant With reduce(into:)

If you prefer a more functional style, reduce(into:) gives you the same single-pass behavior without creating unnecessary intermediate arrays.

swift
1extension Array {
2    func splitWithReduce(by predicate: (Element) -> Bool) -> (matching: [Element], nonMatching: [Element]) {
3        reduce(into: (matching: [Element](), nonMatching: [Element]())) { result, element in
4            if predicate(element) {
5                result.matching.append(element)
6            } else {
7                result.nonMatching.append(element)
8            }
9        }
10    }
11}
12
13let words = ["swift", "ui", "server", "ios", "db"]
14let grouped = words.splitWithReduce { $0.count >= 4 }
15
16print(grouped.matching)     // ["swift", "server"]
17print(grouped.nonMatching)  // ["ui", "ios", "db"]

This version is concise, but the explicit for loop is often easier to scan during maintenance. Efficiency is effectively the same, so pick the style your team reads fastest.

Complexity And Practical Tradeoffs

There are three common strategies:

  1. Two filter calls.
  2. One pass with a loop.
  3. In-place partition(by:).

Two filter calls are easy to write but perform extra work. The single-pass loop is usually the best all-around choice because it keeps the code obvious and avoids repeated predicate evaluation. partition(by:) can reduce allocations in some cases, but it changes element order and mutates the source array.

For most application code, the best definition of efficient is not just fewer operations. It is also predictable semantics. Developers often choose the one-pass function because they want a clear contract: same input order, one predicate evaluation per element, and no hidden mutation.

Common Pitfalls

The first pitfall is using filter twice with an expensive predicate. If the predicate hits the network, parses a date, or performs more than a trivial check, doubling the calls becomes noticeable.

The second pitfall is assuming partition(by:) is stable. It is not. If preserving original order matters, do not use it as a drop-in replacement for a manual split.

Another issue is over-reserving capacity. In the example above, both arrays reserve count, which is safe but slightly generous. It is usually fine because it avoids repeated growth and keeps the sample simple, but you should know that the total reserved space can exceed the final number of stored elements.

Finally, be clear about naming. A function named partition suggests behavior similar to the standard library method. Naming your helper split(by:) or grouped(by:) makes the semantics easier to infer.

Summary

  • The most practical solution is a single pass that appends into two arrays.
  • A manual loop preserves order and evaluates the predicate once per element.
  • 'partition(by:) is in-place and fast, but it mutates the array and is not stable.'
  • 'reduce(into:) provides the same efficiency in a more functional style.'
  • Avoid two filter calls when the predicate is expensive or the array is large.

Course illustration
Course illustration

All Rights Reserved.