CopyShare.swift (8391B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 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 /// The Share function can NOT be called from a sheet. 164 /// Thus we need to know whether we were called from the .sheet view hierarchy 165 /// If yes, then we must first dismiss the sheet before posting the .ShareAction notification 166 extension EnvironmentValues { 167 @Entry var fromSheet: Bool = false 168 } 169 // MARK: - 170 @MainActor 171 struct ShareButton: View { 172 private let symLog = SymLogV(0) 173 let textToShare: String 174 let image: UIImage? 175 let title: String 176 177 init(_ textToShare: String, image: UIImage? = nil) { 178 self.textToShare = textToShare 179 self.image = image 180 self.title = String(localized: "Share") 181 } 182 init(_ textToShare: String, title: String, image: UIImage? = nil) { 183 self.textToShare = textToShare 184 self.image = image 185 self.title = title 186 } 187 188 @Environment(\.fromSheet) private var fromSheet 189 @Environment(\.isEnabled) private var isEnabled: Bool 190 @EnvironmentObject private var controller: Controller 191 192 @State private var scale: CGFloat = 1.0 193 194 @MainActor 195 func dismissAndPost() { 196 if fromSheet { 197 dismissTop() 198 } 199 let shareType = ShareType(textToShare: textToShare, image: image) 200 let userInfo = [NOTIFICATIONSHARE: shareType] 201 NotificationCenter.default.post(name: .ShareAction, object: nil, userInfo: userInfo) // will trigger NavigationLink 202 } 203 204 func shareAction() -> Void { 205 symLog.log(textToShare) 206 controller.hapticFeedback(.soft) 207 Task { 208 // First scale down quickly 209 withAnimation(.easeIn(duration: 0.1)) { 210 scale = 0.8 211 } 212 // Then bounce back bigger 213 DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 214 withAnimation(.easeOut(duration: 0.25)) { 215 scale = 1.15 216 } 217 dismissAndPost() 218 } 219 // Finally settle to normal 220 DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { 221 withAnimation(.easeInOut(duration: 0.25)) { 222 scale = 1.0 223 } 224 } 225 } 226 } 227 228 var body: some View { 229 Button(action: shareAction) { 230 HStack { 231 Image(systemName: SHARE) // 232 .accessibility(hidden: true) 233 Text(title) 234 } 235 } 236 .tint(WalletColors().primaryAccent) 237 .talerFont(.body) 238 .scaleEffect(scale) 239 .disabled(!isEnabled) 240 } 241 } 242 // MARK: - 243 struct CopyShare: View { 244 @Environment(\.isEnabled) private var isEnabled: Bool 245 246 let textToCopy: String 247 let image: UIImage? 248 249 init(_ string: String, image: UIImage? = nil) { 250 self.textToCopy = string 251 self.image = image 252 } 253 254 var body: some View { 255 let copyB = CopyButton(textToCopy, vertical: false, image: image) 256 .buttonStyle(TalerButtonStyle(type: .bordered)) 257 let share = ShareButton(textToCopy, image: image) 258 .buttonStyle(TalerButtonStyle(type: .bordered)) 259 260 let vLayout = VStack { 261 copyB 262 share 263 } 264 265 if #available(iOS 16.0, *) { 266 let hLayout = HStack(spacing: HSPACING) { 267 copyB 268 share 269 } 270 ViewThatFits(in: .horizontal) { 271 hLayout 272 vLayout 273 } 274 } else { 275 vLayout 276 } 277 } 278 } 279 // MARK: - 280 struct CopyShare_Previews: PreviewProvider { 281 static var previews: some View { 282 CopyShare("Hallö", image: nil) 283 } 284 }