Building a Bluesky client for iOS means making a critical architectural decision early: UIKit or SwiftUI? After months of development on Skyscraper for iOS, we made a significant choice: we rewrote our entire feed timeline from SwiftUI to UIKit.

This post shares the complete pros and cons of both frameworks for social media app development, why we switched, and when each framework excels for building Bluesky apps that help users grow their following.

The Bottom Line: Use Both

Our recommendation after building a production Bluesky client: use a hybrid approach. UIKit for the feed timeline, SwiftUI for everything else. Here's our current architecture:

  • UIKit (UICollectionView): Main timeline feed, notifications feed
  • SwiftUI: Post composer, profile views, settings, DMs, discovery
  • UIViewRepresentable: Bridges UIKit components into SwiftUI navigation

This gives us the best of both worlds: buttery-smooth scrolling where it matters most, with rapid development for the rest of the app.

SwiftUI Pros: Why We Started With It

When we began building Skyscraper, SwiftUI was the obvious choice. Here's what it does well:

1. Faster Development Speed

SwiftUI's declarative syntax means less code for common patterns. What takes 50 lines in UIKit often takes 10 in SwiftUI:

// SwiftUI: Clean and concise
struct ProfileHeader: View {
    let user: UserProfile

    var body: some View {
        VStack {
            AsyncImage(url: user.avatarURL)
                .frame(width: 80, height: 80)
                .clipShape(Circle())
            Text(user.displayName)
                .font(.headline)
            Text("@\(user.handle)")
                .foregroundColor(.secondary)
        }
    }
}

2. Automatic State Management

SwiftUI's @State, @StateObject, @Observable, and @Environment property wrappers handle UI updates automatically. Change a value, and the UI updates. No manual reload calls.

3. Built-in Animations

Adding polish is trivial:

Button("Like") {
    withAnimation(.spring(response: 0.3)) {
        isLiked.toggle()
    }
}

4. Native Dark Mode Support

SwiftUI handles dark mode automatically with semantic colors. No separate color assets needed for basic theming.

5. Previews During Development

Xcode Previews let you see changes instantly without running the app. This speeds up iteration dramatically for UI work.

6. Modern Concurrency Integration

SwiftUI integrates beautifully with async/await. The .task modifier handles lifecycle automatically:

var body: some View {
    PostView(post: post)
        .task {
            await loadComments()
        }
}

SwiftUI Cons: Why We Hit a Wall

Despite these advantages, we encountered serious issues when building an infinite-scroll social media feed:

1. Scroll Performance with Variable-Height Content

This was the dealbreaker. SwiftUI's LazyVStack struggles with self-sizing cells containing images, embedded posts, link cards, and varying text lengths. We experienced:

  • Stuttery scrolling when new posts loaded
  • Visible cell height recalculation during scroll
  • Memory spikes with large post counts
  • Frame drops below 60fps during fast scrolling

2. Cell Recycling Limitations

UICollectionView has battle-tested cell recycling. SwiftUI's LazyVStack does reuse views, but with less control. For a timeline with thousands of posts, UIKit's explicit dequeueReusableCell pattern gives better memory management.

3. Layout Overflow Issues

We had persistent issues with embed cards and link previews overflowing their containers in SwiftUI. The layout system sometimes calculated incorrect sizes, causing content to extend beyond screen bounds.

4. Stack Overflow with Large Data Structures

Bluesky's FeedViewPost structures are complex and deeply nested. We encountered stack overflow crashes when SwiftUI tried to diff large changes in post arrays.

5. Limited Swipe Action Control

SwiftUI's swipeActions modifier is convenient but limited. UIKit's UISwipeActionsConfiguration offers more customization for the swipe-to-reply and swipe-to-view-details gestures users expect.

UIKit Pros: Why the Feed is UIKit

When we rewrote the timeline in UIKit, these advantages became clear:

1. Butter-Smooth Scrolling

UICollectionView with UICollectionViewCompositionalLayout delivers consistent 60fps scrolling, even with complex cells containing images, videos, and nested content.

2. DiffableDataSource for Efficient Updates

UICollectionViewDiffableDataSource calculates minimal changes when new posts load:

var snapshot = NSDiffableDataSourceSnapshot<Section, PostID>()
snapshot.appendSections([.main])
snapshot.appendItems(postIDs)
dataSource.apply(snapshot, animatingDifferences: true)

3. Precise Cell Recycling

Explicit control over cell lifecycle means predictable memory usage:

