From 085bb89b3689bad9e2a3736adb178312a6b24da8 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 11 Mar 2025 22:18:21 +0800 Subject: [PATCH] feat(mobile): add native iOS modules for shared functionality - Implement native iOS modules for WebView, Toaster, Galeria, and Helper - Add SPIndicator package for native toast notifications - Create ImageViewer package for enhanced image preview - Introduce utility extensions and helper methods for iOS development - Remove SPIndicator dependency from podspec - Update toast implementation to use native module Signed-off-by: Innei --- .../ios/{ => Extensions}/UIColor+Hex.swift | 0 apps/mobile/native/ios/FollowNative.podspec | 1 - .../AppleIntelligenceGlowEffect.swift | 0 .../AppleIntelligenceGlowEffectModule.swift | 0 .../Galeria/EntryGaleriaAccessoryView.swift | 0 .../Galeria/GaleriaAccessoryModule.swift | 0 .../{ => Modules}/Galeria/GaleriaModule.swift | 0 .../{ => Modules}/Galeria/GaleriaView.swift | 0 .../{ => Modules}/Helper/Helper+Image.swift | 0 .../{ => Modules}/Helper/HelperModule.swift | 0 .../CustomURLSchemeHandler.swift | 0 .../SharedWebView/FOWebView.swift | 0 .../SharedWebView/Injected/at_end.js | 0 .../SharedWebView/Injected/at_start.js | 0 .../SharedWebView+BridgeData.swift | 0 .../SharedWebView/SharedWebView.swift | 0 .../SharedWebView/SharedWebViewModule.swift | 0 .../SharedWebView/WebViewManager.swift | 0 .../SharedWebView/WebViewState.swift | 0 .../{ => Modules}/Toaster/ToasterModule.swift | 1 - .../ImageCarouselViewController.swift | 0 .../ImageViewer_swift/ImageItem.swift | 0 .../ImageViewer_swift/ImageLoader.swift | 0 .../ImageViewerController.swift | 0 .../ImageViewer_swift/ImageViewerOption.swift | 0 ...eViewerTransitionPresentationManager.swift | 0 .../ImageViewer_swift/ImageViewer_swift.h | 0 .../{ => Packages}/ImageViewer_swift/LISENCE | 0 .../SimpleImageDatasource.swift | 0 .../UIImageView_Extensions.swift | 0 .../UINavigationBar_Extensions.swift | 0 .../ImageViewer_swift/UIView_Extensions.swift | 0 .../Extensions/SwiftUIExtension.swift | 88 +++ .../Extensions/UIColorExtension.swift | 80 +++ .../Extensions/UIFontExtension.swift | 32 + .../Extensions/UILabelExtension.swift | 37 + .../Icons/SPAlertIconErrorView.swift | 84 +++ .../Icons/SPIndicatorIconDoneView.swift | 51 ++ .../native/ios/Packages/SPIndicator/LICENSE | 21 + .../Models/SPIndicatorLayout.swift | 43 ++ .../Models/SPIndicatorPresentSide.swift | 32 + .../Protocols/SPIndicatorIconAnimatable.swift | 33 + .../Packages/SPIndicator/SPIndicator.swift | 83 +++ .../SPIndicator/SPIndicatorIconPreset.swift | 119 ++++ .../SPIndicator/SPIndicatorView.swift | 654 ++++++++++++++++++ .../Services/SPIndicatorHaptic.swift | 47 ++ apps/mobile/src/lib/toast.tsx | 2 +- apps/mobile/src/screens/(headless)/debug.tsx | 2 +- 48 files changed, 1406 insertions(+), 4 deletions(-) rename apps/mobile/native/ios/{ => Extensions}/UIColor+Hex.swift (100%) rename apps/mobile/native/ios/{ => Modules}/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffect.swift (100%) rename apps/mobile/native/ios/{ => Modules}/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffectModule.swift (100%) rename apps/mobile/native/ios/{ => Modules}/Galeria/EntryGaleriaAccessoryView.swift (100%) rename apps/mobile/native/ios/{ => Modules}/Galeria/GaleriaAccessoryModule.swift (100%) rename apps/mobile/native/ios/{ => Modules}/Galeria/GaleriaModule.swift (100%) rename apps/mobile/native/ios/{ => Modules}/Galeria/GaleriaView.swift (100%) rename apps/mobile/native/ios/{ => Modules}/Helper/Helper+Image.swift (100%) rename apps/mobile/native/ios/{ => Modules}/Helper/HelperModule.swift (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/CustomURLSchemeHandler.swift (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/FOWebView.swift (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/Injected/at_end.js (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/Injected/at_start.js (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/SharedWebView+BridgeData.swift (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/SharedWebView.swift (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/SharedWebViewModule.swift (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/WebViewManager.swift (100%) rename apps/mobile/native/ios/{ => Modules}/SharedWebView/WebViewState.swift (100%) rename apps/mobile/native/ios/{ => Modules}/Toaster/ToasterModule.swift (98%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/ImageCarouselViewController.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/ImageItem.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/ImageLoader.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/ImageViewerController.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/ImageViewerOption.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/ImageViewerTransitionPresentationManager.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/ImageViewer_swift.h (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/LISENCE (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/SimpleImageDatasource.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/UIImageView_Extensions.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/UINavigationBar_Extensions.swift (100%) rename apps/mobile/native/ios/{ => Packages}/ImageViewer_swift/UIView_Extensions.swift (100%) create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Extensions/SwiftUIExtension.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIColorExtension.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIFontExtension.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Extensions/UILabelExtension.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Icons/SPAlertIconErrorView.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Icons/SPIndicatorIconDoneView.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/LICENSE create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorLayout.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorPresentSide.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Protocols/SPIndicatorIconAnimatable.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/SPIndicator.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorIconPreset.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift create mode 100644 apps/mobile/native/ios/Packages/SPIndicator/Services/SPIndicatorHaptic.swift diff --git a/apps/mobile/native/ios/UIColor+Hex.swift b/apps/mobile/native/ios/Extensions/UIColor+Hex.swift similarity index 100% rename from apps/mobile/native/ios/UIColor+Hex.swift rename to apps/mobile/native/ios/Extensions/UIColor+Hex.swift diff --git a/apps/mobile/native/ios/FollowNative.podspec b/apps/mobile/native/ios/FollowNative.podspec index f9506bd13..11b5232b0 100644 --- a/apps/mobile/native/ios/FollowNative.podspec +++ b/apps/mobile/native/ios/FollowNative.podspec @@ -21,7 +21,6 @@ Pod::Spec.new do |s| s.dependency 'ExpoModulesCore' s.dependency 'SnapKit', '~> 5.7.0' s.dependency 'SDWebImage', '~> 5.0' - s.dependency 'SPIndicator', '~> 1.6.4' # Swift/Objective-C compatibility s.pod_target_xcconfig = { diff --git a/apps/mobile/native/ios/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffect.swift b/apps/mobile/native/ios/Modules/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffect.swift similarity index 100% rename from apps/mobile/native/ios/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffect.swift rename to apps/mobile/native/ios/Modules/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffect.swift diff --git a/apps/mobile/native/ios/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffectModule.swift b/apps/mobile/native/ios/Modules/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffectModule.swift similarity index 100% rename from apps/mobile/native/ios/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffectModule.swift rename to apps/mobile/native/ios/Modules/AppleIntelligenceGlowEffect/AppleIntelligenceGlowEffectModule.swift diff --git a/apps/mobile/native/ios/Galeria/EntryGaleriaAccessoryView.swift b/apps/mobile/native/ios/Modules/Galeria/EntryGaleriaAccessoryView.swift similarity index 100% rename from apps/mobile/native/ios/Galeria/EntryGaleriaAccessoryView.swift rename to apps/mobile/native/ios/Modules/Galeria/EntryGaleriaAccessoryView.swift diff --git a/apps/mobile/native/ios/Galeria/GaleriaAccessoryModule.swift b/apps/mobile/native/ios/Modules/Galeria/GaleriaAccessoryModule.swift similarity index 100% rename from apps/mobile/native/ios/Galeria/GaleriaAccessoryModule.swift rename to apps/mobile/native/ios/Modules/Galeria/GaleriaAccessoryModule.swift diff --git a/apps/mobile/native/ios/Galeria/GaleriaModule.swift b/apps/mobile/native/ios/Modules/Galeria/GaleriaModule.swift similarity index 100% rename from apps/mobile/native/ios/Galeria/GaleriaModule.swift rename to apps/mobile/native/ios/Modules/Galeria/GaleriaModule.swift diff --git a/apps/mobile/native/ios/Galeria/GaleriaView.swift b/apps/mobile/native/ios/Modules/Galeria/GaleriaView.swift similarity index 100% rename from apps/mobile/native/ios/Galeria/GaleriaView.swift rename to apps/mobile/native/ios/Modules/Galeria/GaleriaView.swift diff --git a/apps/mobile/native/ios/Helper/Helper+Image.swift b/apps/mobile/native/ios/Modules/Helper/Helper+Image.swift similarity index 100% rename from apps/mobile/native/ios/Helper/Helper+Image.swift rename to apps/mobile/native/ios/Modules/Helper/Helper+Image.swift diff --git a/apps/mobile/native/ios/Helper/HelperModule.swift b/apps/mobile/native/ios/Modules/Helper/HelperModule.swift similarity index 100% rename from apps/mobile/native/ios/Helper/HelperModule.swift rename to apps/mobile/native/ios/Modules/Helper/HelperModule.swift diff --git a/apps/mobile/native/ios/SharedWebView/CustomURLSchemeHandler.swift b/apps/mobile/native/ios/Modules/SharedWebView/CustomURLSchemeHandler.swift similarity index 100% rename from apps/mobile/native/ios/SharedWebView/CustomURLSchemeHandler.swift rename to apps/mobile/native/ios/Modules/SharedWebView/CustomURLSchemeHandler.swift diff --git a/apps/mobile/native/ios/SharedWebView/FOWebView.swift b/apps/mobile/native/ios/Modules/SharedWebView/FOWebView.swift similarity index 100% rename from apps/mobile/native/ios/SharedWebView/FOWebView.swift rename to apps/mobile/native/ios/Modules/SharedWebView/FOWebView.swift diff --git a/apps/mobile/native/ios/SharedWebView/Injected/at_end.js b/apps/mobile/native/ios/Modules/SharedWebView/Injected/at_end.js similarity index 100% rename from apps/mobile/native/ios/SharedWebView/Injected/at_end.js rename to apps/mobile/native/ios/Modules/SharedWebView/Injected/at_end.js diff --git a/apps/mobile/native/ios/SharedWebView/Injected/at_start.js b/apps/mobile/native/ios/Modules/SharedWebView/Injected/at_start.js similarity index 100% rename from apps/mobile/native/ios/SharedWebView/Injected/at_start.js rename to apps/mobile/native/ios/Modules/SharedWebView/Injected/at_start.js diff --git a/apps/mobile/native/ios/SharedWebView/SharedWebView+BridgeData.swift b/apps/mobile/native/ios/Modules/SharedWebView/SharedWebView+BridgeData.swift similarity index 100% rename from apps/mobile/native/ios/SharedWebView/SharedWebView+BridgeData.swift rename to apps/mobile/native/ios/Modules/SharedWebView/SharedWebView+BridgeData.swift diff --git a/apps/mobile/native/ios/SharedWebView/SharedWebView.swift b/apps/mobile/native/ios/Modules/SharedWebView/SharedWebView.swift similarity index 100% rename from apps/mobile/native/ios/SharedWebView/SharedWebView.swift rename to apps/mobile/native/ios/Modules/SharedWebView/SharedWebView.swift diff --git a/apps/mobile/native/ios/SharedWebView/SharedWebViewModule.swift b/apps/mobile/native/ios/Modules/SharedWebView/SharedWebViewModule.swift similarity index 100% rename from apps/mobile/native/ios/SharedWebView/SharedWebViewModule.swift rename to apps/mobile/native/ios/Modules/SharedWebView/SharedWebViewModule.swift diff --git a/apps/mobile/native/ios/SharedWebView/WebViewManager.swift b/apps/mobile/native/ios/Modules/SharedWebView/WebViewManager.swift similarity index 100% rename from apps/mobile/native/ios/SharedWebView/WebViewManager.swift rename to apps/mobile/native/ios/Modules/SharedWebView/WebViewManager.swift diff --git a/apps/mobile/native/ios/SharedWebView/WebViewState.swift b/apps/mobile/native/ios/Modules/SharedWebView/WebViewState.swift similarity index 100% rename from apps/mobile/native/ios/SharedWebView/WebViewState.swift rename to apps/mobile/native/ios/Modules/SharedWebView/WebViewState.swift diff --git a/apps/mobile/native/ios/Toaster/ToasterModule.swift b/apps/mobile/native/ios/Modules/Toaster/ToasterModule.swift similarity index 98% rename from apps/mobile/native/ios/Toaster/ToasterModule.swift rename to apps/mobile/native/ios/Modules/Toaster/ToasterModule.swift index 4abfdca5d..5f988f7bb 100644 --- a/apps/mobile/native/ios/Toaster/ToasterModule.swift +++ b/apps/mobile/native/ios/Modules/Toaster/ToasterModule.swift @@ -5,7 +5,6 @@ // import ExpoModulesCore -import SPIndicator enum ToastType: String, Enumerable { case error diff --git a/apps/mobile/native/ios/ImageViewer_swift/ImageCarouselViewController.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageCarouselViewController.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/ImageCarouselViewController.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/ImageCarouselViewController.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/ImageItem.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageItem.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/ImageItem.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/ImageItem.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/ImageLoader.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageLoader.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/ImageLoader.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/ImageLoader.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/ImageViewerController.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewerController.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/ImageViewerController.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewerController.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/ImageViewerOption.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewerOption.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/ImageViewerOption.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewerOption.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/ImageViewerTransitionPresentationManager.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewerTransitionPresentationManager.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/ImageViewerTransitionPresentationManager.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewerTransitionPresentationManager.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/ImageViewer_swift.h b/apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewer_swift.h similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/ImageViewer_swift.h rename to apps/mobile/native/ios/Packages/ImageViewer_swift/ImageViewer_swift.h diff --git a/apps/mobile/native/ios/ImageViewer_swift/LISENCE b/apps/mobile/native/ios/Packages/ImageViewer_swift/LISENCE similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/LISENCE rename to apps/mobile/native/ios/Packages/ImageViewer_swift/LISENCE diff --git a/apps/mobile/native/ios/ImageViewer_swift/SimpleImageDatasource.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/SimpleImageDatasource.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/SimpleImageDatasource.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/SimpleImageDatasource.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/UIImageView_Extensions.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/UIImageView_Extensions.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/UIImageView_Extensions.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/UIImageView_Extensions.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/UINavigationBar_Extensions.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/UINavigationBar_Extensions.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/UINavigationBar_Extensions.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/UINavigationBar_Extensions.swift diff --git a/apps/mobile/native/ios/ImageViewer_swift/UIView_Extensions.swift b/apps/mobile/native/ios/Packages/ImageViewer_swift/UIView_Extensions.swift similarity index 100% rename from apps/mobile/native/ios/ImageViewer_swift/UIView_Extensions.swift rename to apps/mobile/native/ios/Packages/ImageViewer_swift/UIView_Extensions.swift diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Extensions/SwiftUIExtension.swift b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/SwiftUIExtension.swift new file mode 100644 index 000000000..a764f08d1 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/SwiftUIExtension.swift @@ -0,0 +1,88 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +@available(iOSApplicationExtension, unavailable) + +extension View { + + public func SPIndicator( + isPresent: Binding, + indicatorView: SPIndicatorView, + duration: TimeInterval = 2.0, + haptic: SPIndicatorHaptic = .none + ) -> some View { + + if isPresent.wrappedValue { + let indicatorCompletion = indicatorView.completion + let indicatorDismiss = { + isPresent.wrappedValue = false + indicatorCompletion?() + } + indicatorView.duration = duration + indicatorView.present(haptic: haptic, completion: indicatorDismiss) + } + return self + } + + public func SPIndicator( + isPresent: Binding, + title: String = "", + message: String? = nil, + duration: TimeInterval = 2.0, + presentSide: SPIndicatorPresentSide = .top, + dismissByDrag: Bool = true, + preset: SPIndicatorIconPreset = .done, + haptic: SPIndicatorHaptic = .none, + layout: SPIndicatorLayout? = nil, + completion: (() -> Void)? = nil + ) -> some View { + + let indicatorView = SPIndicatorView(title: title, message: message, preset: preset) + indicatorView.presentSide = presentSide + indicatorView.dismissByDrag = dismissByDrag + indicatorView.layout = layout ?? SPIndicatorLayout(for: preset) + indicatorView.completion = completion + return SPIndicator( + isPresent: isPresent, indicatorView: indicatorView, duration: duration, haptic: haptic) + } + + public func SPIndicator( + isPresent: Binding, + title: String, + duration: TimeInterval = 2.0, + presentSide: SPIndicatorPresentSide = .top, + dismissByDrag: Bool = true, + preset: SPIndicatorIconPreset = .done, + haptic: SPIndicatorHaptic = .none, + completion: (() -> Void)? = nil + ) -> some View { + + let indicatorView = SPIndicatorView(title: title, preset: preset) + indicatorView.presentSide = presentSide + indicatorView.dismissByDrag = dismissByDrag + indicatorView.completion = completion + return SPIndicator( + isPresent: isPresent, indicatorView: indicatorView, duration: duration, haptic: haptic) + } + +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIColorExtension.swift b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIColorExtension.swift new file mode 100644 index 000000000..bac4f8abb --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIColorExtension.swift @@ -0,0 +1,80 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +extension UIColor { + + enum Compability { + + static var systemBackground: UIColor { + + return UIColor.systemBackground + + } + + static var secondarySystemBackground: UIColor { + + return UIColor.secondarySystemBackground + + } + + static var separator: UIColor { + + return UIColor.separator + + } + + static var label: UIColor { + + return UIColor.label + + } + + static var secondaryLabel: UIColor { + + return UIColor.secondaryLabel + + } + } + + static var buttonArea: UIColor { + + return UIColor { (traits) -> UIColor in + return traits.userInterfaceStyle == .dark + ? UIColor(red: 61 / 255, green: 62 / 255, blue: 66 / 255, alpha: 1) + : UIColor(red: 238 / 255, green: 238 / 255, blue: 240 / 255, alpha: 1) + } + + } + + @available(iOSApplicationExtension, unavailable) + static var tint: UIColor { + get { + let value = UIApplication.shared.windows.first?.tintColor + guard let tint = value else { return .systemBlue } + return tint + } + set { + UIApplication.shared.windows.forEach({ $0.tintColor = newValue }) + } + } +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIFontExtension.swift b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIFontExtension.swift new file mode 100644 index 000000000..b9f7d778f --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UIFontExtension.swift @@ -0,0 +1,32 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +extension UIFont { + + static func preferredFont(forTextStyle style: TextStyle, weight: Weight, addPoints: CGFloat = 0) -> UIFont { + let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: style) + let font = UIFont.systemFont(ofSize: descriptor.pointSize + addPoints, weight: weight) + let metrics = UIFontMetrics(forTextStyle: style) + return metrics.scaledFont(for: font) + } +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UILabelExtension.swift b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UILabelExtension.swift new file mode 100644 index 000000000..2618b746a --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Extensions/UILabelExtension.swift @@ -0,0 +1,37 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +extension UILabel { + + func layoutDynamicHeight(width: CGFloat) { + layoutDynamicHeight(x: frame.origin.x, y: frame.origin.y, width: width) + } + + func layoutDynamicHeight(x: CGFloat, y: CGFloat, width: CGFloat) { + frame = CGRect.init(x: x, y: y, width: width, height: frame.height) + sizeToFit() + if frame.width != width { + frame = .init(x: x, y: y, width: width, height: frame.height) + } + } +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Icons/SPAlertIconErrorView.swift b/apps/mobile/native/ios/Packages/SPIndicator/Icons/SPAlertIconErrorView.swift new file mode 100644 index 000000000..18a9a1855 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Icons/SPAlertIconErrorView.swift @@ -0,0 +1,84 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +public class SPIndicatorIconErrorView: UIView, SPIndicatorIconAnimatable { + + public func animate() { + animateTopToBottomLine() + animateBottomToTopLine() + } + + private func animateTopToBottomLine() { + let length = frame.width + + let topToBottomLine = UIBezierPath() + topToBottomLine.move(to: CGPoint(x: length * 0, y: length * 0)) + topToBottomLine.addLine(to: CGPoint(x: length * 1, y: length * 1)) + + let animatableLayer = CAShapeLayer() + animatableLayer.path = topToBottomLine.cgPath + animatableLayer.fillColor = UIColor.clear.cgColor + animatableLayer.strokeColor = tintColor?.cgColor + animatableLayer.lineWidth = 4 + animatableLayer.lineCap = .round + animatableLayer.lineJoin = .round + animatableLayer.strokeEnd = 0 + self.layer.addSublayer(animatableLayer) + + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = 0.22 + animation.fromValue = 0 + animation.toValue = 1 + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + animatableLayer.strokeEnd = 1 + animatableLayer.add(animation, forKey: "animation") + } + + private func animateBottomToTopLine() { + let length = frame.width + + let bottomToTopLine = UIBezierPath() + bottomToTopLine.move(to: CGPoint(x: length * 0, y: length * 1)) + bottomToTopLine.addLine(to: CGPoint(x: length * 1, y: length * 0)) + + let animatableLayer = CAShapeLayer() + animatableLayer.path = bottomToTopLine.cgPath + animatableLayer.fillColor = UIColor.clear.cgColor + animatableLayer.strokeColor = tintColor?.cgColor + animatableLayer.lineWidth = 4 + animatableLayer.lineCap = .round + animatableLayer.lineJoin = .round + animatableLayer.strokeEnd = 0 + self.layer.addSublayer(animatableLayer) + + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = 0.22 + animation.fromValue = 0 + animation.toValue = 1 + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + + animatableLayer.strokeEnd = 1 + animatableLayer.add(animation, forKey: "animation") + } +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Icons/SPIndicatorIconDoneView.swift b/apps/mobile/native/ios/Packages/SPIndicator/Icons/SPIndicatorIconDoneView.swift new file mode 100644 index 000000000..0df11ab3e --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Icons/SPIndicatorIconDoneView.swift @@ -0,0 +1,51 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +public class SPIndicatorIconDoneView: UIView, SPIndicatorIconAnimatable { + + public func animate() { + let length = frame.width + let animatablePath = UIBezierPath() + animatablePath.move(to: CGPoint(x: length * 0.196, y: length * 0.527)) + animatablePath.addLine(to: CGPoint(x: length * 0.47, y: length * 0.777)) + animatablePath.addLine(to: CGPoint(x: length * 0.99, y: length * 0.25)) + + let animatableLayer = CAShapeLayer() + animatableLayer.path = animatablePath.cgPath + animatableLayer.fillColor = UIColor.clear.cgColor + animatableLayer.strokeColor = tintColor?.cgColor + animatableLayer.lineWidth = 4 + animatableLayer.lineCap = .round + animatableLayer.lineJoin = .round + animatableLayer.strokeEnd = 0 + layer.addSublayer(animatableLayer) + + let animation = CABasicAnimation(keyPath: "strokeEnd") + animation.duration = 0.25 + animation.fromValue = 0 + animation.toValue = 1 + animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + animatableLayer.strokeEnd = 1 + animatableLayer.add(animation, forKey: "animation") + } +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/LICENSE b/apps/mobile/native/ios/Packages/SPIndicator/LICENSE new file mode 100644 index 000000000..a943a888a --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ivan Vorobei + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorLayout.swift b/apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorLayout.swift new file mode 100644 index 000000000..f0b837ea6 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorLayout.swift @@ -0,0 +1,43 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/** + SPIndicator: Layout model for view. + */ +open class SPIndicatorLayout { + + /** + SPIndicator: Icon size. + */ + open var iconSize: CGSize + + /** + SPIndicator: Alert margings for each side. + */ + open var margins: UIEdgeInsets + + public init(iconSize: CGSize, margins: UIEdgeInsets) { + self.iconSize = iconSize + self.margins = margins + } +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorPresentSide.swift b/apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorPresentSide.swift new file mode 100644 index 000000000..434ee3f07 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Models/SPIndicatorPresentSide.swift @@ -0,0 +1,32 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +/** + SPIndicator: Cases from preset styles. + */ +public enum SPIndicatorPresentSide { + + case top + case center + case bottom +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Protocols/SPIndicatorIconAnimatable.swift b/apps/mobile/native/ios/Packages/SPIndicator/Protocols/SPIndicatorIconAnimatable.swift new file mode 100644 index 000000000..389ba9e48 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Protocols/SPIndicatorIconAnimatable.swift @@ -0,0 +1,33 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/** + SPIndicator: Protocol for animatable views. + */ +public protocol SPIndicatorIconAnimatable { + + /** + SPIndicator: Shoud call when need start animation. + */ + func animate() +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/SPIndicator.swift b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicator.swift new file mode 100644 index 000000000..2c6473cf9 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicator.swift @@ -0,0 +1,83 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +@available(iOSApplicationExtension, unavailable) +public enum SPIndicator { + + /** + SPIndicator: Present alert with preset and custom haptic. + + - parameter title: Title text in alert. + - parameter message: Subtitle text in alert. Optional. + - parameter preset: Icon ready-use style or custom image. + - parameter haptic: Haptic response with present. Default is `.success`. + - parameter presentSide: Choose from side appear indicator. + - parameter completion: Will call with dismiss alert. + */ + public static func present( + title: String, message: String? = nil, preset: SPIndicatorIconPreset, + haptic: SPIndicatorHaptic, from presentSide: SPIndicatorPresentSide = .top, + completion: (() -> Void)? = nil + ) { + let alertView = SPIndicatorView(title: title, message: message, preset: preset) + alertView.presentSide = presentSide + alertView.present(haptic: haptic, completion: completion) + } + + /** + SPIndicator: Present alert with preset and automatically detect type haptic. + + - parameter title: Title text in alert. + - parameter message: Subtitle text in alert. Optional. + - parameter preset: Icon ready-use style or custom image. + - parameter presentSide: Choose from side appear indicator. Default is `.top`. + - parameter completion: Will call with dismiss alert. + */ + public static func present( + title: String, message: String? = nil, preset: SPIndicatorIconPreset, + from presentSide: SPIndicatorPresentSide = .top, completion: (() -> Void)? = nil + ) { + let alertView = SPIndicatorView(title: title, message: message, preset: preset) + alertView.presentSide = presentSide + let haptic = preset.getHaptic() + alertView.present(haptic: haptic, completion: completion) + } + + /** + SPIndicator: Show only message, without title and icon. + + - parameter message: Title text. + - parameter haptic: Haptic response with present. Default is `.success`. + - parameter presentSide: Choose from side appear indicator. Default is `.top`. + - parameter completion: Will call with dismiss alert. + */ + public static func present( + title: String, message: String? = nil, haptic: SPIndicatorHaptic, + from presentSide: SPIndicatorPresentSide = .top, completion: (() -> Void)? = nil + ) { + let alertView = SPIndicatorView(title: title, message: message) + alertView.presentSide = presentSide + alertView.present(haptic: haptic, completion: completion) + } + +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorIconPreset.swift b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorIconPreset.swift new file mode 100644 index 000000000..8d06e1728 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorIconPreset.swift @@ -0,0 +1,119 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/** + SPIndicator: Represent icon wrapper. + Included default styles and can be custom image. + */ +public enum SPIndicatorIconPreset { + + case done + case error + case spin(_ style: UIActivityIndicatorView.Style) + case custom(_ image: UIImage) +} + +// Get view and haptic by Preset. + +public extension SPIndicatorIconPreset { + + func createView() -> UIView { + switch self { + case .done: + let view = SPIndicatorIconDoneView() + view.tintColor = .systemBlue + return view + case .error: + let view = SPIndicatorIconErrorView() + view.tintColor = UIColor.systemRed + return view + case .spin(let style): + let view = UIActivityIndicatorView(style: style) + view.startAnimating() + return view + case .custom(let image): + let imageView = UIImageView(image: image) + imageView.contentMode = .scaleAspectFit + return imageView + } + } + + func getHaptic() -> SPIndicatorHaptic { + switch self { + case .error: return .error + case .done: return .success + case .spin(_): return .none + case .custom(_): return .success + } + } +} + +// Get layout by preset. + +public extension SPIndicatorLayout { + + convenience init() { + self.init( + iconSize: .init( + width: Self.defaultIconSideSize, + height: Self.defaultIconSideSize + ), + margins: .init( + top: Self.defaultVerticallInset, + left: Self.defaultHorizontalInset, + bottom: Self.defaultVerticallInset, + right: Self.defaultHorizontalInset + ) + ) + } + + static func message() -> SPIndicatorLayout { + let layout = SPIndicatorLayout() + return layout + } + + convenience init(for preset: SPIndicatorIconPreset) { + switch preset { + case .done: + self.init() + iconSize = .init(width: 24, height: 24) + case .error: + self.init() + iconSize = .init(width: 14, height: 14) + margins.left = 19 + margins.right = margins.left + case .spin(_): + self.init() + case .custom(_): + self.init() + } + } + + // Default values. + + private static var defaultIconSideSize: CGFloat { 28 } + private static var defaultSpaceBetweenIconAndTitle: CGFloat { 26 } + private static var defaultVerticallInset: CGFloat { 8 } + private static var defaultHorizontalInset: CGFloat { 15 } +} + diff --git a/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift new file mode 100644 index 000000000..24aa4fb9d --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift @@ -0,0 +1,654 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/// SPIndicator: Main view. Can be customisable if need. +/// +/// For change duration, check method `present` and pass duration and other specific property if need customise. +/// +/// Here available set window on which shoud be present. +/// If you have some windows, you shoud configure it. Check property `presentWindow`. +/// +/// For disable dismiss by drag, check property `.dismissByDrag`. +/// +/// Recomended call `SPIndicator` and choose style func. +@available(iOSApplicationExtension, unavailable) +open class SPIndicatorView: UIView { + + // MARK: - UIAppearance + + @objc dynamic open var duration: TimeInterval = 1.5 + + // MARK: - Properties + + /** + SPIndicator: Change it for set `top` or `bottom` present side. + Shoud be change before present, instead of no effect. + */ + open var presentSide: SPIndicatorPresentSide = .top + + /** + SPIndicator: By default allow drag indicator for hide. + While indicator is dragging, dismiss not work. + This behaviar can be disabled. + */ + open var dismissByDrag: Bool = true { + didSet { + setGesture() + } + } + + /** + SPIndicator: Completion call after hide indicator. + */ + open var completion: (() -> Void)? = nil + + // MARK: - Views + + open var titleLabel: UILabel? + open var subtitleLabel: UILabel? + open var iconView: UIView? + + private lazy var backgroundView: UIVisualEffectView = { + let view: UIVisualEffectView = { + if #available(iOS 13.0, *) { + return UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial)) + } else { + return UIVisualEffectView(effect: UIBlurEffect(style: .light)) + } + }() + view.isUserInteractionEnabled = false + return view + }() + + weak open var presentWindow: UIWindow? + + // MARK: - Init + + public init(title: String, message: String? = nil, preset: SPIndicatorIconPreset) { + super.init(frame: CGRect.zero) + commonInit() + layout = SPIndicatorLayout(for: preset) + setTitle(title) + if let message = message { + setMessage(message) + } + setIcon(for: preset) + } + + public init(title: String, message: String?) { + super.init(frame: CGRect.zero) + titleAreaFactor = 1.8 + minimumAreaWidth = 100 + commonInit() + layout = SPIndicatorLayout.message() + setTitle(title) + if let message = message { + setMessage(message) + } + } + + public required init?(coder aDecoder: NSCoder) { + self.presentSide = .top + super.init(coder: aDecoder) + commonInit() + } + + private func commonInit() { + preservesSuperviewLayoutMargins = false + insetsLayoutMarginsFromSafeArea = false + + backgroundColor = .clear + backgroundView.layer.masksToBounds = true + addSubview(backgroundView) + + setShadow() + setGesture() + } + + // MARK: - Configure + + private func setTitle(_ text: String) { + let label = UILabel() + label.font = UIFont.preferredFont( + forTextStyle: .footnote, weight: .semibold, addPoints: 0) + label.numberOfLines = 1 + let style = NSMutableParagraphStyle() + style.lineBreakMode = .byTruncatingTail + style.lineSpacing = 3 + label.attributedText = NSAttributedString( + string: text, attributes: [.paragraphStyle: style] + ) + label.textAlignment = .center + label.textColor = UIColor.Compability.label + titleLabel = label + addSubview(label) + } + + private func setMessage(_ text: String) { + let label = UILabel() + label.font = UIFont.preferredFont( + forTextStyle: .footnote, weight: .semibold, addPoints: 0) + label.numberOfLines = 0 + let style = NSMutableParagraphStyle() + style.lineBreakMode = .byTruncatingTail + style.lineSpacing = 2 + label.attributedText = NSAttributedString( + string: text, attributes: [.paragraphStyle: style] + ) + label.textAlignment = .center + label.textColor = UIColor.Compability.label.withAlphaComponent(0.8) + subtitleLabel = label + addSubview(label) + } + + private func setIcon(for preset: SPIndicatorIconPreset) { + let view = preset.createView() + self.iconView = view + addSubview(view) + } + + private func setShadow() { + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.22 + layer.shadowOffset = .init(width: 0, height: 7) + layer.shadowRadius = 40 + + // Not use render shadow becouse backgorund is visual effect. + // If turn on it, background will hide. + // layer.shouldRasterize = true + } + + private func setGesture() { + if dismissByDrag { + let gestureRecognizer = UIPanGestureRecognizer( + target: self, action: #selector(handlePan)) + addGestureRecognizer(gestureRecognizer) + self.gestureRecognizer = gestureRecognizer + } else { + self.gestureRecognizer = nil + } + } + + // MARK: - Present + + private var presentAndDismissDuration: TimeInterval = 0.6 + + private var presentWithOpacity: Bool { + if presentSide == .center { return true } + return false + } + + open func present(haptic: SPIndicatorHaptic = .success, completion: (() -> Void)? = nil) { + present(duration: self.duration, haptic: haptic, completion: completion) + } + + open func present( + duration: TimeInterval, haptic: SPIndicatorHaptic = .success, + completion: (() -> Void)? = nil + ) { + + if self.presentWindow == nil { + self.presentWindow = UIApplication.shared.keyWindow + } + + guard let window = self.presentWindow else { return } + + window.addSubview(self) + + // Prepare for present + + self.whenGestureEndShoudHide = false + self.completion = completion + + isHidden = true + sizeToFit() + layoutSubviews() + center.x = window.frame.midX + toPresentPosition(.prepare(presentSide)) + + self.alpha = presentWithOpacity ? 0 : 1 + + // Present + + isHidden = false + haptic.impact() + UIView.animate( + withDuration: presentAndDismissDuration, delay: 0, usingSpringWithDamping: 1, + initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveEaseOut], + animations: { + self.toPresentPosition(.visible(self.presentSide)) + if self.presentWithOpacity { self.alpha = 1 } + }, + completion: { finished in + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + duration) { + if self.gestureIsDragging { + self.whenGestureEndShoudHide = true + } else { + self.dismiss() + } + } + }) + + if let iconView = self.iconView as? SPIndicatorIconAnimatable { + DispatchQueue.main.asyncAfter( + deadline: DispatchTime.now() + presentAndDismissDuration / 3 + ) { + iconView.animate() + } + } + } + + @objc open func dismiss() { + UIView.animate( + withDuration: presentAndDismissDuration, delay: 0, usingSpringWithDamping: 1, + initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveEaseIn], + animations: { + self.toPresentPosition(.prepare(self.presentSide)) + if self.presentWithOpacity { self.alpha = 0 } + }, + completion: { finished in + self.removeFromSuperview() + self.completion?() + }) + } + + // MARK: - Internal + + private var minimumYTranslationForHideByGesture: CGFloat = -10 + private var maximumYTranslationByGesture: CGFloat = 60 + + private var gestureRecognizer: UIPanGestureRecognizer? + private var gestureIsDragging: Bool = false + private var whenGestureEndShoudHide: Bool = false + + @objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + + if gestureRecognizer.state == .began || gestureRecognizer.state == .changed { + self.gestureIsDragging = true + let translation = gestureRecognizer.translation(in: self) + let newTranslation: CGFloat = { + switch presentSide { + case .top: + if translation.y <= 0 { + return translation.y + } else { + return min(maximumYTranslationByGesture, translation.y.squareRoot()) + } + case .bottom: + if translation.y >= 0 { + return translation.y + } else { + let absolute = abs(translation.y) + return -min(maximumYTranslationByGesture, absolute.squareRoot()) + } + case .center: + let absolute = abs(translation.y).squareRoot() + let newValue = translation.y < 0 ? -absolute : absolute + return min(maximumYTranslationByGesture, newValue) + } + }() + toPresentPosition(.fromVisible(newTranslation, from: (presentSide))) + } + + if gestureRecognizer.state == .ended { + gestureIsDragging = false + + var shoudDismissWhenEndAnimation: Bool = false + + UIView.animate( + withDuration: presentAndDismissDuration, delay: 0, usingSpringWithDamping: 1, + initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveEaseIn], + animations: { + if self.whenGestureEndShoudHide { + self.toPresentPosition(.prepare(self.presentSide)) + shoudDismissWhenEndAnimation = true + } else { + let translation = gestureRecognizer.translation(in: self) + if translation.y < self.minimumYTranslationForHideByGesture { + self.toPresentPosition(.prepare(self.presentSide)) + shoudDismissWhenEndAnimation = true + } else { + self.toPresentPosition(.visible(self.presentSide)) + } + } + }, + completion: { _ in + if shoudDismissWhenEndAnimation { + self.dismiss() + } + }) + } + } + + private func toPresentPosition(_ position: PresentPosition) { + + let getPrepareTransform: ((_ side: SPIndicatorPresentSide) -> CGAffineTransform) = { + [weak self] side in + guard let self = self else { return .identity } + guard let window = UIApplication.shared.windows.first else { return .identity } + switch side { + case .top: + let topInset = window.safeAreaInsets.top + let position = -(topInset + 50) + return CGAffineTransform.identity.translatedBy(x: 0, y: position) + case .bottom: + let height = window.frame.height + let bottomInset = window.safeAreaInsets.bottom + let position = height + bottomInset + 50 + return CGAffineTransform.identity.translatedBy(x: 0, y: position) + case .center: + return CGAffineTransform.identity.translatedBy( + x: 0, y: window.frame.height / 2 - self.frame.height / 2 + ).scaledBy(x: 0.9, y: 0.9) + } + } + + let getVisibleTransform: ((_ side: SPIndicatorPresentSide) -> CGAffineTransform) = { + [weak self] side in + guard let self = self else { return .identity } + guard let window = UIApplication.shared.windows.first else { return .identity } + switch side { + case .top: + var topSafeAreaInsets = window.safeAreaInsets.top + if topSafeAreaInsets < 20 { topSafeAreaInsets = 20 } + let position = topSafeAreaInsets - 3 + self.offset + return CGAffineTransform.identity.translatedBy(x: 0, y: position) + case .bottom: + let height = window.frame.height + var bottomSafeAreaInsets = window.safeAreaInsets.top + if bottomSafeAreaInsets < 20 { bottomSafeAreaInsets = 20 } + let position = + height - bottomSafeAreaInsets - 3 - self.frame.height - self.offset + return CGAffineTransform.identity.translatedBy(x: 0, y: position) + case .center: + return CGAffineTransform.identity.translatedBy( + x: 0, y: window.frame.height / 2 - self.frame.height / 2) + } + } + + switch position { + case .prepare(let presentSide): + transform = getPrepareTransform(presentSide) + case .visible(let presentSide): + transform = getVisibleTransform(presentSide) + case .fromVisible(let translation, let presentSide): + transform = getVisibleTransform(presentSide).translatedBy(x: 0, y: translation) + } + } + + // MARK: - Layout + + /** + SPIndicator: Wraper of layout values. + */ + open var layout: SPIndicatorLayout = .init() + + /** + SPIndicator: Alert offset + */ + open var offset: CGFloat = 0 + + private var minimumAreaHeight: CGFloat = 50 + private var minimumAreaWidth: CGFloat = 196 + private var maximumAreaWidth: CGFloat = 260 + private var titleAreaFactor: CGFloat = 2.5 + private var spaceBetweenTitles: CGFloat = 1 + private var spaceBetweenTitlesAndImage: CGFloat = 16 + + private var titlesCompactWidth: CGFloat { + if let iconView = self.iconView { + let space = iconView.frame.maxY + spaceBetweenTitlesAndImage + return frame.width - space * 2 + } else { + return frame.width - layoutMargins.left - layoutMargins.right + } + } + + private var titlesFullWidth: CGFloat { + if let iconView = self.iconView { + let space = iconView.frame.maxY + spaceBetweenTitlesAndImage + return frame.width - space - layoutMargins.right - self.spaceBetweenTitlesAndImage + } else { + return frame.width - layoutMargins.left - layoutMargins.right + } + } + + open override func sizeThatFits(_ size: CGSize) -> CGSize { + var width = minimumAreaWidth + + // First determine the width we'll use + titleLabel?.sizeToFit() + let titleWidth: CGFloat = titleLabel?.frame.width ?? 0 + + subtitleLabel?.sizeToFit() + let subtitleWidth: CGFloat = subtitleLabel?.frame.width ?? 0 + + width = (max(titleWidth, subtitleWidth) * titleAreaFactor).rounded() + + if width < minimumAreaWidth { width = minimumAreaWidth } + if width > maximumAreaWidth { width = maximumAreaWidth } + + // Now calculate heights based on this width + let availableTextWidth = width - layoutMargins.left - layoutMargins.right + + var titleHeight: CGFloat = 0 + if let titleLabel = titleLabel { + titleLabel.frame.size.width = availableTextWidth + titleLabel.sizeToFit() + titleHeight = titleLabel.frame.height + } + + var subtitleHeight: CGFloat = 0 + if let subtitleLabel = subtitleLabel { + subtitleLabel.frame.size.width = availableTextWidth + subtitleLabel.sizeToFit() + subtitleHeight = subtitleLabel.frame.height + } + + // Calculate dynamic height based on content + var height: CGFloat = 0 + if titleLabel != nil && subtitleLabel != nil { + height = + titleHeight + subtitleHeight + spaceBetweenTitles + layoutMargins.top + + layoutMargins.bottom + } else if titleLabel != nil { + height = titleHeight + layoutMargins.top + layoutMargins.bottom + } else if subtitleLabel != nil { + height = subtitleHeight + layoutMargins.top + layoutMargins.bottom + } + + // Ensure minimum height + if height < minimumAreaHeight { + height = minimumAreaHeight + } + + return .init(width: width, height: height) + } + + open override func layoutSubviews() { + super.layoutSubviews() + + layoutMargins = layout.margins + layer.cornerRadius = frame.height / 2 + backgroundView.frame = bounds + backgroundView.layer.cornerRadius = layer.cornerRadius + + // Flags + + let hasIcon = (self.iconView != nil) + let hasTitle = (self.titleLabel != nil) + let hasSubtite = (self.subtitleLabel != nil) + + let fitTitleToCompact: Bool = { + guard let titleLabel = self.titleLabel else { return true } + titleLabel.numberOfLines = 1 + titleLabel.sizeToFit() + return titleLabel.frame.width < titlesCompactWidth + }() + + let fitSubtitleToCompact: Bool = { + guard let subtitleLabel = self.subtitleLabel else { return true } + let originalLines = subtitleLabel.numberOfLines + subtitleLabel.sizeToFit() + subtitleLabel.numberOfLines = originalLines + return subtitleLabel.frame.width < titlesCompactWidth + }() + + let notFitAnyLabelToCompact: Bool = { + if !fitTitleToCompact { return true } + if !fitSubtitleToCompact { return true } + return false + }() + + var layout: LayoutGrid = .iconTitleCentered + + if (hasIcon && hasTitle && hasSubtite) && !notFitAnyLabelToCompact { + layout = .iconTitleMessageCentered + } + + if (hasIcon && hasTitle && hasSubtite) && notFitAnyLabelToCompact { + layout = .iconTitleMessageLeading + } + + if hasIcon && hasTitle && !hasSubtite { + layout = .iconTitleCentered + } + + if !hasIcon && hasTitle && !hasSubtite { + layout = .title + } + + if !hasIcon && hasTitle && hasSubtite { + layout = .titleMessage + } + + // Actions + + let layoutIcon = { [weak self] in + guard let self = self else { return } + guard let iconView = self.iconView else { return } + iconView.frame = .init( + origin: .init(x: self.layoutMargins.left, y: iconView.frame.origin.y), + size: self.layout.iconSize + ) + iconView.center.y = self.bounds.midY + } + + let layoutTitleCenteredCompact = { [weak self] in + guard let self = self else { return } + guard let titleLabel = self.titleLabel else { return } + titleLabel.textAlignment = .center + titleLabel.layoutDynamicHeight(width: self.titlesCompactWidth) + titleLabel.center.x = self.frame.width / 2 + } + + let layoutTitleCenteredFullWidth = { [weak self] in + guard let self = self else { return } + guard let titleLabel = self.titleLabel else { return } + titleLabel.textAlignment = .center + titleLabel.layoutDynamicHeight(width: self.titlesFullWidth) + titleLabel.center.x = self.frame.width / 2 + } + + let layoutTitleLeadingFullWidth = { [weak self] in + guard let self = self else { return } + guard let titleLabel = self.titleLabel else { return } + guard let iconView = self.iconView else { return } + let rtl = self.effectiveUserInterfaceLayoutDirection == .rightToLeft + titleLabel.textAlignment = rtl ? .right : .left + titleLabel.layoutDynamicHeight(width: self.titlesFullWidth) + titleLabel.frame.origin.x = + self.layoutMargins.left + iconView.frame.width + self.spaceBetweenTitlesAndImage + } + + let layoutSubtitle = { [weak self] in + guard let self = self else { return } + guard let titleLabel = self.titleLabel else { return } + guard let subtitleLabel = self.subtitleLabel else { return } + subtitleLabel.textAlignment = titleLabel.textAlignment + subtitleLabel.layoutDynamicHeight(width: titleLabel.frame.width) + subtitleLabel.frame.origin.x = titleLabel.frame.origin.x + } + + let layoutTitleSubtitleByVertical = { [weak self] in + guard let self = self else { return } + guard let titleLabel = self.titleLabel else { return } + guard let subtitleLabel = self.subtitleLabel else { + titleLabel.center.y = self.bounds.midY + return + } + let allHeight = + titleLabel.frame.height + subtitleLabel.frame.height + self.spaceBetweenTitles + titleLabel.frame.origin.y = (self.frame.height - allHeight) / 2 + subtitleLabel.frame.origin.y = titleLabel.frame.maxY + self.spaceBetweenTitles + } + + // Apply + + switch layout { + case .iconTitleMessageCentered: + layoutIcon() + layoutTitleCenteredCompact() + layoutSubtitle() + case .iconTitleMessageLeading: + layoutIcon() + layoutTitleLeadingFullWidth() + layoutSubtitle() + case .iconTitleCentered: + layoutIcon() + titleLabel?.numberOfLines = 2 + layoutTitleCenteredCompact() + case .iconTitleLeading: + layoutIcon() + titleLabel?.numberOfLines = 2 + layoutTitleLeadingFullWidth() + case .title: + titleLabel?.numberOfLines = 2 + layoutTitleCenteredFullWidth() + case .titleMessage: + layoutTitleCenteredFullWidth() + layoutSubtitle() + } + + layoutTitleSubtitleByVertical() + } + + // MARK: - Models + + enum PresentPosition { + + case prepare(_ from: SPIndicatorPresentSide) + case visible(_ from: SPIndicatorPresentSide) + case fromVisible(_ translation: CGFloat, from: SPIndicatorPresentSide) + } + + enum LayoutGrid { + + case iconTitleMessageCentered + case iconTitleMessageLeading + case iconTitleCentered + case iconTitleLeading + case title + case titleMessage + } +} diff --git a/apps/mobile/native/ios/Packages/SPIndicator/Services/SPIndicatorHaptic.swift b/apps/mobile/native/ios/Packages/SPIndicator/Services/SPIndicatorHaptic.swift new file mode 100644 index 000000000..cfee98e34 --- /dev/null +++ b/apps/mobile/native/ios/Packages/SPIndicator/Services/SPIndicatorHaptic.swift @@ -0,0 +1,47 @@ +// The MIT License (MIT) +// Copyright © 2021 Ivan Vorobei (hello@ivanvorobei.io) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import UIKit + +/// SPIndicator: Wrapper of haptic styles. +public enum SPIndicatorHaptic { + + case success + case warning + case error + case none + + func impact() { + + let generator = UINotificationFeedbackGenerator() + switch self { + case .success: + generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.success) + case .warning: + generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.warning) + case .error: + generator.notificationOccurred(UINotificationFeedbackGenerator.FeedbackType.error) + case .none: + break + } + + } +} diff --git a/apps/mobile/src/lib/toast.tsx b/apps/mobile/src/lib/toast.tsx index 2b27b4804..13a80199c 100644 --- a/apps/mobile/src/lib/toast.tsx +++ b/apps/mobile/src/lib/toast.tsx @@ -35,7 +35,7 @@ export const toast = { if (Platform.OS === "ios") { const NativeToaster = requireNativeModule("Toaster") NativeToaster.toast({ - title: "", + // title: message, message, type, duration: options.duration ? options.duration / 1000 : 1.5, diff --git a/apps/mobile/src/screens/(headless)/debug.tsx b/apps/mobile/src/screens/(headless)/debug.tsx index 47a4dc620..7d2970614 100644 --- a/apps/mobile/src/screens/(headless)/debug.tsx +++ b/apps/mobile/src/screens/(headless)/debug.tsx @@ -106,7 +106,7 @@ export default function DebugPanel() { { title: "Toast", onPress: () => { - toast.error("Hello, world!".repeat(10)) + toast.success("Hello, world!".repeat(3)) }, }, {