ABSTRACT
Swift Arrays are a standard data type in Swift, but, like all data types, they are also a type of struct. As such, Arrays can be extended.
The issue with extending Arrays, though, is that they are generic, and can contain a whole bunch of different data types, impacting the practical application of Array extensions.
In this post, we’ll explore a way to constrain Array extensions to apply only when the contained type is a certain class or struct.
BASIC STRING EXAMPLE
For example. You can join(separator:) Array<String>, but only an Array of String, so this generic Array extension wouldn’t work:
extension Array {
var description: String {
return self.joined(separator: ",")
}
}
You will get an error, stating that the operation is ambiguous (There’s no guarantee that a String join will work on an arbitrary data type).
However, you can cast the type, to make it work, but it’s messy:
extension Array {
var description1: String {
if let selfie = self as? [String] {
return selfie.joined(separator: ",")
}
return "UN-STRING"
}
}
That will work. Test it like so:
let testArray0 = [["One", "Two", "Three"],["Four", "Five", "Six"],["Seven","eight","Nine"]] let testArray1 = ["One", "Two", "Three"] let description = testArray0.description1 // Contains "UN-STRING" let description0 = testArray1.description1 // Contains "One,Two,Three"
ADVANCED STRING EXAMPLE
In a more complex example. You can’t iterate a single string (Array<String>), but you can iterate over an Array of String (Array<[String]>). Let’s say that we want to create a simple CSV (Comma-Separated Values) String from an Array of an Array of String, using pretty much the same mechanism:
extension Array {
var description: String {
if let selfie = self as? [String] {
return selfie.joined(separator: ",")
} else if let selfie = self as? [[String]] {
return selfie.reduce(into: [String]()) { (accumulator: inout [String], element) in
let line = element.description
accumulator.append(line)
}.joined(separator: "\n")
}
return "HUH?"
}
}
That will work. It’s convoluted, but will do the trick. Test it like so (Same two test Arrays):
let csv0 = testArray0.description // Contains "One,Two,Three\nFour,Five,Six\nSeven,eight,Nine" let csv1 = testArray0[0].description // Contains "One,Two,Three" let csv2 = testArray1.description // Contains "One,Two,Three"
SO, WHAT’S THE PROBLEM? IT WORKS, DOESN’T IT?
Yeah…but it’s messy. In particular, the casts:
extension Array {
var description: String {
if let selfie = self as? [String] {
return selfie.joined(separator: ",")
} else if let selfie = self as? [[String]] {
return selfie.reduce(into: [String]()) { (accumulator: inout [String], element) in
let line = element.description
accumulator.append(line)
}.joined(separator: "\n")
}
return "HUH?"
}
}
Also, the very fact that we need to do the if..else is messy. Wouldn’t it be great if there were a way to avoid that?
ENTER THE DRAGON

