diff --git a/Application/App/Sources/Resource/Localizable.xcstrings b/Application/App/Sources/Resource/Localizable.xcstrings index 5d85cd87..bab80bd9 100644 --- a/Application/App/Sources/Resource/Localizable.xcstrings +++ b/Application/App/Sources/Resource/Localizable.xcstrings @@ -290,6 +290,23 @@ } } }, + "common_copy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "복사" + } + } + } + }, "common_delete" : { "extractionState" : "manual", "localizations" : { diff --git a/Application/Presentation/PresentationShared/Sources/Common/Component/TagCopyMenuView.swift b/Application/Presentation/PresentationShared/Sources/Common/Component/TagCopyMenuView.swift new file mode 100644 index 00000000..b91cdf0b --- /dev/null +++ b/Application/Presentation/PresentationShared/Sources/Common/Component/TagCopyMenuView.swift @@ -0,0 +1,120 @@ +// +// TagCopyMenuView.swift +// PresentationShared +// +// Created by opfic on 8/9/26. +// + +import SwiftUI +import UIKit + +struct TagCopyMenuView: UIViewRepresentable { + let tagText: String + + func makeCoordinator() -> TagCopyMenuCoordinator { + TagCopyMenuCoordinator() + } + + func makeUIView(context: Context) -> UIView { + let view = UIView() + view.backgroundColor = .clear + view.isAccessibilityElement = false + context.coordinator.install(on: view) + context.coordinator.tagText = tagText + return view + } + + func updateUIView(_ view: UIView, context: Context) { + context.coordinator.tagText = tagText + } +} + +final class TagCopyMenuCoordinator: NSObject { + var tagText = "" + private weak var view: UIView? + private var editMenuInteraction: UIEditMenuInteraction? + + func install(on view: UIView) { + self.view = view + + let editMenuInteraction = UIEditMenuInteraction(delegate: self) + view.addInteraction(editMenuInteraction) + self.editMenuInteraction = editMenuInteraction + + let longPressGesture = UILongPressGestureRecognizer( + target: self, + action: #selector(handleLongPressGesture(_:)) + ) + longPressGesture.allowedTouchTypes = [ + NSNumber(value: UITouch.TouchType.direct.rawValue), + NSNumber(value: UITouch.TouchType.indirectPointer.rawValue) + ] + longPressGesture.cancelsTouchesInView = false + longPressGesture.delegate = self + view.addGestureRecognizer(longPressGesture) + + let secondaryClickGesture = UITapGestureRecognizer( + target: self, + action: #selector(handleSecondaryClickGesture(_:)) + ) + secondaryClickGesture.allowedTouchTypes = [ + NSNumber(value: UITouch.TouchType.indirectPointer.rawValue) + ] + secondaryClickGesture.buttonMaskRequired = .secondary + secondaryClickGesture.cancelsTouchesInView = false + secondaryClickGesture.delegate = self + view.addGestureRecognizer(secondaryClickGesture) + } + + @objc + private func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) { + guard gesture.state == .began, let view else { return } + + presentEditMenu(at: gesture.location(in: view)) + } + + @objc + private func handleSecondaryClickGesture(_ gesture: UITapGestureRecognizer) { + guard gesture.state == .ended, let view else { return } + + presentEditMenu(at: gesture.location(in: view)) + } + + private func presentEditMenu(at sourcePoint: CGPoint) { + let configuration = UIEditMenuConfiguration( + identifier: nil, + sourcePoint: sourcePoint + ) + editMenuInteraction?.presentEditMenu(with: configuration) + } +} + +extension TagCopyMenuCoordinator: UIEditMenuInteractionDelegate { + func editMenuInteraction( + _ interaction: UIEditMenuInteraction, + menuFor configuration: UIEditMenuConfiguration, + suggestedActions: [UIMenuElement] + ) -> UIMenu? { + let tagText = tagText + let copyAction = UIAction(title: String(localized: "common_copy")) { _ in + UIPasteboard.general.string = tagText + } + return UIMenu(children: [copyAction]) + } + + func editMenuInteraction( + _ interaction: UIEditMenuInteraction, + targetRectFor configuration: UIEditMenuConfiguration + ) -> CGRect { + view?.bounds ?? .zero + } +} + +extension TagCopyMenuCoordinator: UIGestureRecognizerDelegate { + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + true + } +} diff --git a/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift b/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift index 76bce642..f2a94e0a 100644 --- a/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift +++ b/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift @@ -12,11 +12,31 @@ public struct Tag: View { @State private var height: CGFloat = 0 private let name: String private let isEditing: Bool + private let isCopyEnabled: Bool private var action: (() -> Void)? - public init(_ name: String, isEditing: Bool, action: (() -> Void)? = nil) { + public init( + _ name: String, + isEditing: Bool, + action: (() -> Void)? = nil + ) { + self.init( + name, + isEditing: isEditing, + isCopyEnabled: false, + action: action + ) + } + + init( + _ name: String, + isEditing: Bool, + isCopyEnabled: Bool, + action: (() -> Void)? + ) { self.name = name self.isEditing = isEditing + self.isCopyEnabled = isCopyEnabled self.action = action } @@ -30,6 +50,11 @@ public struct Tag: View { .padding(.vertical, 4) .padding(.leading, 8) .padding(.trailing, isEditing ? 0 : 8) + .overlay { + if isCopyEnabled { + TagCopyMenuView(tagText: name) + } + } .background { GeometryReader { geo in Color.clear @@ -120,7 +145,11 @@ public struct TagList: View { @ViewBuilder private var contentTags: some View { ForEach(tags, id: \.self) { tagText in - Tag(tagText, isEditing: isEditing) { + Tag( + tagText, + isEditing: isEditing, + isCopyEnabled: true + ) { action?(tagText) } }