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)
        }
    }
}