├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Sources/VComponents/Resources/Images.xcassets/XMark.imageset/XMark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sources/VComponents/Resources/Images.xcassets/MagnifyGlass.imageset/MagnifyGlass.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sources/VComponents/Resources/Images.xcassets/Visibility.Off.imageset/Visibility.Off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Sources/VComponents/Resources/Images.xcassets/Checkmark.On.imageset/Checkmark.On.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------