~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain-ios

1 collaborator
0
1
main
123 lines · 4.2 KB Raw History
import SwiftUI

/// Lightweight display model shared by `ExifInfoView`.
/// Both `GrainExif` (feed) and `ExifSummary` (create flow) map to this
/// via their `displayData` computed properties defined below.
struct ExifDisplayData {
    var camera: String?
    var lens: String?
    var focalLength: String?
    var fNumber: String?
    var exposureTime: String?
    var iso: String?
}

extension GrainExif {
    var displayData: ExifDisplayData {
        ExifDisplayData(
            camera: cameraName,
            lens: lensName,
            focalLength: formattedFocalLength,
            fNumber: formattedFNumber,
            exposureTime: formattedExposureTime,
            iso: iSO.map { "ISO \($0)" }
        )
    }
}

extension ExifSummary {
    var displayData: ExifDisplayData {
        ExifDisplayData(
            camera: camera,
            lens: lens,
            focalLength: focalLength,
            fNumber: aperture,
            exposureTime: shutterSpeed,
            iso: iso
        )
    }
}

struct ExifInfoView: View {
    let exif: ExifDisplayData?
    /// Always reserve layout space for the camera row even when the current exif is nil.
    /// Set true when any photo in the gallery has a camera name.
    var reserveCameraRow: Bool = false
    /// Always reserve layout space for the lens row even when the current exif is nil.
    var reserveLensRow: Bool = false
    /// Foreground style for all text and icons.
    var style: AnyShapeStyle = .init(.secondary)

    var body: some View {
        let showCamera = reserveCameraRow || exif?.camera != nil
        let showLens = reserveLensRow || exif?.lens != nil
        if showCamera || showLens || exif != nil {
            VStack(alignment: .leading, spacing: 4) {
                if showCamera {
                    HStack(spacing: 6) {
                        Image(systemName: "camera").font(.caption2)
                        Text(exif?.camera ?? " ").font(.caption)
                    }
                    .foregroundStyle(style)
                    .opacity(exif?.camera != nil ? 1 : 0)
                }
                if showLens {
                    HStack(spacing: 6) {
                        Image(systemName: "circle.circle").font(.caption2)
                        Text(exif?.lens ?? " ").font(.caption)
                    }
                    .foregroundStyle(style)
                    .opacity(exif?.lens != nil ? 1 : 0)
                }
                let settingsTokens = [exif?.focalLength, exif?.fNumber, exif?.exposureTime, exif?.iso]
                if settingsTokens.contains(where: { $0 != nil }) {
                    ExifSettingsRow(
                        tokens: settingsTokens,
                        style: style
                    )
                }
            }
            .opacity(exif != nil ? 1 : 0)
            .animation(.easeInOut(duration: 0.2), value: exif == nil)
        }
    }
}

struct ExifSettingsRow: View {
    let tokens: [String?]
    var style: AnyShapeStyle = .init(.secondary)

    var body: some View {
        ZStack(alignment: .leading) {
            Text(" ").hidden() // holds caption line height when all tokens are nil
            HStack(spacing: 6) {
                ForEach(Array(tokens.enumerated()), id: \.offset) { _, value in
                    if let value {
                        Text(value.digitWidthProxy)
                            .hidden()
                            .overlay(Text(value).contentTransition(.identity))
                            // Zero-duration overrides the parent .smooth context for this
                            // view's appearance/disappearance — snaps in/out instantly while
                            // sibling positions still shift smoothly via the HStack animation.
                            .transition(.opacity.animation(.linear(duration: 0)))
                    }
                }
            }
            .animation(.smooth, value: tokens)
        }
        .font(.caption)
        .foregroundStyle(style)
    }
}

#Preview {
    ExifInfoView(exif: ExifDisplayData(
        camera: "Leica M11",
        lens: "Summilux-M 35mm f/1.4",
        focalLength: "35mm",
        fNumber: "f/2",
        exposureTime: "1/500",
        iso: "ISO 200"
    ))
    .padding()
}