Skip to content

Commit

Permalink
Add expense attachment
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-amisha-i committed Nov 25, 2024
1 parent ce8c6f8 commit ab9f9b1
Show file tree
Hide file tree
Showing 38 changed files with 924 additions and 205 deletions.
4 changes: 4 additions & 0 deletions BaseStyle/BaseStyle.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
D8E244C12B986CD800C6C82A /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E244C02B986CD800C6C82A /* ImagePickerView.swift */; };
D8E244C32B986D4F00C6C82A /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E244C22B986D4F00C6C82A /* UIImage+Extension.swift */; };
D8EB0ED82CAD8C9F00AC6A44 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8EB0ED72CAD8C9F00AC6A44 /* ErrorView.swift */; };
D8FFD5C72CF44DBC009A0667 /* ZoomableImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FFD5C62CF44DBC009A0667 /* ZoomableImageView.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -120,6 +121,7 @@
D8E244C02B986CD800C6C82A /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = "<group>"; };
D8E244C22B986D4F00C6C82A /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = "<group>"; };
D8EB0ED72CAD8C9F00AC6A44 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
D8FFD5C62CF44DBC009A0667 /* ZoomableImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableImageView.swift; sourceTree = "<group>"; };
E0B1A6930B9FF35E9142463B /* Pods-BaseStyle.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-BaseStyle.debug.xcconfig"; path = "Target Support Files/Pods-BaseStyle/Pods-BaseStyle.debug.xcconfig"; sourceTree = "<group>"; };
E4AFAF996FB5C233D40D81D5 /* Pods_BaseStyleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_BaseStyleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -272,6 +274,7 @@
21BEF8A42C637E4900FBC9CF /* NavigationBarTopView.swift */,
21D614782CAD527D00779F1E /* NavigationTitleTextView.swift */,
219F43D82CCA4A7000729C67 /* RestoreButton.swift */,
D8FFD5C62CF44DBC009A0667 /* ZoomableImageView.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -503,6 +506,7 @@
D82174BE2BBAD86D00DB42C3 /* ProfileImageView.swift in Sources */,
213F377E2C416C9C00972316 /* ScrollToTopButton.swift in Sources */,
D89C933F2BC3C0F800FACD16 /* ForwardIcon.swift in Sources */,
D8FFD5C72CF44DBC009A0667 /* ZoomableImageView.swift in Sources */,
D89DBE352B88A05F00E5F1BD /* UIApplication+Extension.swift in Sources */,
D8D42A772B85CE2A009B345D /* ButtonStyleTapGestureModifier.swift in Sources */,
21BEF8A52C637E4900FBC9CF /* NavigationBarTopView.swift in Sources */,
Expand Down
217 changes: 217 additions & 0 deletions BaseStyle/BaseStyle/Views/ZoomableImageView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
//
// ZoomableImageView.swift
// Splito
//
// Created by Amisha Italiya on 25/11/24.
//

import SwiftUI
import Kingfisher

// MARK: - ExpenseImageView

public struct ExpenseImageView: View {

@Binding var showImageDisplayView: Bool

var image: UIImage?
var imageUrl: String?

@Namespace private var animationNamespace

public var body: some View {
ZStack {
if let image {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
} else if let imageUrl, let url = URL(string: imageUrl) {
KFImage(url)
.placeholder { _ in
ImageLoaderView()
}
.setProcessor(DownsamplingImageProcessor(size: UIScreen.main.bounds.size)) // Downsample to fit screen size
.cacheMemoryOnly()
.resizable()
.aspectRatio(contentMode: .fill)
}
}
.matchedGeometryEffect(id: "image", in: animationNamespace)
.onTapGestureForced {
showImageDisplayView = true
}
}
}

// MARK: - ExpenseImageZoomView

