Swift Variadic Generic Zip Function

mein
5 min readDec 15, 2022

If you accept the output as an array instead of a tuple then here you go.

This is utilizing the heterogeneous array of type [Any] in Swift 5.7

func zipAny(_ data: [[Any]])->[[Any]]{
let count = data.first!.count
let initArray = Array(repeating: [], count: count)
let reduced : [[Any]] = data.reduce(initArray, {
Array(zip($0,$1)).map({
var arrayCopied : [Any] = $0.0
arrayCopied.append( $0.1 )
return arrayCopied
})
})
return reduced
}

let inputArray = [[1,2,3],["a","b","c"]]
let outputArray = zipAny(inputArray)

print(outputArray)
// [[1, "a"], [2, "b"], [3, "c"]]
  • The story of behind this function

This is a by-product of my project, SwiftUI Animated Bezier Curve Using TCA.

For example, there are 3 series of points, as blue points, red points and yellow points. Each series has its own trajectory as an array of points.

It is very easy to plot the blue line, the red line and the yellow line. But it is very challenging to plot the black line. When I am plotting them, I rely heavily on the array functions, like map, reduce, zip. And finally find the way to plot the black line, that is also the solution for the variadic generic zip function.

  • Step 1: Draw the trajectory of each series

I put the conceptual code here for simplicity. You could find the complete code at the end of this article.

import SwiftUI

let blue : [CGPoint] = [b1,b2,b3,b4,b5]
let red : [CGPoint] = [r1,r2,r3,r4,r5]
let yellow : [CGPoint] = [y1,y2,y3,y4,y5]

let allPoints : [[CGPoint]] = [blue, red, yellow]

ZStack{
ForEach(allPoints) { pointSeries in
Path{path in
path.addLines(pointSeries)
}
.stroke()
}
}
  • Step 2: Draw the latest point of each series
import SwiftUI

let blue : [CGPoint] = [b1,b2,b3,b4,b5]
let red : [CGPoint] = [r1,r2,r3,r4,r5]
let yellow : [CGPoint] = [y1,y2,y3,y4,y5]

let allPoints : [[CGPoint]] = [blue, red, yellow]

ZStack{
ForEach(allPoints) { pointSeries in
Circle()
.position(pointSeries.last!)
}
}
  • Step 3: Connect those last points of each series
import SwiftUI

let blue : [CGPoint] = [b1,b2,b3,b4,b5]
let red : [CGPoint] = [r1,r2,r3,r4,r5]
let yellow : [CGPoint] = [y1,y2,y3,y4,y5]

let allPoints : [[CGPoint]] = [blue, red, yellow]

ZStack{
Path{path in
path.addLines(allPoints.map({$0.last!}))
}
.stroke()
}
  • Step 4: How to draw the black lines?

From data's point of view, I am looking for a function which could give me something like this:

import SwiftUI

let blue : [CGPoint] = [b1,b2,b3,b4,b5]
let red : [CGPoint] = [r1,r2,r3,r4,r5]
let yellow : [CGPoint] = [y1,y2,y3,y4,y5]

let allPoints : [[CGPoint]] = [blue, red, yellow]

let transformedSeries : [[CGPoint]] = [[b1,r1,y1],[b2,r2,y2],[b3,r3,y3],[b4,r4,y4],[b5,r5,y5]]

It looks like a way of zipping data. But people talk often about no solution for the variadic generic zip function.

After some trial and error, I find this function works. It is a combination of map, reduce and zip.

The key point is using the heterogeneous array as output, instead of the original tuple output.

func zipAny(_ data: [[Any]])->[[Any]]{
let count = data.first!.count
let initArray = Array(repeating: [], count: count)
let reduced : [[Any]] = data.reduce(initArray, {
Array(zip($0,$1)).map({
var arrayCopied : [Any] = $0.0
arrayCopied.append( $0.1 )
return arrayCopied
})
})
return reduced
}

let inputArray = [[1,2,3],["a","b","c"]]
let outputArray = zipAny(inputArray)

print(outputArray)
// [[1, "a"], [2, "b"], [3, "c"]]

Also noted from Apple's document, if you are zipping different lengths of arrays, the output would have the same length as the shortest array. The same applied to this function, too.

If the two sequences passed to zip(_:_:) are different lengths, the resulting sequence is the same length as the shorter sequence.

  • Source Code for this article
import SwiftUI

extension CGPoint : Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}
struct ColoredPoint : Identifiable, Hashable{
let id = UUID()
var point : CGPoint
let color : Color
}

func zipAny(_ data: [[Any]])->[[Any]]{
let count = data.first!.count
let initArray = Array(repeating: [], count: count)
let reduced : [[Any]] = data.reduce(initArray, {
Array(zip($0,$1)).map({
var arrayCopied : [Any] = $0.0
arrayCopied.append( $0.1 )
return arrayCopied
})
})
return reduced
}
struct ContentView: View {
let allPoints: [[ColoredPoint]] = [GenerateSeriesPoints(color: .blue, startPoint: CGPoint(x: 150, y: 50)),
GenerateSeriesPoints(color: .red, startPoint: CGPoint(x: 250, y: 50)),
GenerateSeriesPoints(color: .yellow, startPoint: CGPoint(x: 350, y: 50))
]
var body: some View {
ZStack{
ForEach(zipAny(allPoints) as! [[ColoredPoint]], id: \.self) { pointSeries in
Path{path in
path.addLines(pointSeries.map({$0.point}))

}
.stroke(.black,lineWidth: 0.5)
}

Path{path in
path.addLines(allPoints.map({$0.last!.point}))
}
.stroke(Color.green)

ForEach(allPoints, id: \.self) { pointSeries in
Path{path in
path.addLines(pointSeries.map({$0.point}))
}
.stroke(pointSeries.first!.color, lineWidth: 3)

ForEach(pointSeries, id: \.self) { point in
Circle()
.fill(point.color)
.frame(width: 6, height: 6, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
.position(x: point.point.x, y: point.point.y)
}

Circle()
.fill(pointSeries.last!.color)
.frame(width: 15, height: 15, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/)
.position( pointSeries.last!.point)
}

}

}
}

func GenerateSeriesPoints(color: Color, startPoint: CGPoint)->[ColoredPoint]{
var currentPoint = ColoredPoint(point: startPoint, color: color)
var result = [currentPoint]
for _ in 0...3{

currentPoint.point = CGPoint(x: Int(currentPoint.point.x) + Int.random(in: -10...10),
y: Int(currentPoint.point.y) + Int.random(in: 10...50))
result.append(currentPoint)
}
return result
}
  • If you ask ChatGPT ...
https://openai.com/blog/chatgpt/

We can see ChatGPT does not understand the limit of zip function. Or maybe it is seeing the future version of Swift which has a zip function without any limit.

zip(array1, array2, array3) // This can not be compiled on Swift 5.7 yet.

--

--