Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions Rewind/Model/ComparisonModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct ComparisonState {
var orientation: Orientation
var alert: Identified<AlertParams>?
var shareVC: Identified<UIViewController>?
var streetViewAvailability: StreetViewAvailability?

var currentLens: Lens?
var availableLens: [Lens] {
Expand Down Expand Up @@ -84,6 +85,7 @@ enum ComparisonAction {
case imageTaken(UIImage)
case orientationChanged(Orientation)
case shareSheetLoaded(UIViewController)
case streetViewAvailabilityLoaded(StreetViewAvailability)
case setupCapture
}

Expand All @@ -95,7 +97,7 @@ func makeComparisonViewDeps(
captureMode: ComparisonState.CaptureMode,
oldUIImage: UIImage,
oldImageData: Model.Image,
streetViewAvailability: Remote<Void, Bool>
streetViewAvailability: Remote<Void, StreetViewAvailability>
) -> ComparisonViewDeps {
let orientationTracker = OrientationTracker()
weak var comparisonVC: UIViewController?
Expand Down Expand Up @@ -164,10 +166,8 @@ func makeComparisonViewDeps(
case .streetView:
enqueueEffect(.perform { anotherAction in
do {
let isAvailable = try await streetViewAvailability.load()
if !isAvailable {
await anotherAction(.external(.alert(.presentStreetViewUnavailable)))
}
let availability = try await streetViewAvailability.load()
await anotherAction(.internal(.streetViewAvailabilityLoaded(availability)))
} catch {
assertionFailure()
// we block user actions only if streetView is surely not available
Expand Down Expand Up @@ -287,6 +287,11 @@ func makeComparisonViewDeps(
enqueueEffect(.anotherAction(.external(.alert(.presentStreetViewError(error)))))
}
}
case let .streetViewAvailabilityLoaded(availability):
state.streetViewAvailability = availability
if case .unavailable = availability {
enqueueEffect(.anotherAction(.external(.alert(.presentStreetViewUnavailable))))
}
}
}
}
Expand All @@ -301,7 +306,8 @@ func makeComparisonViewDeps(
style: state.style,
oldImageData: state.oldImageData,
oldImage: state.oldUIImage,
captureState: state.captureState
captureState: state.captureState,
streetViewYear: state.streetViewAvailability?.year
)
}
)
Expand Down Expand Up @@ -340,3 +346,12 @@ extension ComparisonState.CaptureState? {
if case .viewfinder = self { true } else { false }
}
}

