Add grid, camera focus

This commit is contained in:
Christoph Hagen
2022-06-21 19:38:51 +02:00
parent 2b3ab859fc
commit 4b91ebcd02
21 changed files with 895 additions and 104 deletions

View File

@@ -0,0 +1,79 @@
import SwiftUI
struct CachedCapImage<Content, T>: View where Content: View, T: Equatable {
@State private var phase: AsyncImagePhase
let id: T
let check: () -> UIImage?
let fetch: () async -> UIImage?
private let transaction: Transaction
private let content: (AsyncImagePhase) -> Content
var body: some View {
content(phase)
.task(id: id, load)
}
init<I, P>(_ id: T, _ image: CapImage, cache: ImageCache, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
self.init(id, image: image, cache: cache) { phase in
if let image = phase.image {
content(image)
} else {
placeholder()
}
}
}
init(_ id: T, image: CapImage, cache: ImageCache, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
self.init(id,
check: { cache.cachedImage(image) },
fetch: { await cache.image(image) },
transaction: transaction,
content: content)
}
init<I, P>(_ id: T, check: @escaping () -> UIImage?, fetch: @escaping () async -> UIImage?, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
self.init(id, check: check, fetch: fetch) { phase in
if let image = phase.image {
content(image)
} else {
placeholder()
}
}
}
init(_ id: T, check: @escaping () -> UIImage?, fetch: @escaping () async -> UIImage?, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
self.id = id
self.check = check
self.fetch = fetch
self.transaction = transaction
self.content = content
self._phase = State(wrappedValue: .empty)
guard let image = check() else {
return
}
let wrapped = Image(uiImage: image)
self._phase = State(wrappedValue: .success(wrapped))
}
@Sendable
private func load() async {
guard let image = await fetch() else {
withAnimation(transaction.animation) {
phase = .empty
}
return
}
let wrapped = Image(uiImage: image)
withAnimation(transaction.animation) {
phase = .success(wrapped)
}
}
}

View File

@@ -1,5 +1,4 @@
import SwiftUI
import CachedAsyncImage
struct CapRowView: View {
@@ -56,7 +55,7 @@ struct CapRowView: View {
}
}//.padding(.vertical)
Spacer()
CachedAsyncImage(url: imageUrl, urlCache: database.imageCache) { image in
CachedCapImage(cap, cap.image, cache: database.images) { image in
image.resizable()
} placeholder: {
ProgressView()

View File

@@ -1,13 +1,171 @@
import SwiftUI
import SFSafeSymbols
struct GridView: View {
@EnvironmentObject
var 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<Bool>) {
self._isPresented = isPresented
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 {
Text("Grid 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()
GridView(isPresented: .constant(true))
.environmentObject(Database.largeMock)
}
}

View File

@@ -0,0 +1,41 @@
import SwiftUI
import SFSafeSymbols
struct IconButton: View {
let action: () -> Void
let icon: SFSymbol
let iconSize: CGFloat
let buttonSize: CGFloat
private var padding: CGFloat {
(buttonSize - iconSize) / 2
}
private var cornerRadius: CGFloat {
buttonSize / 2
}
var body: some View {
Button(action: action) {
Image(systemSymbol: icon)
.resizable()
.frame(width: iconSize, height: iconSize)
.padding(padding)
.background(.thinMaterial)
.cornerRadius(cornerRadius)
}
}
}
struct IconButton_Previews: PreviewProvider {
static var previews: some View {
IconButton(action: { },
icon: .xmark,
iconSize: 20,
buttonSize: 25)
}
}