public struct ExpenseImageZoomView: View {
@Environment(\.dismiss) var dismiss

var image: UIImage?
var imageUrl: String?

@Namespace var animationNamespace

public var body: some View {
GeometryReader { geometry in
ZStack {
if #available(iOS 18.0, *) {
ZoomableImageView(image: image, imageUrl: imageUrl, geometry: geometry)
.matchedGeometryEffect(id: "image", in: animationNamespace)
.navigationTransition(.zoom(sourceID: "zoom", in: animationNamespace))
} else {
ZoomableImageView(image: image, imageUrl: imageUrl, geometry: geometry)
}
}
}
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 18))
.foregroundStyle(disableText)
}
}
}
}
}

// MARK: - ZoomableImageView

private struct ZoomableImageView: View {

var image: UIImage?
var imageUrl: String?
let geometry: GeometryProxy

@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 loadedImage: UIImage = UIImage()

// MagnificationGesture for zooming (pinch-to-zoom)
private var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { gesture in
scaleAnchor = .center // Keep the zoom centered
scale = lastScale * gesture
}
.onEnded { _ in
fixOffsetAndScale(geometry: geometry)
}
}

// DragGesture for panning (drag-to-move)
private var dragGesture: some Gesture {
DragGesture()
.onChanged { gesture in
var newOffset = lastOffset
newOffset.width += gesture.translation.width
newOffset.height += gesture.translation.height
offset = newOffset
}
.onEnded { _ in
fixOffsetAndScale(geometry: geometry)
}
}

public init(image: UIImage? = nil, imageUrl: String? = nil, geometry: GeometryProxy) {
self.image = image
self.imageUrl = imageUrl
self.geometry = geometry
if let image {
self._loadedImage = State(initialValue: image)
}
}

var body: some View {
ZStack {
if let image {
Image(uiImage: 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()
}))
} else if let imageUrl, let url = URL(string: imageUrl) {
KFImage(url)
.placeholder { _ in
ImageLoaderView()
}
.setProcessor(DownsamplingImageProcessor(size: UIScreen.main.bounds.size))
.cacheMemoryOnly()
.onSuccess { result in
loadedImage = result.image
}
.resizable()
.scaledToFit()
.position(x: geometry.size.width / 2, y: geometry.size.height / 2) // Center the image in the available space
.scaleEffect(scale, anchor: scaleAnchor) // Apply zoom scale effect
.offset(offset) // Apply pan offset
.animation(.spring(), value: offset) // Animate the offset change with a spring animation
.animation(.spring(), value: scale) // Animate the scale change with a spring animation
.gesture(dragGesture) // Attach the drag gesture to allow panning
.gesture(magnificationGesture) // Attach the magnification gesture for zooming
.simultaneousGesture(TapGesture(count: 2).onEnded({ _ in
resetZoom()
}))
}
}
}

private func resetZoom() {
scale = lastScale > 1 ? 1 : 3 // Toggle between reset scale (1) and zoom-in scale (3)
offset = .zero // Reset the offset to center the image
scaleAnchor = .center // Keep zooming centered
lastScale = scale // Store the new scale as the last scale
lastOffset = .zero // Reset the offset to zero
}

// Adjust the offset and scale to ensure the image stays within bounds
private func fixOffsetAndScale(geometry: GeometryProxy) {
let newScale: CGFloat = .minimum(.maximum(scale, 1), 4) // Ensure the scale is between 1x and 4x
let screenSize = geometry.size

// Determine the original scale based on the aspect ratio of the image
let originalScale = loadedImage.size.width / loadedImage.size.height >= screenSize.width / screenSize.height ?
geometry.size.width / loadedImage.size.width :
geometry.size.height / loadedImage.size.height

let imageWidth = (loadedImage.size.width * originalScale) * newScale
let imageHeight = (loadedImage.size.height * originalScale) * newScale
var width: CGFloat = .zero
var height: CGFloat = .zero

if imageWidth > screenSize.width {
let widthLimit: CGFloat = imageWidth > screenSize.width ? (imageWidth - screenSize.width) / 2 : 0
width = offset.width > 0 ? .minimum(widthLimit, offset.width) : .maximum(-widthLimit, offset.width)
}

if imageHeight > screenSize.height {
let heightLimit: CGFloat = imageHeight > screenSize.height ? (imageHeight - screenSize.height) / 2 : 0
height = offset.height > 0 ? .minimum(heightLimit, offset.height) : .maximum(-heightLimit, offset.height)
}

let newOffset = CGSize(width: width, height: height)

lastScale = newScale
lastOffset = newOffset
offset = newOffset
scale = newScale
}
}
14 changes: 14 additions & 0 deletions Data/Data/Extension/Date+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ public extension Date {
return dateFormatter.string(from: self)
}

