[BETA] Fun With Combine

I'm an old fart, that's not in any way debatable. But being an old fart, I have done old things, like implementing a databus-like system in a program before. So when I saw Combine, I thought I'd have fun with re-implementing a databus with it.

First things first

Why would I need a databus?

If you've done some complex mobile programming, you will probably have passed notifications around to signal stuff from one leaf of your logic tree to another, something like a network event that went in the background and signalled its task was done, even though the view controller that spawned it has gone dead a long time ago.

Databuses solve that problem, in a way. You have a stream of "stuff", with multiple listeners on it that want to react to, say, the network went down or up, the user changed a crucial global setting, etc. And you also have multiple publishers that generate those events.

That's why we used to use notifications. Once fired, every observer would receive it, independantly of their place in the logic (or visual) tree.

The goal

I wanted to have a databus that could do two things:

  • allow someone to subscribe to certain events or all of them
  • allow to replay the events (for debug, log, or recovery purposes)

I also decided I wanted to have operators that reminded me of C++ for some reason.

The base

Of course, for replay purposes, you need a buffer, and for a buffer, you need a type (this is Swift after all)

public protocol Event {
    
}

public final class EventBus {
    fileprivate var eventBuffer : [Event] = []
    fileprivate var eventStream = PassthroughSubject<Event,Never>()

PassthroughSubject allows me to avoid implementing my own Publisher, and does what it says on the tin. It passes Event objects around, and fails Never.

Now, because I want to replay but not remember everything (old fart, remember), I decided to impose a maximum length to the replay buffer.

    public var bufferLength = 5 {
        didSet {
            truncate()
        }
    }
    
    fileprivate func truncate() {
        while eventBuffer.count > bufferLength {
            eventBuffer.remove(at: 0)
        }
    }

It's a standard FIFO, oldest in front latest at the back. I will just pile them in, and truncate when necessary.

Replaying is fairly easy: you just pick the last x elements of a certain type of Event and process them again. The only issue is reversing twice: once for the cutoff, and once of the replay itself. But since it's going to be seldom used, I figured it was not a big deal.

    public func replay<T>(count: UInt = UInt.max, handler: @escaping (T) -> Void) {
        var b = [T]()
        for e in eventBuffer.reversed() {
            if b.count >= count { break }
            if let e = e as? T {
                b.append(e)
            }
        }
        for e in b.reversed() {
            handler(e)
        }
    }

Yes I could have done it more efficiently, but part of the audience is new to swift (or any other kind) of programming, and, again, it's for demonstration purposes.

Sending an event

That's the easy part: you just make a new event and send it. It has to conform to the Event protocol , so there's that. Oh and I added the << operator.

    public func send(_ event: Event) {
        eventBuffer.append(event)
        truncate()
        eventStream.send(event)
    }
    static public func << (_ bus: EventBus, _ event: Event) {
        bus.send(event)
    }

From now on, I can do bus << myEvent and the event is propagated.

Receiving an event

I wanted to be able to filter at subscription time, so I used the stream transformator compactMap that works exactly like its Array counterpart: if the transformation result is nil, it's not included in the output. Oh and I added the >> operator.

    public func subscribe<T:Event>(_ handler: @escaping (T) -> Void) {
        eventStream.compactMap { $0 as? T }.sink(receiveValue: handler)
    }
    static public func >><T:Event> (_ bus: EventBus, handler: @escaping (T) -> Void) {
        bus.subscribe(handler)
    }

The idea is that you define what kind of event you want from the block's input, and Swift should (hopefully) infer what to do.

I can now write something like

bus >> { (e : EventSubType) in
    print("We haz receifed \(e)")
}

EventSubType implements the Event protocol, and the generic type is correctly inferred.

The End (?)

It was actually super simple to write and test (with very high volumes too), but I'm guessing there would be memory retention issues as I can't figure out a way to properly unsubscribe from the bus, especially if you have self references in the block.

Then again it's a beta and this is a toy sample. I will need to dig deeper in the memory management stuff, but at first glance, it looks like the lifetime of the blocks is exactly the lifetime of the bus, which makes it impractical in real cases. Fun stuff, though.

[Security] Tracking via Image Metadata

From Edin Jusupovic

Facebook is embedding tracking data inside photos you download

Of course they do.

[Rant] Hyperbolae

The backlash over some of the community's lack of enthusiasm for SwiftUI - mine included - was a lot milder than I thought it would be given the current trend of everything being absolutely the best thing that ever happened or the worst in history.

While that definitely surprised me in a positive way, it also made me think about the broader topic of the over-abundance of hyperbolae (or hyperboles if you really insist) in our field.

The need to generate excitement over something that is fundamentally boring information manipulation science drives me fairly bonkers, and in my opinion has some very bad side effects.

Here's a few headlines in my newsfeed (pertaining to CS)

