import SwiftUI import SFSafeSymbols struct GridView: View { let database: Database @AppStorage("currentGridName") private(set) var currentGridName = "default" private var defaultImageGrid: ImageGrid { .init(columns: 40, capPlacements: database.caps.keys.sorted()) } var imageSize: CGFloat { CapsApp.thumbnailImageSize } private let verticalInsetFactor: CGFloat = cos(.pi / 6) private let minScale: CGFloat = 1.0 private let maxScale: CGFloat = 0.5 private let cancelButtonSize: CGFloat = 75 private let cancelIconSize: CGFloat = 25 @Binding var isPresented: Bool var image: ImageGrid @State var scale: CGFloat = 1.0 @State var lastScaleValue: CGFloat = 1.0 init(isPresented: Binding, database: Database) { self._isPresented = isPresented self.database = database self.image = .init(columns: 1, capPlacements: []) if let image = database.load(grid: currentGridName) { self.image = image } else { self.image = defaultImageGrid currentGridName = "default" } } var columnCount: Int { image.columns } var capCount: Int { image.capCount } var imageHeight: CGFloat { (CGFloat(capCount) / CGFloat(columnCount)).rounded(.up) * verticalInsetFactor * imageSize + (1-verticalInsetFactor) * imageSize } var imageWidth: CGFloat { imageSize * (CGFloat(columnCount) + 0.5) } var magnificationGesture: some Gesture { MagnificationGesture() .onChanged { val in let delta = val / self.lastScaleValue self.lastScaleValue = val self.scale = max(min(self.scale * delta, minScale), maxScale) } .onEnded { val in // without this the next gesture will be broken self.lastScaleValue = 1.0 } } var gridView: some View { let gridItems = Array(repeating: GridItem(.fixed(imageSize), spacing: 0), count: columnCount) return LazyVGrid(columns: gridItems, alignment: .leading, spacing: 0) { ForEach(image.items) { item in CachedCapImage( item.id, check: { cachedImage(item.cap) }, fetch: { await fetchImage(item.cap) }, content: { $0.resizable() }, placeholder: { ProgressView() }) .frame(width: imageSize, height: imageSize) .clipShape(Circle()) .offset(x: isEvenRow(item.id) ? 0 : imageSize / 2) .frame(width: imageSize, height: imageSize * verticalInsetFactor) } } .frame(width: imageWidth) } var body: some View { ZStack { ScrollView([.vertical, .horizontal]) { gridView .scaleEffect(scale) .frame( width: imageWidth * scale, height: imageHeight * scale ) } .gesture(magnificationGesture) .onDisappear { database.save(image, named: currentGridName) } VStack { Spacer() HStack { Spacer() IconButton(action: saveScreenshot, icon: .squareAndArrowDown, iconSize: cancelIconSize, buttonSize: cancelButtonSize) .padding() IconButton(action: dismiss, icon: .xmark, iconSize: cancelIconSize, buttonSize: cancelButtonSize) .padding() } } } } func isEvenRow(_ idx: Int) -> Bool { (idx / columnCount) % 2 == 0 } private func cachedImage(_ cap: Int) -> UIImage? { let image = CapImage( cap: cap, version: database .mainImage(for: cap)) return database.images.cachedImage(image) } private func fetchImage(_ cap: Int) async -> UIImage? { let image = CapImage( cap: cap, version: database .mainImage(for: cap)) return await database.images.thumbnail(for: image) } private func dismiss() { isPresented = false } private func saveScreenshot() { let image = gridView.snapshot() UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) } } struct GridView_Previews: PreviewProvider { static var previews: some View { GridView(isPresented: .constant(true), database: .largeMock) } }