From 56003501625f1c71c9d7070cd38024e92099df3f Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 10 Aug 2026 16:23:47 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=ED=83=9C=EA=B7=B8=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=20=EB=A9=94=EB=89=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Resource/Localizable.xcstrings | 17 +++ .../Common/Component/TagCopyMenuView.swift | 120 ++++++++++++++++++ .../Sources/Common/Component/Tags.swift | 3 + 3 files changed, 140 insertions(+) create mode 100644 Application/Presentation/PresentationShared/Sources/Common/Component/TagCopyMenuView.swift 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..0ed5f66d 100644 --- a/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift +++ b/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift @@ -123,6 +123,9 @@ public struct TagList: View { Tag(tagText, isEditing: isEditing) { action?(tagText) } + .overlay { + TagCopyMenuView(tagText: tagText) + } } } } From 1a2d420d706a0c3478a16d80ef0ce1da2dfaf022 Mon Sep 17 00:00:00 2001 From: opficdev Date: Mon, 10 Aug 2026 18:46:46 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=ED=8E=B8=EC=A7=91=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=82=AD=EC=A0=9C=20=EB=B2=84=ED=8A=BC=20=ED=84=B0?= =?UTF-8?q?=EC=B9=98=20=EC=98=81=EC=97=AD=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Common/Component/Tags.swift | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift b/Application/Presentation/PresentationShared/Sources/Common/Component/Tags.swift index 0ed5f66d..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,12 +145,13 @@ 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) } - .overlay { - TagCopyMenuView(tagText: tagText) - } } } }