func collectionView(_ collectionView: UICollectionView,
                   cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "PostCell",
        for: indexPath
    ) as! PostCell
    cell.configure(with: posts[indexPath.row])
    return cell
}

4. Prefetching for Images

UICollectionViewDataSourcePrefetching lets us preload images before cells become visible:

func collectionView(_ collectionView: UICollectionView,
                   prefetchItemsAt indexPaths: [IndexPath]) {
    let urls = indexPaths.compactMap { posts[$0.row].imageURL }
    ImagePrefetcher.prefetch(urls: urls)
}

5. Mature Scroll Position Management

Restoring scroll position after backgrounding, maintaining position when new posts load above, and jumping to specific posts all work reliably with UICollectionView's contentOffset and anchor-based layout.

6. Better Control Over Refresh and Loading

UIRefreshControl provides native pull-to-refresh with proper physics. Infinite scroll triggers are straightforward with scroll delegate methods.

UIKit Cons: The Tradeoffs

UIKit isn't without downsides:

1. More Boilerplate Code

Our UIKit timeline view is significantly longer than the SwiftUI version was. Cell configuration, delegate methods, and constraint setup add up.

2. Manual State Management

No automatic UI updates. You must explicitly reload cells when data changes. We use Combine publishers to bridge state changes to UI updates.

3. Slower Iteration

No Xcode Previews for UIKit. Every change requires building and running. Development velocity is lower for complex layouts.

4. Constraint-Based Layout Verbosity

Auto Layout constraints in code are verbose compared to SwiftUI's layout system. We use a mix of programmatic constraints and helper extensions.

Our Hybrid Architecture

Here's how Skyscraper combines both frameworks:

// SwiftUI wrapper around UIKit timeline
struct TimelineCollectionView: UIViewRepresentable {
    @Binding var posts: [FeedViewPost]
    let onLoadMore: () async -> Void
    let onPostTapped: (FeedViewPost) -> Void

    func makeUIView(context: Context) -> UICollectionView {
        // Create UICollectionView with compositional layout
        let collectionView = UICollectionView(
            frame: .zero,
            collectionViewLayout: createLayout()
        )
        context.coordinator.setupDataSource(collectionView)
        return collectionView
    }

    func updateUIView(_ collectionView: UICollectionView, context: Context) {
        context.coordinator.applySnapshot(posts: posts)
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
}

This gives us:

  • UIKit performance for the scroll-heavy timeline
  • SwiftUI navigation and state management
  • Clean integration with the rest of the SwiftUI app

When to Use Each Framework

Use Case Recommendation Why
Infinite scroll feed UIKit Best scroll performance
Post composer SwiftUI Rapid iteration, text handling
Settings screens SwiftUI Forms are trivial in SwiftUI
Profile view SwiftUI Static layout, animations
Notifications list UIKit Similar to feed, needs performance
DM conversations Either Lower volume than feed
Onboarding flow SwiftUI Animations, page transitions

Performance Comparison

After our migration, here are the real-world improvements we measured:

  • Scroll FPS: 45-55 fps (SwiftUI) → 58-60 fps (UIKit)
  • Memory usage: 180MB (SwiftUI) → 120MB (UIKit) with 500 posts
  • New post load stutter: Noticeable pause → Seamless insertion
  • Time to interactive: 1.2s → 0.8s for initial feed load

Recommendations for Bluesky Developers

If you're building a Bluesky client or any social media app on iOS:

  1. Start with SwiftUI for rapid prototyping and non-feed screens
  2. Plan for UIKit in your feed from the beginning if performance matters
  3. Use UIViewRepresentable to embed UIKit in SwiftUI navigation
  4. Profile early with Instruments to catch scroll performance issues
  5. Don't fight the framework - if SwiftUI struggles, switch rather than hack

The Future: Will SwiftUI Catch Up?

SwiftUI improves every year. iOS 17 and 18 brought better lazy loading and scroll performance. Eventually, pure SwiftUI may match UICollectionView for social media feeds. But in December 2025, UIKit remains the better choice for high-performance infinite scroll.

We'll continue monitoring SwiftUI's evolution and may revisit this decision in future iOS versions. For now, our hybrid approach delivers the best user experience for people using Skyscraper to browse Bluesky, discover trending content, and grow their following.

Try Skyscraper

Want to experience the smooth UIKit timeline yourself? Download Skyscraper for iOS and see the difference a performance-focused architecture makes for your Bluesky experience.

Questions about iOS development for Bluesky? Reach out at contact@getskyscraper.com.