Skip to content

Commit 9a64767

Browse files
committed
feat: Todo 상세 미리보기 구현
1 parent 5752477 commit 9a64767

2 files changed

Lines changed: 163 additions & 0 deletions

File tree

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// TodoDetailPreviewModifier.swift
3+
// PresentationShared
4+
//
5+
// Created by opfic on 8/11/26.
6+
//
7+
8+
import SwiftUI
9+
import ComposableArchitecture
10+
import Core
11+
import Domain
12+
13+
public extension View {
14+
func todoDetailPreview(todoId: String) -> some View {
15+
modifier(TodoDetailPreviewModifier(todoId: todoId))
16+
}
17+
}
18+
19+
private struct TodoDetailPreviewModifier: ViewModifier {
20+
@Environment(\.diContainer) private var container
21+
let todoId: String
22+
23+
func body(content: Content) -> some View {
24+
content.overlay {
25+
TodoDetailPreviewInteractionView(
26+
todoId: todoId,
27+
makePreview: makePreviewViewController
28+
)
29+
}
30+
}
31+
32+
private func makePreviewViewController() -> UIViewController {
33+
let store = Store(
34+
initialState: TodoDetailFeature.State(
35+
todoId: todoId,
36+
showEditButton: false
37+
)
38+
) {
39+
TodoDetailFeature()
40+
} withDependencies: {
41+
$0.fetchTodoByIdUseCase = container.resolve(FetchTodoByIdUseCase.self)
42+
$0.fetchReferenceItemsUseCase = container.resolve(FetchReferenceItemsUseCase.self)
43+
}
44+
return UIHostingController(rootView: TodoDetailPreviewView(store: store))
45+
}
46+
}
47+
48+
private struct TodoDetailPreviewInteractionView: UIViewRepresentable {
49+
let todoId: String
50+
let makePreview: () -> UIViewController
51+
52+
func makeCoordinator() -> TodoDetailPreviewInteractionCoordinator {
53+
TodoDetailPreviewInteractionCoordinator()
54+
}
55+
56+
func makeUIView(context: Context) -> UIView {
57+
let view = UIView()
58+
view.backgroundColor = .clear
59+
view.isAccessibilityElement = false
60+
context.coordinator.update(todoId: todoId, makePreview: makePreview)
61+
context.coordinator.install(on: view)
62+
return view
63+
}
64+
65+
func updateUIView(_ view: UIView, context: Context) {
66+
context.coordinator.update(todoId: todoId, makePreview: makePreview)
67+
}
68+
}
69+
70+
private final class TodoDetailPreviewInteractionCoordinator: NSObject {
71+
private weak var view: UIView?
72+
private var contextMenuInteraction: UIContextMenuInteraction?
73+
private var todoId = ""
74+
private var makePreview: (() -> UIViewController)?
75+
76+
func update(
77+
todoId: String,
78+
makePreview: @escaping () -> UIViewController
79+
) {
80+
self.todoId = todoId
81+
self.makePreview = makePreview
82+
}
83+
84+
func install(on view: UIView) {
85+
guard self.view !== view else { return }
86+
87+
if let contextMenuInteraction {
88+
self.view?.removeInteraction(contextMenuInteraction)
89+
}
90+
91+
let contextMenuInteraction = UIContextMenuInteraction(delegate: self)
92+
view.addInteraction(contextMenuInteraction)
93+
self.view = view
94+
self.contextMenuInteraction = contextMenuInteraction
95+
}
96+
}
97+
98+
extension TodoDetailPreviewInteractionCoordinator: UIContextMenuInteractionDelegate {
99+
func contextMenuInteraction(
100+
_ interaction: UIContextMenuInteraction,
101+
configurationForMenuAtLocation location: CGPoint
102+
) -> UIContextMenuConfiguration? {
103+
guard let makePreview else { return nil }
104+
105+
let preferredContentSize = preferredContentSize(for: interaction.view)
106+
return UIContextMenuConfiguration(
107+
identifier: todoId as NSString,
108+
previewProvider: {
109+
let viewController = makePreview()
110+
viewController.preferredContentSize = preferredContentSize
111+
return viewController
112+
},
113+
actionProvider: nil
114+
)
115+
}
116+
117+
private func preferredContentSize(for view: UIView?) -> CGSize {
118+
let bounds = view?.window?.bounds ?? view?.bounds ?? .zero
119+
let width = min(420, max(280, bounds.width - 32))
120+
let height = min(640, max(320, bounds.height * 0.7))
121+
return CGSize(width: width, height: height)
122+
}
123+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// TodoDetailPreviewView.swift
3+
// PresentationShared
4+
//
5+
// Created by opfic on 8/11/26.
6+
//
7+
8+
import SwiftUI
9+
import ComposableArchitecture
10+
11+
struct TodoDetailPreviewView: View {
12+
@State var store: StoreOf<TodoDetailFeature>
13+
14+
var body: some View {
15+
ZStack {
16+
Color(.systemGroupedBackground).ignoresSafeArea()
17+
if let todo = store.todo {
18+
TodoDetailContentView(
19+
title: todo.title,
20+
content: todo.content,
21+
referenceItems: store.referenceItems,
22+
number: todo.number,
23+
onOpenTodoID: nil
24+
)
25+
} else if store.alert != nil {
26+
ContentUnavailableView {
27+
Label(
28+
String(localized: "common_error_title"),
29+
systemImage: "exclamationmark.triangle"
30+
)
31+
} description: {
32+
Text(String(localized: "common_error_message"))
33+
}
34+
} else {
35+
LoadingView()
36+
}
37+
}
38+
.onAppear { store.send(.onAppear) }
39+
}
40+
}

0 commit comments

Comments
 (0)