  • "Nokia reveals 5G-ready lithium nanotube battery with 2.5X run time"
[at the end of the article]
As is commonly the case with new battery technologies, the researchers are providing no specific timetable for commercialization.
  • "AI was everywhere in 2018 and it will continue to be a major topic in 2019 as we begin to witness AI breakthroughs across businesses and society"

    No we don't. "AI" doesn't exist, and machine learning algorithms are the same as they were 20 years ago.
  • "Flutter will change everything, and Apple won't do anything about it"

    Yea, well... I guess predictions aren't that guy's forte.
  • "Apple's AR glasses arriving in 2020, iPhone will do most of the work"
[just below the title, emphasis mine]
Apple's long-rumored augmented reality headset could arrive mid-way through 2020 prominent analyst Ming-Chi Kuo believes

And that's just stuff people have thrown at me in the past few days... You can add quantum computing, superfine processor lithography, AR/VR news, etc if you feel like it. I will spare you the most outrageous ones.

OK, and?

Look, I get it. Websites that are paid through advertising need to generate traffic and they will use every single clickbait they can find.

My problem is that people that are supposed to be professionals in my field are heavily influenced by those headlines and by, well, influencers, who hype things. It's like everyone, including people who are supposed to actually implement those things for actual paying customers, are succumbing to a mass hysteria. No wonder clients are so harsh and sometimes downright hostile when it comes to evaluating the quality of the work done.

Because "AI" is so hyped up these days, I've had people refusing to pay me for ML work, because the percentage of the predictions were "too low", based on a fluff piece Google had posted somewhere on the theoretical capabilities of their future product that will do better... As if an on-device computationally expensive model built by one guy could outperform a theoretical multi-million cloud-based computer farm, on which a couple of hundred techs will have worked on for a few years...

The five star problem

By over-hyping everything, we end up in a situation where something that's good is a "four stars", something that's good with a very good (and expensive) marketing campaign and tech support mayyyyyyy get a "four and a half", and everything else is a "one star".

Is a new piece of tech like SwiftUI good? No one knows, because we haven't used it in production yet. It's interesting, for sure. It seems to be fairly performant and well-written. Does it have limitations? Of course it does. Will it serve every single purpose? Of course not. Why can't we be interested in a mild manner, recognizing the positive and the drawbacks?

Is ML a useful tool? Yes indeed. To the point where we can build useful stuff with it anyways... Is it going to replace a smart human anytime soon? Of course not.

Lately, it seems like I have to remind people all the time that computer science is a science. Is chemistry cool? Of course it is! Do you see chemists run around everywhere clamoring that they have found a new molecule that will save humanity every goddamn week? No, because despite the advances in AI (without quotes, it means something fairly different than "machine learning"), we still haven't found a way to predict the future.

[Talk] Combine+DSLs=SwiftUI

This week I gave a talk at Cocoaheads in their traditional "Back From WWDC" session. The format is short - 10 minutes - and is supposed to be about something I learned during the conference.

Now, it's not a secret that, while I am fine with SwiftUI being a thing, I can't say I'm impressed or excited about it. I don't think it saves time (compared to other means of achieving UI in either code or with the graphical UI builder in Xcode), I don't think it improves performance, and I definitely don't find it fun to write HTML-CSS-like code to draw pixels on a screen. Buuuuuuut I will grant that it might help newcomers get over the "I can't do it it's different" mentality, and that can't be a bad thing.

However, in order for SwiftUI to exist, Apple had to add some really cool things in Swift that do have a lot of potential.

Combine

The new official face of pubsub in Swift is called Combine. It allows for a fairly performant and clear way to implement event-driven mechanics into your app. I won't get into details, but basically, it obsoletes a lot of things we used to do with other means that were never completely satisfactory:

  • Notifications: that was a baaaaaad way to move messages around. They are fairly inconsistent in terms of delivery, and cause a ton of leaks if you aren't careful. The second part might not be solved yet, but the first one definitely is.
  • Bindings: for us old farts who remember the way we used to do things (it was never ported to iOS). They were the undead of UI tricks on the Mac, and it's fairly certain this will put them out of their misery.
  • KVO: this was always very badly supported in Swift, it always felt hacky and weird, because it relied on the amorphous nature of objects in the Objective-C runtime. After several weird proposals to bring them back in some way in Swift, it seems like we have ourselves a winner.

All in all, while neither revolutionary nor extra-cool, Combine seems to be the final answer from Apple to the age-old question "how in hell do I monitor the changes of a variable?". React and other similar frameworks hacked something together that worked fairly well, and this framework seems to be the simplest and cleanest way to achieve the same thing.

DSLs

Now this gets me excited. We've all had to generate text in specific formats using string interpolation and concatenation. Codable takes care of JSON, and there are some clever XML and whatnot APIs out there.

But allowing for DSLs directly in the language allows people to avoid writing stuff like

"""
SELECT \(columns.sqlFormat) FROM \(table)
WHERE deleted = false AND \(query)
"""

And replace it with something like

Select(from: table) {
  Column(columns.sqlFormat)
  Where {
    table.delete == false && query
  }
}

How's that better?

  • Syntax checking is done at compile time. No more "awwwww I forgot to close the parenthesis!"
  • You can have control code ( if, for, etc... ) directly inside of your "query"
  • Type safety is enforceable and enforced
  • The generator can optimize the output on the fly (or minify, obfuscate, format, it)

Swift UI is just the beginning

The underpinning tech that makes SwiftUI tick is way cooler than SwiftUI itself. I can't wait to dig my teeth into it and spit out some really nifty tricks with it. And sorry for the image.

[Dev Diaries] Force Directed Layout

Sometimes, the worlds of computer graphics and physics collide in the most beautiful ways.

Everybody loves UICollectionViews, but after a while, those grids tend to be a bit restrictive. The "flow" layout is fine and all, but what if we wanted to have the cells arranged in an "organic" manner?

Let's talk about forces

In nature, almost everything is about forces. Gravity is probably the one everyone is the most familiar with, and it goes straight down at roughly 9.8 m/s/s. What that means is that at the beginning, your object goes at 0 m/s, then after a second it goes at 9.8 m/s, then after another second, it goes at 19.6 m/s, etc. When an object is on the ground, the gravity still applies, but it's compensated by the floor pushing up with a force equal to the gravity.

Good ole Newton says that what matters is the sum of all the forces that apply to an object. From that you can deduce the acceleration the object is subjected to (in m/s/s), and by derivation, the speed of an object at a given time. Speed is the distance moved by unit of time, so from the speed, we can determine the position of an object on which the force applies.

F = ma \implies a = F/m

For the rest of this post, we will assume everything has a mass of 1, because it simplifies a bit the operations.

Exemple: gravity on a slope

In terms of code, I decided to have a struct representing forces in a way that will give me the operations I will need later:

  • adding two forces together
  • getting the angle of the force in a plane
  • getting the magnitude (length of the vector)
struct Force {
    let dx: CGFloat
    let dy: CGFloat
    static func +(lhs: Force, rhs: Force) -> Force {
        return Force(lhs.dx+rhs.dx,lhs.dy+rhs.dy)
    }
    
    static func /(lhs: Force, rhs: CGFloat) -> Force {
        return Force(lhs.dx/rhs, lhs.dy/rhs)
    }
    
    init(_ x: CGFloat, _ y: CGFloat) {
        dx = x
        dy = y
    }
    
    var angle : CGFloat {
        return CGFloat(atan2(Double(dy), Double(dx)))
    }
    
