SwiftGridView 1.0: Swift 6 and SwiftUI

SwiftGridView has reached 1.0. I first released it in 2016 as a way to learn Swift and publish a CocoaPod, and it has been sitting at 0.7.x for a while. Enough had drifted out of date that a version bump was not going to cut it, so this release is a full modernization. It is also a breaking one.

SwiftGridView Demo

Swift 6 and Strict Concurrency

The package now builds in Swift 6 language mode with complete strict concurrency checking, and the deployment target has moved to iOS 15. The manifest had been claiming iOS 10 while the podspec said iOS 12, so this also collapses three conflicting answers into one.

The migration was mostly mechanical. The library has no background work in it, and UICollectionView is already main-actor isolated by the SDK, so the work was making that isolation explicit with @MainActor rather than untangling real data races.

A Pure Swift API

The datasource and delegate were @objc protocols using @objc optional methods. That is what made the optional methods possible, but it also capped what the API could express: no value types, no generics, no Sendable. They are now plain Swift protocols, with the optional methods replaced by default implementations in protocol extensions.

Deciding what those defaults should do was the interesting part, because the methods split into two groups. Methods returning values, like header heights and frozen column counts, default to zero, which the call sites already treat as “feature off”. Methods returning views are different. They are only called once you have enabled the corresponding feature, and there is no sensible view to invent as a default. Returning an empty SwiftGridReusableView would bypass the reuse queue and silently render blank headers, hiding the mistake. Those defaults call fatalError instead, naming the method and the feature that requires it, which keeps the failure as loud as it was before.

SwiftUI Support

There is now a SwiftGrid view in the library. The SwiftUI demo previously hand-rolled its own UIViewRepresentable, which meant every SwiftUI adopter had to write the same wrapper:

SwiftGrid(dataSource: model, delegate: model) { grid in
    grid.register(MyCell.self, forCellWithReuseIdentifier: MyCell.reuseIdentifier())
}

The grid is still datasource and delegate driven, so the same objects work in both UIKit and SwiftUI. The wrapper holds onto them for you, since the underlying view references them weakly.

Swift Package Manager Only

CocoaPods support is gone, along with the podspec, the Podfiles, the committed Pods directories, and the old Xcode project and workspace. Maintaining a second distribution channel was adding clutter for little remaining benefit. Xcode opens Package.swift directly and the example apps reference the package by local path.

Tests and CI

The old test target could not actually run. It mixed unit tests with a XCUITest stub that needed a host app, which does not work under SPM. It has been rebuilt with Swift Testing and runs headless on the simulator on every pull request, covering layout math, frozen row and column pinning, grouped header spans, selection, and the SwiftUI wrapper. Formatting is enforced with swift-format, pinned to an exact version in a separate tools manifest so consumers never resolve it as a dependency.

Documentation moved from jazzy to DocC and is built and published by CI rather than generated by hand and committed. The old documentation URLs no longer resolve.

Migrating from 0.7.x

Three things to watch for:

  • Remove @objc from your conformances, and delete any optional methods you implemented only to satisfy the compiler.
  • Assign dataSource and delegate in code. They can no longer be Interface Builder outlets, and a leftover storyboard connection raises NSUnknownKeyException at load. This one caught my own example project.
  • Objective-C is no longer supported.

The 1.0.0 release is on GitHub, along with the source and examples.