// 13 Dec
var shortDate: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMM"
return dateFormatter.string(from: self)
}

// December 2024
var monthWithYear: String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MMMM yyyy"
return dateFormatter.string(from: self)
}

var millisecondsSince1970: Int {
Int((self.timeIntervalSince1970 * 1000.0).rounded())
}
Expand Down
21 changes: 11 additions & 10 deletions Data/Data/Extension/Double+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,27 @@
import Foundation

public extension Double {
func formattedCurrency(removeMinusSign: Bool = true) -> String {
var formattedCurrency: String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale.current

if let formattedAmount = formatter.string(from: NSNumber(value: self)) {
if removeMinusSign && formattedAmount.hasPrefix("-") {
return String(formattedAmount.dropFirst())
}
return formattedAmount
return formattedAmount.hasPrefix("-") ? String(formattedAmount.dropFirst()) : formattedAmount
} else {
return String(format: "%.2f", self.rounded()) // Fallback to a basic decimal format
}
}

var formattedCurrency: String {
return formattedCurrency(removeMinusSign: true)
}

var formattedCurrencyWithSign: String {
return formattedCurrency(removeMinusSign: false)
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale.current

if let formattedAmount = formatter.string(from: NSNumber(value: self)) {
return formattedAmount
} else {
return String(format: "%.2f", self.rounded()) // Fallback to a basic decimal format
}
}
}
3 changes: 3 additions & 0 deletions Data/Data/Helper/Firebase/StorageManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ public class StorageManager: ObservableObject {
public enum ImageStoreType {
case user
case group
case expense

var pathName: String {
switch self {
case .user:
"user_images"
case .group:
"group_images"
case .expense:
"expense_images"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion Data/Data/Model/ActivityLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// ActivityLog.swift
// Data
//
// Created by Nirali Sonani on 14/10/24.
// Created by Amisha Italiya on 14/10/24.
//

import FirebaseFirestore
Expand Down
5 changes: 4 additions & 1 deletion Data/Data/Model/Expense.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ public struct Expense: Codable, Hashable, Identifiable {
public var paidBy: [String: Double]
public let addedBy: String
public var updatedBy: String
public var imageUrl: String?
public var splitTo: [String] // Reference to user ids involved in the split
public var splitType: SplitType
public var splitData: [String: Double]? // Use this to store percentage or share data
public var isActive: Bool

public init(name: String, amount: Double, date: Timestamp, paidBy: [String: Double], addedBy: String,
updatedBy: String, splitTo: [String], splitType: SplitType = .equally,
updatedBy: String, imageUrl: String? = nil, splitTo: [String], splitType: SplitType = .equally,
splitData: [String: Double]? = [:], isActive: Bool = true) {
self.name = name
self.amount = amount
self.date = date
self.paidBy = paidBy
self.addedBy = addedBy
self.updatedBy = updatedBy
self.imageUrl = imageUrl
self.splitTo = splitTo
self.splitType = splitType
self.splitData = splitData
Expand All @@ -45,6 +47,7 @@ public struct Expense: Codable, Hashable, Identifiable {
case paidBy = "paid_by"
case addedBy = "added_by"
case updatedBy = "updated_by"
case imageUrl = "image_url"
case splitTo = "split_to"
case splitType = "split_type"
case splitData = "split_data"
Expand Down
2 changes: 1 addition & 1 deletion Data/Data/Repository/ActivityLogRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// ActivityLogRepository.swift
// Data
//
// Created by Nirali Sonani on 14/10/24.
// Created by Amisha Italiya on 14/10/24.
//

import FirebaseFirestore
Expand Down
Loading

0 comments on commit ab9f9b1

Please sign in to comment.