🍫 CocoaPatterns — iOS & Mac Development Since 2009 RSS
🍫 CocoaPatterns
Because someone has to write this stuff down
"We ship apps. Sometimes they even work."

Send Entire UIViews via Push Notification: Why Stop at Data?

We already proved you can ship binaries via APNS. Now we're shipping the UI itself. Pixel by pixel if we have to.

BK
Brad Kowalski
@bradkow_ios · CocoaPatterns

Three months ago, I published "Ship App Binaries via Push Notification" and the response was overwhelming. Over 200 comments. Fourteen emails from Apple's legal department. A mass unfollowing on Twitter that I'm choosing to interpret as awe.

But one piece of feedback stuck with me. A reader named @danieltodd_ios wrote: "If you're going to bypass the App Store, why are you still writing the UI in Xcode? Just send the views too."

Daniel, wherever you are: you changed my life.

The Problem with Client-Side UI

Think about how absurd the current model is. You write a UIView subclass. You compile it. You archive it into a binary. You upload it to App Store Connect. Apple reviews it — sometimes for weeks. Then the user downloads the entire app just to see your updated button color.

What if instead, you just... sent the view? Directly? Through the air? Into their phone? Right now?

"The fastest deploy pipeline is one that doesn't involve deploying. It involves pushing."
— Brad Kowalski, in a talk that was never invited back

The Architecture: VAPNS (View over APNS)

The core idea is elegant. We serialize a UIView hierarchy using NSKeyedArchiver, chunk it into 4KB payloads (the APNS size limit), and reassemble it on the client. The client then injects the reconstituted view directly into the active view hierarchy using a technique I'm calling Live View Replacement, or LVR.

// Server-side: serialize and chunk the view
NSData *viewData = [NSKeyedArchiver archivedDataWithRootObject:profileView];
NSArray *chunks = [self chunkData:viewData maxSize:4096];

for (NSData *chunk in chunks) {
    [apnsClient sendPayload:@{
        @"vapns_chunk": [chunk base64EncodedStringWithOptions:0],
        @"vapns_seq": @(idx),
        @"vapns_total": @(chunks.count),
        @"vapns_view_id": @"profile_header"
    }];
}
⚠️ Known Issue

APNS does not guarantee delivery order. Or delivery. We've added a reassembly buffer that waits for all chunks before reconstituting the view. Average wait time is 4 seconds. Maximum observed wait time is 11 days. The user saw a partially rendered UITableViewCell floating in their notification center for a week and a half.

Client-Side Reconstitution

// Reassemble and inject
- (void)vapns_injectView:(NSString *)viewID {
    NSData *fullData = [self.chunkBuffer assembleForID:viewID];
    UIView *view = [NSKeyedUnarchiver unarchiveObjectWithData:fullData];

    // Find the existing view and swap it out
    UIView *target = [self.window viewWithTag:viewID.hash];
    [target.superview insertSubview:view aboveSubview:target];
    [target removeFromSuperview];

    // The user will never notice. Probably.
}

Edge Cases We've Encountered

  • Auto Layout constraints don't survive serialization well. We now ship constraints as a separate VAPNS stream. This means each view update requires two parallel push notification sequences that must arrive and reassemble independently. It's fine.
  • Gesture recognizers serialize their target-action pairs, which means the deserialized view tries to call methods on objects that don't exist on the client. We solved this with — you guessed it — swizzling.
  • A UIScrollView we sent arrived with a content offset of 847,000 points. We think this happened because chunk 14 of 23 arrived twice and the reassembly buffer double-counted the scroll state. The user's phone got warm.
  • Dark mode. We haven't figured out dark mode. The view is serialized in whatever trait collection the server was in at archive time. Our server is a Mac mini in Brad's closet. It's always in dark mode because Brad's closet doesn't have a window.

Performance Results

  • Average view delivery time: 4.2 seconds (best case), 11 days (worst case)
  • APNS budget consumed per UI update: ~85 push notifications for a simple form
  • Largest view successfully transmitted: a UICollectionView with 12 cells (347 push notifications, took 9 minutes)
  • Apple's response: a 4-page letter we haven't opened yet
  • App Store review status: not applicable, we don't use the App Store anymore

Next month: sending CoreAnimation layers via SMS. The prototype is almost working and only two phones have caught fire.

4 Comments — Powered by Disqus
MO
mel_ios_eng · 4 days ago
Brad I have been staring at the NSKeyedArchiver payload for twenty minutes. It's 340KB for a single UILabel. You're archiving the entire responder chain.
BK
bradkow_iosAuthor · 4 days ago
@mel_ios_eng That's the responder chain for the LABEL. The button is 1.2MB. We're working on compression.
GA
gashby_data · 3 days ago
The fact that you're using APNS as a UI delivery mechanism and your biggest concern is compression tells me everything.
DP
dpurcell_dev · 2 days ago
I showed this to Apple DTS. They asked me to never contact them again.