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 <tukon479@gmail.com>
This commit is contained in:
parent
8d982eaacb
commit
085bb89b36
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
//
|
||||
|
||||
import ExpoModulesCore
|
||||
import SPIndicator
|
||||
|
||||
enum ToastType: String, Enumerable {
|
||||
case error
|
||||
|
|
@ -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<Bool>,
|
||||
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<Bool>,
|
||||
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<Bool>,
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export default function DebugPanel() {
|
|||
{
|
||||
title: "Toast",
|
||||
onPress: () => {
|
||||
toast.error("Hello, world!".repeat(10))
|
||||
toast.success("Hello, world!".repeat(3))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue