I’m trying to create a dynamic grid view layout that mimics the functionality of Apple’s Journal app, with images in the grid resizing themselves based on the available space within the container.
I’ve attempted to use several libraries to achieve this, like SwiftUIMasonry, WaterfallGrid and ExyteGrid
The closest I could get to the expected result is with the ExyteGrid.
Here’s an example of what I’ve tried:
import SwiftUI
import ExyteGrid
import PhotosUI
struct ExampleView: View {
@StateObject private var imagePickerViewModel = PickerViewModel()
@State var contentMode: GridContentMode = .fill
var body: some View {
VStack{
PhotosPicker(
selection: $imagePickerViewModel.selectedItems,
maxSelectionCount: 12,
matching: .images,
photoLibrary: .shared()
) {
Text("Select Photos")
}
if !imagePickerViewModel.images.isEmpty {
VStack {
Grid(imagePickerViewModel.images, id: .id, tracks: 3) { item in
CardView(image: item.image)
.gridSpan(item.span)
}
.gridContentMode(self.contentMode)
.gridFlow(.rows)
}
.frame(maxWidth: .infinity, maxHeight: 300)
}
}
}
}
struct CardView: View {
let image: UIImage
var body: some View {
VStack {
GeometryReader { geometry in
Image(uiImage: image)
.resizable()
.scaledToFill()
.frame(width: geometry.size.width, height: geometry.size.height)
.clipped()
.layoutPriority(.greatestFiniteMagnitude)
.minimumScaleFactor(0.5)
}
}
}
}
class PickerViewModel: ObservableObject {
@Published var images: [ImageItem] = []
@Published var selectedItems: [PhotosPickerItem] = [] {
didSet {
fetchImages()
}
}
let span: [Int] = [1, 2, 3]
private func fetchImages() {
images.removeAll()
for item in selectedItems {
item.loadTransferable(type: Data.self) { result in
switch result {
case .success(let data):
let id = item.itemIdentifier
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.images.append(
ImageItem(
id: id,
image: image,
data: data,
span: [1 , self.span.randomElement() ?? 1] // [columns, rows]
)
)
}
}
case .failure(let error):
print("Failed to load image: (error.localizedDescription)")
}
}
}
}
}
struct ImageItem: Identifiable {
let id: String?
let image: UIImage
let data: Data
let span: GridSpan
}
#Preview {
ExampleView()
}
The problem here is that the span of each items is generated randomly, but must probably be calculated to take into account the available space to fill. How can I recalculate the span of each items to make sure that all the available space in the container is filled?