    var magnitude : CGFloat {
        return sqrt(dx*dx+dy*dy)
    }
}
Attraction

We'll need our cells to be attracted to a certain point in the plane. Ideally, if there's only one cell, it'll sit at that point, and if there are two, they will be as close as possible to there, while not overlapping (more on that later).

Gravity has flair, but its exponential nature is a bit too powerful. For force directed graphs (like the ones dot produces), we tend to go with its linear cousin: the spring. It also is easier to wrap your head around: you attach all the cells to the center with a spring, and they will try to get back there.

Spring force is expressed with Hooke's law

F = -k•∂
  • k is the stiffness of the spring
  • ∂ is how far we are from the "ideal" distance to the other end of the spring

You can see that compressing a spring produces an "extension" force (it pushes the two ends away because ∂ is negative), and the opposite "attraction" force (when ∂ is positive).

Repulsion

We could use the spring for repulsion as well, but it'd mean "wiring" all the cells with their nearest neighbours and always keeping track of which cells are attached to which other cells. It's too complex to manage, but fortunately, we have another physics law that repulses and looks quite natural: magnets.

Magnets attract obviously, but they also repulse quite heavily. And because we want that repulsion to be extremely high when the objects are super close and negligible when they are far away from each other, this time we kind of need an inverse square law. Coulomb's law to the rescue!

F = k \frac{Q_1 * Q_2}{r^2}
  • k isn't that important for us, but its value is 9×109 N⋅m2⋅C−2
  • The two Qs are the electric charges of the objects
  • r is the distance between them

You can see that objects with the same electric charge repulse each other, while objects with opposite charges attract each other.

So why didn't we use this for our attraction law? The big problem with using that kind of force is that it's very computationally expensive: every cell will affect every other cell. Calculating the resulting force is proportional to the square of the number of cells. Simple spring mechanics is a lot simpler to manage for attraction, but we won't escape the complexity of repulsion.

Sidenote: what's up with that k value? 😳
That's physics for you, I'm afraid. We need to match the formula to the real world, and that's the number that fits what we see.
For our purposes, we have made-up identical "charges" on our cells anyways, so we will have k(Q²/r²). Might as well ignore k altogether and go with Q³/r²
Of course, that value of Q has no "physical" value, it's just convenient.
Let's create a universe

So, we have decided on our forces, now we need something to apply them to. Meet Node, the structure which will handle the physics side of our layout. It needs to hold 3 informations:

  • the position the cell is at : x and y
  • some way to differentiate it from other cells (because of the repulsion bit) : a uuid
  • a way to link it to the data source of the UICollectionView : an IndexPath

Here's the boring first part:

struct Node : Equatable, Hashable {
    var x : CGFloat = 0
    var y : CGFloat = 0
    let uuid = UUID() // for identification purposes
    var indexPath : IndexPath
    
    init(x px: CGFloat = 0, y py: CGFloat = 0, for idx: IndexPath) {
        x = px
        y = py
        indexPath = idx
    }
    
