[XRAY-2] Part 3 - Move swift ui extensions to their own repository (#1)
Works on xiiagency/Xray#2
This commit is contained in:
parent
bcd93e8ddd
commit
1bb027c8e0
|
@ -0,0 +1,29 @@
|
|||
// swift-tools-version:5.5
|
||||
import PackageDescription
|
||||
|
||||
let package =
|
||||
Package(
|
||||
name: "SwiftUIExtensions",
|
||||
platforms: [
|
||||
.iOS(.v15),
|
||||
.watchOS(.v8),
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "SwiftUIExtensions",
|
||||
targets: ["SwiftUIExtensions"]
|
||||
),
|
||||
],
|
||||
dependencies: [],
|
||||
targets: [
|
||||
.target(
|
||||
name: "SwiftUIExtensions",
|
||||
dependencies: []
|
||||
),
|
||||
// NOTE: Re-enable when tests are added.
|
||||
// .testTarget(
|
||||
// name: "SwiftUIExtensionsTests",
|
||||
// dependencies: ["SwiftUIExtensions"]
|
||||
// ),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# SwiftUIExtensions
|
||||
|
||||
Provides extensions and utilities for SwiftUI.
|
|
@ -0,0 +1,12 @@
|
|||
import SwiftUI
|
||||
|
||||
/**
|
||||
Performs the provided block in a transaction with animations disabled.
|
||||
Useful for cancelling some animations that are hard to control (e.g. navigation view transitions).
|
||||
NOTE: This is a bit finicky, use at your own risk.
|
||||
*/
|
||||
public func doWithoutAnimations(_ block: @escaping () -> Void) {
|
||||
var transaction = Transaction(animation: nil)
|
||||
transaction.disablesAnimations = true
|
||||
withTransaction(transaction, block)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import SwiftUI
|
||||
|
||||
/**
|
||||
A specialty view that has no visible representation (clear 0x0 pixel view) but can still have functionality attached (e.g. onChange(of:)).
|
||||
NOTE: If used in a container view that has spacing (e.g. VStack) this view will still be counted as one and will add unintended spacing.
|
||||
*/
|
||||
public struct HiddenView : View {
|
||||
public init() { }
|
||||
|
||||
public var body: some View {
|
||||
Color.clear
|
||||
.frame(width: 0, height: 0)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import Foundation
|
||||
|
||||
/**
|
||||
An implementation of a momentary switch: one that has an on/off state, can be turned on repeatedly, turns off after a delay.
|
||||
|
||||
If triggered multiple times before the off delay is elapses, it re-schedules the reaction (relative to the last time it was triggered).
|
||||
|
||||
Ideally used for triggering animations on user interaction, and auto-reverting once they've stopped interacting for enough time.
|
||||
|
||||
NOTE: The switch itself will operate on the background thread, but the on/off callbacks are executed on the `@MainActor`.
|
||||
*/
|
||||
public class MomentarySwitch {
|
||||
/**
|
||||
Reference to the currently scheduled task to fire the latest known "off" event.
|
||||
*/
|
||||
private var scheduledOffTask: Task<Void, Never>? = nil
|
||||
|
||||
public init() { }
|
||||
|
||||
/**
|
||||
Triggers the supplied `switchOn `closure and schedules to run the supplied `switchOff` closure after a delay.
|
||||
If there's already an unfinished task (switch is on):
|
||||
- we will cancel the previous "off" task and create a new one with an updated time and `switchOff` closure.
|
||||
- the `switchOn` callback will NOT be called.
|
||||
*/
|
||||
public func trigger(
|
||||
offDelaySeconds: Double = 0.0,
|
||||
switchOn: @escaping () -> Void,
|
||||
switchOff: @escaping () -> Void
|
||||
) {
|
||||
if let scheduledOffTask = scheduledOffTask {
|
||||
// If there is a scheduled off task, cancel it.
|
||||
scheduledOffTask.cancel()
|
||||
} else {
|
||||
// Otherwise, trigger the "on" action.
|
||||
switchOn()
|
||||
}
|
||||
|
||||
// Start a task to trigger the "off" action after the requested delay.
|
||||
scheduledOffTask = Task {
|
||||
// The task is kicked off right away, but we need to wait for the requested delay.
|
||||
try? await Task.sleep(nanoseconds: UInt64(offDelaySeconds * 1_000_000_000))
|
||||
|
||||
// If the task was cancelled while we were waiting, nothing to do.
|
||||
if Task.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, trigger the "off" action on the main actor.
|
||||
Task { @MainActor in
|
||||
switchOff()
|
||||
}
|
||||
|
||||
// Also clear the task reference.
|
||||
scheduledOffTask = nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
/**
|
||||
Adds a modifier for this view that fires an action when the `ScenePhase` has changed.
|
||||
|
||||
`action` is called on the main thread. Avoid performing long-running tasks on the main thread.
|
||||
*/
|
||||
public func onScenePhaseChanged(_ action: @escaping (ScenePhase) -> Void) -> some View {
|
||||
modifier(ScenePhaseModifier(onChanged: action))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
A modifier that monitors the `ScenePhase` of the current scene and reports changes to the provided callback.
|
||||
*/
|
||||
private struct ScenePhaseModifier : ViewModifier {
|
||||
/**
|
||||
The current `ScenePhase` of the application scene.
|
||||
*/
|
||||
@Environment(\.scenePhase) private var scenePhase: ScenePhase
|
||||
|
||||
/**
|
||||
Callback to trigger whenever the `ScenePhase` has changed.
|
||||
*/
|
||||
let onChanged: (ScenePhase) -> Void
|
||||
|
||||
/**
|
||||
Attaches an `.onChange` modifier to the modified view that monitors changes in `ScenePhase`.
|
||||
*/
|
||||
func body(content: Content) -> some View {
|
||||
content.onChange(of: scenePhase, perform: onChanged)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
/**
|
||||
Resets the text case of the View.
|
||||
Equivalent to: `textCase(nil)`
|
||||
*/
|
||||
public func standardcase() -> some View {
|
||||
textCase(nil)
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the text case of the View to uppercase.
|
||||
Equivalent to: `textCase(.uppercase)`
|
||||
*/
|
||||
public func uppercase() -> some View {
|
||||
textCase(.uppercase)
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the text case of the View to lowercase.
|
||||
Equivalent to `textCase(.lowercase)`
|
||||
*/
|
||||
public func lowercase() -> some View {
|
||||
textCase(.lowercase)
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the view's frame to stretch to fill its parent by setting maxWidth (and/or) maxHeight based on the requested `Axis`.
|
||||
- By default it will stretch `.horizontal` as it's the more common scenario.
|
||||
- Also allows for adjusting internal alignment with `.center` used as the default.
|
||||
*/
|
||||
public func stretch(
|
||||
_ axes: Axis.Set = [.horizontal],
|
||||
alignment: Alignment = .center
|
||||
) -> some View {
|
||||
frame(
|
||||
maxWidth: axes.contains(.horizontal) ? .infinity : nil,
|
||||
maxHeight: axes.contains(.vertical) ? .infinity : nil,
|
||||
alignment: alignment
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Provide static members for various standard sets of `Axis`.
|
||||
*/
|
||||
extension Axis.Set {
|
||||
/**
|
||||
An empty set of `Axis`.
|
||||
*/
|
||||
public static let None: Axis.Set = []
|
||||
|
||||
/**
|
||||
A set of `Axis` containing only `.horizontal`.
|
||||
*/
|
||||
public static let Horizontal: Axis.Set = [.horizontal]
|
||||
|
||||
/**
|
||||
A set of `Axis` containing only `.vertical`.
|
||||
*/
|
||||
public static let Vertical: Axis.Set = [.vertical]
|
||||
|
||||
/**
|
||||
A set of `Axis` containing both the `.horizontal` and `.vertical` values.
|
||||
*/
|
||||
public static let All: Axis.Set = [.horizontal, .vertical]
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
#if os(iOS)
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
Dismisses the keyboard from anywhere in the application.
|
||||
NOTE: Only available on iOS.
|
||||
*/
|
||||
public func hideKeyboard() {
|
||||
UIApplication.shared
|
||||
.sendAction(
|
||||
#selector(UIResponder.resignFirstResponder),
|
||||
to: nil,
|
||||
from: nil,
|
||||
for: nil
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
Opens the given URL if it can be opened on the current device. Does nothing otherwise.
|
||||
*/
|
||||
public func sendUserToApplicationUrl(_ url: URL) {
|
||||
// Ensure the url for the app can actually be opened.
|
||||
guard UIApplication.shared.canOpenURL(url) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Finally, send the user to the requested app url.
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
/**
|
||||
Opens the given URL string if it can be parsed and opened on the current device. Does nothing otherwise.
|
||||
*/
|
||||
public func sendUserToApplicationUrl(_ urlString: String) {
|
||||
// Make sure the URL can be parsed first.
|
||||
guard let url = URL(string: urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
sendUserToApplicationUrl(url)
|
||||
}
|
||||
|
||||
/**
|
||||
Opens the App's setting screen in the OS' Settings app.
|
||||
*/
|
||||
public func sendUserToApplicationSettings() {
|
||||
sendUserToApplicationUrl(UIApplication.openSettingsURLString)
|
||||
}
|
||||
#endif
|
Loading…
Reference in New Issue