Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 59 additions & 27 deletions ONMIR/Feature/BookDetail/BookDetailViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,25 +142,25 @@ final class BookDetailViewController: UIViewController {
image: UIImage(systemName: "square.and.arrow.up")
) { [weak self] _ in
guard let self = self, let book = self.viewModel.book else { return }
self.shareBook(book)
self.shareBook(book: book)
},
UIAction(title: "Rate Book", image: UIImage(systemName: "star")) {
[weak self] _ in
guard let self = self, let book = self.viewModel.book else { return }
self.rateBook(book)
self.rateBook(book: book)
},
UIAction(title: "Edit Book", image: UIImage(systemName: "pencil")) {
[weak self] _ in
guard let self = self, let book = self.viewModel.book else { return }
self.editBook(book)
self.editBook(book: book)
},
UIAction(
title: "Delete Book",
image: UIImage(systemName: "trash"),
attributes: .destructive
) { [weak self] _ in
guard let self = self, let book = self.viewModel.book else { return }
self.deleteBook(book)
self.deleteBook(book: book)
},
]
)
Expand Down Expand Up @@ -309,7 +309,7 @@ final class BookDetailViewController: UIViewController {
AddActionCell, AddActionCell.ActionType
> { [weak self] cell, indexPath, actionType in
cell.configure(actionType: actionType) {
self?.handleAddAction(actionType)
self?.handleAddAction(actionType: actionType)
}
}

Expand Down Expand Up @@ -471,7 +471,7 @@ final class BookDetailViewController: UIViewController {
switch item {
case .readingLog(let readingLog):
let editAction = UIContextualAction(style: .normal, title: "Edit") { _, _, completion in
self.showEditReadingLog(readingLog)
self.showEditReadingLog(readingLog: readingLog)
completion(true)
}
editAction.backgroundColor = .systemBlue
Expand Down Expand Up @@ -538,7 +538,7 @@ final class BookDetailViewController: UIViewController {
_,
_,
completion in
self.showEditQuote(quote)
self.showEditQuote(quote: quote)
completion(true)
}
editAction.backgroundColor = .systemBlue
Expand Down Expand Up @@ -641,9 +641,9 @@ extension BookDetailViewController: UICollectionViewDelegate {

switch item {
case .readingLog(let readingLog):
showEditReadingLog(readingLog)
showEditReadingLog(readingLog: readingLog)
case .quote(let quote):
showEditQuote(quote)
showEditQuote(quote: quote)
default:
break
}
Expand Down Expand Up @@ -716,7 +716,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
title: "Edit",
image: UIImage(systemName: "pencil")
) { [weak self] _ in
self?.showEditReadingLog(readingLog)
self?.showEditReadingLog(readingLog: readingLog)
}

let deleteAction = UIAction(
Expand All @@ -736,7 +736,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
title: "Edit",
image: UIImage(systemName: "pencil")
) { [weak self] _ in
self?.showEditQuote(quote)
self?.showEditQuote(quote: quote)
}

let deleteAction = UIAction(
Expand Down Expand Up @@ -864,9 +864,9 @@ extension BookDetailViewController: UICollectionViewDelegate {

switch item {
case .readingLog(let readingLog):
showEditReadingLog(readingLog)
showEditReadingLog(readingLog: readingLog)
case .quote(let quote):
showEditQuote(quote)
showEditQuote(quote: quote)
default:
break
}
Expand Down Expand Up @@ -901,7 +901,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
navigationController?.pushViewController(allQuotesVC, animated: true)
}

private func handleAddAction(_ actionType: AddActionCell.ActionType) {
private func handleAddAction(actionType: AddActionCell.ActionType) {
switch actionType {
case .newRecord:
showAddNewRecord()
Expand Down Expand Up @@ -959,7 +959,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
viewModel.loadBook(with: bookObjectID)
}

private func showEditReadingLog(_ readingLog: ReadingLogEntity) {
private func showEditReadingLog(readingLog: ReadingLogEntity) {
guard let book = viewModel.book else { return }

let recordViewModel = BookRecordEditorViewModel(
Expand Down Expand Up @@ -1002,7 +1002,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
title: "Delete",
style: .destructive
) { [weak self] _ in
self?.deleteReadingLogs(readingLogs)
self?.deleteReadingLogs(readingLogs: readingLogs)
}

let cancelAction = UIAlertAction(
Expand All @@ -1016,7 +1016,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
present(alert, animated: true)
}

private func deleteReadingLogs(_ readingLogs: [ReadingLogEntity]) {
private func deleteReadingLogs(readingLogs: [ReadingLogEntity]) {
Task {
do {
try await ContextManager.shared.performAndSave { context in
Expand Down Expand Up @@ -1046,7 +1046,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
}
}

private func showEditQuote(_ quote: QuoteEntity) {
private func showEditQuote(quote: QuoteEntity) {
guard let book = viewModel.book else { return }

let quoteViewModel = QuoteEditorViewModel(
Expand Down Expand Up @@ -1088,7 +1088,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
title: "Delete",
style: .destructive
) { [weak self] _ in
self?.deleteQuotes(quotes)
self?.deleteQuotes(quotes: quotes)
}

let cancelAction = UIAlertAction(
Expand All @@ -1102,7 +1102,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
present(alert, animated: true)
}

private func deleteQuotes(_ quotes: [QuoteEntity]) {
private func deleteQuotes(quotes: [QuoteEntity]) {
Task {
do {
let deleteInteractor = DeleteQuoteInteractor()
Expand Down Expand Up @@ -1140,7 +1140,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
}
}

private func shareBook(_ book: BookEntity) {
private func shareBook(book: BookEntity) {
var items: [Any] = []

if let title = book.title {
Expand Down Expand Up @@ -1170,15 +1170,47 @@ extension BookDetailViewController: UICollectionViewDelegate {
present(activityViewController, animated: true)
}

private func rateBook(_ book: BookEntity) {
#warning("TODO: Rate book")
private func rateBook(book: BookEntity) {
let ratingViewController = BookRatingViewController(
title: book.title ?? "",
initialRating: book.rating
) { [weak self] rating in
self?.updateBookRating(rating: rating)
}

if let sheet = ratingViewController.sheetPresentationController {
sheet.detents = [
.custom { context in context.maximumDetentValue * 0.4 }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

μ‹œνŠΈμ˜ 높이λ₯Ό κ²°μ •ν•˜λŠ” 데 μ‚¬μš©λœ 맀직 λ„˜λ²„ 0.4λ₯Ό μƒμˆ˜λ‘œ μΆ”μΆœν•˜μ—¬ μ½”λ“œμ˜ 가독성과 μœ μ§€λ³΄μˆ˜μ„±μ„ ν–₯μƒμ‹œν‚€λŠ” 것을 ꢌμž₯ν•©λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄, 클래슀 내에 λ‹€μŒκ³Ό 같이 μƒμˆ˜λ₯Ό μ„ μ–Έν•  수 μžˆμŠ΅λ‹ˆλ‹€.

private static let ratingSheetHeightRatio: CGFloat = 0.4

그런 λ‹€μŒ λ‹€μŒκ³Ό 같이 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

.custom { context in context.maximumDetentValue * Self.ratingSheetHeightRatio }

]
sheet.prefersGrabberVisible = true
}

present(ratingViewController, animated: true)
}

private func updateBookRating(rating: Double) {
Task {
do {
try await viewModel.updateRating(rating: rating)
} catch {
await MainActor.run {
let errorAlert = UIAlertController(
title: "Error",
message: "Failed to update rating: \(error.localizedDescription)",
preferredStyle: .alert
)
errorAlert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(errorAlert, animated: true)
}
}
}
}

private func editBook(_ book: BookEntity) {
private func editBook(book: BookEntity) {
#warning("TODO: Edit book")
}

private func deleteBook(_ book: BookEntity) {
private func deleteBook(book: BookEntity) {
let alert = UIAlertController(
title: "Delete Book",
message:
Expand All @@ -1190,7 +1222,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
title: "Delete",
style: .destructive
) { [weak self] _ in
self?.performBookDeletion(book)
self?.performBookDeletion(book: book)
}

let cancelAction = UIAlertAction(
Expand All @@ -1204,7 +1236,7 @@ extension BookDetailViewController: UICollectionViewDelegate {
present(alert, animated: true)
}

private func performBookDeletion(_ book: BookEntity) {
private func performBookDeletion(book: BookEntity) {
Task {
do {
try await ContextManager.shared.performAndSave { context in
Expand Down
13 changes: 13 additions & 0 deletions ONMIR/Feature/BookDetail/BookDetailViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ final class BookDetailViewModel: ObservableObject {
}
} ?? false
}

func updateRating(rating: Double) async throws {
guard let book else { throw BookDetailError.bookNotFound }

try await contextManager.performAndSave { context in
let bookToUpdate = context.object(with: book.objectID) as? BookEntity
bookToUpdate?.rating = rating
}

await MainActor.run {
self.book?.rating = rating
}
}
}

enum BookDetailError: Error, LocalizedError {
Expand Down
124 changes: 124 additions & 0 deletions ONMIR/Feature/BookDetail/BookRatingViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import SwiftUI
import UIKit

struct BookRatingView: View {
let title: String
let initialRating: Double
let onRatingChanged: (Double) -> Void

@Environment(\.dismiss) var dismiss
@State private var currentRating: Double

init(
title: String,
initialRating: Double,
onRatingChanged: @escaping (Double) -> Void,
) {
self.title = title
self.initialRating = initialRating
self.onRatingChanged = onRatingChanged
self._currentRating = State(initialValue: initialRating)
}

var body: some View {
VStack(spacing: 24) {
VStack(spacing: 8) {
Text("Rate this book")
.font(.title2)
.fontWeight(.semibold)

Text(title)
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.lineLimit(2)
}

VStack(spacing: 16) {
StarRatingSlider(
rating: $currentRating,
minimum: 0.5,
maximum: 5.0,
spacing: 12,
starSize: 40,
allowHalfStars: true
)

Text(ratingText)
.font(.headline)
.foregroundStyle(.orange)
.contentTransition(.numericText())
}

Button("Rate Book") {
onRatingChanged(currentRating)
dismiss()
}
.foregroundStyle(Color.buttonText)
.frame(maxWidth: .infinity)
.padding()
.background {
RoundedRectangle(cornerRadius: 16)
.fill(Color.buttonBackground)
}
.disabled(currentRating == 0)
}
.padding(24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemBackground))
}

private var ratingText: String {
if currentRating == 0 {
return "Tap stars to rate"
} else {
let ratingString = currentRating.truncatingRemainder(dividingBy: 1) == 0
? String(format: "%.0f", currentRating)
: String(format: "%.1f", currentRating)
return "\(ratingString) out of 5 stars"
}
}
}

final class BookRatingViewController: UIViewController {
private let bookTitle: String
private let initialRating: Double
private let onRatingChanged: (Double) -> Void

init(title: String, initialRating: Double, onRatingChanged: @escaping (Double) -> Void) {
self.bookTitle = title
self.initialRating = initialRating
self.onRatingChanged = onRatingChanged
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()
setupSwiftUIView()
}

private func setupSwiftUIView() {
let ratingView = BookRatingView(
title: bookTitle,
initialRating: initialRating,
onRatingChanged: onRatingChanged
)

let hostingController = UIHostingController(rootView: ratingView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)

hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
2 changes: 1 addition & 1 deletion ONMIR/Feature/BookSearch/BookSearchRepresentation.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public struct BookSearchRepresentation: Sendable, Hashable {
public struct BookSearchRepresentation: Sendable, Hashable, Identifiable {
public let id: String
public let title: String
public let subtitle: String?
Expand Down
Loading