-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add expense attachment support #73
base: main
Are you sure you want to change the base?
Conversation
* Temp commit * Minor changes * Added add expense cloud function * Added update expense notification cloud function * Minor changes * Added delete expense notification cloud function * Refactored expense cloud functions * Added add/update/delete transaction cloud function * Added delete group/remove member cloud function * Added update group cloud function * Added updatedBy in groups/expenses/transactions &update it * Refactored code * Managed notification tap * Refactored code * Refactored code * Managed notification with activity collection * Managed notification not shown when app killed * Seperated cloud functions file * Seperated function * Fixed scroll * Refactored code * Refactored code * Refactored code * Refactored code * Refactored code * Fixed pipeline * Refactored code * Temp commit * Redesign add expense & fixed date sheet issue * Redesign payment detail, add payment screen & refactored code * localized strings * Fix: not update group memberOweAmount & balance after adding expense from group list * Fix pull to refresh not smooth & added scrollToTop btn * Implement push notifications for each activity * Fix code signin * Added rupees symbol before amount * Added rupees symbol before amount * Fix: alert btn color & image picker menu text colour * Refactored code * Fix getting toast and alert both in record a payment * Minor improvement * Add parallel api call & bottom card amount sign changes in split option&multipayer screen * Minor improvement * Minor improvement * Updated cocoapods version * Fix ui * Update podfile.lock * Changed method name --------- Co-authored-by: Amisha <amisha.i@canopas.com>
WalkthroughThis pull request introduces several enhancements across multiple files, primarily focusing on date handling and expense management. New computed properties for date formatting are added to the Changes
Possibly related PRs
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
🧹 Outside diff range and nitpick comments (20)
Data/Data/Extension/Double+Extension.swift (1)
Line range hint
1-32
: Consider adding currency formatting tests.Given that this extension is critical for expense management and the new attachment feature, it's important to ensure currency formatting remains consistent across different locales and edge cases.
Would you like me to help create comprehensive unit tests covering:
- Different locales and currency formats
- Positive and negative values
- Zero and large numbers
- Edge cases with attachments
Data/Data/Helper/Firebase/StorageManager.swift (1)
Line range hint
31-45
: Consider adding image validation and optimization.The current implementation could benefit from additional safeguards:
- Image size validation and limits
- Support for different image formats beyond JPG
- Image optimization before upload
- Cleanup mechanism for orphaned images
Example improvements for the
uploadImage
method:public func uploadImage(for storeType: ImageStoreType, id: String, imageData: Data) async throws -> String? { + guard imageData.count <= 5 * 1024 * 1024 else { // 5MB limit + throw StorageError.imageTooLarge + } + + // Detect image type and set appropriate content type + let imageType = try detectImageType(from: imageData) + let metadata = StorageMetadata() + metadata.contentType = imageType.mimeType + + // Optimize image if needed + let optimizedData = try await optimizeImageIfNeeded(imageData, maxSize: 1024) let storageRef = storage.reference(withPath: "/\(storeType.pathName)/\(id)") - let metadata = StorageMetadata() - metadata.contentType = "image/jpg"Data/Data/Model/Expense.swift (2)
20-20
: Add documentation for the new imageUrl propertyConsider adding a documentation comment explaining:
- Expected image URL format
- Any size/type limitations
- Whether it supports local or remote URLs
Example:
/// URL to the expense's attached image. /// Supports remote URLs to cloud storage locations. /// - Note: Images should follow the app's size and format guidelines public var imageUrl: String?
Missing test coverage for Expense Codable implementation
The codebase lacks test coverage for the Expense model's Codable implementation. No tests were found that verify:
- JSON encoding/decoding of Expense model
- Handling of optional imageUrl property
- Custom CodingKeys implementation
Action items:
- Add unit tests in
Data/DataTests/DataTests.swift
to verify Codable conformance- Include test cases for both presence and absence of imageUrl
- Verify correct mapping of snake_case keys to camelCase properties
🔗 Analysis chain
Line range hint
20-54
: Verify Codable implementation testingThe changes affect the Codable implementation. Please ensure test coverage for:
- Encoding/decoding with imageUrl present
- Encoding/decoding with imageUrl absent
Let me search for test files and Codable implementation tests specifically.
Let me check the content of the test files we found to look for any Expense model tests.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Search for relevant test files fd -e swift -e test expenseLength of output: 749
Script:
#!/bin/bash # Search for test files and test implementations fd -e swift test # Search for Expense model test cases rg -l "class.*Test.*Expense" # Look for Codable related test implementations rg -A 5 "func test.*Expense.*[Cc]odable|func test.*[Cc]odable.*Expense"Length of output: 309
Script:
#!/bin/bash # Check content of test files for any Expense related tests cat Data/DataTests/DataTests.swift cat SplitoTests/SplitoTests.swift # Search for any Codable conformance tests with different pattern rg -A 5 "XCTestCase.*Expense|test.*Expense" Data/DataTests/DataTests.swift SplitoTests/SplitoTests.swiftLength of output: 2609
Data/Data/Extension/Date+Extension.swift (1)
19-32
: Consider moving date formatting to the Presentation layerThe Data layer currently contains UI-specific date formatting logic. This might violate separation of concerns, as formatting for display typically belongs in the Presentation layer.
Consider:
- Moving these formatting properties to a UI/Presentation layer extension
- Keeping only data-essential date operations in the Data layer
- Creating a dedicated DateFormatter service in the Presentation layer
This would improve maintainability and better align with architectural boundaries.
Splito/UI/Home/ActivityLog/ActivityLogViewModel.swift (1)
Line range hint
65-72
: Consider enhancing error handling for attachment-related scenariosThe current error handling in
fetchMoreActivityLogs
is generic. With the addition of expense attachments, consider providing more specific error feedback for attachment-related issues (e.g., attachment size limits, format issues).Consider updating the error handling like this:
} catch { - viewState = .initial - showToastForError() + viewState = .initial + if let attachmentError = error as? AttachmentError { + showToastFor(attachmentError) + } else { + showToastForError() + } }Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift (2)
Line range hint
168-182
: Add amount validation and handle edge cases.The current implementation could benefit from additional validation:
- Maximum amount validation to prevent overflow
- Handling of negative numbers
- Decimal place limitations
Consider adding these validations to the
formatAmount
function:private func formatAmount(newValue: String) { let numericInput = newValue.replacingOccurrences(of: "₹", with: "").trimmingCharacters(in: .whitespaces) if let value = Double(numericInput) { + // Validate maximum amount + guard value <= 999999999 else { return } + // Ensure positive amount + guard value >= 0 else { return } + // Limit decimal places to 2 + let formatted = String(format: "%.2f", value) + guard let finalValue = Double(formatted) else { return } - amount = value + amount = finalValue } else { amount = 0 } amountString = numericInput.isEmpty ? "" : "₹ " + numericInput }
Line range hint
144-182
: Consider localization for currency formatting.The currency symbol "₹" is hardcoded. Consider using NumberFormatter for proper locale-aware currency formatting.
Here's a suggested approach:
struct AmountRowView: View { + private let currencyFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.locale = Locale(identifier: "en_IN") // Or use system locale + return formatter + }() // ... existing code ... private func formatAmount(newValue: String) { - let numericInput = newValue.replacingOccurrences(of: "₹", with: "").trimmingCharacters(in: .whitespaces) + let numericInput = newValue.replacingOccurrences(of: currencyFormatter.currencySymbol ?? "₹", with: "").trimmingCharacters(in: .whitespaces) if let value = Double(numericInput) { amount = value } else { amount = 0 } - amountString = numericInput.isEmpty ? "" : "₹ " + numericInput + amountString = numericInput.isEmpty ? "" : "\(currencyFormatter.currencySymbol ?? "₹") " + numericInput } }Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift (2)
29-33
: Consider enhancing image handling for better UX.The image display implementation looks good, but could benefit from additional user experience improvements.
Consider adding:
- Loading state while the image is being fetched
- Error handling for failed image loads
- Tap gesture to view the image in full screen
if let imageUrl = viewModel.expense?.imageUrl { ExpenseImageView(imageUrl: imageUrl) .aspectRatio(16/9, contentMode: .fit) .cornerRadius(12) + .onTapGesture { + // TODO: Show full screen image view + } }
29-33
: Consider implementing image caching and size optimization.To improve performance and reduce memory usage:
- Implement image caching to prevent unnecessary network requests
- Consider implementing image size optimization, especially for large images
- Add image compression before upload (in the related upload functionality)
This will help:
- Reduce network bandwidth usage
- Improve loading times for frequently viewed images
- Optimize memory usage when displaying multiple expenses with images
Splito/UI/Home/Groups/Group/GroupHomeViewModel.swift (3)
Line range hint
13-15
: Consider making EXPENSES_LIMIT configurable.The hardcoded
EXPENSES_LIMIT = 10
could be made more flexible by moving it to a configuration file or making it adjustable based on device capabilities and network conditions.- private let EXPENSES_LIMIT = 10 + private var expensesLimit: Int { + // Return different limits based on device/network conditions + return preference.isLowPowerMode ? 5 : 10 + }
Line range hint
232-239
: Optimize expense filtering performance.The filtering logic in
updateGroupExpenses
performs multiple operations that could be optimized:
- Multiple calls to
uniqued()
- Case-insensitive string comparison in a loop
- String to Double conversion attempt for each expense
Consider this optimization:
private func updateGroupExpenses() { - let filteredExpenses = expensesWithUser.uniqued().filter { expense in - searchedExpense.isEmpty || - expense.expense.name.lowercased().contains(searchedExpense.lowercased()) || - expense.expense.amount == Double(searchedExpense) - } - self.groupExpenses = Dictionary(grouping: filteredExpenses.uniqued().sorted { $0.expense.date.dateValue() > $1.expense.date.dateValue() }) { expense in - return expense.expense.date.dateValue().monthWithYear - } + let searchTerm = searchedExpense.lowercased() + let searchAmount = Double(searchedExpense) + + let uniqueExpenses = expensesWithUser.uniqued() + let filteredExpenses = searchedExpense.isEmpty ? uniqueExpenses : uniqueExpenses.filter { expense in + expense.expense.name.lowercased().contains(searchTerm) || + (searchAmount != nil && expense.expense.amount == searchAmount) + } + + self.groupExpenses = Dictionary(grouping: filteredExpenses.sorted { $0.expense.date.dateValue() > $1.expense.date.dateValue() }) { expense in + expense.expense.date.dateValue().monthWithYear + }
Line range hint
64-71
: Structure notification handling more robustly.The notification handling could be more maintainable by:
- Using a dedicated enum for notification names
- Centralizing notification registration
- Adding proper cleanup in deinit
Consider creating a dedicated notification handler:
private extension GroupHomeViewModel { enum GroupNotification: String { case addExpense = "addExpense" case updateExpense = "updateExpense" case deleteExpense = "deleteExpense" case addTransaction = "addTransaction" case updateTransaction = "updateTransaction" case deleteTransaction = "deleteTransaction" case updateGroup = "updateGroup" var name: Notification.Name { return Notification.Name(self.rawValue) } } func registerNotifications() { GroupNotification.allCases.forEach { notification in NotificationCenter.default.addObserver( self, selector: #selector(handleNotification(_:)), name: notification.name, object: nil ) } } @objc private func handleNotification(_ notification: Notification) { guard let notificationType = GroupNotification(rawValue: notification.name.rawValue) else { return } switch notificationType { case .addExpense: handleAddExpense(notification: notification) case .updateExpense: handleUpdateExpense(notification: notification) // ... handle other cases } } }Data/Data/Repository/GroupRepository.swift (2)
Line range hint
213-234
: Enhance bulk member fetching implementationThe current implementation has similar thread safety concerns as
fetchMemberBy
:
- Async barrier writes should be awaited
- Consider using a Set for more efficient duplicate checking
- Add structured error handling for the async operations
Here's a suggested implementation:
let missingMemberIds = memberIds.filter { memberId in let cachedMember = self.groupMembersQueue.sync { self.groupMembers.first { $0.id == memberId } } return cachedMember == nil } if missingMemberIds.isEmpty { return groupMembersQueue.sync { self.groupMembers.filter { memberIds.contains($0.id) } } } try await withThrowingTaskGroup(of: AppUser?.self) { groupTask in for memberId in missingMemberIds { groupTask.addTask { try await self.fetchMemberBy(userId: memberId) } } for try await member in groupTask { if let member { members.append(member) - groupMembersQueue.async(flags: .barrier) { - if !self.groupMembers.contains(where: { $0.id == member.id }) { - self.groupMembers.append(member) - } - } + // Use async/await with barrier to ensure write completion + try await groupMembersQueue.async(flags: .barrier) { + // Use a more efficient way to check duplicates + let memberIds = Set(self.groupMembers.map { $0.id }) + if !memberIds.contains(member.id) { + self.groupMembers.append(member) + } + } } } }
Line range hint
11-23
: Consider adding documentation for concurrent access patternsThe class has a good architecture with proper dependency injection and separation of concerns. However, it would benefit from documentation explaining:
- The thread safety approach for
groupMembers
- When and why barrier operations are used
- The caching strategy for group members
This will help maintainers understand the concurrent access patterns.
Data/Data/Repository/ExpenseRepository.swift (2)
68-73
: UseEquatable
conformance for expense comparisonThe
hasExpenseChanged
method manually compares each property of theExpense
object. This can be error-prone and hard to maintain as theExpense
structure evolves.Consider making
Expense
conform to theEquatable
protocol and using a simple comparison:private func hasExpenseChanged(_ expense: Expense, oldExpense: Expense) -> Bool { return oldExpense != expense }This approach simplifies the code and reduces the risk of missing property comparisons.
Line range hint
75-80
: Handle potential nilgroup.id
before updating expenseIn the
updateExpense
method, you guard against a nilgroupId
but don't provide feedback or throw an error if it's nil. This could lead to silent failures.Consider throwing an error or handling the nil case explicitly to ensure that the calling code is aware of the failure.
public func updateExpense(group: Groups, expense: Expense, oldExpense: Expense, type: ActivityType) async throws { guard let groupId = group.id else { + throw NSError(domain: "ExpenseRepository", code: 0, userInfo: [NSLocalizedDescriptionKey: "Group ID is nil"]) + } try await store.updateExpense(groupId: groupId, expense: expense) try await addActivityLogForExpense(group: group, expense: expense, oldExpense: oldExpense, type: type) }🧰 Tools
🪛 SwiftLint
[Warning] 41-41: Function should have 5 parameters or less: it currently has 6
(function_parameter_count)
Splito/UI/Home/Expense/AddExpenseViewModel.swift (3)
25-25
: Confirm the use ofUIImage
with SwiftUIYou're using
UIImage
forexpenseImage
in a SwiftUI context. Consider using SwiftUI'sImage
type or ensure thatUIImage
is compatible with your SwiftUI views to maintain consistency and take full advantage of SwiftUI features.
363-364
: Refactor image processing to avoid code duplicationThe image resizing and compression logic is duplicated in both
addExpense
andupdateExpense
methods. Extract this code into a helper method to improve maintainability and reduce repetition.Extract the image processing into a helper method:
private func prepareImageData() -> Data? { guard let resizedImage = expenseImage?.aspectFittedToHeight(200) else { return nil } return resizedImage.jpegData(compressionQuality: 0.2) }Update your methods to use the helper:
// In addExpense(group:expense:) - let resizedImage = expenseImage?.aspectFittedToHeight(200) - let imageData = resizedImage?.jpegData(compressionQuality: 0.2) + let imageData = prepareImageData() // In updateExpense(group:expense:oldExpense:) - let resizedImage = expenseImage?.aspectFittedToHeight(200) - let imageData = resizedImage?.jpegData(compressionQuality: 0.2) + let imageData = prepareImageData()Also applies to: 411-412
363-364
: Consider adjusting the image compression qualityThe JPEG compression quality is set to
0.2
, which may result in low image quality. Consider increasing the compression quality to0.5
or0.7
to achieve a better balance between image quality and file size.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
⛔ Files ignored due to path filters (4)
Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CalendarIcon.imageset/date.icon (1).svg
is excluded by!**/*.svg
Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CalendarIcon.imageset/date.icon.svg
is excluded by!**/*.svg
Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CameraIcon.imageset/camera.icon (1).png
is excluded by!**/*.png
Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CameraIcon.imageset/camera.icon (2).png
is excluded by!**/*.png
📒 Files selected for processing (15)
Data/Data/Extension/Date+Extension.swift
(1 hunks)Data/Data/Extension/Double+Extension.swift
(1 hunks)Data/Data/Helper/Firebase/StorageManager.swift
(1 hunks)Data/Data/Model/Expense.swift
(2 hunks)Data/Data/Repository/ExpenseRepository.swift
(2 hunks)Data/Data/Repository/GroupRepository.swift
(4 hunks)Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CalendarIcon.imageset/Contents.json
(1 hunks)Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CameraIcon.imageset/Contents.json
(1 hunks)Splito/UI/Home/ActivityLog/ActivityLogViewModel.swift
(1 hunks)Splito/UI/Home/Expense/AddExpenseView.swift
(7 hunks)Splito/UI/Home/Expense/AddExpenseViewModel.swift
(7 hunks)Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift
(2 hunks)Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift
(1 hunks)Splito/UI/Home/Groups/Group/Group Options/Transactions/GroupTransactionListViewModel.swift
(1 hunks)Splito/UI/Home/Groups/Group/GroupHomeViewModel.swift
(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CalendarIcon.imageset/Contents.json
- Splito/Resource/Assets.xcassets/Images/Group/Add Expense/CameraIcon.imageset/Contents.json
🧰 Additional context used
🪛 SwiftLint
Data/Data/Repository/ExpenseRepository.swift
[Warning] 41-41: Function should have 5 parameters or less: it currently has 6
(function_parameter_count)
🔇 Additional comments (13)
Data/Data/Helper/Firebase/StorageManager.swift (2)
15-15
: LGTM! The new enum case follows existing conventions.
The addition of the expense
case aligns well with the PR objective and maintains consistency with existing cases.
23-24
: Verify storage path compatibility.
While the path naming follows the existing convention, ensure that "expense_images" is compatible with Firebase Storage path requirements and organizational policies.
✅ Verification successful
Let me gather more information about the storage path patterns and related code.
Based on the gathered information, I can now provide the final response:
Storage path follows consistent naming pattern and is safe to use
The expense_images
path follows the established pattern used throughout the StorageManager, consistent with other cases:
user_images
group_images
expense_images
The path uses simple alphanumeric characters with underscores, which is compatible with Firebase Storage path requirements. The consistent naming pattern across all cases indicates an established convention within the codebase.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check if similar path patterns are used consistently across the codebase
# and if there are any existing guidelines for storage paths
# Search for storage path definitions
rg -g '*.swift' 'pathName.*images"'
# Look for any storage path related documentation
fd -g 'README*' -X grep -l 'storage' || fd -g '*.md' -X grep -l 'storage'
Length of output: 124
Script:
#!/bin/bash
# Search for the entire enum and switch case implementation to understand the context
ast-grep --pattern 'enum StorageFolder {
$$$
}'
# Search for the complete pathName implementation
ast-grep --pattern 'var pathName: String {
$$$
}'
# Check the content of the storage documentation
cat docs/index.md
Length of output: 13918
Data/Data/Model/Expense.swift (2)
27-35
: LGTM! Well-structured initializer update
The initializer changes are well-implemented:
- Maintains backward compatibility with optional parameter
- Logical parameter ordering
- Consistent with existing style
50-50
: LGTM! Consistent coding key convention
The new coding key follows the established snake_case convention for JSON serialization.
Splito/UI/Home/ActivityLog/ActivityLogViewModel.swift (2)
100-100
: Verify the monthWithYear property implementation
The code now uses a new monthWithYear
computed property from the Date extension. While this change simplifies the date grouping logic, we should ensure the property is properly implemented.
Let's verify the implementation of this property:
✅ Verification successful
The monthWithYear property is correctly implemented and used consistently
The monthWithYear
property is properly implemented in Date+Extension.swift
using a standard DateFormatter
with "MMMM yyyy" format (e.g., "November 2024"). The property is used consistently across the codebase for date grouping in activity logs, expenses, and transactions.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for the monthWithYear property definition
ast-grep --pattern 'extension Date {
$$$
var monthWithYear: $_ {
$$$
}
$$$
}'
# Backup: Search using ripgrep if the property is defined differently
rg -p "monthWithYear" --type swift
Length of output: 656
Script:
#!/bin/bash
# Let's check the implementation of monthWithYear in Date+Extension.swift
rg -A 5 "var monthWithYear" Data/Data/Extension/Date+Extension.swift
Length of output: 248
Line range hint 82-97
: Consider adding support for attachment-related activities
Since this PR adds expense attachment support, the handleActivityItemTap
method might need to handle new activity types related to expense attachments (e.g., attachment added, removed, or updated).
Let's check if there are any new activity types we need to handle:
Consider adding cases for attachment-related activities if they're defined in the ActivityType enum.
Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift (1)
105-106
: LGTM! Clean styling implementation.
Good optimization by simplifying the view hierarchy and applying styling directly instead of using an overlay.
Splito/UI/Home/Groups/Group/GroupHomeViewModel.swift (1)
239-239
: Verify date formatting consistency across expense grouping.
The change to use monthWithYear
property aligns with the standardized date formatting approach. However, let's verify that all expense date grouping operations use this consistent format.
✅ Verification successful
Let me check the implementation of monthWithYear
to ensure it's properly standardized.
Date formatting is consistently implemented across expense grouping
The verification shows that:
monthWithYear
is properly implemented as a Date extension with a standardized format "MMMM yyyy"- It's consistently used across all relevant views:
- GroupHomeViewModel (expense grouping)
- GroupTransactionListViewModel (transactions)
- ActivityLogViewModel (activity logs)
- Only one legacy date formatting remains in ActivityLogView which uses a different context
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for any remaining direct date formatting in expense grouping logic
# across the codebase to ensure consistency.
# Search for date formatting patterns in Swift files
rg -t swift 'dateFormatter.*month|Date.*format.*month'
# Search for monthWithYear usage to verify consistent adoption
rg -t swift 'monthWithYear'
Length of output: 812
Script:
#!/bin/bash
# Check the implementation of monthWithYear
rg -A 5 -B 2 'var monthWithYear: String'
Length of output: 570
Data/Data/Repository/GroupRepository.swift (1)
23-23
: Well-implemented concurrent queue for thread safety!
Good practice using a concurrent queue with a descriptive label for managing access to shared state.
Splito/UI/Home/Groups/Group/Group Options/Transactions/GroupTransactionListViewModel.swift (2)
211-211
: LGTM: Date formatting refactor looks clean.
The switch to using the monthWithYear
computed property from Date extension makes the code more maintainable and consistent.
Also applies to: 218-218
Line range hint 1-324
: Implementation for expense attachment support appears to be missing.
While the PR's objective is to add expense attachment support, this view model doesn't contain any changes related to handling attachments or images for expenses. Consider adding:
- Properties to track attachment state
- Methods to handle attachment selection/preview
- Logic to upload/download attachments
- UI state updates for attachment loading
Let's verify if attachment-related code exists in other files:
Consider implementing the following components for a robust attachment system:
- Attachment handling methods in the view model
- Image upload/download service integration
- Progress tracking for uploads
- Error handling for failed uploads/downloads
- Cache management for downloaded images
Would you like me to help design these components?
Data/Data/Repository/ExpenseRepository.swift (1)
58-59
: Verify that image changes trigger expense updates correctly
The hasExpenseChanged
method checks whether the expense has changed before updating. Ensure that changes to the image (addition, deletion, or modification) are properly detected so that the expense is updated in the store when image changes occur.
Confirm that hasExpenseChanged
accounts for image-related changes and that the expense is updated accordingly.
Splito/UI/Home/Expense/AddExpenseViewModel.swift (1)
11-12
: Added imports are appropriate
The imports of AVFoundation
and SwiftUI
are necessary for the new functionalities related to camera access and SwiftUI views.
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Outside diff range and nitpick comments (6)
Data/Data/Store/ExpenseStore.swift (1)
24-26
: Consider providing a higher-level API for better usabilityThe current implementation exposes Firebase-specific types (
DocumentReference
) in the public API, which could make the code harder to test and maintain. Consider providing a higher-level API that abstracts these details.+func addExpense(groupId: String, expense: Expense) async throws { + let documentRef = expenseReference(groupId: groupId).document() + try await addExpense(documentRef: documentRef, expense: expense) + return documentRef.documentID +} func addExpense(documentRef: DocumentReference, expense: Expense) async throws { try documentRef.setData(from: expense) }Splito/UI/Home/Groups/Create Group/CreateGroupViewModel.swift (1)
53-55
: LGTM! Consider adding user feedback for edge cases.The nil check for settings URL is a good safety improvement. However, in the rare case where the settings URL is invalid, the user receives no feedback about why they can't proceed.
Consider adding an else clause to handle the edge case:
if let settingsURL = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(settingsURL) +} else { + self?.showToastFor(toast: ToastPrompt( + type: .error, + title: "Error", + message: "Unable to open Settings. Please enable camera access manually." + )) }Data/Data/Repository/ExpenseRepository.swift (1)
45-64
: Consider enhancing error handling for image operationsWhile the implementation handles different image update scenarios well, consider adding more specific error handling for image operations.
public func updateExpenseWithImage(imageData: Data?, newImageUrl: String?, group: Groups, expense: (new: Expense, old: Expense), type: ActivityType) async throws -> Expense { var updatedExpense = expense.new + + enum ImageUpdateError: Error { + case imageUploadFailed + case imageDeletionFailed + } if let imageData { let uploadedImageUrl = try await uploadImage(imageData: imageData, expense: updatedExpense) + guard !uploadedImageUrl.isEmpty else { + throw ImageUpdateError.imageUploadFailed + } updatedExpense.imageUrl = uploadedImageUrl } else if let currentUrl = updatedExpense.imageUrl, newImageUrl == nil { try await storageManager.deleteImage(imageUrl: currentUrl) updatedExpense.imageUrl = nil } else if let newImageUrl = newImageUrl { + guard !newImageUrl.isEmpty else { + throw ImageUpdateError.imageUploadFailed + } updatedExpense.imageUrl = newImageUrl }Splito/UI/Home/Expense/AddExpenseView.swift (2)
118-119
: Consider standardizing the layout spacingThe spacing between ExpenseDetailRow components varies. Consider using consistent spacing values for better visual hierarchy:
- Between group selection and description
- Between amount and payment details
VStack(spacing: 16) { - ExpenseDetailRow(name: $viewModel.expenseName, focusedField: focusedField, subtitle: "With you and:", + VStack(spacing: 24) { // Increased spacing for major sections + ExpenseDetailRow(name: $viewModel.expenseName, focusedField: focusedField, subtitle: "With you and:", inputValue: viewModel.selectedGroup?.name ?? "Select group", showButton: true, onTap: viewModel.handleGroupBtnAction)Also applies to: 122-123, 132-136
237-238
: Consider more flexible date range handlingThe maximum date calculation is hardcoded to the current year. Consider:
- Making the date range configurable through parameters
- Using a computed property for dynamic updates
- private let maximumDate = Calendar.current.date(byAdding: .year, value: 0, to: Date()) ?? Date() + private var maximumDate: Date { + Calendar.current.startOfDay(for: Date()) + }Splito/UI/Home/Expense/AddExpenseViewModel.swift (1)
365-366
: Consider increasing the image compression quality for better image clarityCompressing the image with a
compressionQuality
of 0.2 may result in noticeably low image quality, affecting the user experience. To achieve a better balance between image size and quality, consider using a higher compression quality, such as 0.6 or 0.7.Also applies to: 413-414
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (7)
Data/Data/Repository/ExpenseRepository.swift
(2 hunks)Data/Data/Repository/GroupRepository.swift
(3 hunks)Data/Data/Store/ExpenseStore.swift
(1 hunks)Splito/UI/Home/Expense/AddExpenseView.swift
(7 hunks)Splito/UI/Home/Expense/AddExpenseViewModel.swift
(7 hunks)Splito/UI/Home/Expense/Expense Split Option/ExpenseSplitOptionsView.swift
(1 hunks)Splito/UI/Home/Groups/Create Group/CreateGroupViewModel.swift
(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- Splito/UI/Home/Expense/Expense Split Option/ExpenseSplitOptionsView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- Data/Data/Repository/GroupRepository.swift
🔇 Additional comments (9)
Data/Data/Store/ExpenseStore.swift (1)
17-22
: Verify the necessity of internal access for expenseReference
The method's access level has been changed from private to internal. While this might be needed for image handling coordination, exposing Firestore collection references could lead to inconsistent data access patterns.
✅ Verification successful
Internal access for expenseReference is appropriate
The internal access level is justified as it follows a clean architecture pattern where:
ExpenseRepository
acts as a public-facing repository layerExpenseStore
serves as an internal implementation detail- The reference is only used within the data layer for proper encapsulation
- Usage is controlled and consistent through dependency injection (via
AppAssembly
)
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if this reference is used outside this file
rg -p "expenseReference" --type swift --glob '!**/ExpenseStore.swift'
Length of output: 202
Script:
#!/bin/bash
# Let's check the ExpenseRepository usage to understand the context better
rg -p "class ExpenseRepository" -A 10 --type swift
# Also check if there are any tests or other legitimate uses
rg -p "ExpenseRepository" --type swift
Length of output: 1641
Data/Data/Repository/ExpenseRepository.swift (5)
14-14
: LGTM: StorageManager dependency properly injected
The StorageManager dependency is correctly injected following the existing pattern and will be used for handling image uploads and deletions.
17-31
: LGTM: Improved expense creation flow with proper image handling
The implementation correctly:
- Generates the document ID first
- Handles image upload before storing the expense
- Updates the expense with the image URL if provided
66-69
: Previous comment about nil expense.id handling still applies
The current implementation returns an empty string when expense.id is nil, which could lead to silent failures.
71-77
: LGTM: Comprehensive change detection implementation
The method thoroughly compares all relevant fields, including the new imageUrl field, in a clear and maintainable way.
14-14
: Verify consistent image handling across the codebase
Let's verify that image-related operations are handled consistently across the codebase.
Also applies to: 17-31, 45-64, 66-69
✅ Verification successful
Image handling is consistently implemented across the codebase
Based on the verification results:
- All image operations are centralized through the
StorageManager
class - Image handling is consistently used in repositories (
ExpenseRepository
,GroupRepository
,UserRepository
) - The
StorageManager
provides a unified interface for image upload/delete operations - Image-related operations are properly categorized using
ImageStoreType
enum - All UI components consistently use the same image handling patterns through repository layer
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check for consistent image handling patterns
# Look for image-related operations
echo "Checking for image upload/delete operations:"
rg -l "uploadImage|deleteImage|imageUrl" --type swift
# Check for potential image handling in other repositories
echo "\nChecking for other image handling implementations:"
rg -l "class.*Repository.*{" --type swift | xargs rg "image|photo|attachment" -l
# Look for StorageManager usage
echo "\nChecking StorageManager usage patterns:"
rg "StorageManager" --type swift -A 3
Length of output: 12610
Splito/UI/Home/Expense/AddExpenseView.swift (2)
76-83
: LGTM: Well-structured image picker implementation
The image picker implementation follows iOS best practices with proper sheet presentation and confirmation dialog handling.
Line range hint 163-191
: Verify the necessity of forced tap gesture
The use of onTapGestureForced
might interfere with the natural touch handling of buttons and text fields. Consider using regular onTapGesture
unless there's a specific reason for forcing the gesture.
Splito/UI/Home/Expense/AddExpenseViewModel.swift (1)
227-231
: Ensure UI updates occur on the main thread and handle permission denial appropriately
In the checkCameraPermission
method:
- Thread Safety: The closure in
AVCaptureDevice.requestAccess
may run on a background thread. Wrap theauthorized()
call insideDispatchQueue.main.async
to ensure UI updates occur on the main thread. - Handle Permission Denial: If permission is not granted (
granted == false
), consider informing the user that camera access is required for this feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Outside diff range and nitpick comments (8)
Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift (3)
89-91
: Good URL safety, but consider these improvementsThe URL handling is properly implemented with optional binding. However, consider these enhancements:
- Move the alert dismissal before opening settings
- Add completion handler to handle URL opening failures
- Use
[weak self]
explicitly for better memory management- if let settingsURL = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsURL) - } - self?.showAlert = false + self?.showAlert = false + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsURL, options: [:]) { success in + if !success { + self?.showAlertFor(message: "Could not open Settings") + } + } + }
Line range hint
1-400
: Consider splitting responsibilities for better maintainabilityThe
UserProfileViewModel
handles multiple concerns which could be separated into dedicated services:
- Extract authentication logic into an
AuthenticationService
- Move image handling into an
ImageService
- Create a dedicated
ProfileManager
for profile data operationsThis separation would:
- Improve testability
- Make the code more maintainable
- Allow reuse of these services across the app
Example structure:
// AuthenticationService.swift protocol AuthenticationServiceProtocol { func reAuthenticate(for loginType: LoginType) async throws -> AuthCredential } // ImageService.swift protocol ImageServiceProtocol { func checkCameraPermission() async throws func handleImageSelection(source: ImageSource) async throws -> UIImage } // ProfileManager.swift protocol ProfileManagerProtocol { func updateProfile(user: User, image: UIImage?) async throws func deleteProfile() async throws } // Simplified ViewModel class UserProfileViewModel: BaseViewModel, ObservableObject { private let authService: AuthenticationServiceProtocol private let imageService: ImageServiceProtocol private let profileManager: ProfileManagerProtocol // Reduced complexity with dependency injection }
Line range hint
1-400
: Improve error handling consistency and structureThe current error handling could be enhanced for better maintainability and consistency:
- Use structured error types instead of string comparisons
- Centralize error messages
- Migrate completion handlers to async/await
Example implementation:
enum UserProfileError: LocalizedError { case reauthenticationRequired case invalidCredentials case permissionDenied case networkError(Error) var errorDescription: String? { switch self { case .reauthenticationRequired: return "Recent authentication is required" case .invalidCredentials: return "Invalid credentials provided" case .permissionDenied: return "Camera access is required" case .networkError(let error): return "Network error: \(error.localizedDescription)" } } } // Example usage: func deleteUser() async throws { guard let user = preference.user else { throw UserProfileError.invalidCredentials } do { try await userRepository.deleteUser(id: user.id) preference.clearPreferenceSession() goToOnboardScreen() } catch { if error.localizedDescription.contains(REQUIRE_AGAIN_LOGIN_TEXT) { throw UserProfileError.reauthenticationRequired } throw UserProfileError.networkError(error) } }Splito/UI/Home/Expense/AddExpenseViewModel.swift (1)
Line range hint
364-421
: Improve image processing and validationThe image handling implementation could be enhanced:
- Magic numbers should be constants
- Large images could cause memory issues
- Missing image size validation
Apply these improvements:
+private enum ImageConstants { + static let maxHeight: CGFloat = 200 + static let compressionQuality: CGFloat = 0.2 + static let maxImageSizeInBytes = 5 * 1024 * 1024 // 5MB +} +private func processImage(_ image: UIImage?) -> Data? { + guard let image = image else { return nil } + let resizedImage = image.aspectFittedToHeight(ImageConstants.maxHeight) + guard let imageData = resizedImage?.jpegData(compressionQuality: ImageConstants.compressionQuality), + imageData.count <= ImageConstants.maxImageSizeInBytes else { + showAlertFor(alert: .init( + title: "Image Too Large", + message: "Please select an image smaller than 5MB", + positiveBtnTitle: "OK" + )) + return nil + } + return imageData +} private func addExpense(group: Groups, expense: Expense) async -> Bool { guard let groupId = group.id else { return false } - let resizedImage = expenseImage?.aspectFittedToHeight(200) - let imageData = resizedImage?.jpegData(compressionQuality: 0.2) + let imageData = processImage(expenseImage) do { showLoader = trueApply similar changes to
updateExpense
method.Splito/UI/Home/Expense/AddExpenseView.swift (4)
76-80
: Consider adding error handling for image picker failuresThe image picker implementation should handle potential failures when accessing the camera or photo library.
.sheet(isPresented: $viewModel.showImagePicker) { ImagePickerView(cropOption: .square, sourceType: !viewModel.sourceTypeIsCamera ? .photoLibrary : .camera, - image: $viewModel.expenseImage, isPresented: $viewModel.showImagePicker) + image: $viewModel.expenseImage, + isPresented: $viewModel.showImagePicker, + onError: { error in + viewModel.toast = .init(style: .error, message: error.localizedDescription) + }) }
132-136
: Consider using enum for input types to improve type safetyThe ExpenseDetailRow is used for multiple purposes with different input types. Using an enum would make the intent clearer and prevent potential misuse.
+ enum ExpenseDetailRowType { + case text(Binding<String>) + case button(String, () -> Void) + } struct ExpenseDetailRow: View { - @Binding var name: String + let type: ExpenseDetailRowType // ... rest of the implementation }
250-257
: Add accessibility labels for better VoiceOver supportThe date display should have appropriate accessibility labels to improve the experience for VoiceOver users.
if !isForAddExpense { Text(date.longDate) .font(.subTitle2()) .foregroundStyle(primaryText) .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityLabel("Selected date: \(date.longDate)") } else { DateDisplayView(date: $date) }
326-343
: Add haptic feedback for better user interactionAdding haptic feedback when tapping the image picker button would improve the user experience.
Button(action: { + UIImpactFeedbackGenerator(style: .light).impactOccurred() handleImageBtnTap() }) { // ... rest of the implementation }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (4)
Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift
(1 hunks)Splito/UI/Home/Expense/AddExpenseView.swift
(7 hunks)Splito/UI/Home/Expense/AddExpenseViewModel.swift
(7 hunks)Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
Splito/UI/Home/Expense/AddExpenseView.swift
[Warning] 495-495: File should contain 400 lines or less: currently contains 495
(file_length)
🔇 Additional comments (2)
Splito/UI/Home/Expense/AddExpenseViewModel.swift (2)
Line range hint 11-35
: LGTM: Well-structured property declarations
The new properties for image handling are well-organized with appropriate access levels and follow SwiftUI conventions.
436-439
: LGTM: Improved change detection logic
The change detection has been improved by removing the updatedBy
field and correctly including the imageUrl
comparison.
c0a8cc4
to
ce8c6f8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Outside diff range and nitpick comments (8)
Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift (3)
30-36
: Consider adding loading and error states for image loading.While the image view implementation is solid, consider enhancing the user experience by:
- Adding a loading placeholder while the image loads
- Handling potential image loading failures
Example implementation:
if let imageUrl = viewModel.expense?.imageUrl { ExpenseImageView(showZoomableImageView: $showZoomableImageView, imageUrl: imageUrl) + .overlay { + AsyncImageLoadingView(url: imageUrl) + } .aspectRatio(16/9, contentMode: .fit) .cornerRadius(12) }
59-71
: Simplify toolbar conditional logic.The nested conditionals could be simplified for better readability.
Consider refactoring to:
-if viewModel.viewState != .loading { - if (viewModel.expense?.isActive ?? false) && (viewModel.group?.isActive ?? false) { - ToolbarItem(placement: .topBarTrailing) { - ToolbarButtonView(imageIcon: .binIcon, onClick: viewModel.handleDeleteButtonAction) - } - ToolbarItem(placement: .topBarTrailing) { - ToolbarButtonView(imageIcon: .editPencilIcon, onClick: viewModel.handleEditBtnAction) - } - } else { - ToolbarItem(placement: .topBarTrailing) { - RestoreButton(onClick: viewModel.handleRestoreButtonAction) - } - } -} +if viewModel.viewState == .loading { return } + +let isActiveState = (viewModel.expense?.isActive ?? false) && (viewModel.group?.isActive ?? false) +ToolbarItem(placement: .topBarTrailing) { + if isActiveState { + HStack { + ToolbarButtonView(imageIcon: .binIcon, onClick: viewModel.handleDeleteButtonAction) + ToolbarButtonView(imageIcon: .editPencilIcon, onClick: viewModel.handleEditBtnAction) + } + } else { + RestoreButton(onClick: viewModel.handleRestoreButtonAction) + } +}
74-77
: Enhance image zoom transition animation.The zoomable image view implementation could benefit from proper animation configuration.
Consider creating a shared namespace for smooth transitions:
struct ExpenseDetailsView: View { + @Namespace private var imageNamespace // ... existing code ... .navigationDestination(isPresented: $showZoomableImageView) { if let imageUrl = viewModel.expense?.imageUrl { - ZoomableImageView(imageUrl: imageUrl, animationNamespace: Namespace()) + ZoomableImageView(imageUrl: imageUrl, animationNamespace: imageNamespace) + .transition(.opacity.combined(with: .scale)) } }Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift (3)
72-78
: LGTM! Consider enhancing error handling feedback.The async Task implementation for save action is well structured. However, consider providing more specific error feedback to users instead of a generic save failed error.
Task { let isSucceed = await viewModel.handleSaveAction() if isSucceed { dismiss() } else { - viewModel.showSaveFailedError() + viewModel.showSaveFailedError(with: viewModel.errorMessage ?? "Failed to save payment") } }
185-185
: Consider making date constraints configurable.The maximum date is hardcoded to the current year. Consider making this configurable through an initializer parameter to improve reusability.
- private let maximumDate = Calendar.current.date(byAdding: .year, value: 0, to: Date()) ?? Date() + private let maximumDate: Date + + init(date: Binding<Date>, isForAddExpense: Bool = false, + maximumDate: Date = Calendar.current.date(byAdding: .year, value: 0, to: Date()) ?? Date()) { + self._date = date + self.isForAddExpense = isForAddExpense + self._tempDate = State(initialValue: date.wrappedValue) + self.maximumDate = maximumDate + }
250-250
: Add localization for "Today" string.Consider localizing the "Today" string for better internationalization support.
- Text(date.isToday() ? "Today" : date.shortDate) + Text(date.isToday() ? "Today".localized : date.shortDate)Splito/UI/Home/Expense/AddExpenseView.swift (2)
109-141
: Add accessibility identifiers for UI testingThe ExpenseInfoView contains important user input fields but lacks accessibility identifiers which are crucial for UI testing.
Consider adding accessibility identifiers to key elements:
VStack(spacing: 16) { ExpenseDetailRow(name: $viewModel.expenseName, focusedField: focusedField, subtitle: "With you and:", inputValue: viewModel.selectedGroup?.name ?? "Select group", - showButton: true, onTap: viewModel.handleGroupBtnAction) + showButton: true, onTap: viewModel.handleGroupBtnAction) + .accessibilityIdentifier("group-selection-row") ExpenseDetailRow(name: $viewModel.expenseName, focusedField: focusedField, - subtitle: "Description", field: .expenseName) + subtitle: "Description", field: .expenseName) + .accessibilityIdentifier("expense-description-row")
Line range hint
1-416
: Consider splitting the file into smaller componentsThe file currently contains 416 lines, which exceeds the recommended maximum of 400 lines. Consider moving some components to separate files:
- Move
ZoomableImageView
to a separate file- Move
ExpenseImageView
andExpenseImagePickerView
to a new file focused on image handlingThis will improve maintainability and make the code easier to test.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 417-417: File should contain 400 lines or less: currently contains 417
(file_length)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (3)
Splito/UI/Home/Expense/AddExpenseView.swift
(5 hunks)Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift
(4 hunks)Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift
(3 hunks)
🔇 Additional comments (3)
Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift (2)
15-15
: LGTM: State variable properly declared.
The state variable follows SwiftUI best practices and is appropriately scoped.
113-114
: LGTM: Consistent styling applied.
The background and corner radius styling matches the design system and improves visual hierarchy.
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift (1)
155-177
: LGTM! Clean implementation.
The PaymentDateRow implementation provides a good separation of concerns and consistent styling.
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Outside diff range and nitpick comments (13)
Splito/UI/Home/Groups/Create Group/CreateGroupViewModel.swift (2)
87-91
: Consider adding input validation.While the async/await refactoring is good, consider adding validation for the group name before proceeding with create/update operations.
func handleDoneAction() async -> Bool { + guard !groupName.trimming(spaces: .leadingAndTrailing).isEmpty else { + showToastFor(toast: ToastPrompt(type: .error, title: "Error", message: "Group name cannot be empty")) + return false + } if let group { await updateGroup(group: group) } else { await createGroup() } }
Line range hint
99-143
: Consider optimizing image handling.While the image resizing and compression are good, consider:
- Moving image processing to a background queue to prevent UI blocking
- Implementing proper cleanup of image data after upload
private func createGroup() async -> Bool { guard let userId = preference.user?.id else { return false } let memberBalance = GroupMemberBalance(id: userId, balance: 0, totalSummary: []) let group = Groups(name: groupName.trimming(spaces: .leadingAndTrailing), createdBy: userId, updatedBy: userId, imageUrl: nil, members: [userId], balances: [memberBalance], createdAt: Timestamp(), updatedAt: Timestamp()) - let resizedImage = profileImage?.aspectFittedToHeight(200) - let imageData = resizedImage?.jpegData(compressionQuality: 0.2) + let imageData = await Task.detached { + let resizedImage = self.profileImage?.aspectFittedToHeight(200) + return resizedImage?.jpegData(compressionQuality: 0.2) + }.value do { showLoader = true let group = try await groupRepository.createGroup(group: group, imageData: imageData) + // Clean up the image data + self.profileImage = nil NotificationCenter.default.post(name: .addGroup, object: group) showLoader = false return true } catch {Splito/UI/Home/Expense/AddExpenseView.swift (2)
109-141
: Consider extracting spacing constants and improving component separationThe ExpenseInfoView could benefit from:
- Extracting hardcoded spacing values into constants
- Breaking down into smaller, more focused components
Consider creating a separate spacing constants file:
enum Spacing { static let defaultPadding: CGFloat = 16 static let rowSpacing: CGFloat = 16 static let horizontalInset: CGFloat = 1 }And refactoring the view to use these constants:
-VStack(spacing: 16) { +VStack(spacing: Spacing.rowSpacing) { // ... content ... } -.padding(.horizontal, 1) +.padding(.horizontal, Spacing.horizontalInset)
303-416
: Document gesture handling and extract magic numbersThe ZoomableImageView implementation is robust but would benefit from:
- Documentation explaining the gesture handling logic
- Extraction of magic numbers into named constants
Consider these improvements:
+/// Constants for zoom and pan limits +private enum ZoomLimits { + static let minScale: CGFloat = 1.0 + static let maxScale: CGFloat = 4.0 + static let defaultDoubleTapScale: CGFloat = 3.0 +} struct ZoomableImageView: View { // ... existing properties ... private func fixOffsetAndScale(geometry: GeometryProxy) { - let newScale: CGFloat = .minimum(.maximum(scale, 1), 4) + let newScale: CGFloat = .minimum(.maximum(scale, ZoomLimits.minScale), ZoomLimits.maxScale) // ... rest of the implementation ... } private func resetZoom() { - scale = lastScale > 1 ? 1 : 3 + scale = lastScale > ZoomLimits.minScale ? ZoomLimits.minScale : ZoomLimits.defaultDoubleTapScale // ... rest of the implementation ... } }Splito/UI/Home/Expense/AddExpenseViewModel.swift (4)
201-248
: Enhance user experience for camera permission handling.While the implementation is generally good, consider these improvements for better user experience:
- The camera denied case could guide users better by explaining how to enable camera access in settings
- Consider handling the case when settings URL fails to open
case .restricted, .denied: - showAlertFor(alert: .init(title: "Important!", message: "Camera access is required to take picture for expenses", + showAlertFor(alert: .init(title: "Camera Access Required", + message: "To take expense photos, please enable camera access in Settings > Privacy > Camera > Splito", positiveBtnTitle: "Allow", positiveBtnAction: { [weak self] in if let settingsURL = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsURL) + UIApplication.shared.open(settingsURL) { success in + if !success { + self?.showToastFor(toast: ToastPrompt(type: .error, + title: "Error", + message: "Could not open Settings")) + } + } } self?.showAlert = false }))
313-322
: Optimize member validation logic.The validation logic can be made more efficient by using set operations instead of iterating multiple times.
private func validateMembersInGroup(group: Groups, expense: Expense) -> Bool { - for payer in expense.paidBy where !group.members.contains(payer.key) { - return false - } - for memberId in expense.splitTo where !group.members.contains(memberId) { - return false - } - - return true + let memberSet = Set(group.members) + let payerSet = Set(expense.paidBy.keys) + let splitToSet = Set(expense.splitTo) + + return memberSet.isSuperset(of: payerSet) && memberSet.isSuperset(of: splitToSet) }
367-369
: Extract image processing constants.Consider extracting magic numbers for image dimensions and compression quality into named constants for better maintainability.
+private enum ImageProcessing { + static let maxHeight: CGFloat = 200 + static let compressionQuality: CGFloat = 0.2 +} -let resizedImage = expenseImage?.aspectFittedToHeight(200) -let imageData = resizedImage?.jpegData(compressionQuality: 0.2) +let resizedImage = expenseImage?.aspectFittedToHeight(ImageProcessing.maxHeight) +let imageData = resizedImage?.jpegData(compressionQuality: ImageProcessing.compressionQuality)Also applies to: 415-417
436-441
: Consider using Equatable for expense comparison.The current comparison logic could be made more maintainable by implementing
Equatable
on theExpense
struct and comparing relevant properties there.+extension Expense: Equatable { + static func == (lhs: Expense, rhs: Expense) -> Bool { + return lhs.name == rhs.name && + lhs.amount == rhs.amount && + lhs.date.dateValue() == rhs.date.dateValue() && + lhs.paidBy == rhs.paidBy && + lhs.imageUrl == rhs.imageUrl && + lhs.splitTo == rhs.splitTo && + lhs.splitType == rhs.splitType && + lhs.splitData == rhs.splitData && + lhs.isActive == rhs.isActive + } +} private func hasExpenseChanged(_ expense: Expense, oldExpense: Expense) -> Bool { - return oldExpense.name != expense.name || oldExpense.amount != expense.amount || - oldExpense.date.dateValue() != expense.date.dateValue() || oldExpense.paidBy != expense.paidBy || - oldExpense.imageUrl != expense.imageUrl || oldExpense.splitTo != expense.splitTo || - oldExpense.splitType != expense.splitType || oldExpense.splitData != expense.splitData || - oldExpense.isActive != expense.isActive + return expense != oldExpense }Splito/Localization/Localizable.xcstrings (5)
783-791
: Consider consolidating similar error messages.There are multiple similar error messages for permissions and left group scenarios. Consider using a templated approach with placeholders for better maintainability.
- "This expense could not be deleted. You do not have permission to delete this expense, Sorry!" : { - "extractionState" : "manual" - }, - "This expense could not be edited. You do not have permission to edit this expense, Sorry!" : { - "extractionState" : "manual" - }, + "action_not_allowed_%@_%@" : { + "extractionState" : "manual", + "comment": "First parameter is the entity (expense/payment), second is the action (delete/edit)" + },
804-812
: Consolidate left group error messages.Similar error messages for payments involving users who left the group can be consolidated into a single templated string.
- "This payment involves a person who has left the group, and thus it can no longer be deleted. If you wish to change this payment, you must first add that person back to your group." : { - "extractionState" : "manual" - }, - "This payment involves a person who has left the group, and thus it can no longer be edited. If you wish to change this payment, you must first add that person back to your group." : { - "extractionState" : "manual" - }, - "This payment involves a person who has left the group, and thus it can no longer be restored. If you wish to change this payment, you must first add that person back to your group." : { - "extractionState" : "manual" - }, + "payment_action_blocked_member_left_%@" : { + "extractionState" : "manual", + "comment": "Parameter is the action (delete/edit/restore)" + },
912-913
: Consider adding comments for translators.The restoration-related strings might benefit from translator comments explaining the context, especially for languages with different grammar structures.
"You can restore it to recover all activities, expenses and payments." : { - "extractionState" : "manual" + "extractionState" : "manual", + "comment": "Shown when a group is deleted and can be restored" }, "This will restore all activities, expenses and payments for this group." : { - "extractionState" : "manual" + "extractionState" : "manual", + "comment": "Confirmation message shown before restoring a group" },Also applies to: 816-817
738-738
: Improve clarity of empty state message.The message "Start spending or receiving money to see your payments here." could be more engaging and informative.
- "Start spending or receiving money to see your payments here." : { + "No payments yet! Record your first expense or payment to get started." : { + "extractionState" : "manual", + "comment": "Empty state message for payments list" },
142-146
: Add comments for activity-related strings.The "Activities" and "Activity" strings might need context for proper translation in languages with different plural forms.
"Activities" : { - "extractionState" : "manual" + "extractionState" : "manual", + "comment": "Title for the activities list view (plural)" }, "Activity" : { + "extractionState" : "manual", + "comment": "Title for a single activity item" },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (5)
Splito/Localization/Localizable.xcstrings
(29 hunks)Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift
(1 hunks)Splito/UI/Home/Expense/AddExpenseView.swift
(5 hunks)Splito/UI/Home/Expense/AddExpenseViewModel.swift
(8 hunks)Splito/UI/Home/Groups/Create Group/CreateGroupViewModel.swift
(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Splito/UI/Home/Account/User Profile/UserProfileViewModel.swift
🔇 Additional comments (6)
Splito/UI/Home/Groups/Create Group/CreateGroupViewModel.swift (3)
16-18
: LGTM! Clean dependency injection setup.
The dependencies are well-organized and appropriate for the view model's responsibilities.
45-48
: Improved camera permission handling with safe practices.
Good improvements:
- Using
DispatchQueue.main.async
for UI updates - Safe optional binding for settings URL handling
Also applies to: 53-57
95-97
: LGTM! Good error handling separation.
Clean implementation of error feedback mechanism.
Splito/UI/Home/Expense/AddExpenseView.swift (1)
92-98
: 🛠️ Refactor suggestion
Enhance error handling in save action
The current implementation could benefit from more robust error handling to provide specific feedback to users.
Consider this improved implementation:
Task {
- let isActionSucceed = await viewModel.handleSaveAction()
- if isActionSucceed {
- dismiss()
- } else {
- viewModel.showSaveFailedError()
- }
+ do {
+ let isActionSucceed = try await viewModel.handleSaveAction()
+ if isActionSucceed {
+ dismiss()
+ }
+ } catch {
+ viewModel.showSaveFailedError(with: error)
+ }
}
Likely invalid or redundant comment.
Splito/UI/Home/Expense/AddExpenseViewModel.swift (1)
Line range hint 11-35
: LGTM! Well-structured property declarations.
The new properties for image handling are well-organized with appropriate access levels and follow SwiftUI conventions.
Splito/Localization/Localizable.xcstrings (1)
236-240
: LGTM: Camera access permission strings are well-defined.
The strings for camera access permissions are clear and specific to their use cases (expenses and profile pictures).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Outside diff range and nitpick comments (3)
Splito/UI/Home/Expense/AddExpenseView.swift (3)
162-176
: Add accessibility support for text inputThe text input implementation should include accessibility labels and hints for better VoiceOver support.
Consider these accessibility enhancements:
TextField("Enter a description", text: $name) .font(.subTitle2()) .foregroundStyle(primaryText) + .accessibilityLabel("Expense description") + .accessibilityHint("Enter a description for your expense") .keyboardType(.default)
204-233
: Add haptic feedback for better user interactionConsider adding haptic feedback when the user interacts with the date picker or image selection buttons.
Add a feedback generator:
private struct BottomCardView: View { + private let feedback = UIImpactFeedbackGenerator(style: .light) // ... existing properties ... var body: some View { HStack(spacing: 16) { Spacer() DatePickerView(date: $date, isForAddExpense: true) .onTapGesture { + feedback.impactOccurred() } ExpenseImagePickerView(image: expenseImage, imageUrl: expenseImageUrl) .onTapGesture { + feedback.impactOccurred() handleImageBtnTap() } } } }
288-296
: Optimize image caching strategyThe current Kingfisher implementation could benefit from additional caching options for better performance.
Consider these optimizations:
KFImage(url) .placeholder { _ in ImageLoaderView() } .setProcessor(DownsamplingImageProcessor(size: UIScreen.main.bounds.size)) .cacheMemoryOnly() + .memoryCacheExpiration(.days(7)) + .fadeTransition(.linear(0.3)) + .onFailure { error in + print("Image loading failed: \(error.localizedDescription)") + } .resizable() .aspectRatio(contentMode: .fill)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (1)
Splito/UI/Home/Expense/AddExpenseView.swift
(5 hunks)
🔇 Additional comments (2)
Splito/UI/Home/Expense/AddExpenseView.swift (2)
109-141
: LGTM! Well-structured view implementation
The ExpenseInfoView implementation is clean, consistent, and follows SwiftUI best practices with proper spacing and layout.
92-98
: 🛠️ Refactor suggestion
Enhance error handling in save action
The current implementation could benefit from more specific error handling and user feedback.
Consider this improved implementation:
Task {
- let isActionSucceed = await viewModel.handleSaveAction()
- if isActionSucceed {
- dismiss()
- } else {
- viewModel.showSaveFailedError()
- }
+ do {
+ let isActionSucceed = try await viewModel.handleSaveAction()
+ if isActionSucceed {
+ dismiss()
+ }
+ } catch let error as ExpenseError {
+ viewModel.showSaveFailedError(with: error.localizedDescription)
+ } catch {
+ viewModel.showSaveFailedError(with: "Failed to save expense. Please try again.")
+ }
}
Likely invalid or redundant comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Outside diff range and nitpick comments (7)
Splito/UI/Home/Expense/AddExpenseView.swift (3)
78-82
: Add error handling for image picker failuresWhile the image picker implementation is good, consider adding error handling for cases where the image picker fails or user denies camera/photo library permissions.
.sheet(isPresented: $viewModel.showImagePicker) { ImagePickerView(cropOption: .square, sourceType: !viewModel.sourceTypeIsCamera ? .photoLibrary : .camera, - image: $viewModel.expenseImage, isPresented: $viewModel.showImagePicker) + image: $viewModel.expenseImage, + isPresented: $viewModel.showImagePicker, + onError: { error in + viewModel.handleImagePickerError(error) + }) }
303-332
: Add loading state handling for image displayConsider adding a loading state indicator while the image is being prepared for display, especially for remote images.
struct ImageDisplayView: View { + @State private var isLoading = false var image: UIImage? var imageUrl: String? @Namespace var animationNamespace var body: some View { GeometryReader { geometry in ZStack { + if isLoading { + ProgressView() + .progressViewStyle(.circular) + } if #available(iOS 18.0, *) { if let image { ZoomableImageView(image: image, geometry: geometry) .matchedGeometryEffect(id: "image", in: animationNamespace) .navigationTransition(.zoom(sourceID: "zoom", in: animationNamespace)) } else if let imageUrl { ZoomableImageView(imageUrl: imageUrl, geometry: geometry) + .onAppear { isLoading = true } + .onSuccess { _ in isLoading = false } + .onFailure { _ in isLoading = false } .matchedGeometryEffect(id: "image", in: animationNamespace) .navigationTransition(.zoom(sourceID: "zoom", in: animationNamespace)) }
286-293
: Optimize memory management for image loadingWhile Kingfisher's
cacheMemoryOnly()
is used, consider adding more aggressive memory management:
- Add memory warning handlers to clear the image cache
- Consider implementing a maximum cache size
- Add image size limits for uploads
Example implementation:
// Add to your app delegate or scene delegate NotificationCenter.default.addObserver( forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main ) { _ in ImageCache.default.clearMemoryCache() } // Configure cache size ImageCache.default.memoryStorage.config.totalCostLimit = 300 * 1024 * 1024 // 300MBBaseStyle/BaseStyle/Views/ZoomableImageView.swift (4)
51-55
: Avoid passingGeometryProxy
to the initializerPassing
GeometryProxy
as a parameter is unconventional in SwiftUI and may lead to issues. Consider usingGeometryReader
within thebody
to access geometry information.Refactored code:
public init(image: UIImage? = nil, imageUrl: String? = nil) { self.image = image self.imageUrl = imageUrl - self.geometry = geometry } public var body: some View { + GeometryReader { geometry in ZStack { // Use geometry.size where needed } + } }
59-95
: Reduce code duplication in image view configurationThere is significant code duplication when configuring the image view for local and remote images. To improve maintainability, consider extracting common modifiers and gestures into a shared extension or applying them after the conditional blocks.
Example refactor:
public var body: some View { GeometryReader { geometry in let imageView: AnyView if let image = image { imageView = AnyView( Image(uiImage: image) .resizable() .scaledToFit() ) loadedImage = image } else if let imageUrl = imageUrl, let url = URL(string: imageUrl) { imageView = AnyView( KFImage(url) .placeholder { _ in ImageLoaderView() } .setProcessor(DownsamplingImageProcessor(size: UIScreen.main.bounds.size)) .cacheMemoryOnly() .onSuccess { result in loadedImage = result.image } .resizable() .scaledToFit() ) } else { imageView = AnyView(EmptyView()) } imageView .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() } ) } }
124-124
: Remove trailing whitespaceLine 124 contains trailing whitespace as indicated by the static analysis tool. Please remove the unnecessary whitespace to comply with code style guidelines.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 124-124: Lines should not have trailing whitespace
(trailing_whitespace)
23-23
: InitializeloadedImage
appropriatelyThe
@State
propertyloadedImage
is initialized with an emptyUIImage()
, which may not be necessary and could lead to unintended behavior. Consider makingloadedImage
optional (UIImage?
) and initializing it when needed.Apply this change:
- @State var loadedImage: UIImage = UIImage() + @State var loadedImage: UIImage?Ensure to handle cases where
loadedImage
might benil
in your code.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (4)
BaseStyle/BaseStyle.xcodeproj/project.pbxproj
(5 hunks)BaseStyle/BaseStyle/Views/ZoomableImageView.swift
(1 hunks)Splito/UI/Home/Expense/AddExpenseView.swift
(5 hunks)Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift
(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Splito/UI/Home/Expense/Expense Detail/ExpenseDetailsView.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift
[Warning] 124-124: Lines should not have trailing whitespace
(trailing_whitespace)
🔇 Additional comments (4)
Splito/UI/Home/Expense/AddExpenseView.swift (2)
109-141
: LGTM! Well-structured view composition
The ExpenseInfoView provides a clean separation of concerns and consistent UI structure using ExpenseDetailRow components.
11-11
: LGTM! Consider specifying Kingfisher version
The integration of Kingfisher for image handling is appropriate. However, it's recommended to specify the version in the package dependencies to ensure consistency.
Also applies to: 38-41
BaseStyle/BaseStyle.xcodeproj/project.pbxproj (2)
12-12
: LGTM: ZoomableImageView integration looks good.
The ZoomableImageView.swift file is properly integrated into the project structure and build phases, which aligns with the PR objective of adding expense attachment support.
Let's verify the implementation details:
Also applies to: 75-75, 509-509
✅ Verification successful
Implementation of ZoomableImageView is well-designed and complete
The implementation includes all necessary features for expense attachment viewing:
- Proper zoom functionality with pinch-to-zoom gesture (1x to 4x scale)
- Pan/drag gesture for moving zoomed images
- Double-tap to toggle between normal and zoomed states
- Handles both local UIImage and remote URLs via Kingfisher
- Maintains image bounds within screen limits
- Smooth animations and proper gesture handling
- Image caching and downsampling for performance
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check the implementation of ZoomableImageView
# Expected: SwiftUI view with zoom/pan gestures and image loading capabilities
# Search for the implementation
ast-grep --pattern 'struct ZoomableImageView: View {
$$$
}'
# Check for gesture recognizers
rg -A 5 'gesture|zoom|scale|drag' ZoomableImageView.swift
# Check for image loading
rg -A 5 'Image|UIImage|URL' ZoomableImageView.swift
Length of output: 305
Script:
#!/bin/bash
# Let's first locate the ZoomableImageView.swift file
fd ZoomableImageView.swift
# Then check its content if found
fd ZoomableImageView.swift -x cat {}
Length of output: 5738
15-15
: Clarify the purpose of RestoreButton.
While the file is properly integrated, its purpose seems unrelated to expense attachment functionality. Please clarify how this component contributes to the PR's objective.
Let's check its implementation and usage:
Also applies to: 78-78, 533-533
✅ Verification successful
RestoreButton is a reusable component for restoring deleted items
The RestoreButton component is a generic UI button used for restoring deleted items (both expenses and payments) in the application. Its implementation shows it's a simple SwiftUI button that displays "Restore" text and triggers a provided callback. In ExpenseDetailsView, it's used to implement the restore functionality for deleted expenses, which is relevant to the expense management functionality.
- Used in ExpenseDetailsView with
handleRestoreButtonAction
that prompts a confirmation alert before restoring an expense - Also reused in transaction-related views for restoring payments
- Implementation is clean and follows SwiftUI best practices
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Investigate RestoreButton's purpose and usage
# Expected: UI component potentially used in expense attachment flow
# Search for the implementation
ast-grep --pattern 'struct RestoreButton: View {
$$$
}'
# Check for usage in expense-related files
rg -l 'RestoreButton' | rg -i 'expense'
Length of output: 228
Script:
#!/bin/bash
# Let's check the implementation and usage context of RestoreButton in expense-related files
# Check RestoreButton implementation
rg -A 10 "RestoreButton" ".*\.swift$"
# Specifically check its usage in expense detail files
rg -A 5 "RestoreButton" "ExpenseDetailsView.swift" "ExpenseDetailsViewModel.swift"
Length of output: 302
Script:
#!/bin/bash
# Let's try a different approach to find and analyze RestoreButton
# Find all Swift files containing RestoreButton
fd -e swift -x rg -A 10 "RestoreButton" {}
# Find the specific expense detail files
fd -e swift -x echo {} \; | grep -i "expense.*detail"
Length of output: 2623
#Conflicts: # Data/Data/Repository/GroupRepository.swift # Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift # Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentViewModel.swift
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Outside diff range and nitpick comments (7)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift (4)
11-24
: Add documentation for public API.As this is a public struct, please add documentation comments explaining the purpose, parameters, and usage of the view.
Add documentation like this:
+/// A SwiftUI view that displays an image with zoom and pan capabilities. +/// +/// Example usage: +/// ```swift +/// GeometryReader { geometry in +/// ZoomableImageView(image: myImage, geometry: geometry) +/// } +/// ``` public struct ZoomableImageView: View {
23-23
: Consider makingloadedImage
optional.Initialize
loadedImage
as an optional to better represent its state before an image is loaded.-@State var loadedImage: UIImage = UIImage() +@State var loadedImage: UIImage?
60-99
: Reduce code duplication in view modifiers.The gesture, animation, and positioning modifiers are duplicated between local and remote image views. Consider extracting them into a helper function.
+private func applyImageModifiers<T: View>(_ view: T) -> some View { + view + .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() + })) +} public var body: some View { ZStack { if let image = image { - Image(uiImage: image) + applyImageModifiers(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() - }))) + .scaledToFit())
127-127
: Remove trailing whitespace.Remove the trailing whitespace at the end of this line.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 127-127: Lines should not have trailing whitespace
(trailing_whitespace)
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift (2)
189-189
: Simplify maximumDate calculationThe current calculation of maximumDate can be simplified. Adding 0 years is unnecessary.
- private let maximumDate = Calendar.current.date(byAdding: .year, value: 0, to: Date()) ?? Date() + private let maximumDate = Date()
211-215
: Replace onTapGestureForced with standard gestureUsing forced tap gesture might interfere with accessibility features. Consider using the standard onTapGesture instead.
- .onTapGestureForced { + .onTapGesture { tempDate = date showDatePicker = true UIApplication.shared.endEditing() }Splito/UI/Home/Expense/AddExpenseView.swift (1)
204-233
: Consider adding accessibility labels for better VoiceOver supportThe bottom card implementation looks good, but could benefit from improved accessibility.
Add accessibility identifiers and labels:
HStack(spacing: 16) { Spacer() - DatePickerView(date: $date, isForAddExpense: true) + DatePickerView(date: $date, isForAddExpense: true) + .accessibilityLabel("Expense date picker") - ExpenseImagePickerView(image: expenseImage, imageUrl: expenseImageUrl, handleImageBtnTap: handleExpenseImageTap) + ExpenseImagePickerView(image: expenseImage, imageUrl: expenseImageUrl, handleImageBtnTap: handleExpenseImageTap) + .accessibilityLabel("Expense image picker") }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (4)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift
(1 hunks)Data/Data/Repository/GroupRepository.swift
(3 hunks)Splito/UI/Home/Expense/AddExpenseView.swift
(6 hunks)Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Data/Data/Repository/GroupRepository.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift
[Warning] 127-127: Lines should not have trailing whitespace
(trailing_whitespace)
🔇 Additional comments (7)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift (1)
111-143
: 🛠️ Refactor suggestion
Add null safety for loadedImage
access.
If loadedImage
is made optional as suggested earlier, ensure proper null safety in fixOffsetAndScale
.
private func fixOffsetAndScale(geometry: GeometryProxy) {
+ guard let image = loadedImage else { return }
let newScale: CGFloat = .minimum(.maximum(scale, 1), 4)
let screenSize = geometry.size
let originalScale = loadedImage.size.width / loadedImage.size.height >= screenSize.width / screenSize.height ?
geometry.size.width / loadedImage.size.width :
geometry.size.height / loadedImage.size.height
Likely invalid or redundant comment.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 127-127: Lines should not have trailing whitespace
(trailing_whitespace)
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift (3)
158-158
: Previous currency formatting improvements are still applicable
The previous review comment about enhancing currency formatting robustness is still valid. The current implementation needs improvements in decimal validation, maximum amount validation, and currency symbol handling.
248-268
: LGTM! Clean implementation of date display
The DateDisplayView implementation is well-structured with good accessibility considerations and clear visual hierarchy.
216-245
: Verify sheet presentation lifecycle
The sheet presentation might cause memory leaks if not properly managed. Let's verify the implementation across the codebase.
✅ Verification successful
Sheet presentation implementation is safe and follows SwiftUI lifecycle patterns
The verification of sheet presentations across the codebase shows:
- All sheet presentations use
isPresented
binding, which properly manages the presentation lifecycle - The date picker sheet in
GroupPaymentView
follows the same pattern as other sheets in the codebase - Memory management is handled automatically by SwiftUI's view lifecycle:
- Sheets are dismissed through binding state changes
- Parent views properly clean up resources in
onDisappear
anddeinit
where needed - No manual retain cycle management is needed for simple sheet presentations
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for potential memory leaks in sheet presentations
# Look for patterns where sheet modifiers are used without proper cleanup
# Search for sheet modifier usage
rg "\.sheet\(" -A 5
# Search for cleanup patterns
rg "onDisappear|deinit" -A 3
Length of output: 12617
Splito/UI/Home/Expense/AddExpenseView.swift (3)
11-11
: LGTM! Good choice using Kingfisher for image handling
Kingfisher is an excellent choice for efficient image loading and caching.
303-346
: 🛠️ Refactor suggestion
Consider implementing image cleanup on dismiss
The image display view handles layout and transitions well, but should manage memory explicitly.
Add cleanup logic when the view is dismissed:
struct ImageDisplayView: View {
@Environment(\.dismiss) var dismiss
var image: UIImage?
var imageUrl: String?
+ @State private var loadedImage: UIImage?
@Namespace var animationNamespace
var body: some View {
GeometryReader { geometry in
// ... existing implementation ...
}
.navigationBarBackButtonHidden()
+ .onDisappear {
+ // Clear loaded image when view disappears
+ loadedImage = nil
+ }
}
}
Likely invalid or redundant comment.
78-82
: 🛠️ Refactor suggestion
Verify image size limits before upload
The image picker implementation should verify image sizes before upload to prevent performance issues.
Consider adding size validation:
ImagePickerView(cropOption: .square,
sourceType: !viewModel.sourceTypeIsCamera ? .photoLibrary : .camera,
- image: $viewModel.expenseImage,
+ image: Binding(
+ get: { viewModel.expenseImage },
+ set: { newImage in
+ if let imageData = newImage?.jpegData(compressionQuality: 0.8),
+ imageData.count <= 5 * 1024 * 1024 { // 5MB limit
+ viewModel.expenseImage = newImage
+ } else {
+ // Show error for oversized image
+ viewModel.showAlert = true
+ }
+ }
+ ),
isPresented: $viewModel.showImagePicker)
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Outside diff range and nitpick comments (11)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift (2)
60-99
: Consider extracting common view modifiers to reduce duplication.The view modifiers for both local and remote images are identical. Consider extracting them into a helper function or view extension.
Here's a suggested refactoring:
public var body: some View { ZStack { - if let image = 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 = 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) - .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() - })) + Group { + if let image = image { + Image(uiImage: image) + .resizable() + .scaledToFit() + } else if let imageUrl = 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) + .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() + })) } }
127-127
: Remove trailing whitespace.There is trailing whitespace on this line.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 127-127: Lines should not have trailing whitespace
(trailing_whitespace)
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift (2)
191-192
: Optimize date picker state managementThe current implementation:
- Uses separate tempDate state which might cause unnecessary redraws
- The DatePicker is recreated on each tempDate change due to the id modifier
Consider this optimization:
- @State private var tempDate: Date - @State private var showDatePicker = false + @State private var showDatePicker = false { + didSet { + if showDatePicker { + tempDate = date + } + } + } + private var tempDate: Date = Date() // In the DatePicker view - DatePicker("", selection: $tempDate, in: ...maximumDate, displayedComponents: .date) - .id(tempDate) + DatePicker("", selection: $tempDate, in: ...maximumDate, displayedComponents: .date)Also applies to: 227-232
254-262
: Add accessibility supportThe date display needs accessibility improvements:
- "Today" text should be localized
- Calendar icon needs accessibility label
- Text(date.isToday() ? "Today" : date.shortDate) + Text(date.isToday() ? "Today".localized : date.shortDate) Image(.calendarIcon) .resizable() .scaledToFit() .frame(width: 24, height: 24) + .accessibility(label: Text("Calendar".localized))Splito/UI/Home/Expense/Expense Split Option/ExpenseSplitOptionsTabView.swift (1)
Line range hint
233-251
: Consider refactoring for improved maintainability.The initialization and value calculation logic could be simplified for better maintainability.
Consider these improvements:
- Group related parameters using a configuration struct:
struct MemberCellConfiguration { let suffixText: String let formatString: String let inputFieldWidth: Double // ... other configuration parameters }
- Extract the calculated value logic into a computed property:
private var calculatedAmount: Double { guard let total = totalValue, total != 0 else { return 0 } return (expenseAmount * Double(value)) / total }This would make the code more maintainable and easier to test.
Splito/UI/Home/Groups/Group/Group Options/Transactions/GroupTransactionListView.swift (2)
60-60
: Consider caching the sorted months.The
sorted(by: sortMonthYearStrings)
operation is being performed twice - once forlastMonth
and previously forfirstMonth
. This could be optimized by storing the sorted result.- let firstMonth = viewModel.filteredTransactions.keys.sorted(by: sortMonthYearStrings).first - let lastMonth = viewModel.filteredTransactions.keys.sorted(by: sortMonthYearStrings).last + let sortedMonths = viewModel.filteredTransactions.keys.sorted(by: sortMonthYearStrings) + let firstMonth = sortedMonths.first + let lastMonth = sortedMonths.last
67-67
: Extract the magic number into a constant.The padding value of 50 should be defined as a named constant to improve code maintainability and make the purpose of this specific padding value clear.
+ private let lastItemBottomPadding: CGFloat = 50 + var body: some View { GeometryReader { geometry in ScrollViewReader { scrollProxy inThen update the padding line:
- .padding(.bottom, (month == lastMonth && viewModel.filteredTransactions[month]?.last?.transaction.id == transaction.transaction.id) ? 50 : 0) + .padding(.bottom, (month == lastMonth && viewModel.filteredTransactions[month]?.last?.transaction.id == transaction.transaction.id) ? lastItemBottomPadding : 0)Splito/UI/Home/Expense/AddExpenseView.swift (4)
78-82
: Consider enhancing image picker error handlingWhile the image picker implementation is functional, consider adding error handling for cases where the user denies camera/photo library access.
.sheet(isPresented: $viewModel.showImagePicker) { ImagePickerView(cropOption: .square, - sourceType: !viewModel.sourceTypeIsCamera ? .photoLibrary : .camera, + sourceType: viewModel.sourceTypeIsCamera ? .camera : .photoLibrary, image: $viewModel.expenseImage, isPresented: $viewModel.showImagePicker) + .onDisappear { + if viewModel.expenseImage == nil { + // Handle image selection failure or cancellation + viewModel.handleImageSelectionError() + } + } }
204-233
: Add accessibility support for image selectionThe BottomCardView implementation looks good, but consider adding accessibility labels and hints for the image selection functionality.
ExpenseImagePickerView(image: expenseImage, imageUrl: expenseImageUrl, handleImageBtnTap: handleExpenseImageTap) + .accessibilityLabel("Expense image") + .accessibilityHint("Double tap to add or change expense image")
235-268
: Enhance user experience with haptic feedbackConsider adding haptic feedback when the user taps the camera icon for a better tactile experience.
.onTouchGesture { + UIImpactFeedbackGenerator(style: .light).impactOccurred() handleImageBtnTap() }
303-344
: Add loading state for image displayConsider showing a loading indicator while the image is being prepared for display, especially for remote images.
if let imageUrl { ZoomableImageView(imageUrl: imageUrl, geometry: geometry) + .overlay { + if viewModel.isLoadingImage { + ProgressView() + .progressViewStyle(.circular) + } + } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
📒 Files selected for processing (6)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift
(1 hunks)Data/Data/Repository/GroupRepository.swift
(3 hunks)Splito/UI/Home/Expense/AddExpenseView.swift
(6 hunks)Splito/UI/Home/Expense/Expense Split Option/ExpenseSplitOptionsTabView.swift
(4 hunks)Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift
(1 hunks)Splito/UI/Home/Groups/Group/Group Options/Transactions/GroupTransactionListView.swift
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- Data/Data/Repository/GroupRepository.swift
🧰 Additional context used
🪛 SwiftLint (0.57.0)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift
[Warning] 127-127: Lines should not have trailing whitespace
(trailing_whitespace)
🔇 Additional comments (9)
BaseStyle/BaseStyle/Views/ZoomableImageView.swift (1)
26-49
: Previous review comment is still applicable.
The gesture handlers should store their final values before calling fixOffsetAndScale
.
Splito/UI/Home/Groups/Group/Group Options/Settle up/Payment/GroupPaymentView.swift (3)
158-158
: Enhance currency formatting implementation
The previous review comment about improving currency formatting robustness is still valid. The current implementation needs:
- Decimal point validation
- Maximum amount validation
- Better currency symbol handling
Please refer to the previous review comment for the detailed implementation suggestion.
173-173
: Add missing isForAddExpense parameter
The DatePickerView is instantiated without the isForAddExpense parameter. While it has a default value, it should be explicitly set to false for payment flows to maintain consistency.
189-189
: Review date range constraints
The maximum date constraint only considers the current year. Consider:
- Adding a minimum date constraint
- Making the date range configurable based on business requirements
Splito/UI/Home/Expense/Expense Split Option/ExpenseSplitOptionsTabView.swift (2)
220-221
: LGTM! Consistent implementation of user identification.
The SplitoPreference integration matches the implementation in ExpenseMemberCellView, maintaining consistency across the codebase.
Also applies to: 256-256
Line range hint 103-116
: LGTM! Verify preference injection setup.
The personalization of member display using SplitoPreference
is well implemented. The optional chaining in the comparison member.id == preference.user?.id
safely handles the case when no user is set.
Let's verify the dependency injection setup:
✅ Verification successful
SplitoPreference injection is properly configured
The verification shows that SplitoPreference
is correctly registered in the DI container (AppAssembly.swift
) with container scope and is consistently injected across the app, including in ExpenseSplitOptionsTabView.swift
. The implementation follows the established dependency injection pattern used throughout the codebase.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if SplitoPreference is properly registered in the DI container
rg -A 5 "SplitoPreference" --type swift
Length of output: 24086
Splito/UI/Home/Expense/AddExpenseView.swift (3)
117-118
: LGTM! Clean and consistent implementation
The ExpenseDetailRow usage follows a consistent pattern with proper parameter ordering and clear naming.
Also applies to: 121-122, 131-135
270-301
: Previous review comment about error handling is still applicable
286-294
: Verify memory usage patterns for large images
While Kingfisher's downsampling processor is used, it's important to verify memory usage patterns with large images.
✅ Verification successful
Let me gather more information about the image handling implementation to ensure a comprehensive verification.
Memory management for images follows consistent patterns
The codebase demonstrates a consistent and well-implemented approach to image memory management:
- All image loading uses KFImage with DownsamplingImageProcessor to reduce memory footprint
- Memory-only caching is consistently applied across image views
- Custom UIImage extension includes size management utilities
- ZoomableImageView and other image views follow the same memory optimization patterns
The implementation in AddExpenseView.swift aligns with the project's image handling best practices and doesn't require additional memory optimizations.
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Search for other image processing implementations to ensure consistent memory management
rg -g '*.swift' -B 2 -A 2 'DownsamplingImageProcessor|UIImage.*scale|imageWith|CGImage'
Length of output: 1067
Script:
#!/bin/bash
# Search for KFImage usage patterns and caching configurations
rg -g '*.swift' -B 3 -A 3 'KFImage.*|ImageCache.*|\.cache'
# Look for any custom image processing or memory management utilities
rg -g '*.swift' -B 2 -A 2 'class.*Image|struct.*Image|extension.*Image'
Length of output: 11344
Summary by CodeRabbit
Release Notes
New Features
shortDate
,monthWithYear
, andlongDate
.Improvements
checkCameraPermission
method to ensure safer URL handling.Bug Fixes
NavigationBarTopView
from "Spilt options" to "Split options".Style
Localization