Files
PopupView/Source/Utils.swift
T

317 lines
9.0 KiB
Swift

//
// Utils.swift
// PopupView
//
// Created by Alisa Mylnikova on 01.06.2022.
// Copyright © 2022 Exyte. All rights reserved.
//
import SwiftUI
import Combine
final class DispatchWorkHolder {
var work: DispatchWorkItem?
}
final class ClassReference<T> {
var value: T
init(_ value: T) {
self.value = value
}
}
extension View {
@ViewBuilder
func valueChanged<T: Equatable>(value: T, onChange: @escaping (T) -> Void) -> some View {
if #available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *) {
self.onChange(of: value, perform: onChange)
} else {
self.onReceive(Just(value)) { value in
onChange(value)
}
}
}
}
extension View {
@ViewBuilder
func applyIf<T: View>(_ condition: Bool, apply: (Self) -> T) -> some View {
if condition {
apply(self)
} else {
self
}
}
@ViewBuilder
func addTapIfNotTV(if condition: Bool, onTap: @escaping ()->()) -> some View {
#if os(tvOS)
self
#else
if condition {
self.gesture(
TapGesture().onEnded {
onTap()
}
)
} else {
self
}
#endif
}
}
// MARK: - FrameGetter
struct FrameGetter: ViewModifier {
@Binding var frame: CGRect
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy -> AnyView in
DispatchQueue.main.async {
let rect = proxy.frame(in: .global)
// This avoids an infinite layout loop
if rect.integral != self.frame.integral {
self.frame = rect
}
}
return AnyView(EmptyView())
}
)
}
}
internal extension View {
func frameGetter(_ frame: Binding<CGRect>) -> some View {
modifier(FrameGetter(frame: frame))
}
}
struct SafeAreaGetter: ViewModifier {
@Binding var safeArea: EdgeInsets
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy -> AnyView in
DispatchQueue.main.async {
let area = proxy.safeAreaInsets
// This avoids an infinite layout loop
if area != self.safeArea {
self.safeArea = area
}
}
return AnyView(EmptyView())
}
)
}
}
extension View {
public func safeAreaGetter(_ safeArea: Binding<EdgeInsets>) -> some View {
modifier(SafeAreaGetter(safeArea: safeArea))
}
}
// MARK: - AnimationCompletionObserver
struct AnimationCompletionObserverModifier<Value>: AnimatableModifier where Value: VectorArithmetic, Value: Comparable {
/// While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes.
var animatableData: Value {
didSet {
notifyCompletionIfFinished()
}
}
/// The target value for which we're observing. This value is directly set once the animation starts. During animation, `animatableData` will hold the oldValue and is only updated to the target value once the animation completes.
private var targetValue: Value
/// The completion callback which is called once the animation completes.
private var completion: () -> Void
init(observedValue: Value, completion: @escaping () -> Void) {
self.completion = completion
self.animatableData = observedValue
targetValue = observedValue
}
/// Verifies whether the current animation is finished and calls the completion callback if true.
private func notifyCompletionIfFinished() {
guard animatableData == targetValue else { return }
/// Dispatching is needed to take the next runloop for the completion callback.
/// This prevents errors like "Modifying state during view update, this will cause undefined behavior."
DispatchQueue.main.async {
self.completion()
}
}
func body(content: Content) -> some View {
/// We're not really modifying the view so we can directly return the original input value.
return content
}
}
struct AnimatableModifierDouble: AnimatableModifier {
var targetValue: Double
static var done = false
// SwiftUI gradually varies it from old value to the new value
var animatableData: Double {
didSet {
checkIfFinished()
}
}
var completion: () -> ()
// Re-created every time the control argument changes
init(bindedValue: Double, completion: @escaping () -> ()) {
self.completion = completion
// Set animatableData to the new value. But SwiftUI again directly
// and gradually varies the value while the body
// is being called to animate. Following line serves the purpose of
// associating the extenal argument with the animatableData.
self.animatableData = bindedValue
targetValue = bindedValue
AnimatableModifierDouble.done = false
}
func checkIfFinished() -> () {
if AnimatableModifierDouble.done { return }
let delta = 0.1
if animatableData > targetValue - delta &&
animatableData < targetValue + delta {
//print("check", animatableData, targetValue)
AnimatableModifierDouble.done = true
DispatchQueue.main.async {
self.completion()
}
}
}
func body(content: Content) -> some View {
content
}
}
extension View {
func onAnimationCompleted(for value: Double, completion: @escaping () -> Void) -> some View {
modifier(AnimatableModifierDouble(bindedValue: value, completion: completion))
}
}
// MARK: - TransparentNonAnimatingFullScreenCover
#if os(iOS)
extension View {
func transparentNonAnimatingFullScreenCover<Content: View>(isPresented: Binding<Bool>, content: @escaping () -> Content) -> some View {
modifier(TransparentNonAnimatableFullScreenModifier(isPresented: isPresented, fullScreenContent: content))
}
}
private struct TransparentNonAnimatableFullScreenModifier<FullScreenContent: View>: ViewModifier {
@Binding var isPresented: Bool
let fullScreenContent: () -> (FullScreenContent)
func body(content: Content) -> some View {
content
.onChange(of: isPresented) { isPresented in
UIView.setAnimationsEnabled(false)
}
.fullScreenCover(isPresented: $isPresented, content: {
ZStack {
fullScreenContent()
}
.background(FullScreenCoverBackgroundRemovalView())
.onAppear {
if !UIView.areAnimationsEnabled {
UIView.setAnimationsEnabled(true)
}
}
.onDisappear {
if !UIView.areAnimationsEnabled {
UIView.setAnimationsEnabled(true)
}
}
})
}
}
private struct FullScreenCoverBackgroundRemovalView: UIViewRepresentable {
private class BackgroundRemovalView: UIView {
override func didMoveToWindow() {
super.didMoveToWindow()
superview?.superview?.backgroundColor = .clear
}
}
func makeUIView(context: Context) -> UIView {
return BackgroundRemovalView()
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
#endif
// MARK: - KeyboardHeightHelper
#if os(iOS)
class KeyboardHeightHelper: ObservableObject {
@Published var keyboardHeight: CGFloat = 0
@Published var keyboardDisplayed: Bool = false
init() {
self.listenForKeyboardNotifications()
}
private func listenForKeyboardNotifications() {
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main) { (notification) in
guard let userInfo = notification.userInfo,
let keyboardRect = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
self.keyboardHeight = keyboardRect.height
self.keyboardDisplayed = true
}
NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification,
object: nil,
queue: .main) { (notification) in
self.keyboardHeight = 0
self.keyboardDisplayed = false
}
}
}
#else
class KeyboardHeightHelper: ObservableObject {
@Published var keyboardHeight: CGFloat = 0
@Published var keyboardDisplayed: Bool = false
}
#endif