SwiftUI extensions

for SwiftUI View

import SwiftUI
import Combine
extension View {
//readSize
func readSize(onChange: @escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: ReadSizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(ReadSizePreferenceKey.self, perform: onChange)
}
}
//ReadSizePreferenceKey
private struct ReadSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
/// toImage
func toImage() -> UIImage? {
let controller = UIHostingController(rootView: self)

let view = controller.view
view?.backgroundColor = .clear

let size = controller.sizeThatFits(in: UIScreen.main.bounds.size)
controller.view.bounds = CGRect(origin: .zero, size: size)

let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { _ in
view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
}

return image
}

/// Type casting to AnyView
///
///     myView.eraseToAnyView()
///
/// - Returns: A view as AnyView
@inlinable func eraseToAnyView() -> AnyView {
AnyView(self)
}
/// Encapsulate view in navigation view
///
///     myView.embedInNavigation()
///
/// - Returns: A view encapsulate in navigation view
@available(watchOS, unavailable)
@inlinable func embedInNavigation() -> some View {
NavigationView { self }
}
/// Positions the view within an invisible frame with the specified size.
///
///     myView.frame(size: CGSize(width: 100, height: 100))
///
/// - Returns: A view with fixed dimensions of width and height
@inlinable func frame(size: CGSize) -> some View {
frame(width: size.width, height: size.height)
}
/// Calls a block each time that view is reloaded
///
///     content.onReload(perform: {
///         print("onReload")
///     })
///
/// - Returns: The same view but calling the block asynchronously when is reloaded
@inlinable func onReload(perform: @escaping () -> Void) -> some View {
DispatchQueue.main.async {
perform()
}
return self
}
}
// MARK: Building
public extension View {
/// Apply changes to the view if the condition is true
///
///    myView
///    .if(index == state.currentIndex, then: {
///        $0.background(Color.red)
///    })
///
/// - Parameters:
///   - condition: an boolean to control the condition
///   - then: callback to apply the changes when the condition is true
/// - Returns: some View
@inlinable func `if`<Content: View>(
_ conditional: Bool,
then: (Self) -> Content
) -> TupleView<(Self?, Content?)> {
if conditional {
return TupleView((nil, then(self)))
}
return TupleView((self, nil))
}
/// Apply some changes to the view in place of the condition
///
///    myView
///    .if(index == state.currentIndex, then: {
///        $0.background(Color.red)
///    }, else: {
///        $0.background(Color.yellow)
///    })
///
/// - Parameters:
///   - condition: an boolean to control the condition
///   - then: callback to apply the changes when the condition is true
///   - else: callback to apply the changes when the condition is false
/// - Returns: some View
@inlinable func `if`<A: View, B: View>(
_ conditional: Bool,
then: (Self) -> A,
`else`: (Self) -> B
) -> TupleView<(A?, B?)> {
if conditional {
return TupleView((then(self), nil))
}
return TupleView((nil, `else`(self)))
}
}
// MARK: Modifiers
public extension View {
/// Set one modifier conditionally.
///
///    myView.conditionalModifier(myCondition, myViewModifier)
///
/// - Parameters:
///   - condition: an boolean to control the condition
///   - modifier: modifier to apply
/// - Returns: some View
@inlinable func conditionalModifier<M: ViewModifier>(
_ condition: Bool,
_ modifier: M
) -> TupleView<(Self?, ModifiedContent<Self, M>?)> {
if condition {
return TupleView((nil, self.modifier(modifier)))
}
return TupleView((self, nil))
}
/// Set one modifier or another conditionally.
///
///    myView.conditionalModifier(myCondition, firstViewModifier, secondViewModifier)
///
/// - Parameters:
///   - condition: an boolean to control the condition
///   - trueModifier: modifier to apply when the condition is true
///   - falseModifier: modifier to apply when the condition is false
/// - Returns: some View
@inlinable func conditionalModifier<M: ViewModifier>(
_ condition: Bool,
_ trueModifier: M,
_ falseModifier: M
) -> TupleView<(ModifiedContent<Self, M>?, ModifiedContent<Self, M>?)> {
if condition {
return TupleView((self.modifier(trueModifier), nil))
}
return TupleView((nil, self.modifier(falseModifier)))
}
}
// MARK: Animations
public extension View {
/// Animate an action with an animation on appear.
///
///    myView.animateOnAppear(using: .easeInOut) { self.scale = 0.5 }
///
/// - Parameters:
///   - animation: animation to be applied
///   - action: action to be animated
/// - Returns: some View
@inlinable func animateOnAppear(using animation: Animation = .easeInOut,
action: @escaping () -> Void) -> some View {
return onAppear {
withAnimation(animation) {
action()
}
}
}
/// Animate an action with an animation on disappear.
///
///    myView.animateOnDisappear(using: .easeInOut) { self.scale = 0.5 }
///
/// - Parameters:
///   - animation: animation to be applied
///   - action: action to be animated
/// - Returns: some View
@inlinable func animateOnDisappear(using animation: Animation = .easeInOut,
action: @escaping () -> Void) -> some View {
return onDisappear {
withAnimation(animation) {
action()
}
}
}
}
// MARK: Combine
public extension View {
/// Bind publisher to state
///
/// The following example uses this method to implement an async image view.
/// ```
/// struct AsyncImage: View {
///    @State private var image: UIImage
///    private let source: AnyPublisher<UIImage, Never>
///    private let animation: Animation?
///
///     init(
///         source: AnyPublisher<UIImage, Never>,
///         placeholder: UIImage,
///         animation: Animation? = nil
///     ) {
///         self.source = source
///         self.animation = animation
///         self._image = State(initialValue:placeholder)
///     }
///
///     var body: some View {
///        return Image(uiImage: image)
///             .resizable()
///             .bind(source, to: $image.animation(animation))
///     }
/// }
/// ```
///
/// - Parameters:
///   - publisher: publisher to observe when a value is received
///   - state: state to assign the new value
/// - Returns: some View
@inlinable func bind<P: Publisher, Value>(
_ publisher: P,
to state: Binding<Value>
) -> some View where P.Failure == Never, P.Output == Value {
return onReceive(publisher) { value in
state.wrappedValue = value
}
}
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension View {
/// Tracks the size available for the view
///
///     myView.sizeTrackable($size)
///
/// - Parameter size: This binding will receive the size updates
@inlinable func sizeTrackable(_ size: Binding<CGSize>) -> some View {
self.modifier(SizeViewModifier(size: size))
}
}

for SwiftUI LinearGradient

import SwiftUI
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension LinearGradient {
/// Create a gradient directly from colors
///
///    let myGradient = LinearGradient(Color.red, Color.blue)
///
/// - Parameters:
///   - colors: array of colors
///   - startPoint: unit point where gradient starts
///   - endPoint: unit point where gradient starts
/// - Returns: A new linear gradient
@inlinable init(_ colors: Color..., startPoint: UnitPoint = .topLeading, endPoint: UnitPoint = .bottomTrailing) {
self.init(gradient: Gradient(colors: colors), startPoint: startPoint, endPoint: endPoint)
}
}

for SwiftUI Image

import SwiftUI
#if canImport(UIKit)
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension Image {
/// Create a image with default one
///
///    let image = Image("photo", defaultImage: "empty-photo")
///
/// - Parameters:
///   - name: Image name
///   - defaultImage: Default image name
/// - Returns: A new image
@inlinable init(_ name: String, defaultImage: String) {
if let img = UIImage(named: name) {
self.init(uiImage: img)
} else {
self.init(defaultImage)
}
}
/// Create a image with default one
///
///    let image = Image("photo", defaultSystemImage: "bandage.fill")
///
/// - Parameters:
///   - name: Image name
///   - defaultSystemImage: Default  system image name
/// - Returns: A new image
@available(OSX 10.15, *)
@inlinable init(_ name: String, defaultSystemImage: String) {
if let img = UIImage(named: name) {
self.init(uiImage: img)
} else {
self.init(systemName: defaultSystemImage)
}
}
}
#endif
#if canImport(AppKit)
@available(OSX 10.15, *)
public extension Image {
/// Create a image with default one
///
///    let image = Image("photo", defaultImage: "empty-photo")
///
/// - Parameters:
///   - name: Image name
///   - defaultImage: Default image name
/// - Returns: A new image
@inlinable init(_ name: String, defaultImage: String) {
if let img = NSImage(named: name) {
self.init(nsImage: img)
} else {
self.init(defaultImage)
}
}
}
#endif

for SwiftUI Color

import SwiftUI
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public extension Color {
/// Create a color from hex string
/// It supports hex string of rgb and rgba with the # character as optional
/// Examples: #AABBCC, AABBCC, #AABBCCFF or AABBCCFF.
///
///    let myGradient = Color(hex: "#FFFFFF")
///
/// - Parameters:
///   - hex: Hex color string
/// - Returns: New color from the hex value
@inlinable init?(hex: String) {
let hexColor: String
if hex.hasPrefix("#") {
let start = hex.index(hex.startIndex, offsetBy: 1)
hexColor = String(hex[start...])
} else {
hexColor = hex
}
let count = hexColor.count
guard count == 6 || count == 8 else {
return nil
}
let scanner = Scanner(string: hexColor)
var hexNumber: UInt64 = 0
guard scanner.scanHexInt64(&hexNumber) else {
return nil
}
let r, g, b, a: Double
if count == 6 { //rgb
r = Double((hexNumber & 0x00ff0000) >> 16)
g = Double((hexNumber & 0x0000ff00) >> 8)
b = Double(hexNumber & 0x000000ff)
a = 255
} else { //rgba
r = Double((hexNumber & 0xff000000) >> 24)
g = Double((hexNumber & 0x00ff0000) >> 16)
b = Double((hexNumber & 0x0000ff00) >> 8)
a = Double(hexNumber & 0x000000ff)
}
self.init(.sRGB, red: r / 255, green: g / 255, blue: b / 255, opacity: a / 255)
}
}

SwiftUI Modifiers

import SwiftUI
/// This modifier wraps a view into a `GeometryReader` and tracks the available space.
///
///     @State var size: CGSize = .zero
///
///     myView.modifier(SizeViewModifier(size: $size))
///
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public struct SizeViewModifier: ViewModifier {
@Binding private(set) var size: CGSize
/// Create a size view modifier from a CGSize binding
///
///     myView.modifier(SizeViewModifier(size: $size))
///
/// - Parameters:
///   - size: CGSize binding
/// - Returns: A new modifier
public init(size: Binding<CGSize>) {
self._size = size
}
public func body(content: Content) -> some View {
GeometryReader { proxy in
content
.frame(size: proxy.size)
.onReload(perform: {
self.size = proxy.size
})
}
}
}

swiftUI Utils functions

import SwiftUI
/// It is an utility that adds back a way to use the if let.
///
///    ifLet(myImage, then: {
///       $0.resizable()
///    })
///
/// - Parameters:
///   - value: optional value to verify
///   - then: callback to create a view with the value is not nil
/// - Returns: The built view
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@inlinable public func ifLet<T, ThenOut: View>(_ value: T?, then: (T) -> ThenOut) -> some View {
ViewBuilder.buildIf(value.map { then($0) })
}
/// It is an utility that adds back a way to use the if let with an else option
///
///    ifLet(myImage, then: {
///       $0.resizable()
///    }, else: {
///       Text("Hello")
///    })
///
/// - Parameters:
///   - value: optional value to verify
///   - then: callback to create a view with the value is not nil
///   - else: callback to create a view with the value is nil
/// - Returns: The built view
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@inlinable public func ifLet<T, ThenOut: View, ElseOut: View>(
_ value: T?,
then: (T) -> ThenOut,
`else`: () -> ElseOut) -> some View {
value.map { ViewBuilder.buildEither(first: then($0)) } ??
ViewBuilder.buildEither(second: `else`())
}
/// It is an utility that adds back a way to use the if let for collections checking if it is empty.
///
///    ifLet(myText, empty: false, then: {
///       Text($0)
///    })
///
/// - Parameters:
///   - value: optional value to verify
///   - empty: condition to check if string is valid when is empty.
///   - then: callback to create a view with the value is not nil
/// - Returns: The built view
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@inlinable public func ifLet<T, ThenOut: View>(
_ value: T?,
empty: Bool = false,
then: (T) -> ThenOut
) -> some View where T : Collection {
let value = !empty && value.isNilOrEmpty ? nil : value
return ViewBuilder.buildIf(value.map { then($0) })
}
/// It is an utility that adds back a way to use the if let with an else option
///
///    ifLet(myText, empty: false, then: {
///       $0
///    }, else: {
///       Text("Hello")
///    })
///
/// - Parameters:
///   - value: optional value to verify
///   - empty: condition to check if string is valid when is empty.
///   - then: callback to create a view with the value is not nil
///   - else: callback to create a view with the value is nil
/// - Returns: The built view
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@inlinable public func ifLet<T, ThenOut: View, ElseOut: View>(
_ value: T?,
empty: Bool = false,
then: (T) -> ThenOut,
`else`: () -> ElseOut
) -> some View where T : Collection {
let value = !empty && value.isNilOrEmpty ? nil : value
return value.map { ViewBuilder.buildEither(first: then($0)) } ??
ViewBuilder.buildEither(second: `else`())
}

for SwiftUI UserDefault

import Foundation
/// A type that adds an interface to use the user’s defaults with default types
///
/// Example:
/// ```
/// @UserDefault(key: "nameKey", defaultValue: "Root") var name: String
/// ```
/// Adding the attribute @UserDefault the property works reading and writing from user's defaults
///
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct UserDefault<T> {
private let key: String
private let defaultValue: T
/// Initialize the key and the default value.
public init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
public var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}

for SwiftUI UserDefaultEnum

import Foundation
/// A type that adds an interface to use the user’s defaults with raw representable types where the raw value is of type string
///
/// Example:
/// ```
/// enum Beverage: String, CaseIterable {
///     case coffee, tea, juice
/// }
///
/// @UserDefaultEnum(key: "beverageKey", defaultValue: .coffee) var beverage: Beverage
/// ```
/// Adding the attribute @UserDefaultEnum the property works reading and writing from user's defaults
///
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
@propertyWrapper public struct UserDefaultEnum<T: RawRepresentable> where T.RawValue == String {
private let key: String
private let defaultValue: T
/// Initialize the key and the default value.
public init(key: String, defaultValue: T) {
self.key = key
self.defaultValue = defaultValue
}
public var wrappedValue: T {
get {
if let string = UserDefaults.standard.string(forKey: key) {
return T(rawValue: string) ?? defaultValue
}
return defaultValue
}
set {
UserDefaults.standard.set(newValue.rawValue, forKey: key)
}
}
}