CopyShare.swift (7990B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import UniformTypeIdentifiers 9 import SwiftUI 10 import SymLog 11 12 @MainActor 13 struct FeedbackButton: View { 14 private let symLog = SymLogV(0) 15 let title: String? 16 let image: UIImage? 17 let isDisabled: Bool 18 let action: () -> Void 19 20 @EnvironmentObject private var controller: Controller 21 @State private var scale: CGFloat = 1.0 22 23 public init(_ title: String? = nil, 24 image: UIImage? = nil, 25 disabled: Bool = false, 26 action: @escaping @MainActor () -> Void) { 27 self.title = title 28 self.image = image 29 self.isDisabled = disabled 30 self.action = action 31 } 32 33 private func triggerPulse() { 34 // First scale down quickly 35 withAnimation(.easeIn(duration: 0.1)) { 36 scale = 0.8 37 } 38 // Then bounce back bigger 39 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 40 withAnimation(.easeOut(duration: 0.25)) { 41 scale = 1.15 42 } 43 } 44 // Finally settle to normal 45 DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { 46 withAnimation(.easeInOut(duration: 0.25)) { 47 scale = 1.0 48 } 49 } 50 } 51 52 var body: some View { 53 Button(title ?? EMPTYSTRING) { 54 symLog.log(title ?? EMPTYSTRING) 55 controller.hapticFeedback(.medium) 56 action() 57 triggerPulse() 58 } 59 .buttonStyle(TalerButtonStyle(type: .bordered, disabled: isDisabled)) 60 } 61 } 62 // MARK: - 63 @MainActor 64 struct CopyButton: View { 65 private let symLog = SymLogV(0) 66 let textToCopy: String 67 @Binding var isCopied: Bool? 68 let image: UIImage? 69 let vertical: Bool 70 let title: String? 71 72 @Environment(\.isEnabled) private var isEnabled: Bool 73 @EnvironmentObject private var controller: Controller 74 75 @State private var scale: CGFloat = 1.0 76 77 init(_ textToCopy: String, isCopied: Binding<Bool?>? = nil, 78 vertical: Bool, image: UIImage? = nil) { 79 self.textToCopy = textToCopy 80 self._isCopied = isCopied ?? Binding.constant(nil) 81 self.image = image 82 self.vertical = vertical 83 self.title = nil 84 } 85 86 init(_ textToCopy: String, isCopied: Binding<Bool?>? = nil, 87 title: String, image: UIImage? = nil) { 88 self.textToCopy = textToCopy 89 self._isCopied = isCopied ?? Binding.constant(nil) 90 self.image = image 91 self.vertical = false 92 self.title = title 93 } 94 95 func copyAction() -> Void { 96 symLog.log(textToCopy) 97 triggerPulse() 98 controller.hapticFeedback(.medium) 99 let pasteboard = UIPasteboard.general 100 if let image { 101 // pasteboard.image = image 102 let strItem = [UTType.plainText.identifier : textToCopy] 103 let imgItem = [UTType.image.identifier : image] 104 pasteboard.items = [imgItem, strItem] // iOS27 Notes.app crashes if text comes first! 105 } else { 106 pasteboard.string = textToCopy 107 } 108 if isCopied != nil { 109 isCopied = true 110 } 111 } 112 113 private func triggerPulse() { 114 // First scale down quickly 115 withAnimation(.easeIn(duration: 0.1)) { 116 scale = 0.8 117 } 118 // Then bounce back bigger 119 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 120 withAnimation(.easeOut(duration: 0.25)) { 121 scale = 1.15 122 } 123 } 124 // Finally settle to normal 125 DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { 126 withAnimation(.easeInOut(duration: 0.25)) { 127 scale = 1.0 128 } 129 } 130 } 131 132 var body: some View { 133 Button(action: copyAction) { 134 let image = Image(systemName: COPY1) // 135 .accessibility(hidden: true) 136 137 if vertical { 138 VStack { 139 let shortCopy = String(localized: "Copy.short", defaultValue: "Copy", comment: "5 letters max, else abbreviate") 140 image 141 Text(shortCopy) 142 } 143 } else { 144 let longCopy = String(localized: "Copy.long", defaultValue: "Copy", comment: "may be a bit longer") 145 HStack { 146 image 147 Text(title ?? longCopy) 148 } 149 } 150 } 151 .tint(WalletColors().primaryAccent) 152 .talerFont(.body) 153 .scaleEffect(scale) 154 .disabled(!isEnabled) 155 } 156 } 157 // MARK: - 158 struct ShareType: Hashable { 159 let textToShare: String 160 let image: UIImage? 161 } 162 // MARK: - 163 @MainActor 164 struct ShareButton: View { 165 private let symLog = SymLogV(0) 166 let textToShare: String 167 let image: UIImage? 168 let title: String 169 170 @State private var scale: CGFloat = 1.0 171 172 init(_ textToShare: String, image: UIImage? = nil) { 173 self.textToShare = textToShare 174 self.image = image 175 self.title = String(localized: "Share") 176 } 177 init(_ textToShare: String, title: String, image: UIImage? = nil) { 178 self.textToShare = textToShare 179 self.image = image 180 self.title = title 181 } 182 183 @Environment(\.isEnabled) private var isEnabled: Bool 184 @EnvironmentObject private var controller: Controller 185 186 @MainActor 187 func dismissAndPost() { 188 dismissTop() 189 let shareType = ShareType(textToShare: textToShare, image: image) 190 let userInfo = [NOTIFICATIONSHARE: shareType] 191 NotificationCenter.default.post(name: .ShareAction, object: nil, userInfo: userInfo) // will trigger NavigationLink 192 } 193 194 func shareAction() -> Void { 195 symLog.log(textToShare) 196 controller.hapticFeedback(.soft) 197 Task { 198 // First scale down quickly 199 withAnimation(.easeIn(duration: 0.1)) { 200 scale = 0.8 201 } 202 // Then bounce back bigger 203 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 204 withAnimation(.easeOut(duration: 0.25)) { 205 scale = 1.15 206 } 207 dismissAndPost() 208 } 209 // Finally settle to normal 210 DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { 211 withAnimation(.easeInOut(duration: 0.25)) { 212 scale = 1.0 213 } 214 } 215 } 216 } 217 218 var body: some View { 219 Button(action: shareAction) { 220 HStack { 221 Image(systemName: SHARE) // 222 .accessibility(hidden: true) 223 Text(title) 224 } 225 } 226 .tint(WalletColors().primaryAccent) 227 .talerFont(.body) 228 .scaleEffect(scale) 229 .disabled(!isEnabled) 230 } 231 } 232 // MARK: - 233 struct CopyShare: View { 234 @Environment(\.isEnabled) private var isEnabled: Bool 235 236 let textToCopy: String 237 let image: UIImage? 238 239 init(_ string: String, image: UIImage? = nil) { 240 self.textToCopy = string 241 self.image = image 242 } 243 244 var body: some View { 245 let copyB = CopyButton(textToCopy, vertical: false, image: image) 246 .buttonStyle(TalerButtonStyle(type: .bordered)) 247 let share = ShareButton(textToCopy, image: image) 248 .buttonStyle(TalerButtonStyle(type: .bordered)) 249 250 let vLayout = VStack { 251 copyB 252 share 253 } 254 255 if #available(iOS 16.0, *) { 256 let hLayout = HStack(spacing: HSPACING) { 257 copyB 258 share 259 } 260 ViewThatFits(in: .horizontal) { 261 hLayout 262 vLayout 263 } 264 } else { 265 vLayout 266 } 267 } 268 } 269 // MARK: - 270 struct CopyShare_Previews: PreviewProvider { 271 static var previews: some View { 272 CopyShare("Hallö", image: nil) 273 } 274 }