    static func == (lhs: Node, rhs: Node) -> Bool {
        return lhs.uuid == rhs.uuid
    }
    // ...
}

The first interesting bit is the attraction function. It is extremely verbose for education purposes, you can compactify and optimize it quite a bit.

func attraction(center: CGPoint, stiffness: CGFloat) -> Force {
    // Hooke's Law: F = -k•∂ (∂ being the "ideal" distance minus the actual distance)
    let dx = x - center.x
    let dy = y - center.y
    let angle = CGFloat(atan2(Double(dy), Double(dx)))
    let delta = sqrt(dx*dx+dy*dy)
    let intensity = stiffness * delta
    let ix = abs(intensity * cos(angle))
    let fx : CGFloat
    if center.x > x { // positive force to the right
        fx = ix
    } else {
        fx = -ix
    }
    let iy = abs(intensity * sin(angle))
    let fy : CGFloat
    if center.y > y { // positive force to the bottom
        fy = iy
    } else {
        fy = -iy
    }
    return Force(fx,fy)
}

The really big one, although not that complicated from the code point of view, is the repulsion force. It has to take into account every single other node. As with the attraction, it is verbose for the sake of clarity, and can obviously be optimized. One such optimization is to return early with a 0 if the distance is "too great", but that's beyond the scope of what we are trying to do here.

func repulsion(others: [Node], charge: CGFloat) -> Force {
    var totalForce = Force(0,0)
    for n in others.filter({ (on) -> Bool in
        on.uuid != self.uuid // just in case
    }) {
        // Coulomb’s Law; F = k(Q1•Q2/r²)
        // Since we're dealing with arbitrary "charges" here, we'll simplify to F = C³/r²
        // We want repulsion (Q1=Q2) and not deal with big numbers, so that works
        var dx = x - n.x
        var dy = y - n.y
        if dx == 0 && dy == 0 { // wiggle a bit
            let room : CGFloat = 0.05
            dx += CGFloat.random(in: -room...room)
            dy += CGFloat.random(in: -room...room)
        }
        let angle = CGFloat(atan2(Double(dy), Double(dx)))
        let delta = max(0.000001,sqrt(dx*dx+dy*dy)) // do NOT divide by zero you fool
        let intensity = pow(charge,3)/(delta*delta)
        let ix = abs(intensity * cos(angle))
        let fx : CGFloat
        if n.x > x { // positive force to the left
            fx = -ix
        } else {
            fx = ix
        }
        let fy : CGFloat
        let iy = abs(intensity * sin(angle))
        if n.y > y { // positive force to the bottom
            fy = -iy
        } else {
            fy = iy
        }
        
        totalForce = totalForce + Force(fx,fy)
    }
    
    return totalForce
}

Note the check for divide-by-zero shenanigans (in case two cells overlap), that is covered in two different ways: we wriggle the cells a bit to avoid being exactly on top of each other and we don't divide by zero if the wriggling failed. The resulting force is the sum of being repulsed by every other cell.

Putting it all together, we'll just need to sum attraction and repulsion. For some reason, I have also included the possibility of adding a force to a node.

func globalForce(center: CGPoint, otherNodes: [Node], stiffness: CGFloat, charge: CGFloat) -> Force {
    let a = attraction(center: center, stiffness: stiffness)
    let r = repulsion(others: otherNodes, charge: charge)
    return a + r
}

static func +(lhs: Node, rhs: Force) -> Node {
    return Node(x: lhs.x+rhs.dx, y: lhs.y+rhs.dy, for: lhs.indexPath)
}

Because we have to compute the global force (which depends on every node) for every node, we end up with a square order complexity. But since the force calculation is independant for every node, we can spread the calculations of as many threads as we can to claw a bit of performance back. Thankfully I have a Task system handy.

Here is the function that takes all nodes, and computes where they should go next. springStiffness and electricCharge are instance variables of the layout. Its output is the pair composed of the new node positions and the total movement of all the nodes, for a reason that will become clear later.

func computeNewPositions(center: CGPoint, nodes: [Node]) -> (nodes: [Node], movement: CGFloat) {
    // if the total movement is less than threshold, will return nil
    var totalMovement : CGFloat = 0
    var newNodes : [Node] = []
    var computeTasks : [Task] = []
    let lock = NSLock()
    for n in nodes {
        let t = Task() {
            let f = n.globalForce(center: center, otherNodes: nodes, stiffness: self.springStiffness, charge: self.electricCharge)
            let nn = n + f
            lock.lock()
            newNodes.append(nn)
            totalMovement += f.magnitude
            lock.unlock()
        }
        computeTasks.append(t)
    }
    let waitSem = DispatchSemaphore(value: 0)
    let compute = Task.group(computeTasks)
    compute.perform { (outcome) in
        waitSem.signal()
    }
    waitSem.wait()
    
    return (newNodes, totalMovement)
}

Now's the time to implement a few of the overrides for our custom layout. Let's start with the obvious ones:

protocol ForceDirectedLayoutDelegate {
    func layoutDidFinish()
}
class ForceDirectedLayout : UICollectionViewLayout {
    // in case you want to know
    var delegate : ForceDirectedLayoutDelegate? = nil
    
    // maths
    var springStiffness : CGFloat = 0.02 // max(width,height)/1000 seems ok
    var electricCharge : CGFloat = 10 // max(width,height)/2 seems ok
    var cellSize : CGSize = CGSize(width: 20,height: 20) // obviously too small for real use

    // ...

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true // because the center changes...
    }
    
    override var collectionViewContentSize: CGSize {
        var totalRect = CGRect(origin: CGPoint.zero, size: self.collectionView?.frame.size ?? CGSize.zero)
        for cachedA in cachedAttributes.values {
            totalRect = totalRect.union(
                CGRect(x: cachedA.center.x-cachedA.size.width*0.5,
                       y: cachedA.center.y-cachedA.size.height*0.5,
                       width: cachedA.size.width, height: cachedA.size.height))
        }
        return totalRect.size
    }
}

The eagle eyed among you will have noticed a cachedAttributes instance variable. For some reason, UICollectionView itself doesn't cache the current position and size of a cell. If you call collectionView.layoutAttributesForItem(at: idx) it gives you a nil value, forcing you to regenerate the attributes at every stage (and there are many). I have decided to cache the cell attributes internally as a backup, and if Apple decides at some point to return a non-nil value, I will use it. But what it forces me to do is to make sure I detect cell deletion. If a cell is removed, I need to remove its cached attributes. You can skip that deletion part if you want, or code it more efficiently, it's just for completeness' sake.

/// Unfortunately, collectionView.layoutAttributesForItem doesn't seem to be caching the previous attributes
/// We do it ourselves as a backup
fileprivate var cachedAttributes = [IndexPath:UICollectionViewLayoutAttributes]()

/// Since we cache the data ourselves, we need to cleanup if elements are removed
override func prepare() {
    super.prepare()
    
    guard let collection = self.collectionView else {
        cachedAttributes.removeAll()
        return
    }
    let sectionCount = collection.dataSource?.numberOfSections?(in: collection) ?? 1
    var rowCounts = [Int:Int]()
    for s in 0..<sectionCount {
        rowCounts[s] = collection.dataSource?.collectionView(collection, numberOfItemsInSection: s) ?? 0
    }
    for removed in cachedAttributes.keys.filter({ (idx) -> Bool in
        return idx.section >= sectionCount || idx.row >= (rowCounts[idx.section] ?? 0) // hence the dictionary, no index out of bounds
    }) {
        cachedAttributes.removeValue(forKey: removed)
    }
}

And finally, the override that makes everything work: the layout function itself. Unfortunately, if a node changes position, it affects every other node, potentially, so we can't update only a partial list. We need to do them all every single time.

But wait. We have been talking about forces for 2000+ words here. Forces can help you figure out positions but they aren't positions themselves. Yep, that's right, we have an animated layout: from the current position of the cells, you calculate where the cells should go next. So... when does the animation end?

That's where the movement output of the computation function comes in. When we reach an equilibrium (like the floor force and the gravity exactly compensating each other), the movement drops to zero. Of course, in the real world, it's a neverending process but we can just decide that the layout stops animating as soon as the total movement of every cell falls below a certain threshold. I have decided arbitrarily that a third of a pixel is imperceptible enough, even on a 3x retina screen but if you want to stop the animation sooner, feel free to raise that a bit.

So, the mechanics will be as follows:

  • we take the current positions of all the cells
  • we compute their new positions
  • if they didn't move much, we stop
  • if they still are moving quite a bit, we instruct the collection view to keep going by invalidating the layout again
// unfortunately, every node affects every other node, so we can't do partial updates
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    var attributes : [UICollectionViewLayoutAttributes] = []
    var nodes = [Node]()
    
    if let collectionView = self.collectionView {
        for i in 0..<(collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: 0) ?? 0) {
            let idx = IndexPath(row: i, section: 0)
            let currentAttributes : UICollectionViewLayoutAttributes
            if let ca = (collectionView.layoutAttributesForItem(at: idx) ?? cachedAttributes[idx]) {
                currentAttributes = ca
            } else {
                // randomize start positions, just for funsies
                currentAttributes = UICollectionViewLayoutAttributes(forCellWith: idx)
                currentAttributes.center = CGPoint(
                    x: CGFloat.random(in: 0...self.collectionViewContentSize.width),
                    y: CGFloat.random(in: 0...self.collectionViewContentSize.height)
                )
                currentAttributes.size = cellSize
                cachedAttributes[idx] = currentAttributes
            }
            attributes.append(currentAttributes)
            nodes.append(Node(x: currentAttributes.center.x, y: currentAttributes.center.y, for: idx))
        }
        
        let center = self.collectionView?.absoluteCenter ?? CGPoint.zero
        let nextIteration : (nodes: [Node], movement: CGFloat)
        nextIteration = computeNewPositions(
            center: center,
            nodes: nodes)
        
        for n in nextIteration.nodes {
            if let attrsIdx = attributes.firstIndex(where: { $0.indexPath == n.indexPath }) {
                let attrs = attributes[attrsIdx]
                attrs.center = CGPoint(x: n.x, y: n.y)
                attributes[attrsIdx] = attrs
            }
        }
        
        // if it's still moving, keep going
        if nextIteration.movement > 0.3 { // subpixel animation
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.05) {
                self.invalidateLayout()
            }
        } else {
            DispatchQueue.main.async {
                self.delegate?.layoutDidFinish()
            }
        }
        
    }
    
    return attributes
}

Please note that I position new cells randomly. They could appear in the middle of the other cells. Depending on the effect you want to go for, you can have they come from a specific point, or randomly from the borders, etc.

And that's it! I have made a quick example that adds round cells progressively to a collection view, and here's the result:

You can obviously get the whole thing on GitHub, prepared for use as a swift package for the future Xcode version.