extension StreetViewAvailability {
fileprivate var year: Int? {
switch self {
case let .available(year): year
case .unavailable: nil
}
}
}
2 changes: 1 addition & 1 deletion Rewind/Model/ImageDetailsModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func makeImageDetailsModel(
canOpenURL: @escaping (URL) -> Bool,
urlOpener: @escaping (URL) -> Void,
setOrientationLock: @escaping ResultAction<OrientationLock?>,
streetViewAvailability: Remote<Coordinate, Bool>
streetViewAvailability: Remote<Coordinate, StreetViewAvailability>
) -> ImageDetailsModel {
Reducer(
initial: ImageDetailsState(
Expand Down
2 changes: 1 addition & 1 deletion Rewind/Model/RewindRemotes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
struct RewindRemotes {
var annotations: Remote<AnnotationLoadingParams, ([Model.Image], [Model.Cluster])>
var imageDetails: Remote<Int, Model.ImageDetails>
var streetViewAvailability: Remote<Coordinate, Bool>
var streetViewAvailability: Remote<Coordinate, StreetViewAvailability>
}

struct AnnotationLoadingParams {
Expand Down
5 changes: 5 additions & 0 deletions Rewind/Model/StreetViewFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import UIKit
import WebKit

enum StreetViewAvailability {
case available(year: Int)
case unavailable
}

func makeStreetView(
image: Model.Image
) throws -> WKWebView {
Expand Down
31 changes: 28 additions & 3 deletions Rewind/Network/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ extension Network.Request {
Network.image(path: path, quality: quality)
}

static func streetViewAvailability(coordinate: Coordinate) -> Network.Request<Bool> {
static func streetViewAvailability(coordinate: Coordinate) -> Network
.Request<StreetViewAvailability> {
Network.streetViewAvailability(coordinate: coordinate)
}
}
Expand Down Expand Up @@ -173,7 +174,8 @@ extension Network {
}

// https://developers.google.com/maps/documentation/streetview/metadata
fileprivate static func streetViewAvailability(coordinate: Coordinate) -> Request<Bool> {
fileprivate static func streetViewAvailability(coordinate: Coordinate)
-> Request<StreetViewAvailability> {
struct Response: Decodable {
enum Status: String, Decodable {
case ok = "OK"
Expand All @@ -186,6 +188,24 @@ extension Network {
}

let status: Status
let date: String?
}

func extractYear(date: String?) throws -> Int {
guard let date else { throw HandlingError("Date is missing") }
let s = date.trimmingCharacters(in: .whitespacesAndNewlines)
guard
let yearStr = s.split(
separator: "-",
maxSplits: 1,
omittingEmptySubsequences: true
).first,
yearStr.allSatisfy(\.isNumber),
let year = Int(yearStr)
else {
throw HandlingError("Invalid date format")
}
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The date parsing doesn't validate that the parsed year is reasonable for Google Street View imagery. According to the Google Street View Metadata API documentation, the date field contains imagery capture dates which typically range from 2007 onwards (when Street View was launched).

The current implementation would accept dates like "0001-01-01" or "9999-12-31" without any validation. Consider adding a sanity check to ensure the year falls within a reasonable range (e.g., between 2007 and current year + 1) to catch potentially malformed API responses or data corruption issues early.

Suggested change
}
}
let currentYear = Calendar.current.component(.year, from: Date())
let minYear = 2007
let maxYear = currentYear + 1
guard (minYear...maxYear).contains(year) else {
throw HandlingError("Unreasonable imagery year: \(year)")
}

Copilot uses AI. Check for mistakes.
return year
}

return Request(
Expand All @@ -208,7 +228,12 @@ extension Network {
},
parseResult: { data in
let response = try JSONDecoder().decode(Response.self, from: data)
return response.status == .ok
if response.status == .ok {
let year = try extractYear(date: response.date)
return .available(year: year)
} else {
return .unavailable
}
}
)
}
Expand Down
4 changes: 2 additions & 2 deletions Rewind/View/ComparisonScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ private let shutterButtonSize: CGFloat = 80
captureMode: .camera,
oldUIImage: .panorama,
oldImageData: .mock,
streetViewAvailability: .mock(true)
streetViewAvailability: .mock(.unavailable)
)

ComparisonScreen(deps: deps)
Expand All @@ -220,7 +220,7 @@ private let shutterButtonSize: CGFloat = 80
captureMode: .streetView,
oldUIImage: .panorama,
oldImageData: .mock,
streetViewAvailability: .mock(true)
streetViewAvailability: .mock(.available(year: 1826))
)

ComparisonScreen(deps: deps)
Expand Down
5 changes: 3 additions & 2 deletions Rewind/View/ComparisonView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct ComparisonView: View {
var oldImageData: Model.Image
var oldImage: UIImage
var captureState: ComparisonState.CaptureState?
var streetViewYear: Int?

@State
private var currentYear = Calendar.current.component(.year, from: .now)
Expand All @@ -21,14 +22,14 @@ struct ComparisonView: View {
case .sideBySide:
SideBySideView(
oldYear: oldImageData.date.year,
currentYear: currentYear,
currentYear: streetViewYear ?? currentYear,
old: { ScaleToFillImage(image: oldImage) },
new: { cameraPreview }
)
case .cardOnCard:
CardOnCardView(
oldYear: oldImageData.date.year,
currentYear: currentYear,
currentYear: streetViewYear ?? currentYear,
oldImageAspectRatio: oldImage.size.aspectRatio ?? 3 / 4,
old: { ScaleToFillImage(image: oldImage) },
new: { cameraPreview }
Expand Down
4 changes: 2 additions & 2 deletions Rewind/View/ImageDetailsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ extension SingleFavoriteModel {
canOpenURL: { _ in true },
urlOpener: { _ in },
setOrientationLock: { _ in },
streetViewAvailability: .mock(true)
streetViewAvailability: .mock(.unavailable)
).viewStore

ImageDetailsView(
Expand All @@ -375,7 +375,7 @@ extension SingleFavoriteModel {
canOpenURL: { _ in true },
urlOpener: { _ in },
setOrientationLock: { _ in },
streetViewAvailability: .mock(true)
streetViewAvailability: .mock(.unavailable)
).viewStore

ImageDetailsView(
Expand Down
2 changes: 1 addition & 1 deletion Rewind/View/ImageList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ private let imageDetailsFactoryMock: ImageDetailsFactory = { _, source in
canOpenURL: { _ in false },
urlOpener: { _ in },
setOrientationLock: { _ in },
streetViewAvailability: .mock(true)
streetViewAvailability: .mock(.unavailable)
)
}

Expand Down
Loading