Queue chicken noises…
Swift allows you to add a where clause to constrain extensions, similar to generic constraints, which were discussed earlier.
You apply extension constraints by looking at the Element typealias member, like so:
extension Array where Element == String {
This tells the compiler that this extension only counts if Element is of type String.
Similarly, you can do the same for Array of String:
extension Array where Element == [String] {
The nice thing about this, is that the Element typealias will now be the focused type, so we are free to do this:
extension Array where Element == String {
var csvLine: String {
return self.joined(separator: ",")
}
}
extension Array where Element == [String] {
var csv: String {
return self.reduce(into: [String]()) { (accumulator: inout [String], element) in
let element2 = element.csvLine
accumulator.append(element2)
}.joined(separator: "\n")
}
}
That’s a bit neater, eh?
Test it like so:
let csv3 = testArray0.csv // Contains "One,Two,Three\nFour,Five,Six\nSeven,eight,Nine" let csv4 = testArray0[0].csvLine // Contains "One,Two,Three" let csv5 = testArray1.csvLine // Contains "One,Two,Three"
A MORE ROBUST EXAMPLE
Here’s one way that I’ve leveraged this capability. I like to play with MapKit, and there’s a number of places where I sometimes need to drive into the bush in order to get done what needs doing. I’ve figured a way to help make these tasks a bit easier, by modifying Array to give me a few extra capabilities:
extension Array where Element == CLLocationCoordinate2D {
/* ################################################################## */
/**
- returns: An MKMapRect, containing all the points in the Array.
Nil, if the Array has less than 2 points, or does not contain coordinates.
*/
var rect: MKMapRect! {
if 1 < self.count {
let result = self.reduce(into: (maxLong: -180, maxLat: -180, minLong: 180, minLat: 180)) { (result: inout (maxLong: Double, maxLat: Double, minLong: Double, minLat: Double), inLocationCoords) in
result.maxLong = Swift.max(inLocationCoords.longitude, result.maxLong)
result.maxLat = Swift.max(inLocationCoords.latitude, result.maxLat)
result.minLong = Swift.min(inLocationCoords.longitude, result.minLong)
result.minLat = Swift.min(inLocationCoords.latitude, result.minLat)
}
let topLeft = MKMapPoint(CLLocationCoordinate2D(latitude: result.maxLat, longitude: result.minLong))
let bottomRight = MKMapPoint(CLLocationCoordinate2D(latitude: result.minLat, longitude: result.maxLong))
let size = MKMapSize(width: abs(bottomRight.x - topLeft.x), height: abs(bottomRight.y - topLeft.y))
return MKMapRect(origin: topLeft, size: size)
}
return nil
}
/* ################################################################## */
/**
- returns: The center coordinate of the group of coordinates.
*/
var center: CLLocationCoordinate2D! {
let result = self.reduce(into: (maxLong: -180, maxLat: -180, minLong: 180, minLat: 180)) { (result: inout (maxLong: Double, maxLat: Double, minLong: Double, minLat: Double), inLocationCoords) in
result.maxLong = Swift.max(inLocationCoords.longitude, result.maxLong)
result.maxLat = Swift.max(inLocationCoords.latitude, result.maxLat)
result.minLong = Swift.min(inLocationCoords.longitude, result.minLong)
result.minLat = Swift.min(inLocationCoords.latitude, result.minLat)
}
return CLLocationCoordinate2D(latitude: (result.maxLat + result.minLat) / 2, longitude: (result.maxLong + result.minLong) / 2)
}
/* ################################################################## */
/**
- returns: A coordinate span, encompassing all of the coordinates in the Array.
Nil, if the span cannot be calculated for any reason.
*/
var span: MKCoordinateSpan! {
if 1 < self.count {
let result = self.reduce(into: (maxLong: -180, maxLat: -180, minLong: 180, minLat: 180)) { (result: inout (maxLong: Double, maxLat: Double, minLong: Double, minLat: Double), inLocationCoords) in
result.maxLong = Swift.max(inLocationCoords.longitude, result.maxLong)
result.maxLat = Swift.max(inLocationCoords.latitude, result.maxLat)
result.minLong = Swift.min(inLocationCoords.longitude, result.minLong)
result.minLat = Swift.min(inLocationCoords.latitude, result.minLat)
}
return MKCoordinateSpan(latitudeDelta: result.maxLat - result.minLat, longitudeDelta: result.maxLong - result.minLong)
}
return nil
}
/* ################################################################## */
/**
- returns: A region, encompassing all of the elements in the Array.
Nil, if the region cannot be calculated for any reason.
*/
var region: MKCoordinateRegion! {
if let center = self.center, let span = self.span {
return MKCoordinateRegion(center: center, span: span)
}
return nil
}
}
This adds a few computed properties to the Array struct that will interpret the contents for me. Here’s how I might use these new properties. We’ll create a dataset of a few locations in the Washington DC area:
let washingtonMonument = CLLocationCoordinate2D(latitude: 38.9072, longitude: -77.0369)
let whiteHouse = CLLocationCoordinate2D(latitude: 38.8977, longitude: -77.0365)
let capitolBuilding = CLLocationCoordinate2D(latitude: 38.8899, longitude: -77.0091)
let lincolnMemorial = CLLocationCoordinate2D(latitude: 38.8893, longitude: -77.0502)
let jeffersonMemorial = CLLocationCoordinate2D(latitude: 38.8814, longitude: -77.0365)
let airAndSpaceMuseum = CLLocationCoordinate2D(latitude: 38.8882, longitude: -77.0199)
let ronaldReaganAirport = CLLocationCoordinate2D(latitude: 38.8512, longitude: -77.0402)
let pentagon = CLLocationCoordinate2D(latitude: 38.8719, longitude: 77.0563)
let arlingtonCemetery = CLLocationCoordinate2D(latitude: 38.8783, longitude: -77.0687)
let airAndSpaceMuseumAnnex = CLLocationCoordinate2D(latitude: 38.9109, longitude: -77.4442)
let arrayOfCoordinates = [washingtonMonument,
whiteHouse,
capitolBuilding,
lincolnMemorial,
jeffersonMemorial,
airAndSpaceMuseum,
ronaldReaganAirport,
pentagon,
arlingtonCemetery,
airAndSpaceMuseumAnnex
]
So now, we can extract that nice extra information from the Array:
let rectThatWillEncloseAllLocations = arrayOfCoordinates.rect // Remember that this is in map points (not coordinates), so the numbers will be big:
// left: 76471147.05123556
// top: 102675876.5094804
// width: 324434.0747377872
// height: 57184.79726368189
let greatLocationForAHotel = arrayOfCoordinates.center // This will contain (38.88105, -77.22665000000001), which is a mean of all the data points in the Array.
let aSpanThatIsBigEnoughForEverything = arrayOfCoordinates.span // This contains (0.05969999999999942, 0.4350999999999914), which is in long/lat coordinates.
let aRegionThatFitsEverything = arrayOfCoordinates.region // This is a region, made up of the center and span. It will enclose all the points.
In the sample playground, I’ll add some code to show a map.
Initial Display |
Zoomed Into Washington DC |
One Last Trick
It would be nice to be able to directly assign an Array<CLLocationCoordinate2D> to a CLLocationCoordinate2D property. Since we already have the center computed property, why don’t we use that?
In order to do that, we simply extend the CLLocationCoordinate2D type, like so:
extension CLLocationCoordinate2D {
init(_ inLocationArray: [CLLocationCoordinate2D]) {
self.init(latitude: inLocationArray.center.latitude, longitude: inLocationArray.center.longitude)
}
}
That way, we can simply do this:
let anotherGreatLocationForAHotel = CLLocationCoordinate2D(arrayOfCoordinates) // In this trick, we simply assign the center (38.88105, -77.22665000000001) to the property.
DON’T FORGET PROTOCOLS
You can also use protocols as the variable in the constraints, like so:
extension Array where Element: Equatable {
mutating func removeObject(object: Element) {
if let index = self.firstIndex(of: object) {
self.remove(at: index)
}
}
}
Test it like so:
// These tests apply to the above protocol-based extension. var testArray = [1,2,3,4,5,6,7,8,9,0] testArray.removeObject(object: 6) let newArray = testArray // Contains [1,2,3,4,5,7,8,9,0] var testArray2 = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] testArray2.removeObject(object: "6") let newArray2 = testArray2 // Contains ["1", "2", "3", "4", "5", "7", "8", "9", "0"] var testArray3 = [["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"]] testArray3.removeObject(object: ["4", "5", "6"]) let newArray3 = testArray3 // Contains [["1", "2", "3"], ["7", "8", "9"]] var testArray4 = [CLLocationCoordinate2D(latitude: 36, longitude: -77), CLLocationCoordinate2D(latitude: 26, longitude: -80), CLLocationCoordinate2D(latitude: -26, longitude: 33)] // This will not work. CLLocationCoordinate2D is not Equatable //testArray4.removeObject(object: CLLocationCoordinate2D(latitude: 26, longitude: -80))
In the above example, we filtered the Array to only allow Equatable elements, so we could implement a simple “delete me” method.
The “mutating” is necessary, because Array is a struct, and, as such, requires that methods that modify its state be marked as mutating.
CONCLUSION
This capability adds the ability to integrate some custom tools into generic types. It can go a long ways towards making your code simpler and more easily understood.
It’s not a critical capability, but it’s one of those things that makes Swift a fun language to mess with.
SAMPLE PLAYGROUND
import PlaygroundSupport
import UIKit
import MapKit
// This can be messy. It will work, but...ick:
extension Array {
// ERROR: This will not work
// var description: String {
// return self.joined(separator: ",")
// }
// Simple CSV line.
var description1: String {
if let selfie = self as? [String] { // Filter for a certain type of element.
return selfie.joined(separator: ",") // Now, since it's a String, we can join the party.
}
return "UN-STRING" // This is not a joiner.
}
// Creates a CSV string from multiple contained Arrays.
var description: String {
if let selfie = self as? [String] { // Simple CSV.
return selfie.joined(separator: ",")
} else if let selfie = self as? [[String]] { // An Array of Arrays of String can be CSVed.
return selfie.reduce(into: [String]()) { (accumulator: inout [String], element) in // Pile each line on top of the other.
let line = element.description // This asks each line to render as CSV.
accumulator.append(line)
}.joined(separator: "\n") // Join the lines by linefeeds.
}
return "HUH?" // Can't CSV.
}
}
// These are test datasets for the CSV functionality.
let testArray0 = [["One", "Two", "Three"],["Four", "Five", "Six"],["Seven","eight","Nine"]]
let testArray1 = ["One", "Two", "Three"]
let description = testArray0.description1 // Contains "UN-STRING"
let description0 = testArray1.description1 // Contains "One,Two,Three"
let description1 = testArray0.description // Contains "One,Two,Three\nFour,Five,Six\nSeven,eight,Nine"
let description2 = testArray1.description // Contains "One,Two,Three"
let csv0 = testArray0.description // Contains "One,Two,Three\nFour,Five,Six\nSeven,eight,Nine"
let csv1 = testArray0[0].description // Contains "One,Two,Three"
let csv2 = testArray1.description // Contains "One,Two,Three"
// Now, with type-constrained extensions, we can get the same functionality with drastically less code.
extension Array where Element == String {
var csvLine: String { // Since this is type-constrained, we don't need to take a selfie.
return self.joined(separator: ",")
}
}
extension Array where Element == [String] {
var csv: String {
return self.reduce(into: [String]()) { (accumulator: inout [String], element) in
let element2 = element.csvLine
accumulator.append(element2)
}.joined(separator: "\n")
}
}
let csv3 = testArray0.csv // Contains "One,Two,Three\nFour,Five,Six\nSeven,eight,Nine"
let csv4 = testArray0[0].csvLine // Contains "One,Two,Three"
let csv5 = testArray1.csvLine // Contains "One,Two,Three"
// This demonstrates the capability using a protocol. We extend Array for elements of the Equatable protocol.
extension Array where Element: Equatable {
mutating func removeObject(object: Element) { // This allows us to remove an object by value.
if let index = self.firstIndex(of: object) {
self.remove(at: index)
}
}
}
// These tests apply to the above protocol-based extension.
var testArray = [1,2,3,4,5,6,7,8,9,0]
testArray.removeObject(object: 6)
let newArray = testArray // Contains [1,2,3,4,5,7,8,9,0]
var testArray2 = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
testArray2.removeObject(object: "6")
let newArray2 = testArray2 // Contains ["1", "2", "3", "4", "5", "7", "8", "9", "0"]
var testArray3 = [["1", "2", "3"], ["4", "5", "6"], ["7", "8", "9"]]
testArray3.removeObject(object: ["4", "5", "6"])
let newArray3 = testArray3 // Contains [["1", "2", "3"], ["7", "8", "9"]]
var testArray4 = [CLLocationCoordinate2D(latitude: 36, longitude: -77), CLLocationCoordinate2D(latitude: 26, longitude: -80), CLLocationCoordinate2D(latitude: -26, longitude: 33)]
// This will not work. CLLocationCoordinate2D is not Equatable
//testArray4.removeObject(object: CLLocationCoordinate2D(latitude: 26, longitude: -80))
extension Array where Element == CLLocationCoordinate2D {
/* ################################################################## */
/**
- returns: An MKMapRect, containing all the points in the Array.
Nil, if the Array has less than 2 points, or does not contain coordinates.
*/
var rect: MKMapRect! {
if 1 < self.count {
let result = self.reduce(into: (maxLong: -180, maxLat: -180, minLong: 180, minLat: 180)) { (result: inout (maxLong: Double, maxLat: Double, minLong: Double, minLat: Double), inLocationCoords) in
result.maxLong = Swift.max(inLocationCoords.longitude, result.maxLong)
result.maxLat = Swift.max(inLocationCoords.latitude, result.maxLat)
result.minLong = Swift.min(inLocationCoords.longitude, result.minLong)
result.minLat = Swift.min(inLocationCoords.latitude, result.minLat)
}
let topLeft = MKMapPoint(CLLocationCoordinate2D(latitude: result.maxLat, longitude: result.minLong))
let bottomRight = MKMapPoint(CLLocationCoordinate2D(latitude: result.minLat, longitude: result.maxLong))
let size = MKMapSize(width: abs(bottomRight.x - topLeft.x), height: abs(bottomRight.y - topLeft.y))
return MKMapRect(origin: topLeft, size: size)
}
return nil
}
/* ################################################################## */
/**
- returns: The center coordinate of the group of coordinates.
*/
var center: CLLocationCoordinate2D! {
let result = self.reduce(into: (maxLong: -180, maxLat: -180, minLong: 180, minLat: 180)) { (result: inout (maxLong: Double, maxLat: Double, minLong: Double, minLat: Double), inLocationCoords) in
result.maxLong = Swift.max(inLocationCoords.longitude, result.maxLong)
result.maxLat = Swift.max(inLocationCoords.latitude, result.maxLat)
result.minLong = Swift.min(inLocationCoords.longitude, result.minLong)
result.minLat = Swift.min(inLocationCoords.latitude, result.minLat)
}
return CLLocationCoordinate2D(latitude: (result.maxLat + result.minLat) / 2, longitude: (result.maxLong + result.minLong) / 2)
}
/* ################################################################## */
/**
- returns: A coordinate span, encompassing all of the coordinates in the Array.
Nil, if the span cannot be calculated for any reason.
*/
var span: MKCoordinateSpan! {
if 1 < self.count {
let result = self.reduce(into: (maxLong: -180, maxLat: -180, minLong: 180, minLat: 180)) { (result: inout (maxLong: Double, maxLat: Double, minLong: Double, minLat: Double), inLocationCoords) in
result.maxLong = Swift.max(inLocationCoords.longitude, result.maxLong)
result.maxLat = Swift.max(inLocationCoords.latitude, result.maxLat)
result.minLong = Swift.min(inLocationCoords.longitude, result.minLong)
result.minLat = Swift.min(inLocationCoords.latitude, result.minLat)
}
return MKCoordinateSpan(latitudeDelta: result.maxLat - result.minLat, longitudeDelta: result.maxLong - result.minLong)
}
return nil
}
/* ################################################################## */
/**
- returns: A region, encompassing all of the elements in the Array.
Nil, if the region cannot be calculated for any reason.
*/
var region: MKCoordinateRegion! {
if let center = self.center, let span = self.span {
return MKCoordinateRegion(center: center, span: span)
}
return nil
}
}
extension CLLocationCoordinate2D {
init(_ inLocationArray: [CLLocationCoordinate2D]) {
self.init(latitude: inLocationArray.center.latitude, longitude: inLocationArray.center.longitude)
}
}
// Example dataset:
let washingtonMonument = CLLocationCoordinate2D(latitude: 38.9072, longitude: -77.0369)
let whiteHouse = CLLocationCoordinate2D(latitude: 38.8977, longitude: -77.0365)
let capitolBuilding = CLLocationCoordinate2D(latitude: 38.8899, longitude: -77.0091)
let lincolnMemorial = CLLocationCoordinate2D(latitude: 38.8893, longitude: -77.0502)
let jeffersonMemorial = CLLocationCoordinate2D(latitude: 38.8814, longitude: -77.0365)
let airAndSpaceMuseum = CLLocationCoordinate2D(latitude: 38.8882, longitude: -77.0199)
let ronaldReaganAirport = CLLocationCoordinate2D(latitude: 38.8512, longitude: -77.0402)
let pentagon = CLLocationCoordinate2D(latitude: 38.8719, longitude: -77.0563)
let arlingtonCemetery = CLLocationCoordinate2D(latitude: 38.8783, longitude: -77.0687)
let airAndSpaceMuseumAnnex = CLLocationCoordinate2D(latitude: 38.9109, longitude: -77.4442)
let arrayOfCoordinates = [washingtonMonument,
whiteHouse,
capitolBuilding,
lincolnMemorial,
jeffersonMemorial,
airAndSpaceMuseum,
ronaldReaganAirport,
pentagon,
arlingtonCemetery,
airAndSpaceMuseumAnnex
]
let rectThatWillEncloseAllLocations = arrayOfCoordinates.rect // Remember that this is in map points (not coordinates), so the numbers will be big:
// left: 76471147.05123556
// top: 102675876.5094804
// width: 324434.0747377872
// height: 57184.79726368189
let greatLocationForAHotel = arrayOfCoordinates.center // This will contain (38.88105, -77.22665000000001), which is a mean of all the data points in the Array.
let aSpanThatIsBigEnoughForEverything = arrayOfCoordinates.span // This contains (0.05969999999999942, 0.4350999999999914), which is in long/lat coordinates.
let aRegionThatFitsEverything = arrayOfCoordinates.region // This is a region, made up of the center and span. It will enclose all the points.
let anotherGreatLocationForAHotel = CLLocationCoordinate2D(arrayOfCoordinates) // In this trick, we simply assign the center (38.88105, -77.22665000000001) to the property.
// This should display the map, centered, with markers for each location.
class MapViewController: UIViewController {
var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
self.mapView = MKMapView()
self.view.addSubview(self.mapView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.mapView.frame = self.view.bounds
self.mapView.mapType = .standard
self.mapView.removeAnnotations(self.mapView.annotations)
arrayOfCoordinates.forEach { [unowned self] coordinate in
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
self.mapView.addAnnotation(annotation)
}
self.mapView.setRegion(self.mapView.regionThatFits(arrayOfCoordinates.region), animated: true)
}
}
let controller = MapViewController()
PlaygroundPage.current.liveView = controller
PlaygroundPage.current.needsIndefiniteExecution = true

