diff --git a/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift b/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift index 7041403..e23cc87 100644 --- a/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift +++ b/Data/Sources/Repositories/Default/RealmClimbRecordRepositoryImpl.swift @@ -185,7 +185,11 @@ public final class RealmClimbRecordRepositoryImpl: ClimbRecordRepository { try realm.write { realm.delete(record.images) if let mountain = record.mountain { - realm.delete(mountain) + let otherRecords = realm.objects(ClimbRecordRealm.self) + .filter("mountain == %@", mountain) + if otherRecords.count <= 1 { + realm.delete(mountain) + } } realm.delete(record.timeLog) realm.delete(record) diff --git a/Domain/Sources/Entity/ImageItem.swift b/Domain/Sources/Entity/ImageItem.swift new file mode 100644 index 0000000..58fb72f --- /dev/null +++ b/Domain/Sources/Entity/ImageItem.swift @@ -0,0 +1,25 @@ +// +// ImageItem.swift +// Domain +// +// Created by 김영훈 on 1/31/26. +// + +import Foundation + +public struct ImageItem: Hashable { + let id = UUID() + public let data: Data + + public init(data: Data) { + self.data = data + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: ImageItem, rhs: ImageItem) -> Bool { + return lhs.id == rhs.id + } +} diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift b/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift index 61a163d..3284f6b 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/AddClimbRecord/AddClimbRecordViewController.swift @@ -230,7 +230,7 @@ extension AddClimbRecordViewController: UITableViewDelegate { private func applySnapshot(mountains: [Mountain]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(mountains) + snapshot.appendItems(mountains.uniq(by: \.id)) dataSource.apply(snapshot, animatingDifferences: false) } } diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift index 514b6ed..3d0c786 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewController.swift @@ -288,23 +288,23 @@ extension ClimbRecordDetailViewController { case main } - private func createRegistration() -> UICollectionView.CellRegistration { - return UICollectionView.CellRegistration { cell, indexPath, item in + private func createRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in cell.setImage(imageData: item) } } - private func createDataSource() -> UICollectionViewDiffableDataSource { + private func createDataSource() -> UICollectionViewDiffableDataSource { let registration = createRegistration() - return UICollectionViewDiffableDataSource(collectionView: mainView.imageCollectionView) { collectionView, indexPath, item in + return UICollectionViewDiffableDataSource(collectionView: mainView.imageCollectionView) { collectionView, indexPath, item in collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: item) } } - private func applySnapshot(images: [Data]) { - var snapshot = NSDiffableDataSourceSnapshot() + private func applySnapshot(images: [ImageItem]) { + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) - snapshot.appendItems(images) + snapshot.appendItems(images.uniq(by: \.self)) dataSource.apply(snapshot, animatingDifferences: true) } } diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift index 6b5cd8e..4bbca7a 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordDetail/ClimbRecordDetailViewModel.swift @@ -78,7 +78,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { let timelineButtonTitle: AnyPublisher let presentPhotoActionSheet: AnyPublisher let saveCompleted: AnyPublisher - let imagesFetched: AnyPublisher<[Data], Never> + let imagesFetched: AnyPublisher<[ImageItem], Never> let presentImageDeleteAlert: AnyPublisher let placeholderState: AnyPublisher<(isPlaceholder: Bool, text: String), Never> } @@ -91,7 +91,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { let pushVCSubject = PassthroughSubject() let errorMesssageSubject = PassthroughSubject<(String, String), Never>() let presentPhotoActionSheetSubject = PassthroughSubject() - let imagesFetchedSubject = PassthroughSubject<[Data], Never>() + let imagesFetchedSubject = PassthroughSubject<[ImageItem], Never>() let presentImageDeleteAlertSubject = PassthroughSubject() let isPlaceholderSubject = PassthroughSubject() let commentTextSubject = PassthroughSubject() @@ -203,7 +203,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { // Add 화면이면 pendingImages 반환 if isFromAddRecord { - imagesFetchedSubject.send(pendingImages) + imagesFetchedSubject.send(pendingImages.map { ImageItem(data: $0 )}) return } @@ -224,7 +224,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { return nil } } - imagesFetchedSubject.send(imageDatas) + imagesFetchedSubject.send(imageDatas.map { ImageItem(data: $0 )}) } .store(in: &self.cancellables) } @@ -264,7 +264,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { .sink { [weak self] imageData in guard let self else { return } pendingImages.append(imageData) - imagesFetchedSubject.send(pendingImages) + imagesFetchedSubject.send(pendingImages.map { ImageItem(data: $0 )}) } .store(in: &cancellables) @@ -319,7 +319,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { .eraseToAnyPublisher() } .sink { imageDatas in - imagesFetchedSubject.send(imageDatas) + imagesFetchedSubject.send(imageDatas.map { ImageItem(data: $0 )}) } .store(in: &cancellables) @@ -366,7 +366,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { index < pendingImages.count { pendingImages.remove(at: index) } - imagesFetchedSubject.send(pendingImages) + imagesFetchedSubject.send(pendingImages.map { ImageItem(data: $0 )}) } .store(in: &cancellables) @@ -414,7 +414,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel { .eraseToAnyPublisher() } .sink { imageDatas in - imagesFetchedSubject.send(imageDatas) + imagesFetchedSubject.send(imageDatas.map { ImageItem(data: $0 )}) } .store(in: &cancellables) diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift index cc1ab38..8e05ae1 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordCollectionViewCell.swift @@ -97,7 +97,7 @@ final class ClimbRecordCollectionViewCell: BaseCollectionViewCell { } // 데이터 정보 표기 - final func setData(_ data: ClimbRecord, isFirstVisit: Bool, imageDatas: [Data] = []) { + final func setData(_ data: ClimbRecord, isFirstVisit: Bool, imageDatas: [ImageItem] = []) { let name = data.mountain.name mountainImageView.image = getMountainImage(date: data.climbDate) @@ -129,7 +129,7 @@ final class ClimbRecordCollectionViewCell: BaseCollectionViewCell { // 로딩 중: placeholder imageCollectionView.isHidden = false commentLabel.isHidden = true - applySnapshot(images: [Data()]) // 빈 Data로 indicator 표시 + applySnapshot(images: [ImageItem(data: Data())]) // 빈 Data로 indicator 표시 } else { // 이미지 있음 imageCollectionView.isHidden = false @@ -315,23 +315,23 @@ extension ClimbRecordCollectionViewCell { return UICollectionViewCompositionalLayout(section: section) } - private func createRegistration() -> UICollectionView.CellRegistration { - return UICollectionView.CellRegistration { cell, indexPath, item in - cell.setImage(imageData: item) + private func createRegistration() -> UICollectionView.CellRegistration { + return UICollectionView.CellRegistration { cell, indexPath, item in + cell.setImage(imageData: item.data) } } - private func createDataSource() -> UICollectionViewDiffableDataSource { + private func createDataSource() -> UICollectionViewDiffableDataSource { let registration = createRegistration() - return UICollectionViewDiffableDataSource(collectionView: imageCollectionView) { collectionView, indexPath, item in + return UICollectionViewDiffableDataSource(collectionView: imageCollectionView) { collectionView, indexPath, item in collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: item) } } - private func applySnapshot(images: [Data]) { - var snapshot = NSDiffableDataSourceSnapshot() + private func applySnapshot(images: [ImageItem]) { + var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) - snapshot.appendItems(images) + snapshot.appendItems(images.uniq(by: \.self)) dataSource.apply(snapshot, animatingDifferences: false) } } diff --git a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift index 6473a42..ced847b 100644 --- a/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift +++ b/Presentation/Sources/Scenes/ClimbRecordScene/RecordList/ClimbRecordListViewController.swift @@ -170,7 +170,7 @@ extension ClimbRecordListViewController: UICollectionViewDelegate, UICollectionV .min { $0.climbDate < $1.climbDate } let isFirstVisit = firstRecordOfMountain?.id == climbRecord.id - let imageDatas = viewModel.recordImageDatas[climbRecord.id] ?? [] + let imageDatas = viewModel.recordImageDatas[climbRecord.id]?.map { ImageItem(data: $0) } ?? [] cell.setImages(row: indexPath.item, total: viewModel.climbRecordList.count) cell.setData(climbRecord, isFirstVisit: isFirstVisit, imageDatas: imageDatas) diff --git a/Presentation/Sources/Scenes/MapScene/MapViewController.swift b/Presentation/Sources/Scenes/MapScene/MapViewController.swift index 2e063e3..81cf784 100644 --- a/Presentation/Sources/Scenes/MapScene/MapViewController.swift +++ b/Presentation/Sources/Scenes/MapScene/MapViewController.swift @@ -215,7 +215,7 @@ extension MapViewController: UICollectionViewDelegate { private func applySnapshot(mountains: [MountainDistance]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections(Section.allCases) - snapshot.appendItems(mountains) + snapshot.appendItems(mountains.uniq(by: \.self)) dataSource.apply(snapshot, animatingDifferences: true) } diff --git a/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift b/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift index 447827d..9b188f8 100644 --- a/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift +++ b/Presentation/Sources/Scenes/MeasureScene/MeasureViewController.swift @@ -358,7 +358,7 @@ extension MeasureViewController: UITableViewDelegate { private func applySnapshot(mountains: [MountainInfo]) { var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) - snapshot.appendItems(mountains) + snapshot.appendItems(mountains.uniq(by: \.id)) dataSource.apply(snapshot, animatingDifferences: false) } } diff --git a/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift b/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift index c02d782..419b81a 100644 --- a/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift +++ b/Presentation/Sources/Utils/CustomViews/ImageCollectionViewCell.swift @@ -6,6 +6,7 @@ // import UIKit +import Domain import SnapKit import Kingfisher @@ -19,8 +20,8 @@ final class ImageCollectionViewCell: BaseCollectionViewCell { return imageView }() - func setImage(imageData: Data) { - if let image = UIImage(data: imageData) { + func setImage(imageData: ImageItem) { + if let image = UIImage(data: imageData.data) { imageView.image = image } else { imageView.image = nil diff --git a/Presentation/Sources/Utils/Extensions/Array+Extension.swift b/Presentation/Sources/Utils/Extensions/Array+Extension.swift new file mode 100644 index 0000000..7668e7a --- /dev/null +++ b/Presentation/Sources/Utils/Extensions/Array+Extension.swift @@ -0,0 +1,13 @@ +// +// Array+Extension.swift +// Presentation +// +// Created by 김영훈 on 1/31/26. +// + +extension Array { + func uniq(by keyPath: KeyPath) -> [Element] { + var set = Set() + return filter { set.insert($0[keyPath: keyPath]).inserted } + } +} diff --git a/Project.swift b/Project.swift index 2e64e7a..c7e20f2 100644 --- a/Project.swift +++ b/Project.swift @@ -2,7 +2,7 @@ import ProjectDescription let iOSVersion = "16.0" let teamID = "4QUWH828P3" -let appVersion = "1.4.3" +let appVersion = "1.5.0" let buildNumber = "1" let project = Project( @@ -56,7 +56,8 @@ let project = Project( "$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/GoogleService-Info.plist", "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${PRODUCT_NAME}.debug.dylib" - ] + ], + basedOnDependencyAnalysis: false ) ], dependencies: [ @@ -73,6 +74,7 @@ let project = Project( "DEBUG_INFORMATION_FORMAT": "dwarf-with-dsym", "CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION": "YES", "OTHER_LDFLAGS": "$(inherited) -ObjC", + "ENABLE_DEBUG_DYLIB": "NO", "DEVELOPMENT_TEAM": .string(teamID), "MARKETING_VERSION": .string(appVersion), "CURRENT_PROJECT_VERSION": .string(buildNumber) diff --git a/Tuist/Config.swift b/Tuist.swift similarity index 100% rename from Tuist/Config.swift rename to Tuist.swift