├── Sources └── VComponents │ ├── Resources │ └── Images.xcassets │ │ ├── Contents.json │ │ ├── XMark.imageset │ │ ├── Contents.json │ │ └── XMark.svg │ │ ├── Chevron.Up.imageset │ │ ├── Contents.json │ │ └── Chevron.Up.svg │ │ ├── MagnifyGlass.imageset │ │ ├── Contents.json │ │ └── MagnifyGlass.svg │ │ ├── Visibility.Off.imageset │ │ ├── Contents.json │ │ └── Visibility.Off.svg │ │ ├── Checkmark.Indeterminate.imageset │ │ ├── Checkmark.Indeterminate.svg │ │ └── Contents.json │ │ ├── Checkmark.On.imageset │ │ ├── Contents.json │ │ └── Checkmark.On.svg │ │ └── Visibility.On.imageset │ │ ├── Contents.json │ │ └── Visibility.On.svg │ ├── Components │ ├── Containers │ │ ├── Group Box │ │ │ ├── VGroupBoxContent.swift │ │ │ ├── VGroupBoxUIModel.swift │ │ │ └── VGroupBox.swift │ │ ├── Carousel │ │ │ ├── VCarouselState.swift │ │ │ ├── VCarouselUIModel.swift │ │ │ └── VCarouselInfiniteScrollDataSourceManager.swift │ │ ├── Disclosure Group │ │ │ ├── VDisclosureGroupHeaderLabel.swift │ │ │ └── VDisclosureGroupState.swift │ │ ├── Dynamic Pager Tab View │ │ │ ├── VDynamicPagerTabViewState.swift │ │ │ ├── VDynamicPagerTabViewTabItemLabel.swift │ │ │ └── VDynamicPagerTabViewUIModel.swift │ │ ├── Static Pager Tab View (Wrapped Indicator) │ │ │ ├── VWrappedIndicatorStaticPagerTabViewState.swift │ │ │ ├── VWrappedIndicatorStaticPagerTabViewTabItemLabel.swift │ │ │ └── VWrappedIndicatorStaticPagerTabViewUIModel.swift │ │ └── Static Pager Tab View (Stretched Indicator) │ │ │ ├── VStretchedIndicatorStaticPagerTabViewState.swift │ │ │ ├── VStretchedIndicatorStaticPagerTabViewTabItemLabel.swift │ │ │ └── VStretchedIndicatorStaticPagerTabViewUIModel.swift │ ├── Value Pickers │ │ ├── Slider (Range) │ │ │ ├── VRangeSliderThumb.swift │ │ │ ├── VRangeSliderState.swift │ │ │ └── VRangeSliderUIModel.swift │ │ └── Slider │ │ │ ├── VSliderState.swift │ │ │ └── VSliderUIModel.swift │ ├── Indicators (Definite) │ │ ├── Page Indicator │ │ │ ├── VPageIndicatorState.swift │ │ │ ├── VPageIndicatorDotContent.swift │ │ │ └── VPageIndicatorUIModel.swift │ │ ├── Page Indicator (Compact) │ │ │ ├── VCompactPageIndicatorState.swift │ │ │ └── VCompactPageIndicatorDotContent.swift │ │ └── Progress Bar │ │ │ ├── VProgressBarUIModel.swift │ │ │ └── VProgressBar.swift │ ├── Modals (Alerts) │ │ └── Alert │ │ │ ├── VAlertContent.swift │ │ │ ├── VAlertButtonProtocol.swift │ │ │ ├── VAlertButtonConvertible.swift │ │ │ ├── VAlertButton.swift │ │ │ └── VAlertButtonBuilder.swift │ ├── Buttons │ │ ├── Plain Button │ │ │ ├── VPlainButtonState.swift │ │ │ ├── VPlainButtonLabel.swift │ │ │ └── VPlainButtonUIModel.swift │ │ ├── Wrapped Button │ │ │ ├── VWrappedButtonState.swift │ │ │ └── VWrappedButtonLabel.swift │ │ ├── Stretched Button │ │ │ ├── VStretchedButtonState.swift │ │ │ └── VStretchedButtonLabel.swift │ │ ├── Rectangular Button │ │ │ ├── VRectangularButtonState.swift │ │ │ └── VRectangularButtonLabel.swift │ │ ├── Stretched Button (Loading) │ │ │ ├── VLoadingStretchedButtonState.swift │ │ │ └── VLoadingStretchedButtonLabel.swift │ │ └── Caption Button (Rectangular) │ │ │ ├── VRectangularCaptionButtonState.swift │ │ │ └── VRectangularCaptionButtonCaption.swift │ ├── Inputs │ │ ├── TextView │ │ │ └── VTextViewState.swift │ │ ├── Code Entry View │ │ │ └── VCodeEntryViewState.swift │ │ └── TextField │ │ │ └── VTextFieldState.swift │ ├── State Pickers │ │ ├── Toggle │ │ │ ├── VToggleLabel.swift │ │ │ └── VToggleState.swift │ │ ├── Check Box │ │ │ ├── VCheckBoxLabel.swift │ │ │ └── VCheckBoxState.swift │ │ ├── Radio Button │ │ │ ├── VRadioButtonLabel.swift │ │ │ └── VRadioButtonState.swift │ │ ├── Toggle Button (Rectangular) │ │ │ ├── VRectangularToggleButtonLabel.swift │ │ │ └── VRectangularToggleButtonState.swift │ │ ├── Toggle Button (Wrapped) │ │ │ ├── VWrappedToggleButtonLabel.swift │ │ │ └── VWrappedToggleButtonState.swift │ │ └── Toggle Button (Stretched) │ │ │ ├── VStretchedToggleButtonLabel.swift │ │ │ └── VStretchedToggleButtonState.swift │ ├── Modals (Notifications) │ │ ├── Notification │ │ │ └── VNotificationContent.swift │ │ └── Toast │ │ │ └── View+VToast.swift │ ├── zMisc │ │ ├── Rolling Counter │ │ │ ├── VRollingCounterComponentProtocol.swift │ │ │ ├── VRollingCounterComponent.swift │ │ │ └── VRollingCounterUIModel.swift │ │ ├── Fetching Async Image │ │ │ ├── VFetchingAsyncImageContent.swift │ │ │ └── VFetchingAsyncImageUIModel.swift │ │ ├── Marquee (Bouncing) │ │ │ └── VBouncingMarqueeUIModel.swift │ │ └── Marquee (Wrapping) │ │ │ └── VWrappingMarqueeUIModel.swift │ ├── Indicators (Indefinite) │ │ └── Spinner (Continous) │ │ │ ├── VContinuousSpinner.swift │ │ │ └── VContinuousSpinnerUIModel.swift │ └── Modals (Containers) │ │ ├── Bottom Sheet │ │ └── VBottomSheetSnapAction.swift │ │ ├── SideBar │ │ └── View+VSideBar.swift │ │ └── Modal │ │ └── View+VModal.swift │ ├── Models │ ├── TitleAndIconPlacement.swift │ ├── MarqueeDurationType.swift │ ├── PlatformInterfaceOrientation.swift │ └── ModalComponentSizeGroup.swift │ ├── Extensions │ ├── Color+InitWithRGBA.swift │ ├── LayoutDirection+Flags.swift │ ├── View+GetPlatformInterfaceOrientation.swift │ ├── RectangleCornerRadii+WithHorizontalCornersReversed.swift │ └── RectangleCornerRadii+CornersAdjustedForDirection.swift │ ├── Helpers │ ├── Logging.swift │ ├── BrandBook │ │ └── ImageBook.swift │ ├── Architectural Pattern Helpers │ │ ├── Spinner │ │ │ ├── VSpinnerParameters.swift │ │ │ └── View+VSpinnerWithParameters.swift │ │ └── Alert │ │ │ ├── VAlertParameters.swift │ │ │ └── View+VAlertWithParameters.swift │ └── LayoutDirectionHelpers.swift │ ├── Preview Helpers │ ├── Views │ │ ├── Preview_StretchedButtonFrameModifier.swift │ │ └── Preview_MarqueeContent.swift │ ├── Data │ │ ├── Preview_RGBColor.swift │ │ └── Preview_Weekday.swift │ ├── Views (Containers) │ │ ├── PreviewModalLauncherView.swift │ │ ├── PreviewHeader.swift │ │ ├── PreviewRow.swift │ │ └── PreviewContainer.swift │ └── Extensions │ │ └── View+OnReceiveOfTimerIncrement.swift │ ├── Deprecations.swift │ ├── Services and Managers │ └── HapticManager.swift │ └── API │ └── VComponentsLocalizationManager.swift ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── Package.resolved ├── Package.swift ├── LICENSE.md ├── .gitignore └── Documentation ├── Customization.md └── Animations.md /Sources/VComponents/Resources/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/XMark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "XMark.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Chevron.Up.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Chevron.Up.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/MagnifyGlass.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MagnifyGlass.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Visibility.Off.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Visibility.Off.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Checkmark.Indeterminate.imageset/Checkmark.Indeterminate.svg: -------------------------------------------------------------------------------- 1 | CheckBox.Interm -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Checkmark.Indeterminate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Checkmark.Indeterminate.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Checkmark.On.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Checkmark.On.svg", 5 | "idiom" : "universal", 6 | "language-direction" : "left-to-right" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Visibility.On.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Visibility.On.svg", 5 | "idiom" : "universal", 6 | "language-direction" : "left-to-right" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Group Box/VGroupBoxContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VGroupBoxContent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 4/13/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Group Box Content 11 | enum VGroupBoxContent where Content: View { 12 | case empty 13 | case content(content: () -> Content) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Chevron.Up.imageset/Chevron.Up.svg: -------------------------------------------------------------------------------- 1 | Chevron.Up -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/XMark.imageset/XMark.svg: -------------------------------------------------------------------------------- 1 | XMark -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/MagnifyGlass.imageset/MagnifyGlass.svg: -------------------------------------------------------------------------------- 1 | Search -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Visibility.Off.imageset/Visibility.Off.svg: -------------------------------------------------------------------------------- 1 | Visibility.off -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Checkmark.On.imageset/Checkmark.On.svg: -------------------------------------------------------------------------------- 1 | CheckBox.On -------------------------------------------------------------------------------- /Sources/VComponents/Components/Value Pickers/Slider (Range)/VRangeSliderThumb.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRangeSliderThumb.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 17.02.24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: Thumb 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VRangeSliderThumb { 15 | case low 16 | case high 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Definite)/Page Indicator/VPageIndicatorState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VPageIndicatorState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 03.09.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Page Indicator Dot Internal State 12 | /// Enumeration that represents state. 13 | public typealias VPageIndicatorDotInternalState = GenericState_DeselectedSelected 14 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Value Pickers/Slider/VSliderState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VSliderState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 19.12.20. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Slider Internal State 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | typealias VSliderInternalState = GenericState_EnabledDisabled 16 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Carousel/VCarouselState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCarouselState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 26.07.24. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Carousel Page Internal State 12 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 13 | @available(tvOS, unavailable) 14 | typealias VCarouselCardInternalState = GenericState_DeselectedSelected 15 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Value Pickers/Slider (Range)/VRangeSliderState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRangeSliderState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/12/21. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Range Slider Internal State 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | typealias VRangeSliderInternalState = GenericState_EnabledDisabled 16 | -------------------------------------------------------------------------------- /Sources/VComponents/Models/TitleAndIconPlacement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TitleAndIconPlacement.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 13.11.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Title and Icon Placement 11 | /// Title and icon placement. 12 | public enum TitleAndIconPlacement: Int, Sendable, CaseIterable { 13 | /// Title and icon. 14 | case titleAndIcon 15 | 16 | /// Icon and titile. 17 | case iconAndTitle 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Definite)/Page Indicator (Compact)/VCompactPageIndicatorState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCompactPageIndicatorState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 03.09.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Compact Page Indicator Dot Internal State 12 | /// Enumeration that represents state. 13 | public typealias VCompactPageIndicatorDotInternalState = GenericState_DeselectedSelected 14 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Alerts)/Alert/VAlertContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VAlertContent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 26.05.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Alert Content 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VAlertContent where Content: View { 15 | case empty 16 | case content(content: () -> Content) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Plain Button/VPlainButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VPlainButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 19.12.20. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Plain Button Internal State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public typealias VPlainButtonInternalState = GenericState_EnabledPressedDisabled 16 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Definite)/Page Indicator/VPageIndicatorDotContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VPageIndicatorDotContent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 27.02.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Page Indicator Dot Content 11 | enum VPageIndicatorDotContent where CustomDotContent: View { 12 | case standard 13 | case custom(custom: (VPageIndicatorDotInternalState, Int) -> CustomDotContent) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Inputs/TextView/VTextViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VTextViewState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.10.22. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Text View Internal State 12 | @available(macOS, unavailable) 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | typealias VTextViewInternalState = GenericState_EnabledFocusedDisabled 17 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Wrapped Button/VWrappedButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappedButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12/24/20. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Wrapped Button Internal State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public typealias VWrappedButtonInternalState = GenericState_EnabledPressedDisabled 16 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle/VToggleLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VToggleLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 3/1/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Toggle Label 11 | @available(tvOS, unavailable) 12 | @available(visionOS, unavailable) 13 | enum VToggleLabel where CustomLabel: View { 14 | case empty 15 | case title(title: String) 16 | case custom(custom: (VToggleInternalState) -> CustomLabel) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Stretched Button/VStretchedButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 03.04.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Stretched Button Internal State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public typealias VStretchedButtonInternalState = GenericState_EnabledPressedDisabled 16 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Rectangular Button/VRectangularButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRectangularButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 19.12.20. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Rectangular Button Internal State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public typealias VRectangularButtonInternalState = GenericState_EnabledPressedDisabled 16 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Inputs/Code Entry View/VCodeEntryViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCodeEntryViewState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.09.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Code Entry View Internal State 12 | @available(macOS, unavailable) 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | typealias VCodeEntryViewInternalState = GenericState_EnabledFocusedDisabled 17 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Definite)/Page Indicator (Compact)/VCompactPageIndicatorDotContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCompactPageIndicatorDotContent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 27.02.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Compact Page Indicator Dot Content 11 | enum VCompactPageIndicatorDotContent where CustomDotContent: View { 12 | case standard 13 | case custom(custom: (VCompactPageIndicatorDotInternalState, Int) -> CustomDotContent) 14 | } 15 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Check Box/VCheckBoxLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCheckBoxLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 3/1/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Check Box Label 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VCheckBoxLabel where CustomLabel: View { 15 | case empty 16 | case title(title: String) 17 | case custom(custom: (VCheckBoxInternalState) -> CustomLabel) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Radio Button/VRadioButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRadioButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 3/1/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Radio Button Label 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VRadioButtonLabel where CustomLabel: View { 15 | case empty 16 | case title(title: String) 17 | case custom(custom: (VRadioButtonInternalState) -> CustomLabel) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Rectangular Button/VRectangularButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRectangularButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 2/27/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Rectangular Button Label 11 | @available(tvOS, unavailable) 12 | @available(visionOS, unavailable) 13 | enum VRectangularButtonLabel where CustomLabel: View { 14 | case title(title: String) 15 | case icon(icon: Image) 16 | case custom(custom: (VRectangularButtonInternalState) -> CustomLabel) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Stretched Button (Loading)/VLoadingStretchedButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VLoadingStretchedButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 19.12.20. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Loading Stretched Button Internal State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public typealias VLoadingStretchedButtonInternalState = GenericState_EnabledPressedLoadingDisabled 17 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Caption Button (Rectangular)/VRectangularCaptionButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRectangularCaptionButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 17.08.22. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Rectangular Caption Button Internal State 12 | /// Enumeration that represents state. 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public typealias VRectangularCaptionButtonInternalState = GenericState_EnabledPressedDisabled 17 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Plain Button/VPlainButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VPlainButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 2/27/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Plain Button Label 11 | @available(tvOS, unavailable) 12 | @available(visionOS, unavailable) 13 | enum VPlainButtonLabel where CustomLabel: View { 14 | case title(title: String) 15 | case icon(icon: Image) 16 | case titleAndIcon(title: String, icon: Image) 17 | case custom(custom: (VPlainButtonInternalState) -> CustomLabel) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Extensions/Color+InitWithRGBA.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+InitWithRGBA.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 19.12.20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Color + Init with RGBA 11 | extension Color { 12 | init( 13 | _ red: CGFloat, 14 | _ green: CGFloat, 15 | _ blue: CGFloat, 16 | _ opacity: CGFloat = 1 17 | ) { 18 | self.init( 19 | red: red/255, 20 | green: green/255, 21 | blue: blue/255, 22 | opacity: opacity 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Wrapped Button/VWrappedButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappedButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 2/27/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Wrapped Button Label 11 | @available(tvOS, unavailable) 12 | @available(visionOS, unavailable) 13 | enum VWrappedButtonLabel where CustomLabel: View { 14 | case title(title: String) 15 | case icon(icon: Image) 16 | case titleAndIcon(title: String, icon: Image) 17 | case custom(custom: (VWrappedButtonInternalState) -> CustomLabel) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Stretched Button/VStretchedButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedButton.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 03.04.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Stretched Button Label 11 | @available(tvOS, unavailable) 12 | @available(visionOS, unavailable) 13 | enum VStretchedButtonLabel where CustomLabel: View { 14 | case title(title: String) 15 | case icon(icon: Image) 16 | case titleAndIcon(title: String, icon: Image) 17 | case custom(custom: (VStretchedButtonInternalState) -> CustomLabel) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Disclosure Group/VDisclosureGroupHeaderLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VDisclosureGroupHeaderLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 4/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Disclosure Group Header Label 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VDisclosureGroupHeaderLabel where CustomHeaderLabel: View { 15 | case title(title: String) 16 | case custom(custom: (VDisclosureGroupInternalState) -> CustomHeaderLabel) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle Button (Rectangular)/VRectangularToggleButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRectangularToggleButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Rectangular Toggle Button Label 11 | @available(tvOS, unavailable) 12 | @available(visionOS, unavailable) 13 | enum VRectangularToggleButtonLabel where CustomLabel: View { 14 | case title(title: String) 15 | case icon(icon: Image) 16 | case custom(custom: (VRectangularToggleButtonInternalState) -> CustomLabel) 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Notifications)/Notification/VNotificationContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VNotificationContent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 16.07.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Notification Content 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | enum VNotificationContent where CustomContent: View { 16 | case iconTitleMessage(icon: Image?, title: String?, message: String?) 17 | case custom(custom: () -> CustomContent) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/zMisc/Rolling Counter/VRollingCounterComponentProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRollingCounterComponentProtocol.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - V Rolling Counter Component Protocol 11 | protocol VRollingCounterComponentProtocol { 12 | var id: String { get } 13 | var stringRepresentation: String { get } 14 | 15 | var isHighlighted: Bool { get set } 16 | } 17 | 18 | extension VRollingCounterComponentProtocol { 19 | static func generateID() -> String { 20 | UUID().uuidString 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Dynamic Pager Tab View/VDynamicPagerTabViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedIndicatorStaticPagerTabViewState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Dynamic Pager Tab View Tab Item Internal State 12 | /// Enumeration that represents state. 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS, unavailable) 16 | @available(visionOS, unavailable) 17 | public typealias VDynamicPagerTabViewTabItemInternalState = GenericState_DeselectedSelectedPressedDisabled 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle Button (Wrapped)/VWrappedToggleButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappedToggleButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Wrapped Toggle Button Label 11 | @available(tvOS, unavailable) 12 | @available(visionOS, unavailable) 13 | enum VWrappedToggleButtonLabel where CustomLabel: View { 14 | case title(title: String) 15 | case icon(icon: Image) 16 | case titleAndIcon(title: String, icon: Image) 17 | case custom(custom: (VWrappedToggleButtonInternalState) -> CustomLabel) 18 | } 19 | -------------------------------------------------------------------------------- /Sources/VComponents/Resources/Images.xcassets/Visibility.On.imageset/Visibility.On.svg: -------------------------------------------------------------------------------- 1 | Visibility.on -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle/VToggleState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VToggleState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 19.12.20. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Toggle State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public typealias VToggleState = GenericState_OffOn 16 | 17 | // MARK: - V Toggle Internal State 18 | /// Enumeration that represents state. 19 | @available(tvOS, unavailable) 20 | @available(visionOS, unavailable) 21 | public typealias VToggleInternalState = GenericState_OffOnPressedDisabled 22 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Stretched Button (Loading)/VLoadingStretchedButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VLoadingStretchedButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 2/26/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Loading Stretched Button Label 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VLoadingStretchedButtonLabel where CustomLabel: View { 15 | case title(title: String) 16 | case icon(icon: Image) 17 | case titleAndIcon(title: String, icon: Image) 18 | case custom(custom: (VLoadingStretchedButtonInternalState) -> CustomLabel) 19 | } 20 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle Button (Stretched)/VStretchedToggleButtonLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedToggleButtonLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Stretched Toggle Button Label 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VStretchedToggleButtonLabel where CustomLabel: View { 15 | case title(title: String) 16 | case icon(icon: Image) 17 | case titleAndIcon(title: String, icon: Image) 18 | case custom(custom: (VStretchedToggleButtonInternalState) -> CustomLabel) 19 | } 20 | -------------------------------------------------------------------------------- /Sources/VComponents/Extensions/LayoutDirection+Flags.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutDirection+Flags.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 07.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Layout Direction + Flags 11 | extension LayoutDirection { 12 | var isLeftToRight: Bool { 13 | switch self { 14 | case .leftToRight: true 15 | case .rightToLeft: false 16 | @unknown default: fatalError() 17 | } 18 | } 19 | 20 | var isRightToLeft: Bool { 21 | switch self { 22 | case .leftToRight: false 23 | case .rightToLeft: true 24 | @unknown default: fatalError() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/VComponents/Helpers/Logging.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logging.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12.02.24. 6 | // 7 | 8 | import Foundation 9 | import OSLog 10 | 11 | // MARK: - Loggers 12 | extension Logger { 13 | static let compactPageIndicator: Self = .init(subsystem: "VComponents", category: "VCompactPageIndicator") 14 | static let rangeSlider: Self = .init(subsystem: "VComponents", category: "VRangeSlider") 15 | static let rollingCounter: Self = .init(subsystem: "VComponents", category: "VRollingCounter") 16 | static let wrappedIndicatorStaticPagerTabView: Self = .init(subsystem: "VComponents", category: "VWrappedIndicatorStaticPagerTabView") 17 | } 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Static Pager Tab View (Wrapped Indicator)/VWrappedIndicatorStaticPagerTabViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappedIndicatorStaticPagerTabViewState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Wrapped-Indicator Static Pager Tab View Tab Item Internal State 12 | /// Enumeration that represents state. 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS, unavailable) 16 | @available(visionOS, unavailable) 17 | public typealias VWrappedIndicatorStaticPagerTabViewTabItemInternalState = GenericState_DeselectedSelectedPressedDisabled 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Static Pager Tab View (Stretched Indicator)/VStretchedIndicatorStaticPagerTabViewState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedIndicatorStaticPagerTabViewState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Stretched-Indicator Static Pager Tab View Tab Item Internal State 12 | /// Enumeration that represents state. 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS, unavailable) 16 | @available(visionOS, unavailable) 17 | public typealias VStretchedIndicatorStaticPagerTabViewTabItemInternalState = GenericState_DeselectedSelectedPressedDisabled 18 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Caption Button (Rectangular)/VRectangularCaptionButtonCaption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRectangularCaptionButtonCaption.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 17.08.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Rectangular Caption Button Caption 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VRectangularCaptionButtonCaption where CustomCaption: View { 15 | case title(title: String) 16 | case icon(icon: Image) 17 | case titleAndIcon(title: String, icon: Image) 18 | case custom(custom: (VRectangularCaptionButtonInternalState) -> CustomCaption) 19 | } 20 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Views/Preview_StretchedButtonFrameModifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preview_StretchedButtonFrameModifier.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 21.01.24. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | 12 | // MARK: - Stretched Button Frame Modifier 13 | struct Preview_StretchedButtonFrameModifier: ViewModifier { 14 | func body(content: Content) -> some View { 15 | content 16 | #if os(iOS) 17 | .padding(.horizontal) 18 | #elseif os(macOS) 19 | .frame(width: 250) 20 | #elseif os(watchOS) 21 | .padding(.horizontal) 22 | #elseif os(visionOS) 23 | .frame(width: 250) 24 | #endif 25 | } 26 | } 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /Sources/VComponents/Extensions/View+GetPlatformInterfaceOrientation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+GetPlatformInterfaceOrientation.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 06.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - View + Get Platform Interface Orientation 12 | extension View { 13 | @ViewBuilder 14 | func getPlatformInterfaceOrientation( 15 | _ action: @escaping (PlatformInterfaceOrientation) -> Void 16 | ) -> some View { 17 | #if os(iOS) 18 | self 19 | .getInterfaceOrientation({ action(PlatformInterfaceOrientation(uiIInterfaceOrientation: $0)) }) 20 | #else 21 | self 22 | .onFirstAppear(perform: { action(.portrait) }) 23 | #endif 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Data/Preview_RGBColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preview_RGBColor.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 22.01.24. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | 12 | // MARK: - RGB Color 13 | enum Preview_RGBColor: Int, Hashable, Identifiable, CaseIterable { 14 | // MARK: Cases 15 | case red, green, blue 16 | 17 | // MARK: Properties 18 | var title: String { .init(describing: self).capitalized } 19 | 20 | var color: Color { 21 | switch self { 22 | case .red: Color.red 23 | case .green: Color.green 24 | case .blue: Color.blue 25 | } 26 | } 27 | 28 | // MARK: Identifiable 29 | var id: Int { rawValue } 30 | } 31 | 32 | #endif 33 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "1e1d731e8be67605680f1fc31dfb727f2a03a16fd9c9f1f5b3502d2d40c732a9", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-syntax", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swiftlang/swift-syntax.git", 8 | "state" : { 9 | "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", 10 | "version" : "601.0.1" 11 | } 12 | }, 13 | { 14 | "identity" : "vcore", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/VakhoKontridze/VCore", 17 | "state" : { 18 | "revision" : "7684c077ff0bba6cebd3ea42b2694cef2b0fefb8", 19 | "version" : "7.5.2" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle Button (Wrapped)/VWrappedToggleButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappedToggleButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Wrapped Toggle Button State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public typealias VWrappedToggleButtonState = GenericState_OffOn 16 | 17 | // MARK: - V Wrapped Toggle Button Internal State 18 | /// Enumeration that represents state. 19 | @available(tvOS, unavailable) 20 | @available(visionOS, unavailable) 21 | public typealias VWrappedToggleButtonInternalState = GenericState_OffOnPressedDisabled 22 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Dynamic Pager Tab View/VDynamicPagerTabViewTabItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VDynamicPagerTabViewTabItemLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Dynamic Pager Tab View Tab Item Label 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | enum VDynamicPagerTabViewTabItemLabel 16 | where 17 | Element: Hashable, 18 | CustomTabItemLabel: View 19 | { 20 | case title(title: (Element) -> String) 21 | case custom(custom: (VDynamicPagerTabViewTabItemInternalState, Element) -> CustomTabItemLabel) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/VComponents/Extensions/RectangleCornerRadii+WithHorizontalCornersReversed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleCornerRadii+WithHorizontalCornersReversed.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 09.07.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Rectangle Corner Radii + With Horizontal Corners Reversed 11 | extension RectangleCornerRadii { 12 | func horizontalCornersReversed( 13 | if condition: Bool = true 14 | ) -> Self { 15 | guard condition else { return self } 16 | 17 | return RectangleCornerRadii( 18 | topLeading: topTrailing, 19 | bottomLeading: bottomTrailing, 20 | bottomTrailing: bottomLeading, 21 | topTrailing: topLeading 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Radio Button/VRadioButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRadioButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/19/21. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Radio Button State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public typealias VRadioButtonState = GenericState_OffOn 17 | 18 | // MARK: - V Radio Button Internal State 19 | /// Enumeration that represents state. 20 | @available(tvOS, unavailable) 21 | @available(watchOS, unavailable) 22 | @available(visionOS, unavailable) 23 | public typealias VRadioButtonInternalState = GenericState_OffOnPressedDisabled 24 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Inputs/TextField/VTextFieldState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VTextFieldState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/19/21. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Text Field Internal State 12 | @available(macOS, unavailable) 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | typealias VTextFieldInternalState = GenericState_EnabledFocusedDisabled 17 | 18 | // MARK: - V Text Field Button Internal State 19 | @available(macOS, unavailable) 20 | @available(tvOS, unavailable) 21 | @available(watchOS, unavailable) 22 | @available(visionOS, unavailable) 23 | typealias VTextFieldButtonInternalState = GenericState_EnabledPressedFocusedDisabled 24 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Check Box/VCheckBoxState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCheckBoxState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/18/21. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Check Box State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public typealias VCheckBoxState = GenericState_OffOnIndeterminate 17 | 18 | // MARK: - V Check Box Internal State 19 | /// Enumeration that represents state. 20 | @available(tvOS, unavailable) 21 | @available(watchOS, unavailable) 22 | @available(visionOS, unavailable) 23 | public typealias VCheckBoxInternalState = GenericState_OffOnIndeterminatePressedDisabled 24 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle Button (Rectangular)/VRectangularToggleButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRectangularToggleButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Rectangular Toggle Button State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public typealias VRectangularToggleButtonState = GenericState_OffOn 16 | 17 | // MARK: - V Rectangular Toggle Button Internal State 18 | /// Enumeration that represents state. 19 | @available(tvOS, unavailable) 20 | @available(visionOS, unavailable) 21 | public typealias VRectangularToggleButtonInternalState = GenericState_OffOnPressedDisabled 22 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Disclosure Group/VDisclosureGroupState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VDisclosureGroupState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/11/21. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Disclosure Group State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public typealias VDisclosureGroupState = GenericState_CollapsedExpanded 17 | 18 | // MARK: - V Disclosure Group Internal State 19 | /// Enumeration that represents state. 20 | @available(tvOS, unavailable) 21 | @available(watchOS, unavailable) 22 | @available(visionOS, unavailable) 23 | public typealias VDisclosureGroupInternalState = GenericState_CollapsedExpandedDisabled 24 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/State Pickers/Toggle Button (Stretched)/VStretchedToggleButtonState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedToggleButtonState.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Stretched Toggle Button State 12 | /// Enumeration that represents state. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public typealias VStretchedToggleButtonState = GenericState_OffOn 17 | 18 | // MARK: - V Stretched Toggle Button Internal State 19 | /// Enumeration that represents state. 20 | @available(tvOS, unavailable) 21 | @available(watchOS, unavailable) 22 | @available(visionOS, unavailable) 23 | public typealias VStretchedToggleButtonInternalState = GenericState_OffOnPressedDisabled 24 | -------------------------------------------------------------------------------- /Sources/VComponents/Helpers/BrandBook/ImageBook.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageBook.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/18/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - Image Book 12 | @Uninitializable 13 | struct ImageBook { 14 | static let checkmarkOn: Image = .init(.checkmarkOn) // Mirrored for RTL languages 15 | static let checkmarkIndeterminate: Image = .init(.checkmarkIndeterminate) 16 | 17 | static let magnifyGlass: Image = .init(.magnifyGlass) // Doesn't mirror, like `UISearchBar.searchable(text:)` 18 | 19 | static let visibilityOff: Image = .init(.visibilityOff) // Mirrored for RTL languages 20 | static let visibilityOn: Image = .init(.visibilityOn) 21 | 22 | static let xMark: Image = .init(.xMark) 23 | 24 | static let chevronUp: Image = .init(.chevronUp) 25 | } 26 | -------------------------------------------------------------------------------- /Sources/VComponents/Deprecations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Deprecations.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 2/12/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Bottom Sheet 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | extension VBottomSheetUIModel.Heights { 16 | @available(*, unavailable) 17 | public var isResizable: Bool { fatalError() } 18 | 19 | @available(*, unavailable) 20 | public var isFixed: Bool { fatalError() } 21 | } 22 | 23 | // MARK: - V Wrapping Marquee 24 | extension VWrappingMarqueeUIModel { 25 | @available(*, deprecated, renamed: "wrappedContentSpacing") 26 | public var spacing: CGFloat { 27 | get { wrappedContentSpacing } 28 | set { wrappedContentSpacing = newValue } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Static Pager Tab View (Wrapped Indicator)/VWrappedIndicatorStaticPagerTabViewTabItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappedIndicatorStaticPagerTabViewTabItemLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Wrapped-Indicator Static Pager Tab View Tab Item Label 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | enum VWrappedIndicatorStaticPagerTabViewTabItemLabel 16 | where 17 | Element: Hashable, 18 | CustomTabItemLabel: View 19 | { 20 | case title(title: (Element) -> String) 21 | case custom(custom: (VWrappedIndicatorStaticPagerTabViewTabItemInternalState, Element) -> CustomTabItemLabel) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Data/Preview_Weekday.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preview_Weekday.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 21.01.24. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | 12 | // MARK: - Weekday 13 | enum Preview_Weekday: Int, Hashable, Identifiable, CaseIterable { 14 | // MARK: Cases 15 | case monday, tuesday, wednesday, thursday, friday, saturday, sunday 16 | 17 | // MARK: Properties 18 | var title: String { .init(describing: self).capitalized } 19 | 20 | var color: Color { 21 | switch rawValue.quotientAndRemainder(dividingBy: 3).remainder { 22 | case 0: Color.red 23 | case 1: Color.green 24 | case 2: Color.blue 25 | default: fatalError() 26 | } 27 | } 28 | 29 | // MARK: Identifiable 30 | var id: Int { rawValue } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Static Pager Tab View (Stretched Indicator)/VStretchedIndicatorStaticPagerTabViewTabItemLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedIndicatorStaticPagerTabViewTabItemLabel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Stretched-Indicator Static Pager Tab View Tab Item Label 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | enum VStretchedIndicatorStaticPagerTabViewTabItemLabel 16 | where 17 | Element: Hashable, 18 | CustomTabItemLabel: View 19 | { 20 | case title(title: (Element) -> String) 21 | case custom(custom: (VStretchedIndicatorStaticPagerTabViewTabItemInternalState, Element) -> CustomTabItemLabel) 22 | } 23 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/zMisc/Fetching Async Image/VFetchingAsyncImageContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VFetchingAsyncImageContent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 06.03.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Fetching Async Image Content 11 | enum VFetchingAsyncImageContent 12 | where 13 | CustomContent: View, 14 | CustomPlaceholderContent: View 15 | { 16 | case auto 17 | 18 | case content( 19 | customContent: (Image) -> CustomContent 20 | ) 21 | 22 | case contentAndPlaceholder( 23 | customContent: (Image) -> CustomContent, 24 | customPlaceholderContent: () -> CustomPlaceholderContent 25 | ) 26 | 27 | case contentWithPhase( 28 | customContentWithPhase: (AsyncImagePhase) -> CustomContent 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /Sources/VComponents/Models/MarqueeDurationType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MarqueeDurationType.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 17.02.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: Marquee Duration Type 11 | /// Marquee duration type. 12 | public enum MarqueeDurationType: Sendable { 13 | // MARK: Cases 14 | /// Duration. 15 | case duration(Double) 16 | 17 | /// Velocity, that calculates duration based on the width of the content. 18 | case velocity(CGFloat) 19 | 20 | // MARK: Properties 21 | func toDuration(width: CGFloat) -> Double { 22 | switch self { 23 | case .velocity(let velocity): width / velocity 24 | case .duration(let duration): duration 25 | } 26 | } 27 | 28 | // MARK: Initializers 29 | /// Default value. Set to `velocity` of `20`. 30 | public static var `default`: Self { .velocity(20) } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/zMisc/Fetching Async Image/VFetchingAsyncImageUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VFetchingAsyncImageUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 10.03.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Fetching Async Image UI Model 11 | /// Model that describes UI. 12 | public struct VFetchingAsyncImageUIModel: Sendable { 13 | // MARK: Properties 14 | /// Placeholder color. 15 | public var placeholderColor: Color = .gray.opacity(0.3) 16 | 17 | /// Indicates if `Image` is removed when parameter changes. Set to `true`. 18 | public var removesImageOnParameterChange: Bool = true 19 | 20 | /// Indicates if `Image` is removed when component disappears. Set to `false`. 21 | public var removesImageOnDisappear: Bool = false 22 | 23 | // MARK: Initializers 24 | /// Initializes UI model with default values. 25 | public init() {} 26 | } 27 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Views (Containers)/PreviewModalLauncherView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewModalLauncherView.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 09.08.23. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | 12 | // MARK: - Preview Modal Launcher View 13 | struct PreviewModalLauncherView: View { 14 | // MARK: Properties 15 | @Binding private var isPresented: Bool 16 | 17 | // MARK: Initializers 18 | init( 19 | isPresented: Binding 20 | ) { 21 | self._isPresented = isPresented 22 | } 23 | 24 | // MARK: Body 25 | var body: some View { 26 | #if !(os(tvOS) || os(visionOS)) 27 | VPlainButton( 28 | action: { isPresented = true }, 29 | title: "Present" 30 | ) 31 | #else 32 | Button( 33 | "Present", 34 | action: { isPresented = true } 35 | ) 36 | #endif 37 | } 38 | } 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | 3 | import PackageDescription 4 | 5 | let package: Package = .init( 6 | name: "VComponents", 7 | 8 | platforms: [ 9 | .iOS(.v16), 10 | .macOS(.v13), 11 | .tvOS(.v16), 12 | .watchOS(.v9), 13 | .visionOS(.v1) 14 | ], 15 | 16 | products: [ 17 | .library( 18 | name: "VComponents", 19 | targets: [ 20 | "VComponents" 21 | ] 22 | ) 23 | ], 24 | 25 | dependencies: [ 26 | .package(url: "https://github.com/VakhoKontridze/VCore", "7.5.2"..<"8.0.0") 27 | ], 28 | 29 | targets: [ 30 | .target( 31 | name: "VComponents", 32 | dependencies: [ 33 | "VCore" 34 | ], 35 | exclude: [ 36 | "../../Documentation" 37 | ], 38 | resources: [ 39 | .process("Resources") 40 | ] 41 | ) 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Alerts)/Alert/VAlertButtonProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VAlertButtonProtocol.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.05.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Alert Button Protocol 11 | /// `VAlert` button protocol. 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public protocol VAlertButtonProtocol: VAlertButtonConvertible { 16 | /// Body type. 17 | typealias Body = AnyView 18 | 19 | /// Creates a `View` that represents the body of a button. 20 | @MainActor 21 | func makeBody( 22 | uiModel: VAlertUIModel, 23 | animateOutHandler: @escaping (/*completion*/ (() -> Void)?) -> Void 24 | ) -> Body 25 | } 26 | 27 | @available(tvOS, unavailable) 28 | @available(watchOS, unavailable) 29 | @available(visionOS, unavailable) 30 | extension VAlertButtonProtocol { 31 | public func toButtons() -> [any VAlertButtonProtocol] { 32 | [self] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/VComponents/Models/PlatformInterfaceOrientation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlatformInterfaceOrientation.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 07.08.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Platform Interface Orientation 11 | enum PlatformInterfaceOrientation { 12 | // MARK: Cases 13 | case portrait 14 | case landscape 15 | 16 | // MARK: Initializers 17 | @MainActor 18 | static func initFromDeviceOrientation() -> Self { 19 | #if os(iOS) 20 | if UIDevice.current.orientation.isLandscape { 21 | .landscape 22 | } else { 23 | .portrait 24 | } 25 | #else 26 | .portrait 27 | #endif 28 | } 29 | 30 | #if canImport(UIKit) && !(os(tvOS) || os(watchOS) || os(visionOS)) 31 | init(uiIInterfaceOrientation: UIInterfaceOrientation) { 32 | self = { 33 | if uiIInterfaceOrientation.isLandscape { 34 | .landscape 35 | } else { 36 | .portrait 37 | } 38 | }() 39 | } 40 | #endif 41 | } 42 | -------------------------------------------------------------------------------- /Sources/VComponents/Helpers/Architectural Pattern Helpers/Spinner/VSpinnerParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VSpinnerParameters.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.10.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Spinner Parameters 11 | /// Parameters for presenting an `VSpinner`. 12 | /// 13 | /// @State private var parameters: VSpinnerParameters = .init() 14 | /// 15 | /// var body: some View { 16 | /// content 17 | /// .vContinuousSpinner(parameters: parameters) 18 | /// } 19 | /// 20 | public struct VSpinnerParameters { 21 | // MARK: Properties 22 | /// Indicates if interaction is enabled. 23 | public var isInteractionEnabled: Bool 24 | 25 | /// Attributes. 26 | public var attributes: [String: Any?] 27 | 28 | // MARK: Initializers 29 | /// Initializes `VSpinnerParameters`. 30 | public init( 31 | isInteractionEnabled: Bool = true, 32 | attributes: [String: Any?] = [:] 33 | ) { 34 | self.isInteractionEnabled = isInteractionEnabled 35 | self.attributes = attributes 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/VComponents/Helpers/Architectural Pattern Helpers/Spinner/View+VSpinnerWithParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+VSpinnerWithParameters.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.10.22. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - View + V Spinner 12 | extension View { 13 | /// Presents `VContinuousSpinner` when `parameters` is non-`nil`. 14 | /// 15 | /// @State private var parameters: VSpinnerParameters = .init() 16 | /// 17 | /// var body: some View { 18 | /// content 19 | /// .vContinuousSpinner(parameters: parameters) 20 | /// } 21 | /// 22 | public func vContinuousSpinner( 23 | uiModel: VContinuousSpinnerUIModel = .init(), 24 | parameters: VSpinnerParameters? 25 | ) -> some View { 26 | self 27 | .blocksHitTesting(parameters?.isInteractionEnabled == false) 28 | .overlay(content: { 29 | if parameters != nil { 30 | VContinuousSpinner(uiModel: uiModel) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Vakhtang Kontridze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/VComponents/Helpers/LayoutDirectionHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutDirectionHelpers.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.04.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Layout Directions Helpers 11 | extension CGSize { 12 | func dimension( 13 | isWidth: Bool 14 | ) -> CGFloat { 15 | if isWidth { 16 | width 17 | } else { 18 | height 19 | } 20 | } 21 | } 22 | 23 | extension CGPoint { 24 | func coordinate( 25 | isX: Bool 26 | ) -> CGFloat { 27 | if isX { 28 | x 29 | } else { 30 | y 31 | } 32 | } 33 | } 34 | 35 | extension BinaryFloatingPoint { 36 | func invertedFromMax( 37 | _ max: Self, 38 | if condition: Bool 39 | ) -> Self { 40 | if condition { 41 | max - self 42 | } else { 43 | self 44 | } 45 | } 46 | 47 | mutating func invertFromMax( 48 | _ max: Self, 49 | if condition: Bool 50 | ) { 51 | self = self.invertedFromMax(max, if: condition) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### macOS 2 | ## General 3 | .DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | ## Icon must end with two \r 8 | Icon 9 | 10 | ## Thumbnails 11 | ._* 12 | 13 | ## Files that might appear in the root of a volume 14 | .DocumentRevisions-V100 15 | .fseventsd 16 | .Spotlight-V100 17 | .TemporaryItems 18 | .Trashes 19 | .VolumeIcon.icns 20 | .com.apple.timemachine.donotpresent 21 | 22 | ## Directories potentially created on remote AFP share 23 | .AppleDB 24 | .AppleDesktop 25 | Network Trash Folder 26 | Temporary Items 27 | .apdisk 28 | 29 | ## iCloud generated files 30 | *.icloud 31 | 32 | 33 | ### XCode 34 | ## Project 35 | *.xcodeproj/* 36 | !*.xcodeproj/project.pbxproj 37 | !*.xcodeproj/xcshareddata/ 38 | !*.xcworkspace/contents.xcworkspacedata 39 | /*.gcno 40 | **/xcshareddata/WorkspaceSettings.xcsettings 41 | 42 | ## User settings 43 | xcuserdata/ 44 | 45 | ## Obj-C/Swift specific 46 | *.hmap 47 | 48 | ## App packaging 49 | *.ipa 50 | *.dSYM.zip 51 | *.dSYM 52 | 53 | ## Playgrounds 54 | timeline.xctimeline 55 | playground.xcworkspace 56 | 57 | 58 | ### Swift Package Manager 59 | /.build 60 | /Packages 61 | /*.xcodeproj 62 | xcuserdata/ 63 | DerivedData/ -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Views (Containers)/PreviewHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewHeader.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 08.03.23. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | import VCore 12 | 13 | // MARK: - Preview Header 14 | struct PreviewHeader: View { 15 | // MARK: Properties 16 | private let title: String 17 | 18 | // MARK: Initializers 19 | init(_ title: String) { 20 | self.title = title 21 | } 22 | 23 | // MARK: Body 24 | var body: some View { 25 | HStack(content: { 26 | VStack(content: Divider.init) 27 | 28 | Text(title) 29 | .foregroundStyle(Color.primary) 30 | .font(.caption.bold()) 31 | .dynamicTypeSize(...(.accessibility2)) 32 | 33 | VStack(content: Divider.init) 34 | }) 35 | .padding(.horizontal) 36 | } 37 | } 38 | 39 | // MARK: - Preview 40 | #Preview(body: { 41 | PreviewContainer(content: { 42 | PreviewRow("Lorem Ipsum", content: { 43 | Text("Lorem ipsum") 44 | }) 45 | 46 | PreviewHeader("Lorem Ipsum") 47 | 48 | PreviewRow("Lorem Ipsum", content: { 49 | Text("Lorem ipsum") 50 | }) 51 | }) 52 | }) 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Views (Containers)/PreviewRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewRow.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 08.03.23. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | 12 | // MARK: - Preview Row 13 | struct PreviewRow: View where Content: View { 14 | // MARK: Properties 15 | private let title: String? 16 | private let content: () -> Content 17 | 18 | // MARK: Initializers 19 | init( 20 | _ title: String?, 21 | @ViewBuilder content: @escaping () -> Content 22 | ) { 23 | self.title = title 24 | self.content = content 25 | } 26 | 27 | // MARK: Body 28 | var body: some View { 29 | VStack( 30 | spacing: 10, 31 | content: { 32 | if let title { 33 | Text(title) 34 | .lineLimit(1) 35 | .foregroundStyle(Color.primary) 36 | .font(.caption.bold()) 37 | .dynamicTypeSize(...(.accessibility2)) 38 | } 39 | 40 | content() 41 | } 42 | ) 43 | } 44 | } 45 | 46 | // MARK: - Preview 47 | #Preview(body: { 48 | PreviewRow("Lorem Ipsum", content: { 49 | Text("Lorem ipsum") 50 | }) 51 | }) 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Views/Preview_MarqueeContent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Preview_MarqueeContent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 21.01.24. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | 12 | // MARK: - Marquee Content (Small) 13 | var preview_MarqueeContentSmall: some View { 14 | HStack(content: { 15 | Image(systemName: "swift") 16 | Text("Lorem ipsum") 17 | }) 18 | .drawingGroup() 19 | } 20 | 21 | // MARK: - Marquee Content 22 | var preview_MarqueeContent: some View { 23 | HStack(content: { 24 | Image(systemName: "swift") 25 | 26 | #if os(iOS) 27 | Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") 28 | #elseif os(macOS) 29 | Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis imperdiet eros id tellus porta ullamcorper.") 30 | #elseif os(tvOS) 31 | Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis imperdiet eros id tellus porta ullamcorper. Ut odio purus, posuere sit amet odio non, tempus scelerisque arcu.") 32 | #elseif os(watchOS) 33 | Text("Lorem ipsum dolor sit amet.") 34 | #elseif os(visionOS) 35 | Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis imperdiet eros id tellus porta ullamcorper. Ut odio purus, posuere sit amet odio non, tempus scelerisque arcu.") 36 | #endif 37 | }) 38 | .drawingGroup() 39 | } 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Alerts)/Alert/VAlertButtonConvertible.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VAlertButtonConvertible.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.05.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Alert Button Convertible 11 | /// Type that allows for conversion to `VAlertButtonProtocol`. 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public protocol VAlertButtonConvertible { 16 | /// Converts self to `VAlertButtonProtocol` `Array`. 17 | func toButtons() -> [any VAlertButtonProtocol] 18 | } 19 | 20 | @available(tvOS, unavailable) 21 | @available(watchOS, unavailable) 22 | @available(visionOS, unavailable) 23 | extension Array: VAlertButtonConvertible where Element == any VAlertButtonProtocol { 24 | public func toButtons() -> [any VAlertButtonProtocol] { 25 | self 26 | } 27 | } 28 | 29 | @available(tvOS, unavailable) 30 | @available(watchOS, unavailable) 31 | @available(visionOS, unavailable) 32 | extension Never: VAlertButtonConvertible { 33 | public func toButtons() -> [any VAlertButtonProtocol] { 34 | fatalError() 35 | } 36 | } 37 | 38 | @available(tvOS, unavailable) 39 | @available(watchOS, unavailable) 40 | @available(visionOS, unavailable) 41 | extension EmptyView: VAlertButtonConvertible { 42 | public func toButtons() -> [any VAlertButtonProtocol] { 43 | [] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/VComponents/Extensions/RectangleCornerRadii+CornersAdjustedForDirection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RectangleCornerRadii+CornersAdjustedForDirection.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 14.07.24. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - Rectangle Corner Radii + Corners Adjusted for Direction 12 | extension RectangleCornerRadii { 13 | func cornersAdjustedForDirection( 14 | _ direction: LayoutDirectionOmni 15 | ) -> Self { 16 | switch direction { 17 | case .leftToRight: 18 | self 19 | 20 | case .rightToLeft: 21 | RectangleCornerRadii( 22 | topLeading: topTrailing, 23 | bottomLeading: bottomTrailing, 24 | bottomTrailing: bottomLeading, 25 | topTrailing: topLeading 26 | ) 27 | 28 | case .topToBottom: 29 | RectangleCornerRadii( 30 | topLeading: bottomLeading, 31 | bottomLeading: bottomTrailing, 32 | bottomTrailing: topTrailing, 33 | topTrailing: topLeading 34 | ) 35 | 36 | case .bottomToTop: 37 | RectangleCornerRadii( 38 | topLeading: topTrailing, 39 | bottomLeading: topLeading, 40 | bottomTrailing: bottomLeading, 41 | topTrailing: bottomTrailing 42 | ) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/VComponents/Services and Managers/HapticManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HapticManager.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.04.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - Haptic Manager 11 | @MainActor 12 | final class HapticManager { 13 | // MARK: Properties 14 | static let shared: HapticManager = .init() 15 | 16 | // MARK: Initializers 17 | private init() {} 18 | 19 | // MARK: Haptics 20 | #if os(iOS) 21 | func playImpact(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle?) { 22 | guard let feedbackStyle else { return } 23 | 24 | let generator: UIImpactFeedbackGenerator = .init(style: feedbackStyle) 25 | generator.prepare() 26 | generator.impactOccurred() 27 | } 28 | 29 | func playSelection() { 30 | let generator: UISelectionFeedbackGenerator = .init() 31 | generator.prepare() 32 | generator.selectionChanged() 33 | } 34 | 35 | func playNotification(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType?) { 36 | guard let feedbackType else { return } 37 | 38 | let generator: UINotificationFeedbackGenerator = .init() 39 | generator.prepare() 40 | generator.notificationOccurred(feedbackType) 41 | } 42 | #endif 43 | 44 | #if os(watchOS) 45 | func playImpact(_ hapticType: WKHapticType?) { 46 | guard let hapticType else { return } 47 | 48 | WKInterfaceDevice.current().play(hapticType) 49 | } 50 | #endif 51 | } 52 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Extensions/View+OnReceiveOfTimerIncrement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+OnReceiveOfTimerIncrement.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 08.03.23. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | 12 | // MARK: - View + On Receive of Timer Increment 13 | extension View { 14 | func onReceiveOfTimerIncrement( 15 | _ value: Binding, 16 | to total: Int, 17 | by increment: Int = 1, 18 | timeInterval: TimeInterval = 1 19 | ) -> some View { 20 | self 21 | .onReceive( 22 | Timer.publish(every: timeInterval, on: .main, in: .common).autoconnect(), 23 | perform: { _ in 24 | var valueToSet: Int = value.wrappedValue + increment 25 | if valueToSet > total { valueToSet = 0 } 26 | 27 | value.wrappedValue = valueToSet 28 | } 29 | ) 30 | } 31 | 32 | func onReceiveOfTimerIncrement( 33 | _ value: Binding, 34 | to total: Double, 35 | by increment: Double = 1, 36 | timeInterval: TimeInterval = 1 37 | ) -> some View { 38 | self 39 | .onReceive( 40 | Timer.publish(every: timeInterval, on: .main, in: .common).autoconnect(), 41 | perform: { _ in 42 | var valueToSet: Double = value.wrappedValue + increment 43 | if valueToSet > total { valueToSet = 0 } 44 | 45 | value.wrappedValue = valueToSet 46 | } 47 | ) 48 | } 49 | } 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Indefinite)/Spinner (Continous)/VContinuousSpinner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VContinuousSpinner.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 18.12.20. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Continuous Spinner 12 | /// Indicator component that represents indefinite activity. 13 | /// 14 | /// var body: some View { 15 | /// VContinuousSpinner() 16 | /// } 17 | /// 18 | public struct VContinuousSpinner: View, Sendable { 19 | // MARK: Properties - UI Model 20 | private let uiModel: VContinuousSpinnerUIModel 21 | 22 | // MARK: Properties - State 23 | @State private var isAnimating: Bool = false 24 | 25 | // MARK: Initializers 26 | /// Initializes `VContinuousSpinner`. 27 | public init( 28 | uiModel: VContinuousSpinnerUIModel = .init() 29 | ) { 30 | self.uiModel = uiModel 31 | } 32 | 33 | // MARK: Body 34 | public var body: some View { 35 | Circle() 36 | .trim(from: 0, to: uiModel.length) 37 | .stroke( 38 | uiModel.color, 39 | style: StrokeStyle(lineWidth: uiModel.thickness, lineCap: .round) 40 | ) 41 | .frame(width: uiModel.dimension, height: uiModel.dimension) 42 | .rotationEffect(Angle(degrees: isAnimating ? 360 : 0)) 43 | .onAppear(perform: { 44 | withAnimation( 45 | uiModel.animation.repeatForever(autoreverses: false), 46 | { isAnimating.toggle() } 47 | ) 48 | }) 49 | .environment(\.layoutDirection, .leftToRight) // Like native `ProgressView`, forces LTR 50 | } 51 | } 52 | 53 | // MARK: - Preview 54 | #if DEBUG 55 | 56 | #Preview(body: { 57 | PreviewContainer(content: { 58 | VContinuousSpinner() 59 | }) 60 | }) 61 | 62 | #endif 63 | -------------------------------------------------------------------------------- /Sources/VComponents/Models/ModalComponentSizeGroup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModalComponentSizeGroup.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 22.05.22. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - Modal Component Size Group 12 | /// Modal component sizes. 13 | @MemberwiseInitializable( 14 | comment: "/// Initializes `ModalComponentSizeGroup` with sizes." 15 | ) 16 | public struct ModalComponentSizeGroup { 17 | // MARK: Properties 18 | /// Portrait size . 19 | public var portrait: Size 20 | 21 | /// Landscape size. 22 | public var landscape: Size 23 | 24 | // MARK: Initializers 25 | /// Initializes `ModalComponentSizeGroup` with size. 26 | public init( 27 | _ size: Size 28 | ) { 29 | self.portrait = size 30 | self.landscape = size 31 | } 32 | 33 | // MARK: Current 34 | func current( 35 | orientation: PlatformInterfaceOrientation 36 | ) -> Size { 37 | switch orientation { 38 | case .portrait: portrait 39 | case .landscape: landscape 40 | } 41 | } 42 | } 43 | 44 | extension ModalComponentSizeGroup: Equatable where Size: Equatable {} 45 | 46 | extension ModalComponentSizeGroup: Sendable where Size: Sendable {} 47 | 48 | // MARK: - Modal Component Size 49 | /// Modal component size. 50 | @MemberwiseInitializable( 51 | comment: "/// Initializes `ModalComponentSize` with width and height." 52 | ) 53 | public struct ModalComponentSize { 54 | /// Width. 55 | public var width: Width 56 | 57 | /// Height. 58 | public var height: Height 59 | } 60 | 61 | extension ModalComponentSize: Equatable where Width: Equatable, Height: Equatable {} 62 | 63 | extension ModalComponentSize: Sendable where Width: Sendable, Height: Sendable {} 64 | 65 | // MARK: - Helpers 66 | extension AbsoluteFractionMeasurement { 67 | static var zero: Self { 68 | .absolute(0) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Indefinite)/Spinner (Continous)/VContinuousSpinnerUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VContinuousSpinnerUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12/21/20. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Continuous Spinner UI Model 11 | /// Model that describes UI. 12 | public struct VContinuousSpinnerUIModel: Sendable { 13 | // MARK: Properties 14 | /// Dimension. 15 | /// Set to `15` on `iOS`. 16 | /// Set to `25` on `macOS`. 17 | /// Set to `30` on `tvOS`. 18 | /// Set to `15` on `watchOS`. 19 | /// Set to `30` on `visionOS`. 20 | public var dimension: CGFloat = { 21 | #if os(iOS) 22 | 15 23 | #elseif os(macOS) 24 | 25 25 | #elseif os(tvOS) 26 | 30 27 | #elseif os(watchOS) 28 | 15 29 | #elseif os(visionOS) 30 | 30 31 | #endif 32 | }() 33 | 34 | /// Length of the colored part. Set to `0.75`. 35 | public var length: CGFloat = 0.75 36 | 37 | /// Thickness. 38 | /// Set to `2` on `watchOS`. 39 | /// Set to `3` on `macOS`. 40 | /// Set to `4` on `tvOS`. 41 | /// Set to `2` on `watchOS`. 42 | /// Set to `4` on `visionOS`. 43 | public var thickness: CGFloat = { 44 | #if os(iOS) 45 | 2 46 | #elseif os(macOS) 47 | 3 48 | #elseif os(tvOS) 49 | 4 50 | #elseif os(watchOS) 51 | 2 52 | #elseif os(visionOS) 53 | 4 54 | #endif 55 | }() 56 | 57 | /// Color. 58 | public var color: Color = { 59 | #if os(iOS) 60 | Color.blue 61 | #elseif os(macOS) 62 | Color.gray 63 | #elseif os(tvOS) 64 | Color.gray 65 | #elseif os(watchOS) 66 | Color.gray 67 | #elseif os(visionOS) 68 | Color.gray 69 | #endif 70 | }() 71 | 72 | /// Animation. Set to `linear` with duration `0.75`. 73 | public var animation: Animation = .linear(duration: 0.75) 74 | 75 | // MARK: Initializers 76 | /// Initializes UI model with default values. 77 | public init() {} 78 | } 79 | -------------------------------------------------------------------------------- /Sources/VComponents/Preview Helpers/Views (Containers)/PreviewContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewContainer.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 08.03.23. 6 | // 7 | 8 | #if DEBUG 9 | 10 | import SwiftUI 11 | import VCore 12 | 13 | // MARK: - Preview Container 14 | struct PreviewContainer: View where Content: View { 15 | private let layer: PreviewContainerLayer 16 | private let content: () -> Content 17 | 18 | init( 19 | layer: PreviewContainerLayer = .primary, 20 | @ViewBuilder content: @escaping () -> Content 21 | ) { 22 | self.layer = layer 23 | self.content = content 24 | } 25 | 26 | var body: some View { 27 | ZStack(content: { 28 | Group(content: { 29 | #if os(iOS) 30 | switch layer { 31 | case .primary: Color(uiColor: UIColor.systemBackground) 32 | case .secondary: Color(uiColor: UIColor.secondarySystemBackground) 33 | } 34 | #else 35 | Color.clear 36 | #endif 37 | }) 38 | .ignoresSafeArea() 39 | 40 | ViewThatFits( 41 | in: .vertical, 42 | content: { 43 | vStackedContent 44 | 45 | ScrollView( 46 | .vertical, 47 | content: { vStackedContent } 48 | ) 49 | .clipped() 50 | } 51 | ) 52 | }) 53 | } 54 | 55 | private var vStackedContent: some View { 56 | VStack( 57 | spacing: 20, 58 | content: content 59 | ) 60 | .padding(.vertical, 20) 61 | .frame(maxWidth: .infinity) 62 | } 63 | } 64 | 65 | // MARK: - Preview Container Layer 66 | enum PreviewContainerLayer { 67 | case primary 68 | case secondary 69 | } 70 | 71 | // MARK: - Preview 72 | #Preview(body: { 73 | PreviewContainer(content: { 74 | Text("Lorem ipsum") 75 | }) 76 | }) 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Alerts)/Alert/VAlertButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VAlertButton.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12/26/20. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Alert Button 12 | /// `VAlert` button. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public struct VAlertButton: VAlertButtonProtocol, Sendable { 17 | // MARK: Properties 18 | private var isEnabled: Bool = true 19 | /*private*/ let role: Role 20 | private let action: (@Sendable () -> Void)? 21 | private let title: String 22 | 23 | // MARK: Initializers 24 | /// Initializes `VAlertButton` with action and title. 25 | public init( 26 | role: Role, 27 | action: (@Sendable () -> Void)?, 28 | title: String 29 | ) { 30 | self.role = role 31 | self.action = action 32 | self.title = title 33 | } 34 | 35 | // MARK: Role 36 | /// Model that describes the purpose of a button. 37 | public enum Role: Int, Sendable, CaseIterable { 38 | /// Primary. 39 | case primary 40 | 41 | /// Secondary. 42 | case secondary 43 | 44 | /// Destructive. 45 | case destructive 46 | 47 | /// Cancel. 48 | case cancel 49 | } 50 | 51 | // MARK: Button Protocol 52 | public func makeBody( 53 | uiModel: VAlertUIModel, 54 | animateOutHandler: @escaping (/*completion*/ (() -> Void)?) -> Void 55 | ) -> AnyView { 56 | VStretchedButton( 57 | uiModel: { 58 | switch role { 59 | case .primary: uiModel.primaryButtonSubUIModel 60 | case .secondary: uiModel.secondaryButtonSubUIModel 61 | case .destructive: uiModel.destructiveButtonSubUIModel 62 | case .cancel: uiModel.secondaryButtonSubUIModel 63 | } 64 | }(), 65 | action: { animateOutHandler(/*completion: */action) }, 66 | title: title 67 | ) 68 | .disabled(!isEnabled) 69 | .eraseToAnyView() 70 | } 71 | 72 | // MARK: Modifiers 73 | /// Adds a condition that controls whether users can interact with the button. 74 | public func disabled(_ disabled: Bool) -> Self { 75 | var button = self 76 | button.isEnabled = !disabled 77 | return button 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/VComponents/API/VComponentsLocalizationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VComponentsLocalizationManager.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 23.05.22. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - VComponents Localization Manager 11 | /// Object that manages localization in the package. 12 | /// 13 | /// `localizationProvider` in `shared` instance can be set to override the localized values. 14 | /// 15 | /// struct SomeLocalizationProvider: VComponentsLocalizationProvider { ... } 16 | /// 17 | /// VComponentsLocalizationManager.shared.localizationProvider = SomeLocalizationProvider() 18 | /// 19 | public final class VComponentsLocalizationManager: @unchecked Sendable { 20 | // MARK: Properties - Singleton 21 | /// Shared instance of `VComponentsLocalizationManager`. 22 | public static let shared: VComponentsLocalizationManager = .init() 23 | 24 | // MARK: Properties - Localization 25 | private var _localizationProvider: any VComponentsLocalizationProvider = DefaultVComponentsLocalizationProvider() 26 | 27 | /// Localization provider. Set to `DefaultVComponentsLocalizationProvider`. 28 | public var localizationProvider: any VComponentsLocalizationProvider { 29 | get { lock.withLock({ _localizationProvider }) } 30 | set { lock.withLock({ _localizationProvider = newValue }) } 31 | } 32 | 33 | // MARK: Properties - Lock 34 | private let lock: NSLock = .init() 35 | 36 | // MARK: Initializers 37 | private init() {} 38 | } 39 | 40 | // MARK: - VComponents Localization Provider 41 | /// Localization provider in package. 42 | public protocol VComponentsLocalizationProvider { 43 | /// Localized value for error title in alert. 44 | var vAlertErrorTitle: String { get } 45 | 46 | /// Localized value for `VAlertOKButton`. 47 | var vAlertOKButtonTitle: String { get } 48 | 49 | /// Localized value for `cancel` `VAlertButton`. 50 | var vAlertCancelButtonTitle: String { get } 51 | } 52 | 53 | // MARK: - Default VComponents Localization Provider 54 | /// Defaults VComponents localization provider. 55 | public struct DefaultVComponentsLocalizationProvider: VComponentsLocalizationProvider, Sendable { 56 | // MARK: Initializers 57 | /// Initializes `VComponentsLocalizationProvider`. 58 | public init() {} 59 | 60 | // MARK: VComponents Localization Provider 61 | public var vAlertErrorTitle: String { "Something Went Wrong" } 62 | public var vAlertOKButtonTitle: String { "Ok" } 63 | public var vAlertCancelButtonTitle: String { "Cancel" } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/zMisc/Marquee (Bouncing)/VBouncingMarqueeUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBouncingMarqueeUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 24.02.23. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Bouncing Marquee UI Model 12 | /// Model that describes UI. 13 | public struct VBouncingMarqueeUIModel: Sendable { 14 | // MARK: Properties - Global 15 | /// Scroll direction. Set to `leftToRight`. 16 | public var scrollDirection: LayoutDirection = .leftToRight 17 | 18 | /// Content inset. Set to `0`. 19 | /// 20 | /// Ideal for text content. 21 | /// Alternately, use `insettedGradientMask` instance of `VBouncingMarqueeUIModel`. 22 | /// 23 | /// For best result, should be greater than or equal to `gradientMaskWidth`. 24 | public var inset: CGFloat = 0 25 | 26 | /// Horizontal alignment for non-scrolling stationary content. Set to `leading`. 27 | public var alignmentStationary: HorizontalAlignment = .leading 28 | 29 | // MARK: Properties - Gradient 30 | /// Gradient mask width. Set to `0`. 31 | /// 32 | /// To hide gradient mask, set to `0`. 33 | /// 34 | /// Alternately, use `insettedGradientMask` instance of `VBouncingMarqueeUIModel`. 35 | /// 36 | /// For best result, should be less than or equal to `inset`. 37 | public var gradientMaskWidth: CGFloat = 0 38 | 39 | /// Gradient mask opacity at the edge of the container. Set to `0`. 40 | public var gradientMaskOpacityContainerEdge: CGFloat = 0 41 | 42 | /// Gradient mask opacity at the edge of the content. Set to `1`. 43 | public var gradientMaskOpacityContentEdge: CGFloat = 1 44 | 45 | // MARK: Properties - Transition 46 | /// Animation curve. Set to `linear`. 47 | public var animationCurve: BasicAnimation.AnimationCurve = .linear 48 | 49 | /// Animation duration type. Set to `default`. 50 | public var animationDurationType: MarqueeDurationType = .default 51 | 52 | /// Animation delay. Set to `1` second. 53 | public var animationDelay: Double = 1 54 | 55 | /// Initial animation delay. Set to `1` second. 56 | public var animationInitialDelay: Double = 1 57 | 58 | // MARK: Initializers 59 | /// Initializes UI model with default values. 60 | public init() {} 61 | } 62 | 63 | // MARK: - Factory 64 | extension VBouncingMarqueeUIModel { 65 | /// `VBouncingMarqueeUIModel` that insets content and applies fading gradient. 66 | public static var insettedGradientMask: Self { 67 | var uiModel: Self = .init() 68 | 69 | uiModel.inset = 20 70 | 71 | uiModel.gradientMaskWidth = 20 72 | 73 | return uiModel 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/zMisc/Rolling Counter/VRollingCounterComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRollingCounterComponent.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - V Rolling Counter Component (Digit) 11 | struct VRollingCounterDigitComponent: VRollingCounterComponentProtocol { 12 | // MARK: Properties 13 | let id: String 14 | 15 | let digit: Int 16 | var stringRepresentation: String { .init(digit) } 17 | 18 | var isHighlighted: Bool 19 | 20 | // MARK: Initializers 21 | init( 22 | id: String?, // If `nil`, new one will be generated 23 | digit: Int, 24 | isHighlighted: Bool = false 25 | ) { 26 | self.id = id ?? Self.generateID() 27 | self.digit = digit 28 | self.isHighlighted = isHighlighted 29 | } 30 | } 31 | 32 | // MARK: - V Rolling Counter Component (Fraction Digit) 33 | struct VRollingCounterFractionDigitComponent: VRollingCounterComponentProtocol { 34 | // MARK: Properties 35 | let id: String 36 | 37 | let digit: Int 38 | var stringRepresentation: String { .init(digit) } 39 | 40 | var isHighlighted: Bool 41 | 42 | // MARK: Initializers 43 | init( 44 | id: String?, // If `nil`, new one will be generated 45 | digit: Int, 46 | isHighlighted: Bool = false 47 | ) { 48 | self.id = id ?? Self.generateID() 49 | self.digit = digit 50 | self.isHighlighted = isHighlighted 51 | } 52 | } 53 | 54 | // MARK: - V Rolling Counter Component (Grouping Separator) 55 | struct VRollingCounterGroupingSeparatorComponent: VRollingCounterComponentProtocol { 56 | // MARK: Properties 57 | let id: String 58 | 59 | let value: String 60 | var stringRepresentation: String { value } 61 | 62 | var isHighlighted: Bool 63 | 64 | // MARK: Initializers 65 | init( 66 | id: String?, // If `nil`, new one will be generated 67 | value: String, 68 | isHighlighted: Bool = false 69 | ) { 70 | self.id = id ?? Self.generateID() 71 | self.value = value 72 | self.isHighlighted = isHighlighted 73 | } 74 | } 75 | 76 | // MARK: - V Rolling Counter Component (Decimal Separator) 77 | struct VRollingCounterDecimalSeparatorComponent: VRollingCounterComponentProtocol { 78 | // MARK: Properties 79 | let id: String 80 | 81 | let value: String 82 | var stringRepresentation: String { value } 83 | 84 | var isHighlighted: Bool 85 | 86 | // MARK: Initializers 87 | init( 88 | id: String?, // If `nil`, new one will be generated 89 | value: String, 90 | isHighlighted: Bool = false 91 | ) { 92 | self.id = id ?? Self.generateID() 93 | self.value = value 94 | self.isHighlighted = isHighlighted 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Documentation/Customization.md: -------------------------------------------------------------------------------- 1 | # Customization 2 | 3 | ## Table of Contents 4 | 5 | - [Intro](#intro) 6 | - [Example](#example) 7 | - [Factory Instances](#factory-instances) 8 | - [Pre-Existing Factory Instances](#pre-existing-factory-instances) 9 | 10 | ## Intro 11 | 12 | Components are not meant to be customized with view modifiers, the same way you would customize a `SwiftUI` component. All components have a UI models that they take in initializers. All UI models have default values, and all properties within those UI models have default values as well. 13 | 14 | ## Example 15 | 16 | For instance, you can change the foreground color of a `VPlainButton` by modifying its UI model. 17 | 18 | Not Preferred: 19 | 20 | ```swift 21 | var body: some View { 22 | VPlainButton( 23 | action: doSomething, 24 | title: "Lorem Ipsum" 25 | ) 26 | .foregroundStyle(.primary) 27 | } 28 | ``` 29 | 30 | Preferred: 31 | 32 | ```swift 33 | let uiModel: VPlainButtonUIModel = { 34 | var uiModel: VPlainButtonUIModel = .init() 35 | 36 | uiModel.titleTextColors = VPlainButtonUIModel.StateColors( 37 | enabled: Color.primary, 38 | pressed: Color.secondary, 39 | disabled: Color.secondary 40 | ) 41 | 42 | return uiModel 43 | }() 44 | 45 | var body: some View { 46 | VPlainButton( 47 | uiModel: uiModel, 48 | action: doSomething, 49 | title: "Lorem Ipsum" 50 | ) 51 | } 52 | ``` 53 | 54 | ## Factory Instances 55 | 56 | Alternately, you can create `static` instances of UI models for reusability. 57 | 58 | ```swift 59 | extension VPlainButtonUIModel { 60 | static let someUIModel: Self = { 61 | var uiModel: Self = .init() 62 | 63 | uiModel.titleTextColors = StateColors( 64 | enabled: Color.primary, 65 | pressed: Color.secondary, 66 | disabled: Color.secondary 67 | ) 68 | 69 | return uiModel 70 | }() 71 | } 72 | 73 | var body: some View { 74 | VPlainButton( 75 | uiModel: .someUIModel, 76 | action: doSomething, 77 | title: "Lorem Ipsum" 78 | ) 79 | } 80 | ``` 81 | 82 | ## Pre-Existing Factory Instances 83 | 84 | Frequently, you will discover pre-existing static factory-initialized UI models associated with each component. It's recommended to check UI model files before creating them yourself. 85 | 86 | ```swift 87 | var body: some View { 88 | VWrappingMarquee( 89 | uiModel: .insettedGradientMask, 90 | content: { 91 | HStack(content: { 92 | Image(systemName: "swift") 93 | Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") 94 | }) 95 | .drawingGroup() // For `Image` 96 | } 97 | ) 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Alerts)/Alert/VAlertButtonBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VAlertButtonBuilder.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 16.07.22. 6 | // 7 | 8 | import Foundation 9 | import VCore 10 | 11 | // MARK: - V Alert Button Builder 12 | /// Custom parameter attribute that constructs views from closures. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | @resultBuilder 17 | public struct VAlertButtonBuilder: Sendable { 18 | // MARK: Properties 19 | public typealias Component = any VAlertButtonConvertible 20 | public typealias Result = [any VAlertButtonProtocol] 21 | 22 | // MARK: Build Blocks 23 | public static func buildBlock() -> Result { 24 | [] 25 | } 26 | 27 | public static func buildBlock(_ components: Component...) -> Result { 28 | components.flatMap { $0.toButtons() } 29 | } 30 | 31 | public static func buildOptional(_ component: Component?) -> Result { 32 | component?.toButtons() ?? [] 33 | } 34 | 35 | public static func buildEither(first component: Component) -> Result { 36 | component.toButtons() 37 | } 38 | 39 | public static func buildEither(second component: Component) -> Result { 40 | component.toButtons() 41 | } 42 | 43 | public static func buildArray(_ components: [Component]) -> Result { 44 | components.flatMap { $0.toButtons() } 45 | } 46 | 47 | public static func buildLimitedAvailability(_ component: Component) -> Result { 48 | component.toButtons() 49 | } 50 | 51 | public static func buildFinalResult(_ component: Component) -> Result { 52 | component.toButtons() 53 | } 54 | 55 | // MARK: Processing 56 | // If there are multiple `cancel` `VAlertButton`s, only the last one will be kept. 57 | // `cancel` `VAlertButton` will be moved to the end of the stack. 58 | // If there are no buttons, `VAlertOKButton` will be added. 59 | static func process(_ buttons: [any VAlertButtonProtocol]) -> [any VAlertButtonProtocol] { 60 | var result: [any VAlertButtonProtocol] = [] 61 | 62 | for button in buttons { 63 | if (button as? VAlertButton)?.role == .cancel { 64 | result.removeAll(where: { ($0 as? VAlertButton)?.role == .cancel }) 65 | } 66 | result.append(button) 67 | } 68 | if let cancelButtonIndex: Int = result.firstIndex(where: { ($0 as? VAlertButton)?.role == .cancel }) { 69 | result.append(result.remove(at: cancelButtonIndex)) 70 | } 71 | 72 | if result.isEmpty { 73 | result.append( 74 | VAlertButton( 75 | role: .secondary, 76 | action: nil, 77 | title: VComponentsLocalizationManager.shared.localizationProvider.vAlertOKButtonTitle 78 | ) 79 | ) 80 | } 81 | 82 | return result 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/zMisc/Marquee (Wrapping)/VWrappingMarqueeUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappingMarqueeUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 24.02.23. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Wrapping Marquee UI Model 12 | /// Model that describes UI. 13 | public struct VWrappingMarqueeUIModel: Sendable { 14 | // MARK: Properties - Global 15 | /// Scroll direction. Set to `leftToRight`. 16 | public var scrollDirection: LayoutDirection = .leftToRight 17 | 18 | /// Spacing between wrapped content. Set to `20`. 19 | /// 20 | /// If `inset` is set to non-`0` value or `VWrappingMarqueeUIModel.insettedGradientMask` is used, 21 | /// it's better to set this to `0`. 22 | public var wrappedContentSpacing: CGFloat = 20 23 | 24 | /// Content inset. Set to `0`. 25 | /// 26 | /// Alternately, use `insettedGradientMask` instance of `VWrappingMarqueeUIModel`. 27 | /// 28 | /// For best result, should be greater than or equal to `gradientMaskWidth`. 29 | /// 30 | /// If this is set to non-`0` value, it's better to set `wrappedContentSpacing` to `0`. 31 | public var inset: CGFloat = 0 32 | 33 | /// Horizontal alignment for non-scrolling stationary content. Set to `leading`. 34 | public var alignmentStationary: HorizontalAlignment = .leading 35 | 36 | // MARK: Properties - Gradient 37 | /// Gradient mask width. Set to `0`. 38 | /// 39 | /// To hide gradient mask, set to `0`. 40 | /// 41 | /// Alternately, use `insettedGradientMask` instance of `VBouncingMarqueeUIModel`. 42 | /// 43 | /// For best result, should be less than or equal to `inset`. 44 | public var gradientMaskWidth: CGFloat = 0 45 | 46 | /// Gradient mask opacity at the edge of the container. Set to `0`. 47 | public var gradientMaskOpacityContainerEdge: CGFloat = 0 48 | 49 | /// Gradient mask opacity at the edge of the content. Set to `1`. 50 | public var gradientMaskOpacityContentEdge: CGFloat = 1 51 | 52 | // MARK: Properties - Transition 53 | /// Animation curve. Set to `linear`. 54 | public var animationCurve: BasicAnimation.AnimationCurve = .linear 55 | 56 | /// Animation duration type. Set to `default`. 57 | public var animationDurationType: MarqueeDurationType = .default 58 | 59 | /// Animation delay. Set to `1` second. 60 | public var animationDelay: Double = 1 61 | 62 | /// Initial animation delay. Set to `1` second. 63 | public var animationInitialDelay: Double = 1 64 | 65 | // MARK: Initializers 66 | /// Initializes UI model with default values. 67 | public init() {} 68 | } 69 | 70 | // MARK: - Factory 71 | extension VWrappingMarqueeUIModel { 72 | /// `VWrappingMarqueeUIModel` that insets content and applies fading gradient. 73 | public static var insettedGradientMask: Self { 74 | var uiModel: Self = .init() 75 | 76 | uiModel.wrappedContentSpacing = 0 77 | uiModel.inset = 20 78 | 79 | uiModel.gradientMaskWidth = 20 80 | 81 | return uiModel 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Carousel/VCarouselUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCarouselUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 26.07.24. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Carousel UI Model 12 | /// Model that describes UI. 13 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 14 | @available(tvOS, unavailable) 15 | public struct VCarouselUIModel: Sendable { 16 | // MARK: Properties - Global 17 | /// Indicates if scrolling is enabled. Set to `true`. 18 | public var isScrollingEnabled: Bool = true 19 | 20 | // MARK: Properties - Cards 21 | /// Cards alignment. Set to `center`. 22 | public var cardsAlignment: VerticalAlignment = .center 23 | 24 | /// Spacing between cards. 25 | /// Set to `15` on `iOS`. 26 | /// Set to `15` on `macOS`. 27 | /// Set to `7.5` on `watchOS`. 28 | /// Set to `15` on `visionOS`. 29 | public var cardsSpacing: CGFloat = { 30 | #if os(iOS) 31 | 15 32 | #elseif os(macOS) 33 | 15 34 | #elseif os(watchOS) 35 | 7.5 36 | #elseif os(visionOS) 37 | 15 38 | #else 39 | fatalError() // Not supported 40 | #endif 41 | }() 42 | 43 | // MARK: Properties - Card - Global 44 | /// Card horizontal margin. 45 | /// Set to `30` on `iOS`. 46 | /// Set to `30` on `macOS`. 47 | /// Set to `15` on `watchOS`. 48 | /// Set to `30` on `visionOS`. 49 | /// 50 | /// This property determines margins around the selected card. 51 | public var cardMarginHorizontal: CGFloat = { 52 | #if os(iOS) 53 | 30 54 | #elseif os(macOS) 55 | 30 56 | #elseif os(watchOS) 57 | 15 58 | #elseif os(visionOS) 59 | 30 60 | #else 61 | fatalError() // Not supported 62 | #endif 63 | }() 64 | 65 | /// Card top margin. Set to `0`. 66 | public var cardMarginTop: CGFloat = 0 67 | 68 | /// Card bottom margin. Set to `0`. 69 | public var cardMarginBottom: CGFloat = 0 70 | 71 | /// Card height scales. Set to `1`s. 72 | public var cardHeightScales: CardStateDimensions = .init(1) 73 | 74 | /// Card opacities. Set to `1`s. 75 | public var cardOpacities: CardStateOpacities = .init(1) 76 | 77 | // MARK: Properties - Card - Shadow 78 | /// Card shadow color. 79 | public var cardShadowColor: Color = .clear 80 | 81 | /// Card shadow radius. Set to `0`. 82 | public var cardShadowRadius: CGFloat = 0 83 | 84 | /// Card shadow offset. Set to `zero`. 85 | public var cardShadowOffset: CGPoint = .zero 86 | 87 | // MARK: Properties - Transition - Selection 88 | /// Indicates if `appliesSelectionAnimation` is applied. Set to `true`. 89 | /// 90 | /// Changing this property conditionally will cause view state to be reset. 91 | /// 92 | /// If animation is set to `nil`, a `nil` animation is still applied. 93 | /// If this property is set to `false`, then no animation is applied. 94 | /// 95 | /// One use-case for this property is to externally mutate state using `withAnimation(_:completionCriteria:_:completion:)` function. 96 | public var appliesSelectionAnimation: Bool = true 97 | 98 | /// State change animation. Set to `default`. 99 | public var selectionAnimation: Animation? = .default 100 | 101 | // MARK: Initializers 102 | /// Initializes UI model with default values. 103 | public init() {} 104 | 105 | // MARK: Card State Dimension 106 | /// Model that contains dimensions for component states. 107 | public typealias CardStateDimensions = GenericStateModel_DeselectedSelected 108 | 109 | // MARK: Card State Opacities 110 | /// Model that contains opacities for component states. 111 | public typealias CardStateOpacities = GenericStateModel_DeselectedSelected 112 | } 113 | -------------------------------------------------------------------------------- /Sources/VComponents/Helpers/Architectural Pattern Helpers/Alert/VAlertParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VAlertParameters.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.10.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Alert Parameters 11 | /// Parameters for presenting a `VAlert`. 12 | /// 13 | /// @State private var parameters: VAlertParameters? 14 | /// 15 | /// var body: some View { 16 | /// ZStack(content: { 17 | /// VPlainButton( 18 | /// action: { 19 | /// parameters = VAlertParameters( 20 | /// title: "Lorem Ipsum", 21 | /// message: "Lorem ipsum dolor sit amet", 22 | /// actions: { 23 | /// VAlertButton(role: .primary, action: { print("Confirmed") }, title: "Confirm") 24 | /// VAlertButton(role: .cancel, action: { print("Cancelled") }, title: "Cancel") 25 | /// } 26 | /// ) 27 | /// }, 28 | /// title: "Present" 29 | /// ) 30 | /// .vAlert(id: "some_alert", parameters: $parameters) 31 | /// }) 32 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 33 | /// .presentationHostLayer() // Or declare in `App` on a `WindowScene`-level 34 | /// } 35 | /// 36 | @available(macOS, unavailable) 37 | @available(tvOS, unavailable) 38 | @available(watchOS, unavailable) 39 | @available(visionOS, unavailable) 40 | public struct VAlertParameters { 41 | // MARK: Properties 42 | /// Title. 43 | public var title: String 44 | 45 | /// Message. 46 | public var message: String? 47 | 48 | /// Buttons. 49 | public var buttons: () -> [any VAlertButtonProtocol] 50 | 51 | /// Attributes. 52 | public var attributes: [String: Any?] 53 | 54 | // MARK: Parameters 55 | /// Initializes `VAlertParameters`. 56 | public init( 57 | title: String, 58 | message: String?, 59 | @VAlertButtonBuilder actions buttons: @escaping () -> [any VAlertButtonProtocol], 60 | attributes: [String: Any?] = [:] 61 | ) { 62 | self.title = title 63 | self.message = message 64 | self.buttons = buttons 65 | self.attributes = attributes 66 | } 67 | 68 | /// Initializes `VAlertParameters` with "ok" action. 69 | public init( 70 | title: String, 71 | message: String?, 72 | completion: (@Sendable () -> Void)?, 73 | attributes: [String: Any?] = [:] 74 | ) { 75 | self.init( 76 | title: title, 77 | message: message, 78 | actions: { 79 | VAlertButton( 80 | role: .cancel, 81 | action: completion, 82 | title: VComponentsLocalizationManager.shared.localizationProvider.vAlertCancelButtonTitle 83 | ) 84 | }, 85 | attributes: attributes 86 | ) 87 | } 88 | 89 | /// Initializes `VAlertParameters` with error and "ok" action. 90 | public init( 91 | error: any Error, 92 | completion: (@Sendable () -> Void)?, 93 | attributes: [String: Any?] = [:] 94 | ) { 95 | self.init( 96 | title: VComponentsLocalizationManager.shared.localizationProvider.vAlertErrorTitle, 97 | message: error.localizedDescription, 98 | actions: { 99 | VAlertButton( 100 | role: .secondary, 101 | action: completion, 102 | title: VComponentsLocalizationManager.shared.localizationProvider.vAlertOKButtonTitle 103 | ) 104 | }, 105 | attributes: attributes 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Documentation/Animations.md: -------------------------------------------------------------------------------- 1 | # Animations 2 | 3 | ## Table of Contents 4 | 5 | - [Intro](#intro) 6 | - [Configuring Animations](#configuring-animations) 7 | - [Cancelling Animations and Applying Animations Externally](#cancelling-animations-and-applying-animations-externally) 8 | 9 | ## Intro 10 | 11 | `VComponents` associates animations directly with components and their UI models, rather than with an external state. 12 | 13 | ```swift 14 | @State private var isOn: Bool = false 15 | 16 | var body: some View { 17 | VStack(content: { 18 | VToggle(isOn: $isOn) 19 | 20 | VPlainButton( 21 | action: { isOn.toggle() }, 22 | title: "Toggle" 23 | ) 24 | }) 25 | } 26 | ``` 27 | 28 | ## Configuring Animations 29 | 30 | By default, `VToggle` has an `easeIn` animation with a duration of `0.1`. This applies uniformly to both touch interactions, as well as any external modifications of the state. So, to modify state with a different animation, you'll need to provide a custom UI model. 31 | 32 | ```swift 33 | @State private var isOn: Bool = false 34 | 35 | var body: some View { 36 | VStack(content: { 37 | VToggle( 38 | uiModel: { 39 | var uiModel: VToggleUIModel = .init() 40 | uiModel.stateChangeAnimation = .easeIn(duration: 1) 41 | return uiModel 42 | }(), 43 | isOn: $isOn 44 | ) 45 | 46 | VPlainButton( 47 | action: { isOn.toggle() }, 48 | title: "Toggle" 49 | ) 50 | }) 51 | } 52 | ``` 53 | 54 | ## Cancelling Animations and Applying Animations Externally 55 | 56 | #### Intro 57 | 58 | There are two possible options for completely cancelling animations. 59 | 60 | #### Option 1: nil Animation 61 | 62 | The first is to set `stateChangeAnimation` to `nil`. While this does not completely remove the animation, it essentially applies a `nil` animation. 63 | 64 | ```swift 65 | @State private var isOn: Bool = false 66 | 67 | var body: some View { 68 | VStack(content: { 69 | VToggle( 70 | uiModel: { 71 | var uiModel: VToggleUIModel = .init() 72 | uiModel.stateChangeAnimation = nil 73 | return uiModel 74 | }(), 75 | isOn: $isOn 76 | ) 77 | 78 | VPlainButton( 79 | action: { isOn.toggle() }, 80 | title: "Toggle" 81 | ) 82 | }) 83 | } 84 | ``` 85 | 86 | #### Option 2: Animation Flag 87 | 88 | The second is to set `appliesStateChangeAnimation` to `false`. This option ensures that the `stateChangeAnimation` is not applied at all, thus effectively removing any animation tied to state changes, even `nil`. 89 | 90 | ```swift 91 | @State private var isOn: Bool = false 92 | 93 | var body: some View { 94 | VStack(content: { 95 | VToggle( 96 | uiModel: { 97 | var uiModel: VToggleUIModel = .init() 98 | uiModel.appliesStateChangeAnimation = false 99 | return uiModel 100 | }(), 101 | isOn: $isOn 102 | ) 103 | 104 | VPlainButton( 105 | action: { isOn.toggle() }, 106 | title: "Toggle" 107 | ) 108 | }) 109 | } 110 | ``` 111 | 112 | #### External Animations 113 | 114 | In certain scenarios, the distinction between these two can be substantial. For example, we could set the `appliesStateChangeAnimation` flag to `false` and subsequently mutate the state with an external animation. 115 | 116 | ```swift 117 | @State private var isOn: Bool = false 118 | 119 | var body: some View { 120 | VStack(content: { 121 | VToggle( 122 | uiModel: { 123 | var uiModel: VToggleUIModel = .init() 124 | uiModel.appliesStateChangeAnimation = false 125 | return uiModel 126 | }(), 127 | isOn: $isOn 128 | ) 129 | 130 | VPlainButton( 131 | action: { 132 | withAnimation(.easeInOut(duration: 1), { 133 | isOn.toggle() 134 | }) 135 | }, 136 | title: "Toggle" 137 | ) 138 | }) 139 | } 140 | ``` 141 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Group Box/VGroupBoxUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VGroupBoxUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12/22/20. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Group Box UI Model 12 | /// Model that describes UI. 13 | public struct VGroupBoxUIModel: Sendable { 14 | // MARK: Properties - Corners 15 | /// Corner radii. 16 | /// Set to `15`s on `iOS`. 17 | /// Set to `7.5`s on `macOS`. 18 | /// Set to `20`s on `tvOS`. 19 | /// Set to `10`s on `watchOS`. 20 | /// Set to `15`s on `visionOS`. 21 | public var cornerRadii: RectangleCornerRadii = { 22 | #if os(iOS) 23 | RectangleCornerRadii(15) 24 | #elseif os(macOS) 25 | RectangleCornerRadii(7.5) 26 | #elseif os(tvOS) 27 | RectangleCornerRadii(20) 28 | #elseif os(watchOS) 29 | RectangleCornerRadii(10) 30 | #elseif os(visionOS) 31 | RectangleCornerRadii(15) 32 | #endif 33 | }() 34 | 35 | /// Indicates if horizontal corners should switch to support RTL languages. Set to `true`. 36 | public var reversesHorizontalCornersForRTLLanguages: Bool = true 37 | 38 | // MARK: Properties - Background 39 | /// Background color. 40 | public var backgroundColor: Color = { 41 | #if os(iOS) 42 | Color(uiColor: UIColor.secondarySystemBackground) 43 | #elseif os(macOS) 44 | Color.dynamic(Color.black.opacity(0.03), Color.black.opacity(0.15)) 45 | #elseif os(tvOS) 46 | Color.dynamic(Color.black.opacity(0.05), Color.white.opacity(0.1)) 47 | #elseif os(watchOS) 48 | Color.white.opacity(0.1) 49 | #elseif os(visionOS) 50 | Color.white.opacity(0.2) 51 | #endif 52 | }() 53 | 54 | // MARK: Properties - Border 55 | /// Border width. 56 | /// Set to `0` point on `iOS`. 57 | /// Set to `1` pixel on `macOS`. 58 | /// Set to `0` point on `tvOS`. 59 | /// Set to `0` point on `watchOS`. 60 | /// Set to `0` point on `visionOS`. 61 | /// 62 | /// To hide border, set to `0`. 63 | public var borderWidth: PointPixelMeasurement = { 64 | #if os(iOS) 65 | PointPixelMeasurement.points(0) 66 | #elseif os(macOS) 67 | PointPixelMeasurement.pixels(1) 68 | #elseif os(tvOS) 69 | PointPixelMeasurement.points(0) 70 | #elseif os(watchOS) 71 | PointPixelMeasurement.points(0) 72 | #elseif os(visionOS) 73 | PointPixelMeasurement.points(0) 74 | #endif 75 | }() 76 | 77 | /// Border color. 78 | public var borderColor: Color = { 79 | #if os(iOS) 80 | Color.clear 81 | #elseif os(macOS) 82 | Color.dynamic(Color(200, 200, 200), Color(100, 100, 100)) 83 | #elseif os(tvOS) 84 | Color.clear 85 | #elseif os(watchOS) 86 | Color.clear 87 | #elseif os(visionOS) 88 | Color.clear 89 | #endif 90 | }() 91 | 92 | // MARK: Properties - Content 93 | /// Content margins. 94 | /// Set to `15`s on `iOS`. 95 | /// Set to `7.5`s on `macOS`. 96 | /// Set to `20`s on `tvOS`. 97 | /// Set to `10`s on `watchOS`. 98 | /// Set to `15`s on `visionOS`. 99 | public var contentMargins: Margins = { 100 | #if os(iOS) 101 | Margins(15) 102 | #elseif os(macOS) 103 | Margins(7.5) 104 | #elseif os(tvOS) 105 | Margins(20) 106 | #elseif os(watchOS) 107 | Margins(10) 108 | #elseif os(visionOS) 109 | Margins(15) 110 | #endif 111 | }() 112 | 113 | // MARK: Initializers 114 | /// Initializes UI model with default values. 115 | public init() {} 116 | 117 | // MARK: Margins 118 | /// Model that contains `leading`, `trailing`, `top`, and `bottom` margins. 119 | public typealias Margins = EdgeInsets_LeadingTrailingTopBottom 120 | } 121 | 122 | // MARK: - Factory 123 | @available(macOS, unavailable) 124 | @available(tvOS, unavailable) 125 | @available(watchOS, unavailable) 126 | @available(visionOS, unavailable) 127 | extension VGroupBoxUIModel { 128 | /// `VGroupBoxUIModel` with `UIColor.systemBackground` to be used on `UIColor.secondarySystemBackground`. 129 | public static var systemBackgroundColor: Self { 130 | var uiModel: Self = .init() 131 | 132 | #if os(iOS) 133 | uiModel.backgroundColor = Color(uiColor: UIColor.systemBackground) 134 | #endif 135 | 136 | return uiModel 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Static Pager Tab View (Wrapped Indicator)/VWrappedIndicatorStaticPagerTabViewUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VWrappedIndicatorStaticPagerTabViewUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Wrapped-Indicator Static Pager Tab View UI Model 12 | /// Model that describes UI. 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS, unavailable) 16 | @available(visionOS, unavailable) 17 | public struct VWrappedIndicatorStaticPagerTabViewUIModel: Sendable { 18 | // MARK: Properties - Global 19 | /// Spacing between tab bar and tab view. Set to `0`. 20 | public var tabBarAndTabViewSpacing: CGFloat = 0 21 | 22 | // MARK: Properties - Header 23 | /// Header background color. 24 | public var headerBackgroundColor: Color = { 25 | #if os(iOS) 26 | Color(uiColor: UIColor.systemBackground) 27 | #else 28 | fatalError() // Not supported 29 | #endif 30 | }() 31 | 32 | // MARK: Properties - Tab Bar 33 | /// Tab bar alignment for tab items. Set to `top`. 34 | public var tabBarAlignment: VerticalAlignment = .top 35 | 36 | // MARK: Properties - Tab Bar - Tab Item 37 | /// Tab bar margins. Set to `(10, 10)`. 38 | public var tabItemMargins: VerticalMargins = .init(10) 39 | 40 | // MARK: Properties - Tab Bar - Tab Item - Text 41 | /// Tab item text minimum scale factor. Set to `0.75`. 42 | public var tabItemTextMinimumScaleFactor: CGFloat = 0.75 43 | 44 | /// Tab item text colors. 45 | public var tabItemTextColors: TabItemStateColors = .init( 46 | deselected: Color.primary, 47 | selected: Color.blue, 48 | pressedDeselected: Color.primary.opacity(0.3), 49 | pressedSelected: Color.dynamic(Color(31, 104, 182), Color(36, 106, 186)), 50 | disabled: Color.primary.opacity(0.3) 51 | ) 52 | 53 | /// Tab item text font. Set to `body`. 54 | public var tabItemTextFont: Font = .body 55 | 56 | /// Tab item text `DynamicTypeSize` type. Set to partial range through `accessibility2`. 57 | /// 58 | /// Changing this property conditionally will cause view state to be reset. 59 | public var tabItemTextDynamicTypeSizeType: DynamicTypeSizeType? = .partialRangeThrough(...(.accessibility2)) 60 | 61 | // MARK: Properties - Tab Indicator Strip 62 | /// Tab indicator strip alignment. Set to `bottom`. 63 | public var tabIndicatorStripAlignment: VerticalAlignment = .bottom 64 | 65 | // MARK: Properties - Tab Indicator Strip - Track 66 | /// Tab indicator track height. Set to `2`. 67 | public var tabIndicatorTrackHeight: CGFloat = 2 68 | 69 | /// Tab indicator track color. 70 | public var tabIndicatorTrackColor: Color = .clear 71 | 72 | // MARK: Properties - Tab Indicator Strip - Selection 73 | /// Selected tab indicator height. Set to `2`. 74 | public var selectedTabIndicatorHeight: CGFloat = 2 75 | 76 | /// Selected tab indicator corner radius. Set to `0`. 77 | public var selectedTabIndicatorCornerRadius: CGFloat = 0 78 | 79 | /// Selected tab indicator color. 80 | public var selectedTabIndicatorColor: Color = .blue 81 | 82 | /// Selected tab indicator animation. Set to `default`. 83 | public var selectedTabIndicatorAnimation: Animation? = .default 84 | 85 | // MARK: Properties - Tab View 86 | /// Indicates if tab view scrolling is enabled. Set to `true`. 87 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 88 | public var isTabViewScrollingEnabled: Bool { 89 | get { _isTabViewScrollingEnabled } 90 | set { _isTabViewScrollingEnabled = newValue } 91 | } 92 | private var _isTabViewScrollingEnabled: Bool = true // TODO: iOS 17.0 - Remove 93 | 94 | /// Tab view background color. 95 | public var tabViewBackgroundColor: Color = { 96 | #if os(iOS) 97 | Color(uiColor: UIColor.systemBackground) 98 | #else 99 | fatalError() // Not supported 100 | #endif 101 | }() 102 | 103 | // MARK: Initializers 104 | /// Initializes UI model with default values. 105 | public init() {} 106 | 107 | // MARK: Vertical Margins 108 | /// Model that contains `top` and `bottom` margins. 109 | public typealias VerticalMargins = EdgeInsets_TopBottom 110 | 111 | // MARK: Tab Item State Colors 112 | /// Model that contains colors for component states. 113 | public typealias TabItemStateColors = GenericStateModel_DeselectedSelectedPressedDisabled 114 | } 115 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Definite)/Progress Bar/VProgressBarUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VProgressBarUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/12/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Progress Bar UI Model 12 | /// Model that describes UI. 13 | public struct VProgressBarUIModel: Sendable { 14 | // MARK: Properties - Global 15 | /// Direction. Set to `leftToRight`. 16 | public var direction: LayoutDirectionOmni = .leftToRight 17 | 18 | /// Progress bar height, but width for vertical layout. 19 | /// Set to `10` on `iOS`. 20 | /// Set to `10` on `macOS`. 21 | /// Set to `10` on `tvOS`. 22 | /// Set to `13.5` on `watchOS`. 23 | /// Set to `10` on `visionOS`. 24 | public var height: CGFloat = { 25 | #if os(iOS) 26 | 10 27 | #elseif os(macOS) 28 | 10 29 | #elseif os(tvOS) 30 | 10 31 | #elseif os(watchOS) 32 | 13.5 33 | #elseif os(visionOS) 34 | 10 35 | #endif 36 | }() 37 | 38 | // MARK: Properties - Corners 39 | /// Progress bar corner radius. 40 | /// Set to `5` on `iOS`. 41 | /// Set to `5` on `macOS`. 42 | /// Set to `5` on `tvOS`. 43 | /// Set to `6.75` on `watchOS` 44 | /// Set to `5` on `visionOS`. 45 | public var cornerRadius: CGFloat = { 46 | #if os(iOS) 47 | 5 48 | #elseif os(macOS) 49 | 5 50 | #elseif os(tvOS) 51 | 5 52 | #elseif os(watchOS) 53 | 6.75 54 | #elseif os(visionOS) 55 | 5 56 | #endif 57 | }() 58 | 59 | /// Indicates if progress bar rounds progress view trailing corners. Set to `true`. 60 | public var roundsProgressViewTrailingCorners: Bool = true 61 | 62 | // MARK: Properties - Track 63 | /// Track color. 64 | public var trackColor: Color = { 65 | #if os(iOS) 66 | Color.dynamic(Color(230, 230, 230), Color(45, 45, 45)) 67 | #elseif os(macOS) 68 | Color.dynamic(Color.black.opacity(0.05), Color.white.opacity(0.125)) 69 | #elseif os(tvOS) 70 | Color.dynamic(Color(135, 135, 135), Color(90, 90, 90)) 71 | #elseif os(watchOS) 72 | Color(90, 90, 90) 73 | #elseif os(visionOS) 74 | Color.white.opacity(0.2) 75 | #endif 76 | }() 77 | 78 | // MARK: Properties - Progress 79 | /// Progress color. 80 | public var progressColor: Color = { 81 | #if os(iOS) 82 | Color.blue 83 | #elseif os(macOS) 84 | Color.blue 85 | #elseif os(tvOS) 86 | Color.dynamic(Color(220, 220, 220), Color(220, 220, 220)) 87 | #elseif os(watchOS) 88 | Color.white 89 | #elseif os(visionOS) 90 | Color.blue 91 | #endif 92 | }() 93 | 94 | // MARK: Properties - Border 95 | /// Border width. 96 | /// Set to `0` point on `iOS`. 97 | /// Set to `1` pixel on `macOS`. 98 | /// Set to `0` point on `tvOS`. 99 | /// Set to `0` point on `watchOS`. 100 | /// Set to `0` point on `visionOS`. 101 | /// 102 | /// To hide border, set to `0`. 103 | public var borderWidth: PointPixelMeasurement = { 104 | #if os(iOS) 105 | PointPixelMeasurement.points(0) 106 | #elseif os(macOS) 107 | PointPixelMeasurement.pixels(1) 108 | #elseif os(tvOS) 109 | PointPixelMeasurement.points(0) 110 | #elseif os(watchOS) 111 | PointPixelMeasurement.points(0) 112 | #elseif os(visionOS) 113 | PointPixelMeasurement.points(0) 114 | #endif 115 | }() 116 | 117 | /// Border color. 118 | public var borderColor: Color = { 119 | #if os(iOS) 120 | Color.clear 121 | #elseif os(macOS) 122 | Color.dynamic(Color.black.opacity(0.125), Color.clear) 123 | #elseif os(tvOS) 124 | Color.clear 125 | #elseif os(watchOS) 126 | Color.clear 127 | #elseif os(visionOS) 128 | Color.clear 129 | #endif 130 | }() 131 | 132 | // MARK: Properties - Transition - Progress 133 | /// Indicates if `progress` animation is applied. Set to `true`. 134 | /// 135 | /// Changing this property conditionally will cause view state to be reset. 136 | /// 137 | /// If animation is set to `nil`, a `nil` animation is still applied. 138 | /// If this property is set to `false`, then no animation is applied. 139 | /// 140 | /// One use-case for this property is to externally mutate state using `withAnimation(_:completionCriteria:_:completion:)` function. 141 | public var appliesProgressAnimation: Bool = true 142 | 143 | /// Progress animation. Set to `default`. 144 | public var progressAnimation: Animation? = .default 145 | 146 | // MARK: Initializers 147 | /// Initializes UI model with default values. 148 | public init() {} 149 | } 150 | -------------------------------------------------------------------------------- /Sources/VComponents/Helpers/Architectural Pattern Helpers/Alert/View+VAlertWithParameters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+VAlertWithParameters.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.10.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - View + V Alert 11 | @available(macOS, unavailable) 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | extension View { 16 | /// Presents `VAlert` when `VAlertParameters` is non-`nil`. 17 | /// 18 | /// @State private var parameters: VAlertParameters? 19 | /// 20 | /// var body: some View { 21 | /// ZStack(content: { 22 | /// VPlainButton( 23 | /// action: { 24 | /// parameters = VAlertParameters( 25 | /// title: "Lorem Ipsum", 26 | /// message: "Lorem ipsum dolor sit amet", 27 | /// actions: { 28 | /// VAlertButton(role: .primary, action: { print("Confirmed") }, title: "Confirm") 29 | /// VAlertButton(role: .cancel, action: { print("Cancelled") }, title: "Cancel") 30 | /// } 31 | /// ) 32 | /// }, 33 | /// title: "Present" 34 | /// ) 35 | /// .vAlert(id: "some_alert", parameters: $parameters) 36 | /// }) 37 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 38 | /// .presentationHostLayer() // Or declare in `App` on a `WindowScene`-level 39 | /// } 40 | /// 41 | public func vAlert( 42 | layerID: String? = nil, 43 | id: String, 44 | uiModel: VAlertUIModel = .init(), 45 | parameters: Binding 46 | ) -> some View { 47 | self.vAlert( 48 | layerID: layerID, 49 | id: id, 50 | uiModel: uiModel, 51 | item: parameters, 52 | title: { $0.title }, 53 | message: { $0.message }, 54 | actions: { $0.buttons() } 55 | ) 56 | } 57 | 58 | /// Presents `VAlert` when `parameters` is non-`nil`. 59 | /// 60 | /// @State private var parameters: VAlertParameters? 61 | /// @State private var inputText: String = "Lorem ipsum" 62 | /// 63 | /// var body: some View { 64 | /// ZStack(content: { 65 | /// VPlainButton( 66 | /// action: { 67 | /// parameters = VAlertParameters( 68 | /// title: "Lorem Ipsum", 69 | /// message: "Lorem ipsum dolor sit amet", 70 | /// actions: { 71 | /// VAlertButton(role: .primary, action: { print("Confirmed") }, title: "Confirm") 72 | /// VAlertButton(role: .cancel, action: { print("Cancelled") }, title: "Cancel") 73 | /// }, 74 | /// attributes: [ 75 | /// "input_text_binding": $inputText 76 | /// ] 77 | /// ) 78 | /// }, 79 | /// title: "Present" 80 | /// ) 81 | /// .vAlert( 82 | /// id: "some_alert", 83 | /// parameters: $parameters, 84 | /// content: { parameters in 85 | /// if let inputTextBinding = parameters.attributes["input_text_binding"] as? Binding { 86 | /// TextField("", text: inputTextBinding) 87 | /// .textFieldStyle(.roundedBorder) 88 | /// } 89 | /// } 90 | /// ) 91 | /// }) 92 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 93 | /// .presentationHostLayer() // Or declare in `App` on a `WindowScene`-level 94 | /// 95 | public func vAlert( 96 | layerID: String? = nil, 97 | id: String, 98 | uiModel: VAlertUIModel = .init(), 99 | parameters: Binding, 100 | @ViewBuilder content: @escaping (VAlertParameters) -> Content 101 | ) -> some View 102 | where Content: View 103 | { 104 | self.vAlert( 105 | layerID: layerID, 106 | id: id, 107 | uiModel: uiModel, 108 | item: parameters, 109 | title: { $0.title }, 110 | message: { $0.message }, 111 | content: content, 112 | actions: { $0.buttons() } 113 | ) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Static Pager Tab View (Stretched Indicator)/VStretchedIndicatorStaticPagerTabViewUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStretchedIndicatorStaticPagerTabViewUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 01.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Stretched-Indicator Static Pager Tab View UI Model 12 | /// Model that describes UI. 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS, unavailable) 16 | @available(visionOS, unavailable) 17 | public struct VStretchedIndicatorStaticPagerTabViewUIModel: Sendable { 18 | // MARK: Properties - Global 19 | /// Spacing between tab bar and tab view. Set to `0`. 20 | public var tabBarAndTabViewSpacing: CGFloat = 0 21 | 22 | // MARK: Properties - Header 23 | /// Header background color. 24 | public var headerBackgroundColor: Color = { 25 | #if os(iOS) 26 | Color(uiColor: UIColor.systemBackground) 27 | #else 28 | fatalError() // Not supported 29 | #endif 30 | }() 31 | 32 | // MARK: Properties - Tab Bar 33 | /// Tab bar alignment for tab items. Set to `top`. 34 | public var tabBarAlignment: VerticalAlignment = .top 35 | 36 | // MARK: Properties - Tab Bar - Tab Item 37 | /// Tab bar margins. Set to `(10, 10)`. 38 | public var tabItemMargins: VerticalMargins = .init(10) 39 | 40 | // MARK: Properties - Tab Bar - Tab Item - Text 41 | /// Tab item text minimum scale factor. Set to `0.75`. 42 | public var tabItemTextMinimumScaleFactor: CGFloat = 0.75 43 | 44 | /// Tab item text colors. 45 | public var tabItemTextColors: TabItemStateColors = .init( 46 | deselected: Color.primary, 47 | selected: Color.blue, 48 | pressedDeselected: Color.primary.opacity(0.3), 49 | pressedSelected: Color.dynamic(Color(31, 104, 182), Color(36, 106, 186)), 50 | disabled: Color.primary.opacity(0.3) 51 | ) 52 | 53 | /// Tab item text font. Set to `body`. 54 | public var tabItemTextFont: Font = .body 55 | 56 | /// Tab item text `DynamicTypeSize` type. Set to partial range through `accessibility2`. 57 | /// 58 | /// Changing this property conditionally will cause view state to be reset. 59 | public var tabItemTextDynamicTypeSizeType: DynamicTypeSizeType? = .partialRangeThrough(...(.accessibility2)) 60 | 61 | // MARK: Properties - Tab Indicator Strip 62 | /// Tab indicator strip alignment. Set to `bottom`. 63 | public var tabIndicatorStripAlignment: VerticalAlignment = .bottom 64 | 65 | // MARK: Properties - Tab Indicator Strip - Track 66 | /// Tab indicator track height. Set to `2`. 67 | public var tabIndicatorTrackHeight: CGFloat = 2 68 | 69 | /// Tab indicator track color. 70 | public var tabIndicatorTrackColor: Color = .clear 71 | 72 | // MARK: Properties - Tab Indicator Strip - Selection 73 | /// Selected tab indicator height. Set to `2`. 74 | public var selectedTabIndicatorHeight: CGFloat = 2 75 | 76 | /// Selected tab indicator corner radius. Set to `0`. 77 | public var selectedTabIndicatorCornerRadius: CGFloat = 0 78 | 79 | /// Selected tab indicator color. 80 | public var selectedTabIndicatorColor: Color = .blue 81 | 82 | /// Selected tab indicator animation. Set to `default`. 83 | public var selectedTabIndicatorAnimation: Animation? = .default 84 | 85 | /// Indicates if tab indicator bounces when content is dragged out of frame. Set to `true`. 86 | public var selectedTabIndicatorBounces: Bool = true 87 | 88 | /// Selected tab indicator horizontal margin. Set to `0`. 89 | public var selectedTabIndicatorMarginHorizontal: CGFloat = 0 90 | 91 | // MARK: Properties - Tab View 92 | /// Tab view background color. 93 | public var tabViewBackgroundColor: Color = { 94 | #if os(iOS) 95 | Color(uiColor: UIColor.systemBackground) 96 | #else 97 | fatalError() // Not supported 98 | #endif 99 | }() 100 | 101 | /// Indicates if tab view scrolling is enabled. Set to `true`. 102 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 103 | public var isTabViewScrollingEnabled: Bool { 104 | get { _isTabViewScrollingEnabled } 105 | set { _isTabViewScrollingEnabled = newValue } 106 | } 107 | private var _isTabViewScrollingEnabled: Bool = true // TODO: iOS 17.0 - Remove 108 | 109 | // MARK: Initializers 110 | /// Initializes UI model with default values. 111 | public init() {} 112 | 113 | // MARK: Vertical Margins 114 | /// Model that contains `top` and `bottom` margins. 115 | public typealias VerticalMargins = EdgeInsets_TopBottom 116 | 117 | // MARK: Tab Item State Colors 118 | /// Model that contains colors for component states. 119 | public typealias TabItemStateColors = GenericStateModel_DeselectedSelectedPressedDisabled 120 | } 121 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Containers)/Bottom Sheet/VBottomSheetSnapAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VBottomSheetSnapAction.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 4/19/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // MARK: - V Bottom Sheet Snap Action 11 | @available(tvOS, unavailable) 12 | @available(watchOS, unavailable) 13 | @available(visionOS, unavailable) 14 | enum VBottomSheetSnapAction { 15 | // MARK: Cases 16 | case dismiss 17 | case snap(CGFloat) 18 | 19 | // MARK: Initializers 20 | // Velocity is always non-zero, and exceeds the threshold. 21 | static func dragEndedHighVelocitySnapAction( 22 | containerHeight: CGFloat, 23 | 24 | heights: VBottomSheetUIModel.Heights, 25 | 26 | offset: CGFloat, 27 | 28 | velocity: CGFloat 29 | ) -> VBottomSheetSnapAction { 30 | let region: VBottomSheetRegion = .init(containerHeight: containerHeight, heights: heights, offset: offset) 31 | let isGoingDown: Bool = velocity > 0 32 | 33 | switch (region, isGoingDown) { 34 | case (.idealToMax, false): return .snap(heights.maxOffset(in: containerHeight)) 35 | case (.idealToMax, true): return .snap(heights.idealOffset(in: containerHeight)) 36 | case (.minToIdeal, false): return .snap(heights.idealOffset(in: containerHeight)) 37 | case (.minToIdeal, true): return .snap(heights.minOffset(in: containerHeight)) 38 | case (.swipeToMin, false): return .snap(heights.minOffset(in: containerHeight)) 39 | case (.swipeToMin, true): return .dismiss 40 | } 41 | } 42 | 43 | static func dragEndedSnapAction( 44 | containerHeight: CGFloat, 45 | 46 | heights: VBottomSheetUIModel.Heights, 47 | canSwipeToDismiss: Bool, 48 | swipeDismissDistance: CGFloat, 49 | 50 | offset: CGFloat, 51 | offsetBeforeDrag: CGFloat, 52 | translation: CGFloat 53 | ) -> VBottomSheetSnapAction { 54 | let shouldDismiss: Bool = { 55 | guard canSwipeToDismiss else { return false } 56 | 57 | let isDraggedDown: Bool = translation > 0 58 | guard isDraggedDown else { return false } 59 | 60 | let newOffset: CGFloat = offsetBeforeDrag + translation 61 | guard newOffset - heights.minOffset(in: containerHeight) >= abs(swipeDismissDistance) else { return false } 62 | 63 | return true 64 | }() 65 | 66 | switch shouldDismiss { 67 | case false: 68 | switch VBottomSheetRegion(containerHeight: containerHeight, heights: heights, offset: offset) { 69 | case .idealToMax: 70 | let idealDiff: CGFloat = abs(heights.idealOffset(in: containerHeight) - offset) 71 | let maxDiff: CGFloat = abs(heights.maxOffset(in: containerHeight) - offset) 72 | let newOffset: CGFloat = idealDiff < maxDiff ? heights.idealOffset(in: containerHeight) : heights.maxOffset(in: containerHeight) 73 | 74 | return .snap(newOffset) 75 | 76 | case .swipeToMin, .minToIdeal: 77 | // If `swipe` is disabled, code won't get here. 78 | // So, modal should snap to min heights. 79 | 80 | let minDiff: CGFloat = abs(heights.minOffset(in: containerHeight) - offset) 81 | let idealDiff: CGFloat = abs(heights.idealOffset(in: containerHeight) - offset) 82 | let newOffset: CGFloat = minDiff < idealDiff ? heights.minOffset(in: containerHeight) : heights.idealOffset(in: containerHeight) 83 | 84 | return .snap(newOffset) 85 | } 86 | 87 | case true: 88 | return .dismiss 89 | } 90 | } 91 | } 92 | 93 | // MARK: - V Bottom Sheet Region 94 | @available(tvOS, unavailable) 95 | @available(watchOS, unavailable) 96 | @available(visionOS, unavailable) 97 | private enum VBottomSheetRegion { 98 | // MARK: Cases 99 | case idealToMax 100 | case minToIdeal 101 | case swipeToMin 102 | 103 | // MARK: Initializers 104 | init( 105 | containerHeight: CGFloat, 106 | heights: VBottomSheetUIModel.Heights, 107 | offset: CGFloat 108 | ) { 109 | self = { 110 | if offset >= heights.maxOffset(in: containerHeight) && offset <= heights.idealOffset(in: containerHeight) { 111 | .idealToMax 112 | } else if offset > heights.idealOffset(in: containerHeight) && offset <= heights.minOffset(in: containerHeight) { 113 | .minToIdeal 114 | } else if offset > heights.minOffset(in: containerHeight) { 115 | .swipeToMin 116 | } else { 117 | fatalError() 118 | } 119 | }() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Group Box/VGroupBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VGroupBox.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12/22/20. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Group Box 12 | /// Container component that hosts content. 13 | /// 14 | /// If content is passed during `init`, `VGroupBox` would resize according to the size of the content. 15 | /// If content is not passed, `VGroupBox` would expand to occupy maximum space. 16 | /// 17 | /// var body: some View { 18 | /// ZStack(content: { 19 | /// Color(uiColor: UIColor.secondarySystemBackground) 20 | /// .ignoresSafeArea() 21 | /// 22 | /// VGroupBox( 23 | /// uiModel: .systemBackgroundColor, 24 | /// content: { 25 | /// Text("...") 26 | /// .multilineTextAlignment(.center) 27 | /// } 28 | /// ) 29 | /// .padding() 30 | /// }) 31 | /// } 32 | /// 33 | public struct VGroupBox: View, Sendable where Content: View { 34 | // MARK: Properties - UI Model 35 | private let uiModel: VGroupBoxUIModel 36 | 37 | @Environment(\.displayScale) private var displayScale: CGFloat 38 | @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection 39 | 40 | // MARK: Properties - Content 41 | private let content: VGroupBoxContent 42 | 43 | // MARK: Initializers 44 | /// Initializes `VGroupBox`. 45 | public init( 46 | uiModel: VGroupBoxUIModel = .init() 47 | ) 48 | where Content == Never 49 | { 50 | self.uiModel = uiModel 51 | self.content = .empty 52 | } 53 | 54 | /// Initializes `VGroupBox` with content. 55 | public init( 56 | uiModel: VGroupBoxUIModel = .init(), 57 | @ViewBuilder content: @escaping () -> Content 58 | ) { 59 | self.uiModel = uiModel 60 | self.content = .content(content: content) 61 | } 62 | 63 | // MARK: Body 64 | public var body: some View { 65 | contentView 66 | .background(content: { backgroundView }) 67 | .overlay(content: { borderView }) 68 | .clipShape( 69 | .rect( 70 | cornerRadii: uiModel.cornerRadii 71 | .horizontalCornersReversed(if: 72 | uiModel.reversesHorizontalCornersForRTLLanguages && 73 | layoutDirection.isRightToLeft 74 | ) 75 | ) 76 | ) 77 | } 78 | 79 | private var contentView: some View { 80 | Group(content: { 81 | switch content { 82 | case .empty: 83 | Color.clear // `EmptyView` cannot be used as it doesn't render 84 | 85 | case .content(let content): 86 | content() 87 | } 88 | }) 89 | .padding(uiModel.contentMargins) 90 | } 91 | 92 | private var backgroundView: some View { 93 | uiModel.backgroundColor 94 | } 95 | 96 | @ViewBuilder 97 | private var borderView: some View { 98 | let borderWidth: CGFloat = uiModel.borderWidth.toPoints(scale: displayScale) 99 | 100 | if borderWidth > 0 { 101 | UnevenRoundedRectangle( 102 | cornerRadii: uiModel.cornerRadii 103 | .horizontalCornersReversed(if: 104 | uiModel.reversesHorizontalCornersForRTLLanguages && 105 | layoutDirection.isRightToLeft 106 | ) 107 | ) 108 | .strokeBorder(uiModel.borderColor, lineWidth: borderWidth) 109 | } 110 | } 111 | } 112 | 113 | // MARK: - Preview 114 | #if DEBUG 115 | 116 | #Preview("*", body: { 117 | Preview_ContentView() 118 | }) 119 | 120 | #if !(os(macOS) || os(tvOS) || os(watchOS) || os(visionOS)) 121 | 122 | #Preview("System Background Color", body: { 123 | Preview_ContentView(layer: .secondary, uiModel: .systemBackgroundColor) 124 | }) 125 | 126 | #endif 127 | 128 | private struct Preview_ContentView: View { 129 | private let layer: PreviewContainerLayer 130 | private let uiModel: VGroupBoxUIModel 131 | 132 | init( 133 | layer: PreviewContainerLayer = .primary, 134 | uiModel: VGroupBoxUIModel = .init() 135 | ) { 136 | self.layer = layer 137 | self.uiModel = uiModel 138 | } 139 | 140 | var body: some View { 141 | PreviewContainer(layer: layer, content: { 142 | VGroupBox( 143 | uiModel: uiModel, 144 | content: { 145 | Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla dapibus volutpat enim, vitae blandit justo iaculis sit amet. Aenean vitae leo tincidunt, sollicitudin mauris a, mollis massa. Sed posuere, nibh non fermentum ultrices, ipsum nunc luctus arcu, a auctor velit nisl ac nibh. Donec vel arcu condimentum, iaculis quam sed, commodo orci.") 146 | .multilineTextAlignment(.center) 147 | } 148 | ) 149 | .padding() 150 | }) 151 | } 152 | } 153 | 154 | #endif 155 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Carousel/VCarouselInfiniteScrollDataSourceManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VCarouselInfiniteScrollDataSourceManager.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 26.07.24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - V Carousel Infinite Scroll Data Source Manager 11 | /// Object that inflates data source to create an illusion of infinite scroll when using `VCarousel`. 12 | /// 13 | /// If `data` is represented as `[1, 2, 3]` and `numberOfDuplicateGroups` equals `3`, 14 | /// `dataInflated` will be `[1, 2, 3, 1, 2, 3, 1, 2, 3]`. 15 | /// 16 | /// If, with the same data, `initialSelection` is `2`, and `initialGroupIndex` equals `1`, 17 | /// `selectedIndexInflated` will be calculated from the group at index `1`, plus the index of `2` in original `data`, which is `1`. 18 | /// As a result, `selectedIndexInflated` will be `4`, which is in the middle of the `dataInflated`. 19 | /// 20 | /// private enum RGBColor: Int, Hashable, Identifiable, CaseIterable { 21 | /// case red, green, blue 22 | /// 23 | /// var id: Int { rawValue } 24 | /// 25 | /// var color: Color { 26 | /// switch self { 27 | /// case .red: Color.red 28 | /// case .green: Color.green 29 | /// case .blue: Color.blue 30 | /// } 31 | /// } 32 | /// } 33 | /// 34 | /// @State private var dataSourceManager: VCarouselInfiniteScrollDataSourceManager = .init( 35 | /// data: Preview_RGBColor.allCases, 36 | /// numberOfDuplicateGroups: 9, 37 | /// initialGroupIndex: 4, 38 | /// initialSelection: RGBColor.red 39 | /// ) 40 | /// 41 | /// var body: some View { 42 | /// VStack(spacing: 15, content: { 43 | /// VCarousel( 44 | /// selection: $dataSourceManager.selectedIndexInflated, 45 | /// data: 0.. where Element: Hashable { 67 | // MARK: Properties - Data 68 | /// Original data. 69 | public var data: [Element] 70 | 71 | /// Inflated data. 72 | public var dataInflated: [Element] { 73 | Array(repeating: data, count: numberOfDuplicateGroups) 74 | .flatMap { $0 } 75 | } 76 | 77 | /// Number of duplicate groups. 78 | public var numberOfDuplicateGroups: Int 79 | 80 | // MARK: Properties - Count 81 | /// Count of original data. 82 | public var count: Int { data.count } 83 | 84 | /// Count of inflated data. 85 | public var countInflated: Int { 86 | Self.countInflated( 87 | count: count, 88 | numberOfDuplicateGroups: numberOfDuplicateGroups 89 | ) 90 | } 91 | 92 | // MARK: Properties - Selected Index 93 | /// Index of selected element in original data. 94 | public var selectedIndex: Int { 95 | Self.index( 96 | count: count, 97 | indexInflated: selectedIndexInflated 98 | ) 99 | } 100 | 101 | /// Index of selected element in inflated data. 102 | public var selectedIndexInflated: Int 103 | 104 | // MARK: Properties - Selection 105 | /// Selected element. 106 | public var selection: Element { 107 | element(atInflatedIndex: selectedIndexInflated) 108 | } 109 | 110 | // MARK: Initializers 111 | /// Initializes `VCarouselInfiniteScrollDataSourceManager` with data and inflation parameters. 112 | public init( 113 | data: [Element], 114 | numberOfDuplicateGroups: Int, 115 | initialGroupIndex: Int, 116 | initialSelection: Element 117 | ) { 118 | self.data = data 119 | 120 | self.numberOfDuplicateGroups = numberOfDuplicateGroups 121 | 122 | self.selectedIndexInflated = Self.indexInflated( 123 | count: data.count, 124 | groupIndex: initialGroupIndex, 125 | index: data.firstIndex(of: initialSelection) ?? 0 126 | ) 127 | } 128 | 129 | // MARK: Subscript 130 | /// Retrieves element at an inflated index. 131 | public func element(atInflatedIndex indexInflated: Int) -> Element { 132 | let index: Int = Self.index( 133 | count: count, 134 | indexInflated: indexInflated 135 | ) 136 | 137 | return data[index] 138 | } 139 | 140 | // MARK: Helpers 141 | private static func countInflated( 142 | count: Int, 143 | numberOfDuplicateGroups: Int 144 | ) -> Int { 145 | count * numberOfDuplicateGroups 146 | } 147 | 148 | private static func index( 149 | count: Int, 150 | indexInflated: Int 151 | ) -> Int { 152 | indexInflated % count 153 | } 154 | 155 | private static func indexInflated( 156 | count: Int, 157 | groupIndex: Int, 158 | index: Int 159 | ) -> Int { 160 | groupIndex * count + index 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Buttons/Plain Button/VPlainButtonUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VPlainButtonUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 19.12.20. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Plain Button UI Model 12 | /// Model that describes UI. 13 | @available(tvOS, unavailable) 14 | @available(visionOS, unavailable) 15 | public struct VPlainButtonUIModel: Sendable { 16 | // MARK: Properties - Global 17 | var baseButtonSubUIModel: SwiftUIBaseButtonUIModel { 18 | var uiModel: SwiftUIBaseButtonUIModel = .init() 19 | 20 | uiModel.animatesStateChange = animatesStateChange 21 | 22 | return uiModel 23 | } 24 | 25 | // MARK: Properties - Label 26 | /// Title text and icon placement. Set to `iconAndTitle`. 27 | public var titleTextAndIconPlacement: TitleAndIconPlacement = .iconAndTitle 28 | 29 | /// Spacing between title text and icon. Set to `8`. 30 | /// 31 | /// Applicable only if `init` with icon and title is used. 32 | public var titleTextAndIconSpacing: CGFloat = 8 33 | 34 | /// Ratio to which label scales down on press. 35 | /// Set to `1` on `iOS`. 36 | /// Set to `1` on `macOS`. 37 | /// Set to `0.98` on `watchOS`. 38 | public var labelPressedScale: CGFloat = { 39 | #if os(iOS) 40 | 1 41 | #elseif os(macOS) 42 | 1 43 | #elseif os(watchOS) 44 | 0.98 45 | #else 46 | fatalError() // Not supported 47 | #endif 48 | }() 49 | 50 | // MARK: Properties - Label - Text 51 | /// Title text minimum scale factor. Set to `0.75`. 52 | public var titleTextMinimumScaleFactor: CGFloat = 0.75 53 | 54 | /// Title text colors. 55 | public var titleTextColors: StateColors = .init( 56 | enabled: Color.blue, 57 | pressed: Color.platformDynamic(Color.blue.opacity(0.3), Color.blue.opacity(0.5)), 58 | disabled: Color.platformDynamic(Color.blue.opacity(0.3), Color.blue.opacity(0.5)) 59 | ) 60 | 61 | /// Title text font. Set to `body`. 62 | public var titleTextFont: Font = .body 63 | 64 | /// Title text `DynamicTypeSize` type. Set to `nil`. 65 | /// 66 | /// Changing this property conditionally will cause view state to be reset. 67 | public var titleTextDynamicTypeSizeType: DynamicTypeSizeType? 68 | 69 | // MARK: Properties - Label - Icon 70 | /// Indicates if `resizable(...)` modifier is applied to icon. Set to `true`. 71 | /// 72 | /// Changing this property conditionally will cause view state to be reset. 73 | public var isIconResizable: Bool = true 74 | 75 | /// Icon content mode. Set to `fit`. 76 | /// 77 | /// Changing this property conditionally will cause view state to be reset. 78 | public var iconContentMode: ContentMode? = .fit 79 | 80 | /// Icon size. 81 | /// Set to `(24, 24)` on `iOS`. 82 | /// Set to `(14, 14)` on `macOS`. 83 | /// Set to `(26, 26)` on `watchOS`. 84 | public var iconSize: CGSize? = { 85 | #if os(iOS) 86 | CGSize(dimension: 24) 87 | #elseif os(macOS) 88 | CGSize(dimension: 14) 89 | #elseif os(watchOS) 90 | CGSize(dimension: 26) 91 | #else 92 | fatalError() // Not supported 93 | #endif 94 | }() 95 | 96 | /// Icon colors. 97 | /// 98 | /// Changing this property conditionally will cause view state to be reset. 99 | public var iconColors: StateColors? = .init( 100 | enabled: Color.blue, 101 | pressed: Color.platformDynamic(Color.blue.opacity(0.3), Color.blue.opacity(0.5)), 102 | disabled: Color.platformDynamic(Color.blue.opacity(0.3), Color.blue.opacity(0.5)) 103 | ) 104 | 105 | /// Icon opacities. Set to `nil`. 106 | /// 107 | /// Changing this property conditionally will cause view state to be reset. 108 | public var iconOpacities: StateOpacities? 109 | 110 | /// Icon font. Set to `nil.` 111 | /// 112 | /// Can be used for setting different weight to SF symbol icons. 113 | /// To achieve this, `isIconResizable` should be set to `false`, and `iconSize` should be set to `nil`. 114 | public var iconFont: Font? 115 | 116 | /// Icon `DynamicTypeSize` type. Set to `nil`. 117 | /// 118 | /// Changing this property conditionally will cause view state to be reset. 119 | public var iconDynamicTypeSizeType: DynamicTypeSizeType? 120 | 121 | // MARK: Properties - Hit Box 122 | /// Hit box. Set to `zero. 123 | public var hitBox: HitBox = .zero 124 | 125 | // MARK: Properties - Transition - State Change 126 | /// Indicates if button animates state change. Set to `true`. 127 | /// 128 | /// Changing this property conditionally will cause view state to be reset. 129 | public var animatesStateChange: Bool = true 130 | 131 | // MARK: Properties - Haptic 132 | #if os(iOS) 133 | /// Haptic feedback style. Set to `nil`. 134 | public var haptic: UIImpactFeedbackGenerator.FeedbackStyle? 135 | #elseif os(watchOS) 136 | /// Haptic feedback type. Set to `nil`. 137 | public var haptic: WKHapticType? 138 | #endif 139 | 140 | // MARK: Initializers 141 | /// Initializes UI model with default values. 142 | public init() {} 143 | 144 | // MARK: Hit Box 145 | /// Model that contains `leading`, `trailing`, `top` and `bottom` hit boxes. 146 | public typealias HitBox = EdgeInsets_LeadingTrailingTopBottom 147 | 148 | // MARK: State Colors 149 | /// Model that contains colors for component states. 150 | public typealias StateColors = GenericStateModel_EnabledPressedDisabled 151 | 152 | // MARK: State Opacities 153 | /// Model that contains opacities for component states. 154 | public typealias StateOpacities = GenericStateModel_EnabledPressedDisabled 155 | } 156 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Containers)/SideBar/View+VSideBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+VSideBar.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12/24/20. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - View + V Side Bar - Bool 12 | @available(tvOS, unavailable) 13 | @available(watchOS, unavailable) 14 | @available(visionOS, unavailable) 15 | extension View { 16 | /// Presents side bar when boolean is `true`. 17 | /// 18 | /// Side bar component that draws from an edge with background, and hosts content. 19 | /// 20 | /// @State private var isPresented: Bool = false 21 | /// 22 | /// var body: some View { 23 | /// ZStack(content: { 24 | /// VPlainButton( 25 | /// action: { isPresented = true }, 26 | /// title: "Present" 27 | /// ) 28 | /// .vSideBar( 29 | /// id: "some_side_bar", 30 | /// isPresented: $isPresented, 31 | /// content: { Color.blue } 32 | /// ) 33 | /// }) 34 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 35 | /// .presentationHostLayer() // Or declare in `App` on a `WindowScene`-level 36 | /// } 37 | /// 38 | /// Due to a Presentation Host API, side bar loses its intrinsic safe area properties, and requires custom handling and implementation. 39 | /// UI model contains `contentSafeAreaEdges`, that inserts `Spacer` with the dimension of safe area on specified edges. 40 | /// However, these insets are presents even if side bar content doesn't need them. 41 | /// Therefore, a custom implementation is needed per use-case. 42 | /// By default, `defaultContentSafeAreaEdges(interfaceOrientation:)` method is provided that serves that purpose. 43 | /// 44 | /// @State private var isPresented: Bool = false 45 | /// @State private var interfaceOrientation: UIInterfaceOrientation = .unknown 46 | /// 47 | /// var body: some View { 48 | /// ZStack(content: { 49 | /// VPlainButton( 50 | /// action: { isPresented = true }, 51 | /// title: "Present" 52 | /// ) 53 | /// .getInterfaceOrientation({ interfaceOrientation = $0 }) 54 | /// .vSideBar( 55 | /// id: "some_side_bar", 56 | /// uiModel: { 57 | /// var uiModel: VSideBarUIModel = .leading 58 | /// 59 | /// uiModel.contentSafeAreaEdges = uiModel.defaultContentSafeAreaEdges(interfaceOrientation: interfaceOrientation) 60 | /// 61 | /// return uiModel 62 | /// }(), 63 | /// isPresented: $isPresented, 64 | /// content: { Color.blue } 65 | /// ) 66 | /// }) 67 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 68 | /// .presentationHostLayer() // Or declare in `App` on a `WindowScene`-level 69 | /// } 70 | /// 71 | public func vSideBar( 72 | layerID: String? = nil, 73 | id: String, 74 | uiModel: VSideBarUIModel = .init(), 75 | isPresented: Binding, 76 | onPresent presentHandler: (() -> Void)? = nil, 77 | onDismiss dismissHandler: (() -> Void)? = nil, 78 | @ViewBuilder content: @escaping () -> Content 79 | ) -> some View 80 | where Content: View 81 | { 82 | self 83 | .presentationHost( 84 | layerID: layerID, 85 | id: id, 86 | uiModel: uiModel.presentationHostSubUIModel, 87 | isPresented: isPresented, 88 | onPresent: presentHandler, 89 | onDismiss: dismissHandler, 90 | content: { 91 | VSideBar( 92 | uiModel: uiModel, 93 | isPresented: isPresented, 94 | content: content 95 | ) 96 | } 97 | ) 98 | } 99 | } 100 | 101 | // MARK: - View + V Side Bar - Item 102 | @available(tvOS, unavailable) 103 | @available(watchOS, unavailable) 104 | @available(visionOS, unavailable) 105 | extension View { 106 | /// Presents side bar using the item as data source for content. 107 | /// 108 | /// For additional info, refer to method with `Bool` presentation flag. 109 | public func vSideBar( 110 | layerID: String? = nil, 111 | id: String, 112 | uiModel: VSideBarUIModel = .init(), 113 | item: Binding, 114 | onPresent presentHandler: (() -> Void)? = nil, 115 | onDismiss dismissHandler: (() -> Void)? = nil, 116 | @ViewBuilder content: @escaping (Item) -> Content 117 | ) -> some View 118 | where Content: View 119 | { 120 | item.wrappedValue.map { PresentationHostDataSourceCache.shared.set(key: id, value: $0) } 121 | 122 | let isPresented: Binding = .init( 123 | get: { item.wrappedValue != nil }, 124 | set: { if !$0 { item.wrappedValue = nil } } 125 | ) 126 | 127 | return self 128 | .presentationHost( 129 | layerID: layerID, 130 | id: id, 131 | uiModel: uiModel.presentationHostSubUIModel, 132 | isPresented: isPresented, 133 | onPresent: presentHandler, 134 | onDismiss: dismissHandler, 135 | content: { 136 | VSideBar( 137 | uiModel: uiModel, 138 | isPresented: isPresented, 139 | content: { 140 | if let item = item.wrappedValue ?? PresentationHostDataSourceCache.shared.get(key: id) as? Item { 141 | content(item) 142 | } 143 | } 144 | ) 145 | } 146 | ) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Value Pickers/Slider (Range)/VRangeSliderUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRangeSliderUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/12/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Range Slider UI Model 12 | /// Model that describes UI. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public struct VRangeSliderUIModel: Sendable { 17 | // MARK: Properties - Global 18 | /// Direction. Set to `leftToRight`. 19 | public var direction: LayoutDirectionOmni = .leftToRight 20 | 21 | /// Slider height, but width for vertical layout. Set to `10`. 22 | public var height: CGFloat = { 23 | #if os(iOS) 24 | 10 25 | #elseif os(macOS) 26 | 10 27 | #else 28 | fatalError() // Not supported 29 | #endif 30 | }() 31 | 32 | // MARK: Properties - Corners 33 | /// Slider corner radius. Set to `5`. 34 | public var cornerRadius: CGFloat = { 35 | #if os(iOS) 36 | 5 37 | #elseif os(macOS) 38 | 5 39 | #else 40 | fatalError() // Not supported 41 | #endif 42 | }() 43 | 44 | // MARK: Properties - Track 45 | /// Slider track colors. 46 | public var trackColors: StateColors = { 47 | #if os(iOS) 48 | StateColors( 49 | enabled: Color.dynamic(Color(230, 230, 230), Color(45, 45, 45)), 50 | disabled: Color.dynamic(Color(245, 245, 245), Color(35, 35, 35)) 51 | ) 52 | #elseif os(macOS) 53 | StateColors( 54 | enabled: Color.dynamic(Color.black.opacity(0.05), Color.white.opacity(0.125)), 55 | disabled: Color.dynamic(Color.black.opacity(0.03), Color.white.opacity(0.075)) 56 | ) 57 | #else 58 | fatalError() // Not supported 59 | #endif 60 | }() 61 | 62 | // MARK: Properties - Progress 63 | /// Slider progress colors. 64 | public var progressColors: StateColors = .init( 65 | enabled: Color.blue, 66 | disabled: Color.blue.opacity(0.3) 67 | ) 68 | 69 | // MARK: Properties - Border 70 | /// Border width. 71 | /// Set to `0` point on `iOS`. 72 | /// Set to `1` pixel on `macOS`. 73 | /// 74 | /// To hide border, set to `0`. 75 | public var borderWidth: PointPixelMeasurement = { 76 | #if os(iOS) 77 | PointPixelMeasurement.points(0) 78 | #elseif os(macOS) 79 | PointPixelMeasurement.pixels(1) 80 | #else 81 | fatalError() // Not supported 82 | #endif 83 | }() 84 | 85 | /// Border colors. 86 | public var borderColors: StateColors = { 87 | #if os(iOS) 88 | StateColors(Color.clear) 89 | #elseif os(macOS) 90 | StateColors( 91 | enabled: Color.dynamic(Color.black.opacity(0.125), Color.clear), 92 | disabled: Color.dynamic(Color.black.opacity(0.05), Color.clear) 93 | ) 94 | #else 95 | fatalError() // Not supported 96 | #endif 97 | }() 98 | 99 | // MARK: Properties - Thumb 100 | /// Thumb size. Set to `(20, 20)`. 101 | /// 102 | /// To hide thumb, set to `0`. 103 | public var thumbSize: CGSize = .init(dimension: 20) 104 | 105 | /// Thumb corner radius. Set to `10`. 106 | public var thumbCornerRadius: CGFloat = 10 107 | 108 | /// Thumb colors. 109 | public var thumbColors: StateColors = .init(Color.white) 110 | 111 | // MARK: Properties - Thumb Border 112 | /// Thumb border widths. 113 | /// Set to `0` point on `iOS`. 114 | /// Set to `1` pixel on `macOS`. 115 | /// 116 | /// To hide border, set to `0`. 117 | public var thumbBorderWidth: PointPixelMeasurement = { 118 | #if os(iOS) 119 | PointPixelMeasurement .points(0) 120 | #elseif os(macOS) 121 | PointPixelMeasurement.pixels(1) 122 | #else 123 | fatalError() // Not supported 124 | #endif 125 | }() 126 | 127 | /// Thumb border colors. 128 | public var thumbBorderColors: StateColors = { 129 | #if os(iOS) 130 | StateColors.clearColors 131 | #elseif os(macOS) 132 | StateColors( 133 | enabled: Color.dynamic(Color(200, 200, 200), Color(100, 100, 100)), 134 | disabled: Color.dynamic(Color(230, 230, 230), Color(70, 70, 70)) 135 | ) 136 | #else 137 | fatalError() // Not supported 138 | #endif 139 | }() 140 | 141 | // MARK: Properties - Thumb Shadow 142 | /// Thumb shadow colors. 143 | public var thumbShadowColors: StateColors = .init( 144 | enabled: Color(100, 100, 100, 0.5), 145 | disabled: Color(100, 100, 100, 0.2) 146 | ) 147 | 148 | /// Thumb shadow radius. 149 | /// Set to `2` on `iOS`. 150 | /// Set to `1` on `macOS`. 151 | public var thumbShadowRadius: CGFloat = { 152 | #if os(iOS) 153 | 2 154 | #elseif os(macOS) 155 | 1 156 | #else 157 | fatalError() // Not supported 158 | #endif 159 | }() 160 | 161 | /// Thumb shadow offset. 162 | /// Set to `(0, 2)` on `iOS`. 163 | /// Set to `(0, 1)` on `macOS`. 164 | public var thumbShadowOffset: CGPoint = { 165 | #if os(iOS) 166 | CGPoint(x: 0, y: 2) 167 | #elseif os(macOS) 168 | CGPoint(x: 0, y: 1) 169 | #else 170 | fatalError() // Not supported 171 | #endif 172 | }() 173 | 174 | // MARK: Properties - Transition - Progress 175 | /// Indicates if `progress` animation is applied. Set to `true`. 176 | /// 177 | /// Changing this property conditionally will cause view state to be reset. 178 | /// 179 | /// If animation is set to `nil`, a `nil` animation is still applied. 180 | /// If this property is set to `false`, then no animation is applied. 181 | /// 182 | /// One use-case for this property is to externally mutate state using `withAnimation(_:completionCriteria:_:completion:)` function. 183 | public var appliesProgressAnimation: Bool = true 184 | 185 | /// Progress animation. Set to `nil`. 186 | public var progressAnimation: Animation? 187 | 188 | // MARK: Initializers 189 | /// Initializes UI model with default values. 190 | public init() {} 191 | 192 | // MARK: State Colors 193 | /// Model that contains colors for component states. 194 | public typealias StateColors = GenericStateModel_EnabledDisabled 195 | } 196 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Containers/Dynamic Pager Tab View/VDynamicPagerTabViewUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VDynamicPagerTabViewUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 02.09.23. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Dynamic Pager Tab View UI Model 12 | /// Model that describes UI. 13 | @available(macOS, unavailable) 14 | @available(tvOS, unavailable) 15 | @available(watchOS, unavailable) 16 | @available(visionOS, unavailable) 17 | public struct VDynamicPagerTabViewUIModel: Sendable { 18 | // MARK: Properties - Global 19 | /// Spacing between tab bar and tab view. Set to `0`. 20 | public var tabBarAndTabViewSpacing: CGFloat = 0 21 | 22 | // MARK: Properties - Header 23 | /// Header background color. 24 | public var headerBackgroundColor: Color = { 25 | #if os(iOS) 26 | Color(uiColor: UIColor.systemBackground) 27 | #else 28 | fatalError() // Not supported 29 | #endif 30 | }() 31 | 32 | // MARK: Properties - Tab Bar 33 | /// Tab bar alignment for tab items. Set to `top`. 34 | public var tabBarAlignment: VerticalAlignment = .top 35 | 36 | /// Tab bar horizontal margin. Set to `5`. 37 | public var tabBarMarginHorizontal: CGFloat = 5 38 | 39 | /// Indicates if tab bar scrolling is enabled. Set to `true`. 40 | public var isTabBarScrollingEnabled: Bool = true 41 | 42 | /// Tab bar item spacing. Set to `0`. 43 | /// 44 | /// This property controls spacing between items, as well as selection indicator. 45 | /// When `tabSelectionIndicatorWidthType` is `stretched`, selection indicator won't stretch to occupy this spacing. 46 | public var tabItemSpacing: CGFloat = 0 47 | 48 | // MARK: Properties - Tab Bar - Tab Item 49 | /// Tab bar margins. Set to `(10, 10, 10, 10)`. 50 | public var tabItemMargins: Margins = .init(10) 51 | 52 | // MARK: Properties - Tab Bar - Tab Item - Text 53 | /// Tab item text minimum scale factor. Set to `0.75`. 54 | public var tabItemTextMinimumScaleFactor: CGFloat = 0.75 55 | 56 | /// Tab item text colors. 57 | public var tabItemTextColors: TabItemStateColors = .init( 58 | deselected: Color.primary, 59 | selected: Color.blue, 60 | pressedDeselected: Color.primary.opacity(0.3), 61 | pressedSelected: Color.dynamic(Color(31, 104, 182), Color(36, 106, 186)), 62 | disabled: Color.primary.opacity(0.3) 63 | ) 64 | 65 | /// Tab item text font. Set to `body`. 66 | public var tabItemTextFont: Font = .body 67 | 68 | /// Tab item text `DynamicTypeSize` type. Set to partial range through `accessibility2`. 69 | /// 70 | /// Changing this property conditionally will cause view state to be reset. 71 | public var tabItemTextDynamicTypeSizeType: DynamicTypeSizeType? = .partialRangeThrough(...(.accessibility2)) 72 | 73 | // MARK: Properties - Tab Indicator Strip 74 | /// Tab indicator strip alignment. Set to `bottom`. 75 | public var tabIndicatorStripAlignment: VerticalAlignment = .bottom 76 | 77 | // MARK: Properties - Tab Indicator Strip - Track 78 | /// Tab indicator track height. Set to `2`. 79 | public var tabIndicatorTrackHeight: CGFloat = 2 80 | 81 | /// Tab indicator track color. 82 | public var tabIndicatorTrackColor: Color = .clear 83 | 84 | // MARK: Properties - Tab Indicator Strip - Selection 85 | /// Tab selection indicator width type. Set to `default`. 86 | public var tabSelectionIndicatorWidthType: TabSelectionIndicatorWidthType = .default 87 | 88 | /// Selected tab indicator height. Set to `2`. 89 | public var selectedTabIndicatorHeight: CGFloat = 2 90 | 91 | /// Selected tab indicator corner radius. Set to `0`. 92 | public var selectedTabIndicatorCornerRadius: CGFloat = 0 93 | 94 | /// Selected tab indicator color. 95 | public var selectedTabIndicatorColor: Color = .blue 96 | 97 | /// Selected tab indicator animation. Set to `default`. 98 | public var selectedTabIndicatorAnimation: Animation? = .default 99 | 100 | /// Selected tab indicator scroll anchor. Set to `center`. 101 | public var selectedTabIndicatorScrollAnchor: UnitPoint = .center 102 | 103 | // MARK: Properties - Tab View 104 | /// Tab view background color. 105 | public var tabViewBackgroundColor: Color = { 106 | #if os(iOS) 107 | Color(uiColor: UIColor.systemBackground) 108 | #else 109 | fatalError() // Not supported 110 | #endif 111 | }() 112 | 113 | /// Indicates if tab view scrolling is enabled. Set to `true`. 114 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 115 | public var isTabViewScrollingEnabled: Bool { 116 | get { _isTabViewScrollingEnabled } 117 | set { _isTabViewScrollingEnabled = newValue } 118 | } 119 | private var _isTabViewScrollingEnabled: Bool = true // TODO: iOS 17.0 - Remove 120 | 121 | // MARK: Initializers 122 | /// Initializes UI model with default values. 123 | public init() {} 124 | 125 | // MARK: Margins 126 | /// Model that contains `leading`, `trailing`, `top` and `bottom` hit boxes. 127 | public typealias Margins = EdgeInsets_LeadingTrailingTopBottom 128 | 129 | // MARK: Tab Selection Indicator Width Type 130 | /// Tab selection indicator width type. 131 | public enum TabSelectionIndicatorWidthType: Int, Sendable, CaseIterable { 132 | // MARK: Cases 133 | /// Selection indicator stretches to the width of the label of tab item. 134 | case wrapped 135 | 136 | /// Selection indicator stretches to full width of the tab item, including the margins. 137 | case stretched 138 | 139 | // MARK: Properties 140 | var padsSelectionIndicator: Bool { 141 | switch self { 142 | case .wrapped: true 143 | case .stretched: false 144 | } 145 | } 146 | 147 | // MARK: Initializers 148 | /// Default value. Set to `wrapped`. 149 | public static var `default`: Self { .wrapped } 150 | } 151 | 152 | // MARK: Tab Item State Colors 153 | /// Model that contains colors for component states. 154 | public typealias TabItemStateColors = GenericStateModel_DeselectedSelectedPressedDisabled 155 | } 156 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Definite)/Page Indicator/VPageIndicatorUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VPageIndicatorUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 2/6/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Page Indicator UI Model 12 | /// Model that describes UI. 13 | @MemberwiseInitializable( 14 | accessLevelModifier: .internal, 15 | parameterDefaultValues: [ 16 | "*": .omit 17 | ] 18 | ) 19 | public struct VPageIndicatorUIModel: Sendable { 20 | // MARK: Properties - Global 21 | /// Direction. Set to `leftToRight`. 22 | public var direction: LayoutDirectionOmni = .leftToRight 23 | 24 | /// Dot spacing. 25 | /// Set to `8` on `iOS`. 26 | /// Set to `8` on `macOS`. 27 | /// Set to `10` on `tvOS`. 28 | /// Set to `4` on `watchOS`. 29 | /// Set to `10` on `visionOS`. 30 | public var spacing: CGFloat = { 31 | #if os(iOS) 32 | 8 33 | #elseif os(macOS) 34 | 8 35 | #elseif os(tvOS) 36 | 10 37 | #elseif os(watchOS) 38 | 4 39 | #elseif os(visionOS) 40 | 10 41 | #endif 42 | }() 43 | 44 | // MARK: Properties - Dot 45 | /// Dot widths, but heights for vertical layout. 46 | /// Set to `8`s on `iOS`. 47 | /// Set to `8`s on `macOS`. 48 | /// Set to `10`s on `tvOS`. 49 | /// Set to `4`s on `watchOS`. 50 | /// Set to `10`s on `visionOS`. 51 | /// 52 | /// Set to `nil`s, to make dot stretch to take available space. 53 | public var dotWidths: DotStateOptionalDimensions = { 54 | #if os(iOS) 55 | DotStateOptionalDimensions(8) 56 | #elseif os(macOS) 57 | DotStateOptionalDimensions(8) 58 | #elseif os(tvOS) 59 | DotStateOptionalDimensions(10) 60 | #elseif os(watchOS) 61 | DotStateOptionalDimensions(4) 62 | #elseif os(visionOS) 63 | DotStateOptionalDimensions(10) 64 | #endif 65 | }() 66 | 67 | /// Dot heights, but widths for vertical layout. 68 | /// Set to `8`s on `iOS`. 69 | /// Set to `8`s on `macOS`. 70 | /// Set to `10`s on `tvOS`. 71 | /// Set to `4`s on `watchOS`. 72 | /// Set to `10`s on `visionOS`. 73 | public var dotHeights: DotStateDimensions = { 74 | #if os(iOS) 75 | DotStateDimensions(8) 76 | #elseif os(macOS) 77 | DotStateDimensions(8) 78 | #elseif os(tvOS) 79 | DotStateDimensions(10) 80 | #elseif os(watchOS) 81 | DotStateDimensions(4) 82 | #elseif os(visionOS) 83 | DotStateDimensions(10) 84 | #endif 85 | }() 86 | 87 | /// Dot corner radii. 88 | /// Set to `4`s on `iOS`. 89 | /// Set to `4`s on `macOS`. 90 | /// Set to `5`s on `tvOS`. 91 | /// Set to `2`s on `watchOS`. 92 | /// Set to `5`s on `visionOS`. 93 | /// 94 | /// Applicable on when `init` without dot content is used. 95 | public var dotCornerRadii: DotStateDimensions = { 96 | #if os(iOS) 97 | DotStateDimensions(4) 98 | #elseif os(macOS) 99 | DotStateDimensions(4) 100 | #elseif os(tvOS) 101 | DotStateDimensions(5) 102 | #elseif os(watchOS) 103 | DotStateDimensions(2) 104 | #elseif os(visionOS) 105 | DotStateDimensions(5) 106 | #endif 107 | }() 108 | 109 | /// Dot colors. 110 | public var dotColors: DotStateColors = { 111 | #if os(iOS) 112 | DotStateColors( 113 | deselected: Color.dynamic(Color(190, 190, 190), Color(120, 120, 120)), 114 | selected: Color.blue 115 | ) 116 | #elseif os(macOS) 117 | DotStateColors( 118 | deselected: Color.dynamic(Color(190, 190, 190), Color(120, 120, 120)), 119 | selected: Color.blue 120 | ) 121 | #elseif os(tvOS) 122 | DotStateColors( 123 | deselected: Color.dynamic(Color(190, 190, 190), Color(120, 120, 120)), 124 | selected: Color.blue 125 | ) 126 | #elseif os(watchOS) 127 | DotStateColors( 128 | deselected: Color(120, 120, 120), 129 | selected: Color.blue 130 | ) 131 | #elseif os(visionOS) 132 | DotStateColors( 133 | deselected: Color.white.opacity(0.5), 134 | selected: Color.white 135 | ) 136 | #endif 137 | }() 138 | 139 | // MARK: Properties - Dot Border 140 | /// Dot border widths. Set to `zero` 141 | /// 142 | /// To hide border, set to `zero`. 143 | /// 144 | /// Applicable on when `init` without dot content is used. 145 | public var dotBorderWidths: DotStateDimensions = .zero 146 | 147 | /// Dot border colors. 148 | /// 149 | /// Applicable on when `init` without dot content is used. 150 | public var dotBorderColors: DotStateColors = .clearColors 151 | 152 | // MARK: Properties - Transition 153 | /// Indicates if `transition` animation is applied. Set to `true`. 154 | /// 155 | /// Changing this property conditionally will cause view state to be reset. 156 | /// 157 | /// If animation is set to `nil`, a `nil` animation is still applied. 158 | /// If this property is set to `false`, then no animation is applied. 159 | /// 160 | /// One use-case for this property is to externally mutate state using `withAnimation(_:completionCriteria:_:completion:)` function. 161 | public var appliesTransitionAnimation: Bool = true 162 | 163 | /// Transition animation. Set to `linear` with duration `0.15`. 164 | public var transitionAnimation: Animation? = .linear(duration: 0.15) 165 | 166 | // MARK: Initializers 167 | /// Initializes UI model with default values. 168 | public init() {} 169 | 170 | // MARK: Dot State Dimensions 171 | /// Model that contains dimensions for component states. 172 | public typealias DotStateDimensions = GenericStateModel_DeselectedSelected 173 | 174 | // MARK: Dot State Optional Dimensions 175 | /// Model that contains dimensions for component states. 176 | public typealias DotStateOptionalDimensions = GenericStateModel_DeselectedSelected 177 | 178 | // MARK: Dot State Colors 179 | /// Model that contains colors for component states. 180 | public typealias DotStateColors = GenericStateModel_DeselectedSelected 181 | } 182 | 183 | // MARK: - Factory 184 | extension VPageIndicatorUIModel { 185 | /// `VPageIndicatorUIModel` with vertical layout. 186 | public static var vertical: Self { 187 | var uiModel: Self = .init() 188 | 189 | uiModel.direction = .topToBottom 190 | 191 | return uiModel 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Notifications)/Toast/View+VToast.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+VToast.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 2/7/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - View + V Toast - Bool 12 | @available(macOS, unavailable) 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | extension View { 17 | /// Modal component that presents toast. 18 | /// 19 | /// @State private var isPresented: Bool = false 20 | /// 21 | /// var body: some View { 22 | /// ZStack(content: { 23 | /// VPlainButton( 24 | /// action: { isPresented = true }, 25 | /// title: "Present" 26 | /// ) 27 | /// .vToast( 28 | /// layerID: "notifications", 29 | /// id: "some_toast", 30 | /// isPresented: $isPresented, 31 | /// text: "Lorem ipsum dolor sit amet" 32 | /// ) 33 | /// }) 34 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 35 | /// .presentationHostLayer( // Or declare in `App` on a `WindowScene`-level 36 | /// id: "notifications", 37 | /// uiModel: { 38 | /// var uiModel: PresentationHostLayerUIModel = .init() 39 | /// uiModel.dimmingViewColor = Color.clear 40 | /// uiModel.dimmingViewTapAction = .passTapsThrough 41 | /// return uiModel 42 | /// }() 43 | /// ) 44 | /// } 45 | /// 46 | /// Highlights can be applied using `info`, `success`, `warning`, and `error` instances of `VToastUIModel`. 47 | public func vToast( 48 | layerID: String? = nil, 49 | id: String, 50 | uiModel: VToastUIModel = .init(), 51 | isPresented: Binding, 52 | onPresent presentHandler: (() -> Void)? = nil, 53 | onDismiss dismissHandler: (() -> Void)? = nil, 54 | text: String 55 | ) -> some View { 56 | self 57 | .presentationHost( 58 | layerID: layerID, 59 | id: id, 60 | uiModel: uiModel.presentationHostSubUIModel, 61 | isPresented: isPresented, 62 | onPresent: presentHandler, 63 | onDismiss: dismissHandler, 64 | content: { 65 | VToast( 66 | uiModel: uiModel, 67 | isPresented: isPresented, 68 | text: text 69 | ) 70 | } 71 | ) 72 | } 73 | } 74 | 75 | // MARK: - View + V Toast - Item 76 | @available(macOS, unavailable) 77 | @available(tvOS, unavailable) 78 | @available(watchOS, unavailable) 79 | @available(visionOS, unavailable) 80 | extension View { 81 | /// Modal component that presents toast. 82 | /// 83 | /// For additional info, refer to method with `Bool` presentation flag. 84 | public func vToast( 85 | layerID: String? = nil, 86 | id: String, 87 | uiModel: VToastUIModel = .init(), 88 | item: Binding, 89 | onPresent presentHandler: (() -> Void)? = nil, 90 | onDismiss dismissHandler: (() -> Void)? = nil, 91 | text: @escaping (Item) -> String 92 | ) -> some View { 93 | item.wrappedValue.map { PresentationHostDataSourceCache.shared.set(key: id, value: $0) } 94 | 95 | let isPresented: Binding = .init( 96 | get: { item.wrappedValue != nil }, 97 | set: { if !$0 { item.wrappedValue = nil } } 98 | ) 99 | 100 | return self 101 | .presentationHost( 102 | layerID: layerID, 103 | id: id, 104 | uiModel: uiModel.presentationHostSubUIModel, 105 | isPresented: isPresented, 106 | onPresent: presentHandler, 107 | onDismiss: dismissHandler, 108 | content: { 109 | VToast( 110 | uiModel: uiModel, 111 | isPresented: isPresented, 112 | text: { 113 | if let item = item.wrappedValue ?? PresentationHostDataSourceCache.shared.get(key: id) as? Item { 114 | text(item) 115 | } else { 116 | "" 117 | } 118 | }() 119 | ) 120 | } 121 | ) 122 | } 123 | } 124 | 125 | // MARK: - View + V Toast - Error 126 | @available(macOS, unavailable) 127 | @available(tvOS, unavailable) 128 | @available(watchOS, unavailable) 129 | @available(visionOS, unavailable) 130 | extension View { 131 | /// Modal component that presents toast. 132 | /// 133 | /// For additional info, refer to method with `Bool` presentation flag. 134 | public func vToast( 135 | layerID: String? = nil, 136 | id: String, 137 | uiModel: VToastUIModel = .init(), 138 | isPresented: Binding, 139 | error: E?, 140 | onPresent presentHandler: (() -> Void)? = nil, 141 | onDismiss dismissHandler: (() -> Void)? = nil, 142 | text: @escaping (E) -> String 143 | ) -> some View 144 | where E: Error 145 | { 146 | error.map { PresentationHostDataSourceCache.shared.set(key: id, value: $0) } 147 | 148 | let isPresented: Binding = .init( 149 | get: { isPresented.wrappedValue && error != nil }, 150 | set: { if !$0 { isPresented.wrappedValue = false } } 151 | ) 152 | 153 | return self 154 | .presentationHost( 155 | layerID: layerID, 156 | id: id, 157 | uiModel: uiModel.presentationHostSubUIModel, 158 | isPresented: isPresented, 159 | onPresent: presentHandler, 160 | onDismiss: dismissHandler, 161 | content: { 162 | VToast( 163 | uiModel: uiModel, 164 | isPresented: isPresented, 165 | text: { 166 | if let error = error ?? PresentationHostDataSourceCache.shared.get(key: id) as? E { 167 | text(error) 168 | } else { 169 | "" 170 | } 171 | }() 172 | ) 173 | } 174 | ) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Value Pickers/Slider/VSliderUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VSliderUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 12/21/20. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Slider UI Model 12 | /// Model that describes UI. 13 | @available(tvOS, unavailable) 14 | @available(watchOS, unavailable) 15 | @available(visionOS, unavailable) 16 | public struct VSliderUIModel: Sendable { 17 | // MARK: Properties - Global 18 | /// Direction. Set to `leftToRight`. 19 | public var direction: LayoutDirectionOmni = .leftToRight 20 | 21 | /// Slider height, but width for vertical layout. Set to `10`. 22 | public var height: CGFloat = { 23 | #if os(iOS) 24 | 10 25 | #elseif os(macOS) 26 | 10 27 | #else 28 | fatalError() // Not supported 29 | #endif 30 | }() 31 | 32 | // MARK: Properties - Corners 33 | /// Slider corner radius. Set to `5`. 34 | public var cornerRadius: CGFloat = { 35 | #if os(iOS) 36 | 5 37 | #elseif os(macOS) 38 | 5 39 | #else 40 | fatalError() // Not supported 41 | #endif 42 | }() 43 | 44 | // MARK: Properties - Body 45 | /// Indicates if body is draggable. 46 | /// Set to `false` on `iOS`. 47 | /// Set to `true` on `macOS`. 48 | /// 49 | /// Changing this property conditionally will cause view state to be reset. 50 | public var bodyIsDraggable: Bool = { 51 | #if os(iOS) 52 | false 53 | #elseif os(macOS) 54 | true 55 | #else 56 | fatalError() // Not supported 57 | #endif 58 | }() 59 | 60 | // MARK: Properties - Track 61 | /// Slider track colors. 62 | public var trackColors: StateColors = { 63 | #if os(iOS) 64 | StateColors( 65 | enabled: Color.dynamic(Color(230, 230, 230), Color(45, 45, 45)), 66 | disabled: Color.dynamic(Color(245, 245, 245), Color(35, 35, 35)) 67 | ) 68 | #elseif os(macOS) 69 | StateColors( 70 | enabled: Color.dynamic(Color.black.opacity(0.05), Color.white.opacity(0.125)), 71 | disabled: Color.dynamic(Color.black.opacity(0.03), Color.white.opacity(0.075)) 72 | ) 73 | #else 74 | fatalError() // Not supported 75 | #endif 76 | }() 77 | 78 | // MARK: Properties - Progress 79 | /// Slider progress colors. 80 | public var progressColors: StateColors = .init( 81 | enabled: Color.blue, 82 | disabled: Color.blue.opacity(0.3) 83 | ) 84 | 85 | /// Indicates if slider bar rounds progress view trailing corners. Set to `true`. 86 | public var roundsProgressViewTrailingCorners: Bool = true 87 | 88 | // MARK: Properties - Border 89 | /// Border width. 90 | /// Set to `0` point on `iOS`. 91 | /// Set to `1` pixel on `macOS`. 92 | /// 93 | /// To hide border, set to `0`. 94 | public var borderWidth: PointPixelMeasurement = { 95 | #if os(iOS) 96 | PointPixelMeasurement.points(0) 97 | #elseif os(macOS) 98 | PointPixelMeasurement.pixels(1) 99 | #else 100 | fatalError() // Not supported 101 | #endif 102 | }() 103 | 104 | /// Border colors. 105 | public var borderColors: StateColors = { 106 | #if os(iOS) 107 | StateColors(Color.clear) 108 | #elseif os(macOS) 109 | StateColors( 110 | enabled: Color.dynamic(Color.black.opacity(0.125), Color.clear), 111 | disabled: Color.dynamic(Color.black.opacity(0.05), Color.clear) 112 | ) 113 | #else 114 | fatalError() // Not supported 115 | #endif 116 | }() 117 | 118 | // MARK: Properties - Thumb 119 | /// Thumb size. Set to `(20, 20)`. 120 | /// 121 | /// To hide thumb, set to `0`. 122 | public var thumbSize: CGSize = .init(dimension: 20) 123 | 124 | /// Thumb corner radius. Set to `10`. 125 | public var thumbCornerRadius: CGFloat = 10 126 | 127 | /// Thumb colors. 128 | public var thumbColors: StateColors = .init(Color.white) 129 | 130 | // MARK: Properties - Thumb Border 131 | /// Thumb border widths. 132 | /// Set to `0` point on `iOS`. 133 | /// Set to `1` pixel on `macOS`. 134 | /// 135 | /// To hide border, set to `0`. 136 | public var thumbBorderWidth: PointPixelMeasurement = { 137 | #if os(iOS) 138 | PointPixelMeasurement.points(0) 139 | #elseif os(macOS) 140 | PointPixelMeasurement.pixels(1) 141 | #else 142 | fatalError() // Not supported 143 | #endif 144 | }() 145 | 146 | /// Thumb border colors. 147 | public var thumbBorderColors: StateColors = { 148 | #if os(iOS) 149 | StateColors.clearColors 150 | #elseif os(macOS) 151 | StateColors( 152 | enabled: Color.dynamic(Color(200, 200, 200), Color(100, 100, 100)), 153 | disabled: Color.dynamic(Color(230, 230, 230), Color(70, 70, 70)) 154 | ) 155 | #else 156 | fatalError() // Not supported 157 | #endif 158 | }() 159 | 160 | // MARK: Properties - Thumb Shadow 161 | /// Thumb shadow colors. 162 | public var thumbShadowColors: StateColors = .init( 163 | enabled: Color(100, 100, 100, 0.5), 164 | disabled: Color(100, 100, 100, 0.2) 165 | ) 166 | 167 | /// Thumb shadow radius. 168 | /// Set to `2` on `iOS`. 169 | /// Set to `1` on `macOS`. 170 | public var thumbShadowRadius: CGFloat = { 171 | #if os(iOS) 172 | 2 173 | #elseif os(macOS) 174 | 1 175 | #else 176 | fatalError() // Not supported 177 | #endif 178 | }() 179 | 180 | /// Thumb shadow offset. 181 | /// Set to `(0, 2)` on `iOS`. 182 | /// Set to `(0, 1)` on `macOS`. 183 | public var thumbShadowOffset: CGPoint = { 184 | #if os(iOS) 185 | CGPoint(x: 0, y: 2) 186 | #elseif os(macOS) 187 | CGPoint(x: 0, y: 1) 188 | #else 189 | fatalError() // Not supported 190 | #endif 191 | }() 192 | 193 | // MARK: Properties - Transition - Progress 194 | /// Indicates if `progress` animation is applied. Set to `true`. 195 | /// 196 | /// Changing this property conditionally will cause view state to be reset. 197 | /// 198 | /// If animation is set to `nil`, a `nil` animation is still applied. 199 | /// If this property is set to `false`, then no animation is applied. 200 | /// 201 | /// One use-case for this property is to externally mutate state using `withAnimation(_:completionCriteria:_:completion:)` function. 202 | public var appliesProgressAnimation: Bool = true 203 | 204 | /// Progress animation. Set to `nil`. 205 | public var progressAnimation: Animation? 206 | 207 | // MARK: Initializers 208 | /// Initializes UI model with default values. 209 | public init() {} 210 | 211 | // MARK: State Colors 212 | /// Model that contains colors for component states. 213 | public typealias StateColors = GenericStateModel_EnabledDisabled 214 | } 215 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/zMisc/Rolling Counter/VRollingCounterUIModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VRollingCounterUIModel.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 25.08.23. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Rolling Counter UI Model 12 | /// Model that describes UI. 13 | public struct VRollingCounterUIModel: Sendable { 14 | // MARK: Properties - Global 15 | /// Spacing between the components. Set to `0`. 16 | public var spacing: CGFloat = 0 17 | 18 | /// Vertical alignment of components in horizontal layout. Set to `center`. 19 | /// 20 | /// Baselines may result in janky animations. 21 | public var verticalAlignment: VerticalAlignment = .center 22 | 23 | // MARK: Properties - Digit Text 24 | /// Digit text color. 25 | public var digitTextColor: Color = .primary 26 | 27 | /// Digit text font. Set to `bold` `body`. 28 | public var digitTextFont: Font = .body.bold() 29 | 30 | /// Digit text `DynamicTypeSize` type. Set to `nil`. 31 | /// 32 | /// Changing this property conditionally will cause view state to be reset. 33 | public var digitTextDynamicTypeSizeType: DynamicTypeSizeType? 34 | 35 | /// Digit text margins. Set to `zero`. 36 | public var digitTextMargins: Margins = .zero 37 | 38 | /// Digit text `Y` offset relative to other components. Set to `0`. 39 | public var digitTextOffsetY: CGFloat = 0 40 | 41 | /// Digit text increment rolling edge. Set to `bottom`. 42 | public var digitTextIncrementRollingEdge: VerticalEdge? = .bottom 43 | 44 | /// Digit text decrement rolling edge. Set to `top`. 45 | public var digitTextDecrementRollingEdge: VerticalEdge? = .top 46 | 47 | // MARK: Properties - Fraction Digits 48 | /// Indicates of counter has fraction digits. Set to `true`. 49 | public var hasFractionDigits: Bool = true 50 | 51 | /// Minimum number of fraction digits. Set to `2`. 52 | /// 53 | /// To hide fractions, set `hasFractionDigits` to `false`. 54 | public var minFractionDigits: Int = 2 55 | 56 | /// Maximum number of fraction digits. Set to `2`. 57 | /// 58 | /// To hide fractions, set `hasFractionDigits` to `false`. 59 | public var maxFractionDigits: Int = 2 60 | 61 | /// Fraction digit text color. 62 | public var fractionDigitTextColor: Color = .primary 63 | 64 | /// Digit text font. Set to `bold` `body`. 65 | public var fractionDigitTextFont: Font = .body.bold() 66 | 67 | /// Fraction digit text `DynamicTypeSize` type. Set to `nil`. 68 | /// 69 | /// Changing this property conditionally will cause view state to be reset. 70 | public var fractionDigitTextDynamicTypeSizeType: DynamicTypeSizeType? 71 | 72 | /// Fraction digit text margins. Set to `zero`. 73 | public var fractionDigitTextMargins: Margins = .zero 74 | 75 | /// Fraction digit text `Y` offset relative to other components. Set to `0`. 76 | public var fractionDigitTextOffsetY: CGFloat = 0 77 | 78 | /// Fraction digit text increment rolling edge. Set to `bottom`. 79 | public var fractionDigitTextIncrementRollingEdge: VerticalEdge? = .bottom 80 | 81 | /// Fraction digit text decrement rolling edge. Set to `top`. 82 | public var fractionDigitTextDecrementRollingEdge: VerticalEdge? = .top 83 | 84 | // MARK: Properties - Grouping Separator 85 | /// Indicates if counter has grouping separator. Set to `true`. 86 | public var hasGroupingSeparator: Bool = true 87 | 88 | /// Grouping separator. Set to comma. 89 | /// 90 | /// To hide grouping separator, set `hasGroupingSeparator` to `false`. 91 | public var groupingSeparator: String = "," 92 | 93 | /// Grouping separator text color. 94 | public var groupingSeparatorTextColor: Color = .primary 95 | 96 | /// Grouping separator text font. Set to `bold` `body`. 97 | public var groupingSeparatorTextFont: Font = .body.bold() 98 | 99 | /// Grouping separator text `DynamicTypeSize` type. Set to `nil`. 100 | /// 101 | /// Changing this property conditionally will cause view state to be reset. 102 | public var groupingSeparatorTextDynamicTypeSizeType: DynamicTypeSizeType? 103 | 104 | /// Grouping separator text margins. Set to `zero`. 105 | public var groupingSeparatorTextMargins: Margins = .zero 106 | 107 | /// Grouping separator text `Y` offset relative to other components. Set to `0`. 108 | public var groupingSeparatorTextOffsetY: CGFloat = 0 109 | 110 | // MARK: Properties - Decimal Separator 111 | /// Decimal separator. Set to dot. 112 | public var decimalSeparator: String = "." 113 | 114 | /// Decimal separator text color. 115 | public var decimalSeparatorTextColor: Color = .primary 116 | 117 | /// Decimal separator text font. Set to `bold` `body`. 118 | public var decimalSeparatorTextFont: Font = .body.bold() 119 | 120 | /// Decimal separator text `DynamicTypeSize` type. Set to `nil`. 121 | /// 122 | /// Changing this property conditionally will cause view state to be reset. 123 | public var decimalSeparatorTextDynamicTypeSizeType: DynamicTypeSizeType? 124 | 125 | /// Decimal separator text margins. Set to `zero`. 126 | public var decimalSeparatorTextMargins: Margins = .zero 127 | 128 | /// Decimal separator text `Y` offset relative to other components. Set to `0`. 129 | public var decimalSeparatorTextOffsetY: CGFloat = 0 130 | 131 | // MARK: Properties - Highlight 132 | /// Indicates if grouping separator text is highlightable. Set to `false`. 133 | public var groupingSeparatorTextIsHighlightable: Bool = false 134 | 135 | /// Indicates if decimal separator text is highlightable. Set to `false`. 136 | public var decimalSeparatorTextIsHighlightable: Bool = false 137 | 138 | /// Indicate if only the affected characters are highlighted. Set to `true`. 139 | public var highlightsOnlyTheAffectedCharacters: Bool = true 140 | 141 | /// Increment highlight color. 142 | /// 143 | /// To hide highlight, set to `nil`. 144 | public var incrementHighlightColor: Color? = .green 145 | 146 | /// Decrement highlight color. 147 | /// 148 | /// To hide highlight, set to `nil`. 149 | public var decrementHighlightColor: Color? = .red 150 | 151 | // MARK: Properties - Transition - Highlight/Dehighlight 152 | /// Highlight and rolling animation. Set to `easeOut` with duration `0.25`. 153 | public var highlightAnimation: BasicAnimation? = .init(curve: .easeOut, duration: 0.25) 154 | 155 | /// Dehighlight animation. Set to `easeOut` with duration `0.25`. 156 | public var dehighlightAnimation: BasicAnimation? = .init(curve: .easeOut, duration: 0.25) 157 | 158 | // MARK: Initializers 159 | /// Initializes UI model with default values. 160 | public init() {} 161 | 162 | /// Model that contains `leading`, `trailing`, `top`, and `bottom` margins. 163 | public typealias Margins = EdgeInsets_LeadingTrailingTopBottom 164 | } 165 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Indicators (Definite)/Progress Bar/VProgressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VProgressBar.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/12/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - V Progress Bar 12 | /// Indicator component that represents progress towards the completion of a task. 13 | /// 14 | /// @State private var progress: Double = 0.5 15 | /// 16 | /// var body: some View { 17 | /// VProgressBar(value: progress) 18 | /// .padding() 19 | /// } 20 | /// 21 | public struct VProgressBar: View, Sendable { 22 | // MARK: Properties - UI Model 23 | private let uiModel: VProgressBarUIModel 24 | 25 | @Environment(\.displayScale) private var displayScale: CGFloat 26 | 27 | @State private var progressBarSize: CGSize = .zero 28 | 29 | // MARK: Properties - Data 30 | private let range: ClosedRange 31 | private let value: Double 32 | 33 | // MARK: Initializers 34 | /// Initializes `VProgressBar` with value. 35 | public init( 36 | uiModel: VProgressBarUIModel = .init(), 37 | total: V = 1, 38 | value: V 39 | ) 40 | where V: BinaryFloatingPoint 41 | { 42 | self.uiModel = uiModel 43 | self.range = 0...Double(total) 44 | self.value = { 45 | let value: Double = .init(value) 46 | let min: Double = 0 47 | let max: Double = .init(total) 48 | 49 | return value.clamped(min: min, max: max) 50 | }() 51 | } 52 | 53 | // MARK: Body 54 | public var body: some View { 55 | ZStack(alignment: uiModel.direction.toAlignment, content: { 56 | trackView 57 | progressView 58 | borderView 59 | }) 60 | .clipShape(.rect(cornerRadius: uiModel.cornerRadius)) 61 | .frame( 62 | width: uiModel.direction.isHorizontal ? nil : uiModel.height, 63 | height: uiModel.direction.isHorizontal ? uiModel.height : nil 64 | ) 65 | .getSize({ progressBarSize = $0 }) 66 | .applyIf(uiModel.appliesProgressAnimation, transform: { 67 | $0.animation(uiModel.progressAnimation, value: value) 68 | }) 69 | } 70 | 71 | private var trackView: some View { 72 | Rectangle() 73 | .foregroundStyle(uiModel.trackColor) 74 | } 75 | 76 | private var progressView: some View { 77 | UnevenRoundedRectangle( 78 | cornerRadii: RectangleCornerRadii( 79 | trailingCorners: uiModel.roundsProgressViewTrailingCorners ? uiModel.cornerRadius : 0 80 | ) 81 | .cornersAdjustedForDirection(uiModel.direction) 82 | ) 83 | .frame( 84 | width: uiModel.direction.isHorizontal ? progressWidth : nil, 85 | height: uiModel.direction.isHorizontal ? nil : progressWidth 86 | ) 87 | .foregroundStyle(uiModel.progressColor) 88 | } 89 | 90 | @ViewBuilder 91 | private var borderView: some View { 92 | let borderWidth: CGFloat = uiModel.borderWidth.toPoints(scale: displayScale) 93 | 94 | if borderWidth > 0 { 95 | RoundedRectangle(cornerRadius: uiModel.cornerRadius) 96 | .strokeBorder(uiModel.borderColor, lineWidth: borderWidth) 97 | } 98 | } 99 | 100 | // MARK: Progress Width 101 | private var progressWidth: CGFloat { 102 | let width: CGFloat = progressBarSize.dimension(isWidth: uiModel.direction.isHorizontal) 103 | 104 | return value * width 105 | } 106 | } 107 | 108 | // MARK: - Preview 109 | #if DEBUG 110 | 111 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 112 | #Preview("*", body: { 113 | @Previewable @State var value: Double = 0 114 | 115 | PreviewContainer(content: { 116 | VProgressBar(value: value) 117 | .padding(.horizontal) 118 | }) 119 | .onReceiveOfTimerIncrement($value, to: 1, by: 0.1) 120 | }) 121 | 122 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 123 | #Preview("States", body: { 124 | @Previewable @State var value: Double = 0 125 | 126 | PreviewContainer(content: { 127 | PreviewRow(nil, content: { 128 | VProgressBar(value: value) 129 | .padding(.horizontal) 130 | }) 131 | 132 | PreviewHeader("Native") 133 | 134 | PreviewRow(nil, content: { 135 | ProgressView(value: value) 136 | .progressViewStyle(.linear) 137 | .padding(.horizontal) 138 | }) 139 | }) 140 | .onReceiveOfTimerIncrement($value, to: 1, by: 0.1) 141 | }) 142 | 143 | @available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) 144 | #Preview("Layout Directions", body: { 145 | @Previewable @State var value: Double = 0 // '@Previewable' items must be at the beginning of the preview block 146 | 147 | let length: CGFloat = { 148 | #if os(iOS) 149 | 250 150 | #elseif os(macOS) 151 | 200 152 | #elseif os(tvOS) 153 | 250 154 | #elseif os(watchOS) 155 | 100 156 | #elseif os(visionOS) 157 | 250 158 | #endif 159 | }() 160 | 161 | PreviewContainer(content: { 162 | PreviewRow("Left-to-Right", content: { 163 | VProgressBar( 164 | uiModel: { 165 | var uiModel: VProgressBarUIModel = .init() 166 | uiModel.direction = .leftToRight 167 | return uiModel 168 | }(), 169 | value: value 170 | ) 171 | .frame(width: length) 172 | .padding(.horizontal) 173 | }) 174 | 175 | PreviewRow("Right-to-Left", content: { 176 | VProgressBar( 177 | uiModel: { 178 | var uiModel: VProgressBarUIModel = .init() 179 | uiModel.direction = .rightToLeft 180 | return uiModel 181 | }(), 182 | value: value 183 | ) 184 | .frame(width: length) 185 | .padding(.horizontal) 186 | }) 187 | 188 | HStack(spacing: 20, content: { 189 | PreviewRow("Top-to-Bottom", content: { 190 | VProgressBar( 191 | uiModel: { 192 | var uiModel: VProgressBarUIModel = .init() 193 | uiModel.direction = .topToBottom 194 | return uiModel 195 | }(), 196 | value: value 197 | ) 198 | .frame(height: length) 199 | .padding(.horizontal) 200 | }) 201 | 202 | PreviewRow("Bottom-to-Top", content: { 203 | VProgressBar( 204 | uiModel: { 205 | var uiModel: VProgressBarUIModel = .init() 206 | uiModel.direction = .bottomToTop 207 | return uiModel 208 | }(), 209 | value: value 210 | ) 211 | .frame(height: length) 212 | .padding(.horizontal) 213 | }) 214 | }) 215 | }) 216 | .onReceiveOfTimerIncrement($value, to: 1, by: 0.1) 217 | }) 218 | 219 | #endif 220 | -------------------------------------------------------------------------------- /Sources/VComponents/Components/Modals (Containers)/Modal/View+VModal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+VModal.swift 3 | // VComponents 4 | // 5 | // Created by Vakhtang Kontridze on 1/13/21. 6 | // 7 | 8 | import SwiftUI 9 | import VCore 10 | 11 | // MARK: - View + V Modal - Bool 12 | @available(watchOS, unavailable) 13 | extension View { 14 | /// Modal component that hosts slide-able content on the edge of the container. 15 | /// 16 | /// @State private var isPresented: Bool = false 17 | /// 18 | /// var body: some View { 19 | /// ZStack(content: { 20 | /// VPlainButton( 21 | /// action: { isPresented = true }, 22 | /// title: "Present" 23 | /// ) 24 | /// .vModal( 25 | /// id: "some_modal", 26 | /// isPresented: $isPresented, 27 | /// content: { Color.blue } 28 | /// ) 29 | /// }) 30 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 31 | /// .presentationHostLayer() // Or declare in `App` on a `WindowScene`-level 32 | /// } 33 | /// 34 | /// Modal can also wrap navigation system. 35 | /// 36 | /// @State private var isPresented: Bool = false 37 | /// @State private var modalDidAppear: Bool = false 38 | /// 39 | /// var body: some View { 40 | /// ZStack(content: { 41 | /// VPlainButton( 42 | /// action: { isPresented = true }, 43 | /// title: "Present" 44 | /// ) 45 | /// .vModal( 46 | /// id: "test", 47 | /// isPresented: $isPresented, 48 | /// onPresent: { modalDidAppear = true }, 49 | /// onDismiss: { modalDidAppear = false }, 50 | /// content: { 51 | /// NavigationStack(root: { 52 | /// HomeView(isPresented: $isPresented) 53 | /// // Disables possible `NavigationStack` animations 54 | /// .transaction({ 55 | /// if !modalDidAppear { $0.animation = nil } 56 | /// }) 57 | /// }) 58 | /// } 59 | /// ) 60 | /// }) 61 | /// .frame(maxWidth: .infinity, maxHeight: .infinity) 62 | /// .presentationHostLayer() // Or declare in `App` on a `WindowScene`-level 63 | /// } 64 | /// 65 | /// struct HomeView: View { 66 | /// @Binding private var isPresented: Bool 67 | /// 68 | /// init(isPresented: Binding) { 69 | /// self._isPresented = isPresented 70 | /// } 71 | /// 72 | /// var body: some View { 73 | /// NavigationLink( 74 | /// "To Destination", 75 | /// destination: { DestinationView(isPresented: $isPresented) } 76 | /// ) 77 | /// .inlineNavigationTitle("Home") 78 | /// .toolbar(content: { 79 | /// VPlainButton( 80 | /// action: { isPresented = false }, 81 | /// title: "Dismiss" 82 | /// ) 83 | /// }) 84 | /// } 85 | /// } 86 | /// 87 | /// struct DestinationView: View { 88 | /// @Environment(\.dismiss) private var dismissAction: DismissAction 89 | /// 90 | /// @Binding private var isPresented: Bool 91 | /// 92 | /// init(isPresented: Binding) { 93 | /// self._isPresented = isPresented 94 | /// } 95 | /// 96 | /// var body: some View { 97 | /// VPlainButton( 98 | /// action: { dismissAction.callAsFunction() }, 99 | /// title: "To Home" 100 | /// ) 101 | /// .inlineNavigationTitle("Destination") 102 | /// .toolbar(content: { 103 | /// VPlainButton( 104 | /// action: { isPresented = false }, 105 | /// title: "Dismiss" 106 | /// ) 107 | /// }) 108 | /// } 109 | /// } 110 | /// 111 | public func vModal( 112 | layerID: String? = nil, 113 | id: String, 114 | uiModel: VModalUIModel = .init(), 115 | isPresented: Binding, 116 | onPresent presentHandler: (() -> Void)? = nil, 117 | onDismiss dismissHandler: (() -> Void)? = nil, 118 | @ViewBuilder content: @escaping () -> Content 119 | ) -> some View 120 | where Content: View 121 | { 122 | self 123 | .presentationHost( 124 | layerID: layerID, 125 | id: id, 126 | uiModel: uiModel.presentationHostSubUIModel, 127 | isPresented: isPresented, 128 | onPresent: presentHandler, 129 | onDismiss: dismissHandler, 130 | content: { 131 | VModal( 132 | uiModel: uiModel, 133 | isPresented: isPresented, 134 | content: content 135 | ) 136 | } 137 | ) 138 | } 139 | } 140 | 141 | // MARK: - View + V Modal - Item 142 | @available(watchOS, unavailable) 143 | extension View { 144 | /// Modal component that hosts slide-able content on the edge of the container. 145 | /// 146 | /// For additional info, refer to method with `Bool` presentation flag. 147 | public func vModal( 148 | layerID: String? = nil, 149 | id: String, 150 | uiModel: VModalUIModel = .init(), 151 | item: Binding, 152 | onPresent presentHandler: (() -> Void)? = nil, 153 | onDismiss dismissHandler: (() -> Void)? = nil, 154 | @ViewBuilder content: @escaping (Item) -> Content 155 | ) -> some View 156 | where Content: View 157 | { 158 | item.wrappedValue.map { PresentationHostDataSourceCache.shared.set(key: id, value: $0) } 159 | 160 | let isPresented: Binding = .init( 161 | get: { item.wrappedValue != nil }, 162 | set: { if !$0 { item.wrappedValue = nil } } 163 | ) 164 | 165 | return self 166 | .presentationHost( 167 | layerID: layerID, 168 | id: id, 169 | uiModel: uiModel.presentationHostSubUIModel, 170 | isPresented: isPresented, 171 | onPresent: presentHandler, 172 | onDismiss: dismissHandler, 173 | content: { 174 | VModal( 175 | uiModel: uiModel, 176 | isPresented: isPresented, 177 | content: { 178 | if let item = item.wrappedValue ?? PresentationHostDataSourceCache.shared.get(key: id) as? Item { 179 | content(item) 180 | } 181 | } 182 | ) 183 | } 184 | ) 185 | } 186 | } 187 | --------------------------------------------------------------------------------