├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── MaterialUIKit
├── Color Scheme
├── MUIKitColorScheme.swift
└── MaterialClassic.swift
├── Components
├── Dialog And Alerts
│ ├── Dialog.swift
│ ├── DialogSheet.swift
│ └── Snackbar.swift
├── Form Controls
│ ├── Checkbox.swift
│ ├── DateSelector.swift
│ ├── RadioButtonGroup.swift
│ ├── SecureTextBox.swift
│ ├── Switch.swift
│ ├── TextBox.swift
│ └── TimeSelector.swift
├── Interactive Elements
│ ├── ActionButton.swift
│ ├── DropdownMenu.swift
│ ├── DropdownMenuLabel.swift
│ ├── FAB.swift
│ └── IconButton.swift
├── Navigation And Structure
│ ├── Collection.swift
│ ├── Container.swift
│ ├── NavigationContainer.swift
│ ├── NavigationRoute.swift
│ ├── NavigationRouteLabel.swift
│ ├── SegmentedButton.swift
│ └── TabBar.swift
├── Presentation
│ ├── ProgressBar.swift
│ └── Separator.swift
└── Search
│ └── SearchBox.swift
├── Configuration
├── MUIKitConfiguration.swift
└── MaterialUIKit.swift
├── Environment Keys
├── CornerRadiusKey.swift
├── FontKey.swift
├── FontWeightKey.swift
└── FrameSizeKey.swift
├── Extensions
├── Internal
│ ├── Color+UIColors.swift
│ ├── Date.swift
│ ├── StyledBackgroundModifiers.swift
│ ├── UINavigationController.swift
│ ├── View+FallbackModifiers.swift
│ └── ViewModifiers.swift
└── Public
│ ├── Color+MUIColorScheme.swift
│ └── ShapeStyle.swift
└── Styles & Types
├── ActionButtonAnimationStyle.swift
├── ActionButtonStyle.swift
├── CheckboxStyle.swift
├── CollectionStyle.swift
├── IconButtonStyle.swift
├── NavigationContainerHeaderStyle.swift
├── SeparatorOrientationStyle.swift
├── SwitchStyle.swift
└── TabBarItem.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Aum Chauhan
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 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "MaterialUIKit",
8 | platforms: [.iOS(.v15)],
9 | products: [
10 | .library(
11 | name: "MaterialUIKit",
12 | targets: ["MaterialUIKit"]
13 | )
14 | ],
15 | targets: [
16 | .target(name: "MaterialUIKit")
17 | ]
18 | )
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # MaterialUIKit
4 |
5 | SwiftUI components inspired by Material Design.
6 |
7 | ## Overview
8 |
9 | MaterialUIKit is a SwiftUI package offering a range of UI components inspired by Material Design. It includes dialogs, form controls, navigation, and more, with flexible configuration options for customizing component appearance and behavior.
10 |
11 | ## Requirements
12 |
13 | Compatible with iOS 15.0 and later.
14 |
15 | ## Documentation
16 |
17 | For comprehensive information on each component, including usage examples and customization options, visit the MaterialUIKit [documentation](https://swift-packages.gitbook.io/materialuikit/).
18 |
19 | ## Demonstration
20 |
21 | For a practical demonstration and example usage of MaterialUIKit components, check out the [Sample Project](https://github.com/aumChauhan/MaterialUIKit_SampleProject).
22 |
23 | ## Communication and Contribution
24 |
25 | For bug reports, feature requests, or contributions, please open an issue or submit a pull request. All contributions are welcome and appreciated.
26 |
27 | ## License
28 |
29 | MaterialUIKit is released under the [MIT License](LICENSE).
30 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Color Scheme/MUIKitColorScheme.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MUIKitColorScheme.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// Definines the color scheme for MaterialUIKit components.
12 | @available(iOS 15.0, *)
13 | public protocol MUIKitColorScheme {
14 |
15 | /// The accent color used for emphasis.
16 | var accent: Color { get set }
17 |
18 | /// The tonal color used for tonal variations.
19 | var tonal: Color { get set }
20 |
21 | /// The color used for indicating errors and alerts.
22 | var onError: Color { get set }
23 |
24 | /// The primary background color.
25 | var primaryBackground: Color { get set }
26 |
27 | /// The secondary background color.
28 | var secondaryBackground: Color { get set }
29 |
30 | /// The tertiary background color.
31 | var tertiaryBackground: Color { get set }
32 |
33 | /// The color used for highlighting elements.
34 | var highlight: Color { get set }
35 |
36 | /// The primary title color.
37 | var primaryTitle: Color { get set }
38 |
39 | /// The secondary title color.
40 | var secondaryTitle: Color { get set }
41 |
42 | /// The color used for dividers and separator.
43 | var separator: Color { get set }
44 |
45 | /// The color used for outlines and stroke.
46 | var outline: Color { get set }
47 |
48 | /// The color used for representing disabled or inactive elements in the UI.
49 | var onDisabled: Color { get set }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Color Scheme/MaterialClassic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MaterialClassic.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 06/07/24
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// The `MaterialClassic` provides a predefined color scheme with a classic appearance.
12 | package struct MaterialClassic: MUIKitColorScheme {
13 | package var accent = Color(lightHex: "246488", darkHex: "94CDF8")
14 |
15 | package var tonal = Color(lightHex: "EDF2F6", darkHex: "202529")
16 |
17 | package var primaryBackground = Color(lightHex: "f8f9fa", darkHex: "212529")
18 |
19 | package var secondaryBackground = Color(lightHex: "e9ecef", darkHex: "343a40")
20 |
21 | package var tertiaryBackground = Color(lightHex: "dee2e6", darkHex: "495057")
22 |
23 | package var primaryTitle = Color(lightHex: "343a40", darkHex: "e9ecef")
24 |
25 | package var secondaryTitle = Color(lightHex: "6c757d", darkHex: "ced4da")
26 |
27 | package var highlight = Color(lightHex: "246488", darkHex: "94CDF8")
28 |
29 | package var separator: Color = Color(lightHex: "ced4da", darkHex: "6c757d").opacity(0.8)
30 |
31 | package var onError: Color = Color(lightHex: "8b575c", darkHex: "c98986")
32 |
33 | package var onDisabled: Color = Color(lightHex: "33415c", darkHex: "7d8597").opacity(0.7)
34 |
35 | package var outline: Color = Color(lightHex: "505050", darkHex: "7d8597").opacity(0.7)
36 | }
37 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Dialog And Alerts/Dialog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Dialog.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: - PUBLIC
12 |
13 | extension View {
14 |
15 | /// Presents a dialog box dialog with a title and message.
16 | ///
17 | /// - Parameters:
18 | /// - isPresented: A binding to a Boolean value that determines whether to present the alert.
19 | /// - titleKey: The title of the alert.
20 | /// - message: The message displayed in the alert.
21 | @available(iOS 15.0, *)
22 | public func dialog(
23 | isPresented: Binding,
24 | titleKey: String,
25 | message: String
26 | ) -> some View {
27 | self.dialog(
28 | isPresented: isPresented,
29 | titleKey: titleKey,
30 | message: message,
31 | primaryActionKey: "Cancel",
32 | primaryAction: {}
33 | )
34 | }
35 |
36 | /// Presents a dialog box with a title, message, and primary button.
37 | ///
38 | /// - Parameters:
39 | /// - isPresented: A binding to a Boolean value that determines whether to present the alert.
40 | /// - titleKey: The title of the alert.
41 | /// - message: The message displayed in the alert.
42 | /// - primaryActionKey: The title key of the primary button.
43 | /// - primaryAction: The action to perform when the primary button is tapped.
44 | @available(iOS 15.0, *)
45 | public func dialog(
46 | isPresented: Binding,
47 | titleKey: String,
48 | message: String,
49 | primaryActionKey: String,
50 | primaryAction: @escaping () -> Void
51 | ) -> some View {
52 | self.dialog(
53 | isPresented: isPresented,
54 | titleKey: titleKey,
55 | message: message,
56 | primaryActionKey: primaryActionKey,
57 | primaryAction: primaryAction,
58 | secondaryActionKey: nil,
59 | secondaryAction: nil
60 | )
61 | }
62 |
63 | /// Presents a dialog box with a title, message, primary and secondary buttons.
64 | ///
65 | /// - Parameters:
66 | /// - isPresented: A binding to a Boolean value that determines whether to present the alert.
67 | /// - titleKey: The title of the alert.
68 | /// - message: The message displayed in the alert.
69 | /// - primaryActionKey: The title key of the primary button.
70 | /// - primaryAction: The action to perform when the primary button is tapped.
71 | /// - secondaryActionKey: The title key of the secondary button.
72 | /// - secondaryAction: The action to perform when the secondary button is tapped.
73 | @available(iOS 15.0, *)
74 | public func dialog(
75 | isPresented: Binding,
76 | titleKey: String,
77 | message: String,
78 | primaryActionKey: String,
79 | primaryAction: @escaping () -> Void,
80 | secondaryActionKey: String?,
81 | secondaryAction: (() -> Void)?
82 | ) -> some View {
83 | self.modifier(
84 | DialogViewModifier(
85 | isPresented: isPresented,
86 | titleKey: titleKey,
87 | message: message,
88 | primaryActionKey: primaryActionKey,
89 | primaryAction: primaryAction,
90 | secondaryActionKey: secondaryActionKey,
91 | secondaryAction: secondaryAction
92 | )
93 | )
94 | }
95 | }
96 |
97 | // MARK: - FILE PRIVATE
98 |
99 | /// Adds dialog sheet over the current view.
100 | fileprivate struct DialogViewModifier: ViewModifier {
101 |
102 | // MARK: - PROPERTIES
103 |
104 | @Binding var isPresented: Bool
105 |
106 | let titleKey: String
107 | let message: String?
108 |
109 | let primaryActionKey: String
110 | let primaryAction: () -> Void
111 |
112 | let secondaryActionKey: String?
113 | let secondaryAction: (() -> Void)?
114 |
115 | // MARK: - BODY
116 |
117 | func body(content: Content) -> some View {
118 | content.overlay(
119 | Dialog(
120 | isPresented: $isPresented,
121 | titleKey: titleKey,
122 | message: message,
123 | primaryActionKey: primaryActionKey,
124 | primaryAction: primaryAction,
125 | secondaryActionKey: secondaryActionKey,
126 | secondaryAction: secondaryAction
127 | )
128 | )
129 | }
130 | }
131 |
132 | /// Represents a Material Design styled dialog box.
133 | fileprivate struct Dialog: View {
134 |
135 | // MARK: - PROPERTIES
136 |
137 | @Binding var isPresented: Bool
138 | @State private var animationFlag: Bool = false
139 |
140 | let titleKey: String
141 | let message: String?
142 |
143 | let primaryActionKey: String
144 | let primaryAction: () -> Void
145 |
146 | let secondaryActionKey: String?
147 | let secondaryAction: (() -> Void)?
148 |
149 | // MARK: - VIEW BODY
150 |
151 | var body: some View {
152 | VStack(alignment: .leading, spacing: MaterialUIKit.configuration.verticalStackSpacing) {
153 | Text(titleKey)
154 | .font(MaterialUIKit.configuration.h2)
155 | .fontWeightWithFallback(.semibold)
156 | .foregroundStyle(.materialUIPrimaryTitle)
157 | .lineLimit(1)
158 |
159 | if let message {
160 | Text(message)
161 | .font(MaterialUIKit.configuration.h4)
162 | .foregroundStyle(.materialUISecondaryTitle)
163 | .multilineTextAlignment(.leading)
164 | }
165 |
166 | HStack(spacing: MaterialUIKit.configuration.contentPadding) {
167 | if let secondaryActionKey = secondaryActionKey {
168 | Button {
169 | secondaryAction?()
170 |
171 | withMaterialAnimation {
172 | isPresented = false
173 | }
174 |
175 | hapticFeedback()
176 | } label: {
177 | Text(secondaryActionKey)
178 | .font(MaterialUIKit.configuration.h4)
179 | .fontWeightWithFallback(.semibold)
180 | }
181 | .tint(.materialUIAccent)
182 | .align(.trailing)
183 | }
184 |
185 | Button {
186 | primaryAction()
187 |
188 | withMaterialAnimation {
189 | isPresented = false
190 | }
191 |
192 | hapticFeedback()
193 | } label: {
194 | Text(primaryActionKey)
195 | .font(MaterialUIKit.configuration.h4)
196 | .fontWeightWithFallback(.semibold)
197 | .foregroundStyle(.materialUITonal)
198 | .padding(.vertical, 10)
199 | .padding(.horizontal, 18)
200 | .background(.materialUIAccent)
201 | .cornerRadius(.infinity)
202 | }
203 | }
204 | .align(.trailing)
205 | }
206 | .frame(width: UIScreen.main.bounds.width/1.3)
207 | .secondaryBackground()
208 | .scaleEffect(animationFlag ? 1 : 1.1)
209 | .modalBackdrop(isPresented: $isPresented, animationFlag: $animationFlag)
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Dialog And Alerts/DialogSheet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DialogSheet.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 24/01/24
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: - PUBLIC
12 |
13 | extension View {
14 |
15 | /// Presents a dialog sheet over the current view.
16 | ///
17 | /// - Parameters:
18 | /// - isPresented: A binding to a Boolean value that determines whether to present the dialog sheet.
19 | /// - content: A closure returning the view content to be displayed in the dialog sheet.
20 | @available(iOS 15.0, *)
21 | public func dialogSheet(
22 | isPresented: Binding,
23 | _ content: @escaping () -> Content
24 | ) -> some View where Content: View {
25 | self.modifier(
26 | DialogSheetViewModifier(isPresented: isPresented, dialogSheetContent: AnyView(content()))
27 | )
28 | }
29 | }
30 |
31 | // MARK: - FILE PRIVATE
32 |
33 | /// Adds dialog sheet over the current view.
34 | fileprivate struct DialogSheetViewModifier: ViewModifier {
35 |
36 | @Binding var isPresented: Bool
37 | let dialogSheetContent: AnyView
38 |
39 | func body(content: Content) -> some View {
40 | content.overlay(
41 | DialogSheet(isPresented: $isPresented, content: dialogSheetContent)
42 | )
43 | }
44 | }
45 |
46 | /// Represents Material Desgin styled dialog sheet.
47 | fileprivate struct DialogSheet: View {
48 |
49 | // MARK: - PROPERTIES
50 |
51 | @Binding var isPresented: Bool
52 | @State private var animationFlag: Bool = false
53 |
54 | let content: AnyView
55 |
56 | // MARK: - INITIALIZER
57 |
58 | init(isPresented: Binding, content: AnyView) {
59 | self._isPresented = isPresented
60 | self.content = content
61 | }
62 |
63 | // MARK: - VIEW BODY
64 |
65 | var body: some View {
66 | VStack(alignment: .leading, spacing: MaterialUIKit.configuration.verticalStackSpacing) {
67 | Button {
68 | isPresented.toggle()
69 | hapticFeedback()
70 | } label: {
71 | Image(systemName: "xmark")
72 | .fontWeightWithFallback(.medium)
73 | .foregroundStyle(.materialUIPrimaryTitle)
74 | }
75 | .align(.leading)
76 |
77 | content
78 | }
79 | .frame(width: UIScreen.main.bounds.width/1.3)
80 | .secondaryBackground()
81 | .scaleEffect(animationFlag ? 1 : 1.1)
82 | .modalBackdrop(isPresented: $isPresented, animationFlag: $animationFlag)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Dialog And Alerts/Snackbar.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Snackbar.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: - PUBLIC
12 |
13 | extension View {
14 |
15 | /// Presents a snackbar with a description message with a default time of 5s.
16 | ///
17 | /// - Parameters:
18 | /// - isPresented: A binding to control the presentation state of the snackbar.
19 | /// - message: The message displayed in the snackbar.
20 | @available(iOS 15.0, *)
21 | public func snackbar(
22 | isPresented: Binding,
23 | message: String
24 | ) -> some View {
25 | self.modifier(
26 | SnackbarViewModifier(
27 | isPresented: isPresented,
28 | message: message,
29 | duration: nil,
30 | actionButtonKey: nil,
31 | action: nil
32 | )
33 | )
34 | }
35 |
36 | /// Presents a snackbar with a message, duration, and optional button.
37 | ///
38 | /// - Parameters:
39 | /// - isPresented: A binding to control the presentation state of the snackbar.
40 | /// - message: The message displayed in the snackbar.
41 | /// - duration: The duration (in seconds) for which the snackbar is visible before automatically hiding. Defaults to `nil`.
42 | /// - actionButtonKey: The title key of the action button. If `nil`, no button is displayed. Defaults to `nil`.
43 | /// - action: The action to perform when the primary button is tapped. Defaults to `nil`.
44 | @available(iOS 15.0, *)
45 | public func snackbar(
46 | isPresented: Binding,
47 | message: String,
48 | duration: Double,
49 | actionButtonKey: String,
50 | action: (() -> Void)?
51 | ) -> some View {
52 | self.modifier(
53 | SnackbarViewModifier(
54 | isPresented: isPresented,
55 | message: message,
56 | duration: duration,
57 | actionButtonKey: actionButtonKey,
58 | action: action
59 | )
60 | )
61 | }
62 | }
63 |
64 | // MARK: - FILE PRIVATE
65 |
66 | /// Adds snackbar presentation over the current view.
67 | fileprivate struct SnackbarViewModifier: ViewModifier {
68 |
69 | @Binding var isPresented: Bool
70 | let message: String
71 | let duration: Double?
72 | let actionButtonKey: String?
73 | let action: (() -> Void)?
74 |
75 | func body(content: Content) -> some View {
76 | content.overlay(
77 | Snackbar(
78 | isPresented: $isPresented,
79 | message: message,
80 | duration: duration,
81 | actionButtonKey: actionButtonKey,
82 | action: action
83 | )
84 | )
85 | }
86 | }
87 |
88 | /// Represents Material Design styled snackbar.
89 | fileprivate struct Snackbar: View {
90 |
91 | // MARK: PROPERTIES
92 |
93 | @Binding var isPresented: Bool
94 | @State private var animationFlag: Bool = false
95 |
96 | let message: String
97 | let duration: Double?
98 | let defaultDuration = 5.0
99 | let actionButtonKey: String?
100 | let action: (() -> Void)?
101 |
102 | // MARK: - VIEW_BODY
103 |
104 | var body: some View {
105 | HStack(spacing: MaterialUIKit.configuration.horizontalStackSpacing) {
106 | Text(message)
107 | .font(MaterialUIKit.configuration.h4)
108 | .fontWeightWithFallback(.medium)
109 | .foregroundStyle(.materialUIPrimaryTitle)
110 | .align(.leading)
111 |
112 | if let actionButtonKey = actionButtonKey {
113 | Button {
114 | action?()
115 |
116 | withMaterialAnimation {
117 | isPresented = false
118 | }
119 |
120 | hapticFeedback()
121 | } label: {
122 | Text(actionButtonKey)
123 | .font(MaterialUIKit.configuration.h4)
124 | .fontWeightWithFallback(.semibold)
125 | }
126 | .tint(.materialUIAccent)
127 | .align(.trailing)
128 | }
129 | }
130 | .lineLimit(1)
131 | .tertiaryBackground()
132 | .align(.bottom)
133 | .frame(width: UIScreen.main.bounds.width/1.1)
134 | .offset(y: animationFlag ? 0 : UIScreen.main.bounds.height)
135 | .shadow(color: .black.opacity(0.15), radius: 40, x: 0, y: 0)
136 | .onChangeWithFallback(of: isPresented) { _ , _ in
137 | withMaterialAnimation {
138 | animationFlag = isPresented
139 | toggleOffSnackbar(isPresented: $isPresented, duration: duration)
140 | }
141 | }
142 | }
143 |
144 | /// Toggles off the snackbar after a specified duration.
145 | private func toggleOffSnackbar(isPresented: Binding, duration: Double?) {
146 | DispatchQueue.main.asyncAfter(deadline: .now() + (duration ?? defaultDuration)) {
147 | withMaterialAnimation {
148 | isPresented.wrappedValue = false
149 | }
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Form Controls/Checkbox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Checkbox.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// Represent a Material UI styled checkbox for toggling a boolean state.
12 | @available(iOS 15.0, *)
13 | public struct Checkbox: View {
14 |
15 | // MARK: - PROPERTIES
16 |
17 | private let titleKey: String
18 | @Binding private var isOn: Bool
19 |
20 | // MARK: - INITIALIZERS
21 |
22 | /// Creates a checkbox with a specified title and state binding.
23 | ///
24 | /// - Parameters:
25 | /// - titleKey: The title displayed next to the checkbox.
26 | /// - isOn: Binding to control the checkbox's state (checked or unchecked).
27 | public init(_ titleKey: String, isOn: Binding) {
28 | self.titleKey = titleKey
29 | self._isOn = isOn
30 | }
31 |
32 | // MARK: - VIEW BODY
33 |
34 | public var body: some View {
35 | Toggle(titleKey, isOn: $isOn)
36 | .font(MaterialUIKit.configuration.h4)
37 | .foregroundStyle(.materialUIPrimaryTitle)
38 | .labelsHidden()
39 | .toggleStyle(CheckboxStyle())
40 | .hapticFeedbackOnChange(of: isOn)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Form Controls/DateSelector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateSelector.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: - PUBLIC
12 |
13 | extension View {
14 |
15 | /// Presents a date selector with a binding to control the presentation state and a binding to manage the selected date.
16 | ///
17 | /// - Parameters:
18 | /// - isPresented: A binding to control the presentation state of the date picker.
19 | /// - selection: A binding to manage the selected date.
20 | @available(iOS 15.0, *)
21 | public func dateSelector(isPresented: Binding, selection: Binding) -> some View {
22 | return self.modifier(DateSelectorViewModifier(isPresented: isPresented, selection: selection))
23 | }
24 |
25 | }
26 |
27 | // MARK: - FILE PRIVATE
28 |
29 | /// Adds date selector presentation over the view.
30 | fileprivate struct DateSelectorViewModifier: ViewModifier {
31 |
32 | // MARK: - PROPERTIES
33 |
34 | @Binding var isPresented: Bool
35 | @Binding var selection: Date
36 |
37 | // MARK: - BODY
38 |
39 | func body(content: Content) -> some View {
40 | content.overlay(
41 | DateSelector(isPresented: $isPresented, selection: $selection)
42 | )
43 | }
44 | }
45 |
46 | /// Represents a Material UI styled date selector for picking date values.
47 | fileprivate struct DateSelector: View {
48 |
49 | // MARK: - PROPERTIES
50 |
51 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass
52 | @Environment(\.verticalSizeClass) private var verticalSizeClass
53 |
54 | @Binding var isPresented: Bool
55 | @Binding var selection: Date
56 | @State private var animationFlag: Bool = false
57 |
58 | // MARK: - VIEW BODY
59 |
60 | var body: some View {
61 | VStack(alignment: .leading) {
62 | datePicker()
63 | }
64 | .secondaryBackground()
65 | .scaleEffect(animationFlag ? 1 : 1.1)
66 | .modalBackdrop(isPresented: $isPresented, animationFlag: $animationFlag)
67 | .hapticFeedbackOnChange(of: selection)
68 | }
69 |
70 | /// Returns the dismiss button bar for the date picker.
71 | private func dismissDatePicker() -> some View {
72 | Button {
73 | withMaterialAnimation {
74 | isPresented.toggle()
75 | }
76 | } label: {
77 | Text("Done")
78 | .font(MaterialUIKit.configuration.h4)
79 | .fontWeightWithFallback(.semibold)
80 | }
81 | .tint(.materialUIAccent)
82 | .align(.trailing)
83 | .padding(.horizontal, 10)
84 | }
85 |
86 | /// Returns the date picker.
87 | private func datePicker() -> some View {
88 | VStack(alignment: .leading) {
89 | Text("\(selection.formattedMUIDate())")
90 | .font(MaterialUIKit.configuration.h1)
91 | .fontWeightWithFallback(.semibold)
92 | .foregroundStyle(.materialUIPrimaryTitle)
93 |
94 | Separator()
95 |
96 | DatePicker("", selection: $selection, displayedComponents: .date)
97 | .datePickerStyle(.graphical)
98 | .tint(.materialUIAccent)
99 | .if(isLandscape()) { view in
100 | ScrollView { view }
101 | }
102 |
103 | dismissDatePicker()
104 | }
105 | .frame(width: isLandscape() ? UIScreen.main.bounds.width/2 : UIScreen.main.bounds.width/1.3)
106 | }
107 |
108 | /// Determines whether the current device orientation is portrait.
109 | private func isPortrait() -> Bool {
110 | return horizontalSizeClass == .compact && verticalSizeClass == .regular
111 | }
112 |
113 | /// Determines whether the current device orientation is landscape.
114 | private func isLandscape() -> Bool {
115 | return horizontalSizeClass == .compact && verticalSizeClass == .compact
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Form Controls/RadioButtonGroup.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RadioButtonGroup.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 16/04/24
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// Represents a Material UI styled group of radio buttons for selecting a single option.
12 | @available(iOS 15.0, *)
13 | public struct RadioButtonGroup: View where Data: RandomAccessCollection, ID: Hashable, Content: View, Data.Element: Hashable, Data: Hashable {
14 |
15 | // MARK: - PROPERTIES
16 |
17 | private let data: Data
18 | private let id: KeyPath
19 | private let content: (Data.Element) -> Content
20 |
21 | @Binding public var selection: Data.Element
22 |
23 | // MARK: - INITIALIZER
24 |
25 | /// Creates a radio button group view with the provided items and a binding to the selected value.
26 | ///
27 | /// - Parameters:
28 | /// - data: A collection of elements to display in the radio-group options.
29 | /// - id: A key path to an `ID` property on each element to uniquely identify them.
30 | /// - selection: A binding to the currently selected element in the radio-group options.
31 | /// - content: A closure that returns the content view for a given element.
32 | public init(
33 | _ data: Data,
34 | id: KeyPath,
35 | selection: Binding,
36 | @ViewBuilder _ content: @escaping (Data.Element) -> Content
37 | ) {
38 | self.data = data
39 | self.id = id
40 | self._selection = selection
41 | self.content = content
42 | }
43 |
44 | // MARK: - VIEW BODY
45 |
46 | public var body: some View {
47 | VStack(alignment: .leading, spacing: MaterialUIKit.configuration.verticalStackSpacing) {
48 | ForEach(data, id: id) { item in
49 | HStack {
50 | content(item)
51 | .tag(item)
52 | .font(MaterialUIKit.configuration.h4)
53 | .foregroundStyle(.materialUIPrimaryTitle)
54 |
55 | Spacer()
56 |
57 | Image(systemName: item == selection ? "circle.circle.fill" : "circle")
58 | .font(.title3)
59 | .foregroundStyle(item == selection ? .materialUIAccent : .materialUIOnDisabled)
60 | }
61 | .onTapGesture {
62 | withMaterialAnimation {
63 | selection = item
64 | }
65 | }
66 |
67 | if !isLastElement(data: data, item: item) {
68 | Separator()
69 | }
70 | }
71 | }
72 | .align(.leading)
73 | .hapticFeedbackOnChange(of: selection)
74 | }
75 |
76 | /// Checks if the current element is the last one in the collection.
77 | private func isLastElement(data: Data, item: Data.Element) -> Bool {
78 | return data.last == item
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Form Controls/SecureTextBox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecureTextBox.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// Represents a Material UI styled text box for secure fields.
12 | @available(iOS 15.0, *)
13 | public struct SecureTextBox: View {
14 |
15 | // MARK: - PROPERTIES
16 |
17 | @Binding private var text: String
18 | private let systemImage: String?
19 | private let titleKey: String
20 | private let background: Color?
21 |
22 | @FocusState private var isFocused: Bool
23 | @State private var secureFieldIsFocused: Bool = false
24 | @Environment(\.cornerRadius) private var cornerRadius: CGFloat
25 |
26 | // MARK: - INITIALIZERS
27 |
28 | /// Creates a default secure text box.
29 | ///
30 | /// - Parameters:
31 | /// - titleKey: Title key for the secure text box.
32 | /// - text: Binding to the text value of the secure text box.
33 | public init(_ titleKey: String, text: Binding) {
34 | self.titleKey = titleKey
35 | self._text = text
36 | self.systemImage = nil
37 | self.background = nil
38 | }
39 |
40 | /// Creates a secure text box with a system symbol.
41 | ///
42 | /// - Parameters:
43 | /// - systemImage: System symbol for the secure text box.
44 | /// - titleKey: Title key for the secure text box.
45 | /// - text: Binding to the text value of the secure text box.
46 | public init(systemImage: String, _ titleKey: String, text: Binding) {
47 | self.titleKey = titleKey
48 | self._text = text
49 | self.systemImage = systemImage
50 | self.background = nil
51 | }
52 |
53 | /// Creates a secure text box with a system symbol and custom background color.
54 | ///
55 | /// - Parameters:
56 | /// - systemImage: System symbol for the secure text box.
57 | /// - titleKey: Title key for the secure text boxfield.
58 | /// - text: Binding to the text value of the secure text box.
59 | /// - background: Custom background color for the secure text box.
60 | public init(systemImage: String, _ titleKey: String, text: Binding, background: Color) {
61 | self.titleKey = titleKey
62 | self._text = text
63 | self.systemImage = systemImage
64 | self.background = background
65 | }
66 |
67 |
68 | // MARK: - VIEW BODY
69 |
70 | public var body: some View {
71 | HStack(spacing: MaterialUIKit.configuration.horizontalStackSpacing) {
72 | if let systemImage {
73 | Image(systemName: systemImage)
74 | .font(.callout)
75 | .foregroundStyle(secureFieldIsFocused ? .materialUIAccent : .materialUIOnDisabled)
76 | .padding(.leading, MaterialUIKit.configuration.horizontalPadding)
77 | }
78 |
79 | SecureField(titleKey, text: $text)
80 | .font(MaterialUIKit.configuration.h4)
81 | .tint(.materialUIAccent)
82 | .padding(.vertical, MaterialUIKit.configuration.verticalPadding)
83 | .padding(.horizontal, systemImage != nil ? 0 : MaterialUIKit.configuration.horizontalPadding)
84 |
85 | if !(text.isEmpty) {
86 | Button {
87 | text = ""
88 | hapticFeedback()
89 | } label: {
90 | Image(systemName: "xmark.circle.fill")
91 | .foregroundStyle(.materialUISecondaryTitle)
92 | .padding(.trailing, MaterialUIKit.configuration.horizontalPadding)
93 | }
94 | }
95 | }
96 | .background(background ?? .materialUISecondaryBackground)
97 | .cornerRadius(cornerRadius)
98 | .stroke(background: secureFieldIsFocused ? .materialUIAccent : .materialUIOutline, cornerRadius: cornerRadius)
99 | .focused($isFocused)
100 | .onTapGesture {
101 | isFocused.toggle()
102 | hapticFeedback()
103 | }
104 | .onChangeWithFallback(of: isFocused) { oldValue, newValue in
105 | withMaterialAnimation {
106 | secureFieldIsFocused = newValue
107 | }
108 | }
109 | }
110 | }
111 |
112 | extension View {
113 |
114 | /// Sets the corner radius for the secure field.
115 | ///
116 | /// - Parameter cornerRadius: The corner radius to be applied to the secure field.
117 | ///
118 | /// - Returns: A view modified to include the specified corner radius.
119 | public func secureTextBoxCornerRadius(_ cornerRadius: CGFloat) -> some View {
120 | self.environment(\.cornerRadius, cornerRadius)
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Form Controls/Switch.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Switch.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// Represents a Material UI styled switch for toggling boolean states.
12 | @available(iOS 15.0, *)
13 | public struct Switch: View {
14 |
15 | // MARK: - PROPERTIES
16 |
17 | private let titleKey: String
18 | @Binding private var isOn: Bool
19 |
20 | // MARK: - INITIALIZERS
21 |
22 | /// Creates a Material UI style switch.
23 | ///
24 | /// - Parameters:
25 | /// - titleKey: The title key of the switch.
26 | /// - isOn: A binding to a boolean value that determines the on/off state of the switch.
27 | public init(_ titleKey: String, isOn: Binding) {
28 | self.titleKey = titleKey
29 | self._isOn = isOn
30 | }
31 |
32 | // MARK: - VIEW BODY
33 |
34 | public var body: some View {
35 | Toggle(titleKey, isOn: $isOn)
36 | .font(MaterialUIKit.configuration.h4)
37 | .foregroundStyle(.materialUIPrimaryTitle)
38 | .labelsHidden()
39 | .toggleStyle(SwitchStyle())
40 | .hapticFeedbackOnChange(of: isOn)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Form Controls/TextBox.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TextBox.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | /// Represents a Material UI styled text box for user input.
12 | @available(iOS 15.0, *)
13 | public struct TextBox: View {
14 |
15 | // MARK: - PROPERTIES
16 |
17 | @Binding private var text: String
18 | private let systemImage: String?
19 | private let titleKey: String
20 | private let background: Color?
21 |
22 | @FocusState private var isFocused: Bool
23 | @State private var textFieldIsFocused: Bool = false
24 | @Environment(\.cornerRadius) private var cornerRadius: CGFloat
25 |
26 | // MARK: - INITIALIZERS
27 |
28 | /// Creates a default text box.
29 | ///
30 | /// - Parameters:
31 | /// - titleKey: Title key for the text box.
32 | /// - text: Binding to the text value of the text field.
33 | public init(_ titleKey: String, text: Binding) {
34 | self.titleKey = titleKey
35 | self._text = text
36 | self.systemImage = nil
37 | self.background = nil
38 | }
39 |
40 | /// Creates a text box with a system symbol.
41 | ///
42 | /// - Parameters:
43 | /// - systemImage: System symbol for the text box.
44 | /// - titleKey: Title key for the text box.
45 | /// - text: Binding to the text value of the text box.
46 | public init(systemImage: String, _ titleKey: String, text: Binding) {
47 | self.titleKey = titleKey
48 | self._text = text
49 | self.systemImage = systemImage
50 | self.background = nil
51 | }
52 |
53 | /// Creates a MaterialUI style text box with a system symbol and custom background color.
54 | ///
55 | /// - Parameters:
56 | /// - systemImage: System symbol for the text box.
57 | /// - titleKey: Title key for the text box.
58 | /// - text: Binding to the text value of the text box.
59 | /// - background: Custom background color for the text box.
60 | public init(systemImage: String, _ titleKey: String, text: Binding, background: Color) {
61 | self.titleKey = titleKey
62 | self._text = text
63 | self.systemImage = systemImage
64 | self.background = background
65 | }
66 |
67 | // MARK: - VIEW BODY
68 |
69 | public var body: some View {
70 | HStack(spacing: MaterialUIKit.configuration.horizontalStackSpacing) {
71 | if let systemImage {
72 | Image(systemName: systemImage)
73 | .font(.callout)
74 | .foregroundStyle(textFieldIsFocused ? .materialUIAccent : .materialUIOnDisabled)
75 | .padding(.leading, MaterialUIKit.configuration.horizontalPadding)
76 | }
77 |
78 | TextField(titleKey, text: $text)
79 | .font(MaterialUIKit.configuration.h4)
80 | .tint(.materialUIAccent)
81 | .padding(.vertical, MaterialUIKit.configuration.verticalPadding)
82 | .padding(.horizontal, systemImage != nil ? 0 : MaterialUIKit.configuration.horizontalPadding)
83 |
84 | if !text.isEmpty {
85 | Button {
86 | text = ""
87 | hapticFeedback()
88 | } label: {
89 | Image(systemName: "xmark.circle.fill")
90 | .foregroundStyle(.materialUISecondaryTitle)
91 | .padding(.trailing, MaterialUIKit.configuration.horizontalPadding)
92 | }
93 | }
94 | }
95 | .background(background ?? .materialUISecondaryBackground)
96 | .cornerRadius(cornerRadius)
97 | .stroke(background: textFieldIsFocused ? .materialUIAccent : .materialUIOutline, cornerRadius: cornerRadius)
98 | .focused($isFocused)
99 | .onTapGesture {
100 | isFocused.toggle()
101 | hapticFeedback()
102 | }
103 | .onChangeWithFallback(of: isFocused) { oldValue, newValue in
104 | withMaterialAnimation {
105 | textFieldIsFocused = newValue
106 | }
107 | }
108 | }
109 | }
110 |
111 | extension View {
112 |
113 | /// Sets the corner radius for the text box.
114 | ///
115 | /// - Parameter cornerRadius: The corner radius to be applied to the text box.
116 | ///
117 | /// - Returns: A view modified to include the specified corner radius.
118 | public func textBoxCornerRadius(_ cornerRadius: CGFloat) -> some View {
119 | self.environment(\.cornerRadius, cornerRadius)
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Form Controls/TimeSelector.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimeSelector.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: - PUBLIC
12 |
13 | extension View {
14 |
15 | /// Presents a time selector with a binding to control the presentation state and a binding to manage the selected time.
16 | ///
17 | /// - Parameters:
18 | /// - isPresented: A binding to control the presentation of the time picker.
19 | /// - selection: A binding to manage the selected time.
20 | @available(iOS 15.0, *)
21 | public func timeSelector(isPresented: Binding, selection: Binding) -> some View {
22 | return self.modifier(TimeSelectorViewModifier(isPresented: isPresented, selection: selection))
23 | }
24 | }
25 |
26 | // MARK: - FILE PRIVATE
27 |
28 | /// Adds time picker over the view.
29 | fileprivate struct TimeSelectorViewModifier: ViewModifier {
30 |
31 | // MARK: - PROPERTIES
32 |
33 | @Binding var isPresented: Bool
34 | @Binding var selection: Date
35 |
36 | // MARK: - BODY
37 |
38 | func body(content: Content) -> some View {
39 | content.overlay(
40 | TimePicker(isPresented: $isPresented, selection: $selection)
41 | )
42 | }
43 | }
44 |
45 | /// Represents a Material UI styled time selector for picking time values.
46 | fileprivate struct TimePicker: View {
47 |
48 | // MARK: - PROPERTIES
49 |
50 | @Environment(\.horizontalSizeClass) private var horizontalSizeClass
51 | @Environment(\.verticalSizeClass) private var verticalSizeClass
52 |
53 | @Binding var isPresented: Bool
54 | @Binding var selection: Date
55 | @State private var animationFlag: Bool = false
56 |
57 | // MARK: - VIEW BODY
58 |
59 | var body: some View {
60 | VStack(alignment: .leading) {
61 | timePicker()
62 | }
63 | .secondaryBackground()
64 | .scaleEffect(animationFlag ? 1 : 1.1)
65 | .modalBackdrop(isPresented: $isPresented, animationFlag: $animationFlag)
66 | .hapticFeedbackOnChange(of: selection)
67 | }
68 |
69 | /// Returns the dismiss button bar for the time picker.
70 | private func dismissDatePicker() -> some View {
71 | Button {
72 | withMaterialAnimation {
73 | isPresented.toggle()
74 | }
75 | } label: {
76 | Text("Done")
77 | .font(MaterialUIKit.configuration.h4)
78 | .fontWeightWithFallback(.semibold)
79 | }
80 | .tint(.materialUIAccent)
81 | .align(.trailing)
82 | .padding(.horizontal, 10)
83 | }
84 |
85 | /// Returns the time picker view.
86 | private func timePicker() -> some View {
87 | VStack {
88 | Text("\(selection.formatted(date: .omitted, time: .shortened))")
89 | .font(MaterialUIKit.configuration.h1)
90 | .fontWeightWithFallback(.semibold)
91 | .foregroundStyle(.materialUIPrimaryTitle)
92 |
93 | Separator()
94 |
95 | DatePicker("", selection: $selection, displayedComponents: .hourAndMinute)
96 | .datePickerStyle(.wheel)
97 | .tint(.materialUIAccent)
98 | .if(isLandscape()) { view in
99 | view
100 | }
101 |
102 | dismissDatePicker()
103 | }
104 | .frame(width: isLandscape() ? UIScreen.main.bounds.width/3 : UIScreen.main.bounds.width/1.3)
105 | }
106 |
107 | /// Determines whether the current device orientation is portrait.
108 | private func isPortrait() -> Bool {
109 | return horizontalSizeClass == .compact && verticalSizeClass == .regular
110 | }
111 |
112 | /// Determines whether the current device orientation is landscape.
113 | private func isLandscape() -> Bool {
114 | return horizontalSizeClass == .compact && verticalSizeClass == .compact
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Interactive Elements/ActionButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ActionButton.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 30/12/23
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: - PUBLIC
12 |
13 | /// Represents a Material UI styled button for performing actions with various visual styles.
14 | @available(iOS 15.0, *)
15 | public struct ActionButton: View {
16 |
17 | // MARK: - PROPERTIES
18 |
19 | private let titleKey: String
20 | private let style: ActionButtonStyle
21 | private let action: () -> Void
22 |
23 | @Environment(\.cornerRadius) private var cornerRadius: CGFloat
24 | @Environment(\.font) private var font: Font
25 | @Environment(\.fontWeight) private var fontWeight: Font.Weight
26 |
27 | // MARK: - INITIALIZERS
28 |
29 | /// Creates an action button with a default `.filled` style.
30 | ///
31 | /// - Parameters:
32 | /// - titleKey: The text key to display on the button.
33 | /// - action: The closure to execute when the button is pressed.
34 | public init(_ titleKey: String, action: @escaping () -> Void) {
35 | self.titleKey = titleKey
36 | self.style = ActionButtonStyle.filled
37 | self.action = action
38 | }
39 |
40 | /// Creates an action button with a specific style.
41 | ///
42 | /// - Parameters:
43 | /// - titleKey: The text key to display on the button.
44 | /// - style: The style of the button.
45 | /// - action:The closure to execute when the button is pressed.
46 | public init(_ titleKey: String, style: ActionButtonStyle, action: @escaping () -> Void) {
47 | self.titleKey = titleKey
48 | self.style = style
49 | self.action = action
50 | }
51 |
52 | // MARK: - VIEW BODY
53 |
54 | public var body: some View {
55 | switch style {
56 | case .elevated:
57 | elevatedButtonStyle()
58 |
59 | case .filled:
60 | filledButtonStyle()
61 |
62 | case .tonal:
63 | tonalButtonStyle()
64 |
65 | case .outline:
66 | outlineButtonStyle()
67 |
68 | case .text:
69 | textButtonStyle()
70 |
71 | case .elevatedStretched:
72 | elevatedStretchedButtonStyle()
73 |
74 | case .filledStretched:
75 | filledStretchedButtonStyle()
76 |
77 | case .tonalStretched:
78 | tonalStretchedButtonStyle()
79 |
80 | case .outlineStretched:
81 | outlineStretchedButtonStyle()
82 | }
83 | }
84 | }
85 |
86 | extension View {
87 |
88 | /// Sets the corner radius for an action buttons.
89 | ///
90 | /// - Parameter cornerRadius: The corner radius value to apply.
91 | public func actionButtonCornerRadius(_ cornerRadius: CGFloat) -> some View {
92 | self.environment(\.cornerRadius, cornerRadius)
93 | }
94 |
95 | /// Sets the font size for an action buttons.
96 | ///
97 | /// - Parameter font: The font size to apply.
98 | public func actionButtonFontStyle(_ font: Font) -> some View {
99 | self.environment(\.font, font)
100 | }
101 |
102 | /// Sets the font weight for an action buttons.
103 | ///
104 | /// - Parameter fontWeight: The font weight to apply.
105 | public func actionButtonFontWeight(_ fontWeight: Font.Weight) -> some View {
106 | self.environment(\.fontWeight, fontWeight)
107 | }
108 | }
109 |
110 | // MARK: - FILE PRIVATE
111 |
112 | fileprivate extension ActionButton {
113 |
114 | /// A button with an elevated background and rounded corners.
115 | func elevatedButtonStyle() -> some View {
116 | Button(action: action) {
117 | Text(titleKey)
118 | .elevatedStyledBackground()
119 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
120 | .shadow(color: .black.opacity(0.15), radius: 1.5, x: 0, y: 1)
121 | .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1)
122 | }
123 | .buttonStyle(ActionButtonAnimationStyle())
124 | }
125 |
126 | /// A button with an filled background and rounded corners.
127 | func filledButtonStyle() -> some View {
128 | return Button(action: action) {
129 | Text(titleKey)
130 | .filledStyledBackground()
131 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
132 | }
133 | .buttonStyle(ActionButtonAnimationStyle())
134 | }
135 |
136 | /// A button with an tonal background and rounded corners.
137 | func tonalButtonStyle() -> some View {
138 | return Button(action: action) {
139 | Text(titleKey)
140 | .tonalStyledBackground()
141 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
142 | }
143 | .buttonStyle(ActionButtonAnimationStyle())
144 | }
145 |
146 | /// A button with an outlined border and rounded corners.
147 | func outlineButtonStyle() -> some View {
148 | return Button(action: action) {
149 | Text(titleKey)
150 | .outlineStyledBackground(cornerRadius: cornerRadius)
151 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
152 | }
153 | }
154 |
155 | /// A button with only text.
156 | func textButtonStyle() -> some View {
157 | return Button(action: action) {
158 | Text(titleKey)
159 | .foregroundStyle(.materialUIAccent)
160 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
161 | }
162 | .buttonStyle(ActionButtonAnimationStyle())
163 | }
164 |
165 | /// A button with elevated background and rounded corners, occupying maximum available width.
166 | func elevatedStretchedButtonStyle() -> some View {
167 | return Button(action: action) {
168 | Text(titleKey)
169 | .elevatedStretchedStyledBackground()
170 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
171 | .shadow(color: .black.opacity(0.15), radius: 1.5, x: 0, y: 1)
172 | .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1)
173 | }
174 | .buttonStyle(ActionButtonAnimationStyle())
175 | }
176 |
177 | /// A button with filled background and rounded corners, occupying maximum available width.
178 | func filledStretchedButtonStyle() -> some View {
179 | return Button(action: action) {
180 | Text(titleKey)
181 | .filledStretchedStyledBackground()
182 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
183 | }
184 | .buttonStyle(ActionButtonAnimationStyle())
185 | }
186 |
187 | /// A button with tonal background and rounded corners, occupying maximum available width.
188 | func tonalStretchedButtonStyle() -> some View {
189 | return Button(action: action) {
190 | Text(titleKey)
191 | .tonalStretchedStyledBackground()
192 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
193 | }
194 | .buttonStyle(ActionButtonAnimationStyle())
195 | }
196 |
197 | /// A button with an outlined border and rounded corners, occupying maximum available width.
198 | func outlineStretchedButtonStyle() -> some View {
199 | return Button(action: action) {
200 | Text(titleKey)
201 | .outlineStretchedStyledBackground(cornerRadius: cornerRadius)
202 | .materialActionButtonStyle(font: font, fontWeight: fontWeight, cornerRadius: cornerRadius)
203 | }
204 | .buttonStyle(ActionButtonAnimationStyle())
205 | }
206 | }
207 |
208 | fileprivate extension View {
209 |
210 | /// Applies a specific font, font weight, and corner radius to the view's environment.
211 | func materialActionButtonStyle(font: Font, fontWeight: Font.Weight, cornerRadius: CGFloat) -> some View {
212 | self
213 | .font(font)
214 | .fontWeightWithFallback(fontWeight)
215 | .cornerRadius(cornerRadius)
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/Sources/MaterialUIKit/Components/Interactive Elements/DropdownMenu.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DropdownMenu.swift
3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git
4 | //
5 | // Author: Aum Chauhan
6 | // Created On: 25/03/24
7 | //
8 |
9 | import SwiftUI
10 |
11 | // MARK: - PUBLIC
12 |
13 | /// Represents a Material UI styled dropdown menu for selecting items from a list.
14 | @available(iOS 15.0, *)
15 | public struct DropdownMenu