Skip to content

Instantly share code, notes, and snippets.

@lifeutilityapps
Last active January 2, 2025 08:35
Show Gist options
  • Save lifeutilityapps/dcb8137ea097a94034ddeeb25253fc2e to your computer and use it in GitHub Desktop.
Save lifeutilityapps/dcb8137ea097a94034ddeeb25253fc2e to your computer and use it in GitHub Desktop.
A reusable SwiftUI context menu component for programmatically creating a menu of user-copiable values
//
// StandardCopyMenu.swift
// DownPay for iOS
// lifeutilityapps.com/downpay
//
// Created by Life Utility Apps on 12/15/24.
//
import SwiftUI
enum EStandardCopyMenuItem {
case value
case label
case divider
case date
}
struct StandardCopyMenuItem: Identifiable {
var label: String = ""
var toCopy: String = ""
var type: EStandardCopyMenuItem = .value
var onCopy: (() -> Void)? = nil
var isLabeled: Bool = false
let id = UUID()
}
extension Array where Element == StandardCopyMenuItem {
mutating func addDivider() {
self.append(.init(type: .divider))
}
mutating func addLabel(_ label: String) {
self.append(.init(label: label, type: .label))
}
}
// Scroll below for how to use this
struct StandardCopyMenu: View {
var labelMenu: String = "Copy"
var systemImage: String = SCL.icons.copy.iconFill()
var items: [StandardCopyMenuItem] = []
func handleCopy(_ item: StandardCopyMenuItem) {
SCL.util.copyText(item.toCopy)
SCL.util.vibrateDevice(.rigid)
// Optional callback
if let onCopy = item.onCopy {
onCopy()
}
}
var body: some View {
Menu(labelMenu, systemImage: systemImage) {
ForEach(items) { item in
if(item.type == .divider) {
Divider()
} else if(item.type == .date) {
let dateItems = StandardCopyMenu.getDateVariations(item.toCopy)
ForEach(dateItems) { itemDate in
Button {
handleCopy(itemDate)
} label: {
Label(itemDate.toCopy, systemImage: SCL.icons.copy)
}
}
} else if(item.type == .label) {
Text(item.label)
} else {
if(item.isLabeled) {
Text(item.label)
}
Button {
handleCopy(item)
} label: {
Label(item.toCopy, systemImage: SCL.icons.copy)
}
}
}
}
}
static func getDateVariations(_ dateISO: String, _ onCopy: (() -> Void)? = nil) -> [StandardCopyMenuItem] {
var result:[StandardCopyMenuItem] = []
let dateTypes: [EStandardDateTextVariant] = [.Mdyyyy, .MMMMdyyyy, .fullDate]
let date = Date(fromISOStringJS: dateISO)
dateTypes.forEach { dateType in
let dateString = StandardDateText.getDateString(dateType, date)
result.append(.init(label: "", toCopy: dateString, onCopy: onCopy))
}
return result
}
}
// Example usage creating the copiable values in a model
extension ModelUserTransaction {
var copyItems: [StandardCopyMenuItem] {
var result: [StandardCopyMenuItem] = []
result.append(.init(label: "Amount", toCopy: SCL.cash(self.amount, false, .small, true), isLabeled: true))
result.append(.init(label: "Amount Number", toCopy: String(self.amount)))
// Divider
result.append(.init(type: .divider))
let hasNotes = self.notes.isNotEmpty
if(self.label.isNotEmpty) {
let label = "Label\(hasNotes ? " & Notes" : "")"
result.append(.init(label: label, toCopy: self.label, isLabeled: true))
}
if(hasNotes) {
result.append(.init(label: "Notes", toCopy: self.notes))
}
// Divider
result.addDivider()
// Manually created label
result.addLabel("Date")
// Date (will automatically create variations)
result.append(.init(label: "Date", toCopy: self.dateTransactionOccured.ISO8601Format(), type: .date))
// Another closing divider
result.addDivider()
if(self.transactionType == .payment) {
result.append(.init(label: "Category", toCopy: self.wrappedPaymentCategory.displayName, isLabeled: true))
}
return result
}
}
// Example usage in any view
/*
SomeView()
.contextMenu {
StandardCopyMenu(items: transaction.copyItems)
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment