A SwiftUI library for creating masonry grid layouts with items of varying heights.
- Pure SwiftUI - Based on a SwiftUI Grid
- Masonry layout - Items of varying heights flow naturally
- Responsive columns - Column count adjusts based on available width
- Lazy rendering - Only visible cells are rendered for optimal performance
- Focus state support - Full integration with SwiftUI's focus system
Add Masonry to your Package.swift:
dependencies: [
.package(url: "https://github.com/imbn123/Masonry.git", branch: "main")
]import Masonry
import SwiftUI
// 1. Define your item type conforming to MasonryItem
struct PhotoItem: MasonryItem {
let id: UUID
let imageURL: URL
let aspectRatio: CGFloat
var preferredHeight: MasonryItemHeight {
switch aspectRatio {
case ..<0.7: return .extraLarge
case 0.7..<0.9: return .large
case 0.9..<1.1: return .medium
case 1.1..<1.4: return .small
default: return .extraSmall
}
}
}
// 2. Create your view
struct PhotoGrid: View {
let photos: [PhotoItem]
var body: some View {
ScrollView {
MasonryView(items: photos, spacing: 8) { photo, focusBinding in
AsyncImage(url: photo.imageURL) { image in
image.resizable().scaledToFill()
} placeholder: {
Color.gray
}
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
}
}The main view component for creating masonry layouts.
MasonryView(
items: [Item],
minItemWidth: CGFloat? = nil,
width: CGFloat? = nil,
spacing: CGFloat = 8,
itemView: (Item, FocusState<Item.ID?>.Binding) -> ItemView
)All items displayed in a MasonryView must conform to this protocol.
public protocol MasonryItem: Hashable, Identifiable, Sendable {
@MainActor var preferredHeight: MasonryItemHeight { get }
}An enum representing five height levels for masonry items.
| Case | Height Multiplier | Visual Height |
|---|---|---|
.extraSmall |
0.5x | 50% of cell height |
.small |
0.75x | 75% of cell height |
.medium |
1.0x | 100% (one full cell) |
.large |
1.25x | 125% of cell height |
.extraLarge |
1.5x | 150% of cell height |
// Get the height multiplier for calculations
let multiplier = MasonryItemHeight.large.heightMultiplier // 1.25
// Iterate all cases
for height in MasonryItemHeight.allCases {
print(height.debugDescription)
}struct BasicGrid: View {
@State private var items: [MyItem] = []
var body: some View {
ScrollView {
MasonryView(items: items) { item, _ in
RoundedRectangle(cornerRadius: 8)
.fill(item.color)
}
.padding()
}
}
}MasonryView(items: items) { item, focusBinding in
ItemView(item: item)
.focused(focusBinding, equals: item.id)
}On iOS 17+ and macOS 14+, Masonry supports keyboard navigation:
- ← Move to the previous item
- → Move to the next item
- ↑ Move to the item above
- ↓ Move to the item below
Focus state is automatically managed through the FocusState<Item.ID?>.Binding provided in the item view closure.
Request a layout recalculation when item content changes dynamically.
struct DynamicItem: View {
@Environment(\.requestHeightUpdate) private var requestHeightUpdate
@State private var isExpanded = false
var body: some View {
VStack {
Text("Content")
if isExpanded {
Text("More content...")
}
}
.onTapGesture {
isExpanded.toggle()
requestHeightUpdate()
}
}
}- iOS 16.0+ / macOS 14.0+
- Swift 6.2+
- Xcode 16.0+
See the LICENSE file for details.
