Skip to content

Commit

Permalink
Added zoom navigation transition
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-nirali-s committed Nov 22, 2024
1 parent 4c4655d commit 52635fb
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 179 deletions.
230 changes: 76 additions & 154 deletions Splito/UI/Home/Expense/AddExpenseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ struct AddExpenseView: View {
.scrollIndicators(.hidden)
.scrollBounceBehavior(.basedOnSize)

BottomCardView(date: $viewModel.expenseDate, expenseImage: viewModel.expenseImage,
expenseImageUrl: viewModel.expenseImageUrl, handleExpenseImageTap: viewModel.handleExpenseImageTap)
BottomCardView(date: $viewModel.expenseDate, showImagePickerOptions: $viewModel.showImagePickerOptions,
expenseImage: viewModel.expenseImage, expenseImageUrl: viewModel.expenseImageUrl,
handleExpenseImageTap: viewModel.handleExpenseImageTap,
handleActionSelection: viewModel.handleActionSelection(_:))
}
}
.background(surfaceColor)
Expand Down Expand Up @@ -78,9 +80,6 @@ struct AddExpenseView: View {
sourceType: !viewModel.sourceTypeIsCamera ? .photoLibrary : .camera,
image: $viewModel.expenseImage, isPresented: $viewModel.showImagePicker)
}
.confirmationDialog("", isPresented: $viewModel.showImagePickerOptions, titleVisibility: .hidden) {
ImagePickerOptionsView(image: viewModel.expenseImage, imageUrl: viewModel.expenseImageUrl, handleActionSelection: viewModel.handleActionSelection(_:))
}
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Cancel") {
Expand Down Expand Up @@ -205,11 +204,13 @@ private struct ExpenseDetailRow: View {
private struct BottomCardView: View {

@Binding var date: Date
@Binding var showImagePickerOptions: Bool

let expenseImage: UIImage?
let expenseImageUrl: String?

let handleExpenseImageTap: (() -> Void)
let handleActionSelection: ((ActionsOfSheet) -> Void)

var body: some View {
Divider()
Expand All @@ -219,137 +220,65 @@ private struct BottomCardView: View {
HStack(spacing: 16) {
Spacer()

DatePickerView(date: $date)
DatePickerView(date: $date, isForAddExpense: true)

ExpenseImagePickerView(image: expenseImage, imageUrl: expenseImageUrl, handleImageBtnTap: handleExpenseImageTap)
}
.padding(.vertical, 12)
.padding(.horizontal, 16)
.confirmationDialog("", isPresented: $showImagePickerOptions, titleVisibility: .hidden) {
ImagePickerOptionsView(image: expenseImage, imageUrl: expenseImageUrl, handleActionSelection: handleActionSelection)
}
}
}

struct DatePickerView: View {

@Binding var date: Date

var isForAddExpense: Bool
private struct ExpenseImagePickerView: View {

private let maximumDate = Calendar.current.date(byAdding: .year, value: 0, to: Date()) ?? Date()
let image: UIImage?
let imageUrl: String?

@State private var tempDate: Date
@State private var showDatePicker = false
let handleImageBtnTap: (() -> Void)

init(date: Binding<Date>, isForAddExpense: Bool = true) {
self._date = date
self.isForAddExpense = isForAddExpense
self._tempDate = State(initialValue: date.wrappedValue)
}
@State private var showZoomableImageView = false

var body: some View {
HStack {
if !isForAddExpense {
Text(date.longDate)
.font(.subTitle2())
.foregroundStyle(primaryText)
.frame(maxWidth: .infinity, alignment: .leading)
} else {
DateDisplayView(date: $date)
}
}
.onTapGestureForced {
tempDate = date
showDatePicker = true
UIApplication.shared.endEditing()
}
.sheet(isPresented: $showDatePicker) {
VStack(spacing: 0) {
NavigationBarTopView(title: "Choose date", leadingButton: EmptyView(),
trailingButton: DismissButton(padding: (16, 0), foregroundColor: primaryText, onDismissAction: {
showDatePicker = false
})
.fontWeight(.regular)
)
.padding(.leading, 16)

ScrollView {
DatePicker("", selection: $tempDate, in: ...maximumDate, displayedComponents: .date)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
.padding(24)
.id(tempDate)
}
.scrollIndicators(.hidden)

Spacer()

PrimaryButton(text: "Done") {
date = tempDate
showDatePicker = false
}
.padding(16)
HStack(spacing: 0) {
if image != nil || imageUrl != nil {
ExpenseImageView(showZoomableImageView: $showZoomableImageView, image: image, imageUrl: imageUrl)
.frame(width: 24, height: 24)
.cornerRadius(4)
.padding(.leading, 8)
.padding(.vertical, 4)
}
.background(surfaceColor)
}
}
}

private struct DateDisplayView: View {

@Binding var date: Date

var body: some View {
HStack(spacing: 8) {
Text(date.isToday() ? "Today" : date.shortDate)
.font(.subTitle2())
.foregroundStyle(primaryText)

Image(.calendarIcon)
Image(.cameraIcon)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.onTouchGesture {
handleImageBtnTap()
}
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(container2Color)
.cornerRadius(8)
}
}

private struct ExpenseImagePickerView: View {

let image: UIImage?
let imageUrl: String?

let handleImageBtnTap: (() -> Void)

var body: some View {
Button(action: handleImageBtnTap) {
HStack(spacing: 8) {
if image != nil || imageUrl != nil {
ExpenseImageView(image: image, imageUrl: imageUrl)
.frame(width: 24, height: 24)
.cornerRadius(4)
}

Image(.cameraIcon)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.navigationDestination(isPresented: $showZoomableImageView) {
if let imageUrl {
ZoomableImageView(imageUrl: imageUrl, animationNamespace: Namespace())
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(container2Color)
.cornerRadius(8)
}
}
}

struct ExpenseImageView: View {

@Binding var showZoomableImageView: Bool

var image: UIImage?
var imageUrl: String?

@State var showZoomedImageView: Bool = false
@Namespace private var animationNamespace

var body: some View {
ZStack {
Expand All @@ -359,37 +288,33 @@ struct ExpenseImageView: View {
.aspectRatio(contentMode: .fill)
} else if let imageUrl, let url = URL(string: imageUrl) {
KFImage(url)
.placeholder({ _ in
.placeholder { _ in
ImageLoaderView()
})
}
.resizable()
.aspectRatio(contentMode: .fill)
}
}
.onTapGesture {
showZoomedImageView = true
}
.fullScreenCover(isPresented: $showZoomedImageView) {
if let imageUrl {
ZoomedImageView(showZoomedImageView: $showZoomedImageView, imageUrl: imageUrl)
}
.matchedGeometryEffect(id: "image", in: animationNamespace)
.onTapGestureForced {
showZoomableImageView = true
}
}
}

private struct ZoomedImageView: View {

@Binding var showZoomedImageView: Bool
struct ZoomableImageView: View {

let imageUrl: String

@Namespace var animationNamespace

@State var scale: CGFloat = 1
@State var scaleAnchor: UnitPoint = .center
@State var lastScale: CGFloat = 1
@State var offset: CGSize = .zero
@State var lastOffset: CGSize = .zero

@State var image: UIImage = UIImage()
@State private var image: UIImage = UIImage()

var body: some View {
GeometryReader { geometry in
Expand All @@ -413,46 +338,43 @@ private struct ZoomedImageView: View {
fixOffsetAndScale(geometry: geometry)
}

let imageView = KFImage(URL(string: imageUrl))
.onSuccess { result in
image = result.image
}
.resizable()
.scaledToFit()
.position(x: geometry.size.width / 2,
y: geometry.size.height / 2)
.scaleEffect(scale, anchor: scaleAnchor)
.offset(offset)
.animation(.spring(), value: offset)
.animation(.spring(), value: scale)
.gesture(dragGesture)
.gesture(magnificationGesture)
.simultaneousGesture(TapGesture(count: 2).onEnded({ _ in
resetZoom()
}))

ZStack {
KFImage(URL(string: imageUrl))
.onSuccess({ result in
image = result.image
})
.resizable()
.scaledToFit()
.position(x: geometry.size.width / 2,
y: geometry.size.height / 2)
.scaleEffect(scale, anchor: scaleAnchor)
.offset(offset)
.animation(.spring(), value: offset)
.animation(.spring(), value: scale)
.gesture(dragGesture)
.gesture(magnificationGesture)
.simultaneousGesture(TapGesture(count: 2).onEnded({ _ in
scale = lastScale > 1 ? 1 : 3
offset = .zero
scaleAnchor = .center
lastScale = scale
lastOffset = .zero
}))

VStack {
HStack {
Spacer()
Button {
showZoomedImageView = false
} label: {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(primaryText)
.padding(16)
}
}
Spacer()
if #available(iOS 18.0, *) {
imageView
.matchedGeometryEffect(id: "image", in: animationNamespace)
.navigationTransition(.zoom(sourceID: "zoom", in: animationNamespace))
} else {
imageView
}
}
}
.toolbarRole(.editor)
}

private func resetZoom() {
scale = lastScale > 1 ? 1 : 3
offset = .zero
scaleAnchor = .center
lastScale = scale
lastOffset = .zero
}

private func fixOffsetAndScale(geometry: GeometryProxy) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Data
struct ExpenseDetailsView: View {

@StateObject var viewModel: ExpenseDetailsViewModel
@State private var showZoomableImageView = false

var body: some View {
VStack(alignment: .leading, spacing: 0) {
Expand All @@ -27,7 +28,7 @@ struct ExpenseDetailsView: View {
ExpenseInfoView(viewModel: viewModel)

if let imageUrl = viewModel.expense?.imageUrl {
ExpenseImageView(imageUrl: imageUrl)
ExpenseImageView(showZoomableImageView: $showZoomableImageView, imageUrl: imageUrl)
.aspectRatio(16/9, contentMode: .fit)
.cornerRadius(12)
}
Expand Down Expand Up @@ -70,6 +71,11 @@ struct ExpenseDetailsView: View {
}
}
}
.navigationDestination(isPresented: $showZoomableImageView) {
if let imageUrl = viewModel.expense?.imageUrl {
ZoomableImageView(imageUrl: imageUrl, animationNamespace: Namespace())
}
}
}
}

Expand Down
Loading

0 comments on commit 52635fb

Please sign in to comment.