Skip to content
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

홈화면 책커버 Drag & Drop 구현 #103

Merged
merged 10 commits into from
Nov 30, 2024
4 changes: 4 additions & 0 deletions MemorialHouse/MHData/MHData/DTO/BookCoverDTO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import MHDomain

public struct BookCoverDTO {
let id: UUID
let order: Int
let title: String
let imageURL: String?
let color: String
Expand All @@ -11,13 +12,15 @@ public struct BookCoverDTO {

public init(
id: UUID,
order: Int,
title: String,
imageURL: String?,
color: String,
category: String?,
favorite: Bool
) {
self.id = id
self.order = order
self.title = title
self.imageURL = imageURL
self.color = color
Expand All @@ -30,6 +33,7 @@ public struct BookCoverDTO {

return BookCover(
id: self.id,
order: self.order,
title: self.title,
imageURL: self.imageURL,
color: color,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ extension CoreDataBookCoverStorage {

return BookCoverDTO(
id: id,
order: Int(bookCover.order),
title: title,
imageURL: bookCover.imageURL,
color: color,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B2082" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B2091" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="BookCategoryEntity" representedClassName="BookCategoryEntity" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="order" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
Expand All @@ -10,6 +10,7 @@
<attribute name="favorite" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="imageURL" optional="YES" attributeType="String"/>
<attribute name="order" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="title" optional="YES" attributeType="String"/>
</entity>
<entity name="BookEntity" representedClassName="BookEntity" syncable="YES" codeGenerationType="class">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct LocalBookCoverRepository: BookCoverRepository {
public func create(bookCover: BookCover) async {
let bookCoverDTO = BookCoverDTO(
id: bookCover.id,
order: bookCover.order,
title: bookCover.title,
imageURL: bookCover.imageURL,
color: bookCover.color.rawValue,
Expand Down Expand Up @@ -51,6 +52,7 @@ public struct LocalBookCoverRepository: BookCoverRepository {
public func update(id: UUID, bookCover: BookCover) async {
let bookCoverDTO = BookCoverDTO(
id: bookCover.id,
order: bookCover.order,
title: bookCover.title,
imageURL: bookCover.imageURL,
color: bookCover.color.rawValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ struct CoreDataBookCoverStorageTests {
private static let bookCovers = [
BookCoverDTO(
id: UUID(),
order: 1,
title: "test1",
imageURL: nil,
color: "pink",
category: nil,
favorite: true),
BookCoverDTO(
id: UUID(),
order: 2,
title: "test2",
imageURL: nil,
color: "blue",
category: nil,
favorite: false),
BookCoverDTO(
id: UUID(),
order: 3,
title: "test3",
imageURL: nil,
color: "beige",
Expand All @@ -41,6 +44,7 @@ struct CoreDataBookCoverStorageTests {
// Arrange
let newBookCover = BookCoverDTO(
id: UUID(),
order: 4,
title: "test4",
imageURL: nil,
color: "green",
Expand Down Expand Up @@ -83,6 +87,7 @@ struct CoreDataBookCoverStorageTests {
let oldBookCover = CoreDataBookCoverStorageTests.bookCovers[0]
let newBookCover = BookCoverDTO(
id: oldBookCover.id,
order: 4,
title: "test4",
imageURL: nil,
color: "green",
Expand Down
6 changes: 5 additions & 1 deletion MemorialHouse/MHData/MHDataTests/MHFileManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
private let testDirectory = "TestDirectory"
private let newTestDirectory = "NewTestDirectory"
private let testFileName = "testFile.txt"
private let testFileData = "Hello, File!".data(using: .utf8)!

Check warning on line 10 in MemorialHouse/MHData/MHDataTests/MHFileManagerTests.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Non-optional String -> Data Conversion Violation: Prefer non-optional `Data(_:)` initializer when converting `String` to `Data` (non_optional_string_data_conversion)

init() {
self.fileManager = MHFileManager(directoryType: .documentDirectory)
Expand All @@ -30,6 +30,7 @@
#expect(false, "\(#function) 실패함: \(error)")
}
}

@Test func test파일읽기_성공() async {
// Arrange
let path = testDirectory
Expand All @@ -46,6 +47,7 @@
#expect(false, "\(#function) 실패함: \(error)")
}
}

@Test func test없는_파일읽기_실패() async {
// Arrange
let path = testDirectory
Expand All @@ -61,6 +63,7 @@
#expect(error == .fileNotExists)
}
}

@Test func test파일삭제_성공() async {
// Arrange
let path = testDirectory
Expand All @@ -79,6 +82,7 @@
#expect(false, "\(#function) 실패함: \(error)")
}
}

@Test func test없는_파일삭제_실패() async {
// Arrange
let path = testDirectory
Expand All @@ -94,6 +98,7 @@
#expect(error == .fileDeletionFailure)
}
}

@Test func test파일이동_성공() async {
// Arrange
let path = testDirectory
Expand All @@ -114,7 +119,6 @@
}
}


deinit {
// 테스트 디렉토리와 관련된 파일 정리
let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
Expand Down
3 changes: 3 additions & 0 deletions MemorialHouse/MHDomain/MHDomain/Entity/BookCover.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import MHFoundation

public struct BookCover: Identifiable, Equatable, Sendable {
public let id: UUID
public let order: Int
public let title: String
public let imageURL: String?
public let color: BookColor
Expand All @@ -10,13 +11,15 @@ public struct BookCover: Identifiable, Equatable, Sendable {

public init(
id: UUID = .init(),
order: Int,
title: String,
imageURL: String?,
color: BookColor,
category: String?,
favorite: Bool = false
) {
self.id = id
self.order = order
self.title = title
self.imageURL = imageURL
self.color = color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public final class HomeViewController: UIViewController {
view.backgroundColor = .baseBackground
collectionView.delegate = self
collectionView.dataSource = self
collectionView.dragDelegate = self
collectionView.dropDelegate = self
Comment on lines +82 to +83
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3: delegate가 두 줄이네용

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

하나는 drag 하나는 Drop 입니당 !!

Copy link
Collaborator

Choose a reason for hiding this comment

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

호곡 헷갈렸네요 ㅎㅎ;; 좋습니다 .ᐟ.ᐟ

collectionView.register(
BookCollectionViewCell.self,
forCellWithReuseIdentifier: BookCollectionViewCell.identifier
Expand All @@ -95,7 +97,7 @@ public final class HomeViewController: UIViewController {
switch event {
case .fetchedMemorialHouseAndCategory:
self.updateMemorialHouse()
case .filteredBooks:
case .filteredBooks, .dragAndDropFinished:
self.collectionView.reloadData()
case .fetchedFailure(let errorMessage):
self.handleError(with: errorMessage)
Expand Down Expand Up @@ -285,8 +287,80 @@ extension HomeViewController: UICollectionViewDataSource {
}
}

// MARK: - UICollectionViewDragDelegate
extension HomeViewController: UICollectionViewDragDelegate {
public func collectionView(
_ collectionView: UICollectionView,
itemsForBeginning session: any UIDragSession,
at indexPath: IndexPath
) -> [UIDragItem] {
let dragItem = UIDragItem(itemProvider: NSItemProvider())
return [dragItem]
}
}

// MARK: - UICollectionViewDropDelegate
extension HomeViewController: UICollectionViewDropDelegate {
public func collectionView(
_ collectionView: UICollectionView,
dropSessionDidUpdate session: UIDropSession,
withDestinationIndexPath destinationIndexPath: IndexPath?
) -> UICollectionViewDropProposal {
guard collectionView.hasActiveDrag else { return UICollectionViewDropProposal(operation: .forbidden) }
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}

public func collectionView(
_ collectionView: UICollectionView,
performDropWith coordinator: UICollectionViewDropCoordinator
) {
var destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationIndexPath = IndexPath(item: row - 1, section: 0)
}

moveItems(
coordinator: coordinator,
destinationIndexPath: destinationIndexPath,
collectionView: collectionView
)
}

private func moveItems(
coordinator: UICollectionViewDropCoordinator,
destinationIndexPath: IndexPath,
collectionView: UICollectionView
) {
guard
coordinator.proposal.operation == .move,
let item = coordinator.items.first,
let sourceIndexPath = item.sourceIndexPath
else { return }

collectionView.performBatchUpdates { [weak self] in
guard let self else { return }
input.send(
.dragAndDropBookCover(
currentIndex: sourceIndexPath.item,
destinationIndex: destinationIndexPath.item
)
)

collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}
}
}

// MARK: - BookCategoryViewControllerDelegate
extension HomeViewController: BookCategoryViewControllerDelegate {
func categoryViewController(_ categoryViewController: BookCategoryViewController, didSelectCategory category: String) {
func categoryViewController(
_ categoryViewController: BookCategoryViewController,
didSelectCategory category: String
) {
currentCategory = category
input.send(.selectedCategory(category: category))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ public final class HomeViewModel: ViewModelType {
public enum Input {
case viewDidLoad
case selectedCategory(category: String)
case dragAndDropBookCover(currentIndex: Int, destinationIndex: Int)
}

public enum Output {
case fetchedMemorialHouseAndCategory
case filteredBooks
case fetchedFailure(String)
case dragAndDropFinished
}

private let output = PassthroughSubject<Output, Never>()
Expand Down Expand Up @@ -41,6 +43,8 @@ public final class HomeViewModel: ViewModelType {
}
case .selectedCategory(let category):
self?.filterBooks(by: category)
case .dragAndDropBookCover(let currentIndex, let destinationIndex):
self?.dragAndDropBookCover(from: currentIndex, to: destinationIndex)
}
}.store(in: &cancellables)

Expand All @@ -66,4 +70,14 @@ public final class HomeViewModel: ViewModelType {

output.send(.filteredBooks)
}

private func dragAndDropBookCover(from currentIndex: Int, to destinationIndex: Int) {
let currentBookCover = currentBookCovers[currentIndex]
let targetBookCover = currentBookCovers[destinationIndex]
bookCovers.remove(at: currentBookCover.order)
bookCovers.insert(currentBookCover, at: targetBookCover.order)
currentBookCovers.remove(at: currentIndex)
currentBookCovers.insert(currentBookCover, at: destinationIndex)
output.send(.dragAndDropFinished)
}
}
Loading