├── .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 | ![MaterialUIKit@70px](https://github.com/user-attachments/assets/c6e6b9be-e61d-423d-82c4-1156dd14d068) 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 : View where Label : View, Content : View { 16 | 17 | // MARK: - PROPERTIES 18 | 19 | @State private var height = 0.0 20 | @State private var showMenu: Bool = false 21 | @State private var screenWidth: Double = 0 22 | @State private var screenHeight: Double = 0 23 | @Binding private var isActive: Bool 24 | 25 | private var label: Label! 26 | private var content: Content! 27 | private var width: Double = 200 28 | 29 | // MARK: - INITIALIZERS 30 | 31 | /// Creates a dropdown menu with custom content and label. 32 | /// 33 | /// - Parameters: 34 | /// - content: A closure that returns the content view for the menu. 35 | /// - label: A closure that returns the label view for the menu. 36 | public init( 37 | @ViewBuilder content: () -> Content, 38 | @ViewBuilder label: () -> Label 39 | ) { 40 | self.content = content() 41 | self.label = label() 42 | self._isActive = .constant(false) 43 | } 44 | 45 | /// Creates a dropdown menu with a title-based label and custom content. 46 | /// 47 | /// - Parameters: 48 | /// - titleKey: A string representing the title of the menu button. 49 | /// - content: A closure that returns the content view for the menu. 50 | public init( 51 | _ titleKey: S, 52 | @ViewBuilder content: () -> Content 53 | ) where Label == Text, S : StringProtocol { 54 | self.content = content() 55 | self.label = Text(titleKey) 56 | self._isActive = .constant(false) 57 | } 58 | 59 | /// Creates a dropdown menu with custom content and label, and a binding to control its activation state. 60 | /// 61 | /// - Parameters: 62 | /// - isActive: A binding to a Boolean value that indicates whether the menu is active. 63 | /// - content: A closure that returns the content view for the menu. 64 | /// - label: A closure that returns the label view for the menu. 65 | public init( 66 | isActive: Binding, 67 | @ViewBuilder content: () -> Content, 68 | @ViewBuilder label: () -> Label 69 | ) { 70 | self.content = content() 71 | self.label = label() 72 | self._isActive = isActive 73 | } 74 | 75 | // MARK: - VIEW BODY 76 | 77 | public var body: some View { 78 | Button { 79 | hapticFeedback() 80 | withMaterialAnimation { 81 | showMenu.toggle() 82 | } 83 | } label: { 84 | label 85 | .tint(.materialUIAccent) 86 | } 87 | .onAppear { 88 | screenWidth = UIScreen.main.bounds.size.width 89 | screenHeight = UIScreen.main.bounds.size.height * 2 90 | } 91 | .overlay(backgroundOverlay()) 92 | .overlay(menuOverlay()) 93 | .onChangeWithFallback(of: isActive) { oldValue, newValue in 94 | withMaterialAnimation { 95 | showMenu.toggle() 96 | } 97 | } 98 | .zIndex(.infinity) 99 | .onChangeWithFallback(of: showMenu) { oldValue, newValue in 100 | withMaterialAnimation { 101 | showMenu.toggle() 102 | } 103 | } 104 | } 105 | } 106 | 107 | // MARK: - FILE PRIVATE 108 | 109 | fileprivate extension DropdownMenu { 110 | 111 | /// Adds a background overlay to the menu, allowing to close the menu when tapped outside of it. 112 | func backgroundOverlay() -> some View { 113 | GeometryReader { geo in 114 | VStack { 115 | if showMenu { 116 | Button { 117 | withMaterialAnimation { 118 | showMenu.toggle() 119 | } 120 | } label: { 121 | Rectangle() 122 | .foregroundStyle(.clear) 123 | .contentShape(Rectangle()) 124 | } 125 | .offset(x: -geo.frame(in: .global).origin.x) 126 | } 127 | } 128 | } 129 | .frame(width: screenWidth, height: screenHeight) 130 | } 131 | 132 | /// Creates the overlay for the menu content. 133 | @ViewBuilder func menuOverlay() -> some View { 134 | GeometryReader { geo in 135 | if showMenu { 136 | VStack { 137 | // Uses scrollable frame with fixed height 138 | if height > 190 { 139 | ScrollView { 140 | _VariadicView.Tree(DropdownMenuViewLayout()) { 141 | content 142 | } 143 | .contentShape(Rectangle()) 144 | } 145 | .frame(width: 180, height: 190) 146 | } else { 147 | // Uses static vertical frame (for less content inside the menu) with dynamic height 148 | VStack { 149 | _VariadicView.Tree(DropdownMenuViewLayout()) { 150 | content 151 | } 152 | .contentShape(Rectangle()) 153 | } 154 | .frame(width: 180) 155 | } 156 | } 157 | .padding(MaterialUIKit.configuration.contentPadding) 158 | .background( 159 | GeometryReader { geo in 160 | ZStack { 161 | Color.materialUITertiaryBackground 162 | } 163 | .cornerRadius(MaterialUIKit.configuration.cornerRadius) 164 | .shadow(color: .black.opacity(0.15), radius: 40, x: 0, y: geo.frame(in: .global).origin.y > screenHeight / 3.5 ? -10 : 10) 165 | .onAppear { 166 | self.height = geo.size.height 167 | } 168 | } 169 | ) 170 | .offset(x: menuOffsetX(geo.frame(in: .global).origin.x), y: menuOffsetY(geo.frame(in: .global).origin.y)) 171 | .transition(.scale.combined(with: .opacity)) 172 | } 173 | } 174 | .frame(width: width, height: height) 175 | } 176 | 177 | /// Calculates the horizontal offset for the menu based on its position on the screen. 178 | func menuOffsetX(_ x: Double) -> Double { 179 | if x < 8 { 180 | return -x + 8 181 | } else if x + width > screenWidth - 8 { 182 | return screenWidth - x - width - 8 183 | } else { 184 | return 0 185 | } 186 | } 187 | 188 | /// Calculates the vertical offset for the menu based on its position on the screen. 189 | func menuOffsetY(_ y: Double) -> Double { 190 | return y > screenHeight / 3.5 ? -height / 1.5 + 16 : height / 2.0 + 16 191 | } 192 | } 193 | 194 | /// A layout view that arranges the menu items vertically. 195 | fileprivate struct DropdownMenuViewLayout: _VariadicView_UnaryViewRoot { 196 | @ViewBuilder func body(children: _VariadicView.Children) -> some View { 197 | VStack(spacing: MaterialUIKit.configuration.verticalStackSpacing) { 198 | ForEach(children) { child in 199 | child 200 | .font(MaterialUIKit.configuration.h4) 201 | .fontWeightWithFallback(.regular) 202 | .foregroundStyle(.materialUIPrimaryTitle) 203 | .align(.leading) 204 | 205 | if child.id != children.last?.id { 206 | Separator() 207 | } 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Interactive Elements/DropdownMenuLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DropdownMenuLabel.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 24/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Represents a Material UI styled label for a dropdown menu item. 12 | @available(iOS 15.0, *) 13 | public struct DropdownMenuLabel: View { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | private let systemImage: String 18 | private let titleKey: String 19 | 20 | // MARK: - INITIALIZERS 21 | 22 | /// Creates a menu label with the given system image and title. 23 | /// 24 | /// - Parameters: 25 | /// - systemImage: The name of the system image. 26 | /// - titleKey: The title displayed next to the system image. 27 | public init(systemImage: String, _ titleKey: String) { 28 | self.systemImage = systemImage 29 | self.titleKey = titleKey 30 | } 31 | 32 | // MARK: - VIEW BODY 33 | 34 | public var body: some View { 35 | HStack(spacing: MaterialUIKit.configuration.horizontalStackSpacing) { 36 | Image(systemName: systemImage) 37 | .foregroundStyle(.materialUIAccent) 38 | 39 | Text(titleKey) 40 | .font(MaterialUIKit.configuration.h4) 41 | .foregroundStyle(.materialUIPrimaryTitle) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Interactive Elements/FAB.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FAB.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 FAB over the view with a bottom trailing alignment. 16 | /// 17 | /// - Parameters: 18 | /// - systemImage: The system symbol name for the button icon. 19 | /// - titleKey: The title key displayed on the button. 20 | /// - action: A closure to be executed when the button is tapped. 21 | @available(iOS 15.0, *) 22 | public func floatingActionButton( 23 | systemImage: String, 24 | titleKey: String, 25 | action: @escaping () -> Void 26 | ) -> some View { 27 | self.modifier( 28 | FABViewModifier(systemImage: systemImage, titleKey: titleKey, cornerRadius: nil, action: action) 29 | ) 30 | } 31 | /// Presents a FAB over the view with a bottom trailing alignment and a customizable corner radius. 32 | /// 33 | /// - Parameters: 34 | /// - systemImage: The system symbol name for the button icon. 35 | /// - titleKey: The title key displayed on the button. 36 | /// - cornerRadius: The corner radius to apply to the button. 37 | /// - action: A closure to be executed when the button is tapped. 38 | @available(iOS 15.0, *) 39 | public func floatingActionButton( 40 | systemImage: String, 41 | titleKey: String, 42 | cornerRadius: CGFloat, 43 | action: @escaping () -> Void 44 | ) -> some View { 45 | self.modifier( 46 | FABViewModifier(systemImage: systemImage, titleKey: titleKey, cornerRadius: cornerRadius, action: action) 47 | ) 48 | } 49 | } 50 | 51 | // MARK: - FILE PRIVATE 52 | 53 | /// Adds a floating action button with specified system symbol and title aligned at the bottom of the screen. 54 | fileprivate struct FABViewModifier: ViewModifier { 55 | let systemImage: String 56 | let titleKey: String 57 | let cornerRadius: CGFloat? 58 | let action: () -> Void 59 | 60 | func body(content: Content) -> some View { 61 | content.overlay( 62 | FAB(systemImage: systemImage, titleKey: titleKey, action, cornerRadius: cornerRadius) 63 | .align(.bottomTrailing) 64 | .padding(MaterialUIKit.configuration.contentPadding) 65 | ) 66 | } 67 | } 68 | 69 | /// Represents a Material UI styled floating action button for primary actions. 70 | fileprivate struct FAB: View { 71 | 72 | // MARK: - PROPERTIES 73 | 74 | let systemImage: String 75 | let image: String? 76 | let titleKey: String 77 | let cornerRadius: CGFloat? 78 | let action: () -> Void 79 | 80 | // MARK: - INITIALIZERS 81 | 82 | /// Creates a FAB with a system symbol, title, and an action. 83 | init(systemImage: String, titleKey: String, _ action: @escaping () -> Void, cornerRadius: CGFloat?) { 84 | self.systemImage = systemImage 85 | self.titleKey = titleKey 86 | self.action = action 87 | self.cornerRadius = cornerRadius 88 | self.image = nil 89 | } 90 | 91 | // MARK: - VIEW BODY 92 | 93 | var body: some View { 94 | Button { 95 | action() 96 | hapticFeedback() 97 | }label: { 98 | HStack { 99 | Image(systemName: systemImage) 100 | Text(titleKey) 101 | .font(MaterialUIKit.configuration.h4) 102 | } 103 | .fontWeightWithFallback(.medium) 104 | .filledStyledBackground() 105 | .cornerRadius(cornerRadius ?? MaterialUIKit.configuration.cornerRadius) 106 | } 107 | .buttonStyle(ActionButtonAnimationStyle()) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Interactive Elements/IconButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconButton.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 icon button for user interaction. 14 | @available(iOS 15.0, *) 15 | public struct IconButton: View { 16 | 17 | // MARK: - PROPERTIES 18 | 19 | private let systemImage: String? 20 | private let imageName: String? 21 | private let style: IconButtonStyle 22 | private let action: () -> () 23 | 24 | @Environment(\.frameSize) private var frameSize: CGFloat 25 | @Environment(\.font) private var fontSize: Font 26 | 27 | // MARK: - INITIALIZERS 28 | 29 | /// Creates a circular icon button with a system symbol, style, and action. 30 | /// 31 | /// - Parameters: 32 | /// - systemImage: SF Symbol string for system-provided icons. 33 | /// - style: The style of the button, defined by `IconButtonStyle`. 34 | /// - action: The closure to execute when the button is pressed. 35 | public init( 36 | systemImage: String, 37 | style: IconButtonStyle, 38 | _ action: @escaping () -> Void 39 | ) { 40 | self.systemImage = systemImage 41 | self.style = style 42 | self.action = action 43 | self.imageName = nil 44 | } 45 | 46 | /// Creates a circular icon button with a custom image, style, and action. 47 | /// 48 | /// - Parameters: 49 | /// - imageName: String representing the name of a custom image. 50 | /// - style: The style of the button, defined by `IconButtonStyle`. 51 | /// - action: The closure to execute when the button is pressed. 52 | public init( 53 | imageName: String, 54 | style: IconButtonStyle, 55 | _ action: @escaping () -> Void 56 | ) { 57 | self.style = style 58 | self.action = action 59 | self.imageName = imageName 60 | self.systemImage = nil 61 | } 62 | 63 | // MARK: - VIEW BODY 64 | 65 | public var body: some View { 66 | switch style { 67 | case .elevated: 68 | elevatedIconStyle() 69 | 70 | case .filled: 71 | filledIconStyle() 72 | 73 | case .tonal: 74 | tonalIconStyle() 75 | } 76 | } 77 | } 78 | 79 | extension View { 80 | 81 | /// Sets the frame size for the icon button. 82 | /// 83 | /// - Parameter frameSize: The frame size value to apply. 84 | public func iconButtonSize(_ frameSize: CGFloat) -> some View { 85 | self.environment(\.frameSize, frameSize) 86 | } 87 | 88 | /// Sets the font weight for the icon button. 89 | /// 90 | /// - Parameter fontWeight: The font weight to apply. 91 | public func iconButtonFontSize(_ fontSize: Font) -> some View { 92 | self.environment(\.font, fontSize) 93 | } 94 | } 95 | 96 | // MARK: - FILE PRIVATE 97 | 98 | fileprivate extension IconButton { 99 | /// An icon from SFSymbol or bundle image. 100 | func icon() -> some View { 101 | VStack { 102 | if let systemImage = systemImage { 103 | Image(systemName: systemImage) 104 | .font(fontSize) 105 | } else if let imageName = imageName { 106 | Image(imageName) 107 | .resizable() 108 | .renderingMode(.template) 109 | .font(fontSize) 110 | } 111 | } 112 | .fontWeightWithFallback(.semibold) 113 | } 114 | 115 | /// A circular icon button with a elevated background. 116 | func elevatedIconStyle() -> some View { 117 | Button(action: action) { 118 | icon() 119 | .frame(width: frameSize, height: frameSize) 120 | .elevatedStyledBackground() 121 | .clipShape(Circle()) 122 | .shadow(color: .black.opacity(0.15), radius: 1.5, x: 0, y: 1) 123 | .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1) 124 | } 125 | .buttonStyle(ActionButtonAnimationStyle()) 126 | } 127 | 128 | /// A circular button with a filled background. 129 | func filledIconStyle() -> some View { 130 | Button(action: action) { 131 | icon() 132 | .frame(width: frameSize, height: frameSize) 133 | .filledStyledBackground() 134 | .clipShape(Circle()) 135 | } 136 | .buttonStyle(ActionButtonAnimationStyle()) 137 | } 138 | 139 | /// A circular button with a tonal background. 140 | func tonalIconStyle() -> some View { 141 | Button(action: action) { 142 | icon() 143 | .frame(width: frameSize, height: frameSize) 144 | .tonalStyledBackground() 145 | .clipShape(Circle()) 146 | } 147 | .buttonStyle(ActionButtonAnimationStyle()) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Navigation And Structure/Collection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection.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 collection of items arranged in a grid or list. 14 | @available(iOS 15.0, *) 15 | public struct Collection: View where Content: View { 16 | 17 | // MARK: - PROPERTIES 18 | 19 | private var content: Content 20 | private var style: CollectionStyle 21 | 22 | // MARK: - INITIALIZERS 23 | 24 | /// Creates a stylized collection with a default `.insetGrouped` list style. 25 | /// 26 | /// - Parameters: 27 | /// - content: A closure that returns the content view for a given element. 28 | public init(@ViewBuilder content: () -> Content) { 29 | self.content = content() 30 | self.style = .insetGrouped 31 | } 32 | 33 | /// Creates a stylized collection with a custom collection style. 34 | /// 35 | /// - Parameters: 36 | /// - style: The style of the collection(list), such as `.plain`, `.inset`, or `.insetGrouped`. 37 | /// - content: A closure that returns the content view for a given element. 38 | public init(style: CollectionStyle, @ViewBuilder _ content: () -> Content) { 39 | self.content = content() 40 | self.style = style 41 | } 42 | 43 | // MARK: - VIEW BODY 44 | 45 | public var body: some View { 46 | ScrollView(.vertical) { 47 | switch style { 48 | case .plain: 49 | plainStyle() 50 | 51 | case .inset: 52 | insetStyle() 53 | 54 | case .insetGrouped: 55 | insetGroupedStyle() 56 | } 57 | } 58 | } 59 | } 60 | 61 | // MARK: - FILE PRIVATE 62 | 63 | fileprivate extension Collection { 64 | 65 | /// Returns a plain-style list with a vertical stack of content items. 66 | func plainStyle() -> some View { 67 | _VariadicView.Tree(CollectionViewLayout(style: style)) { 68 | content 69 | } 70 | } 71 | 72 | /// Returns an inset-style list with rounded rectangles as background on individual list item. 73 | func insetStyle() -> some View { 74 | _VariadicView.Tree(CollectionViewLayout(style: style)) { 75 | content 76 | } 77 | } 78 | 79 | /// Returns an inset-grouped-style list with rounded rectangles as background. 80 | func insetGroupedStyle() -> some View { 81 | _VariadicView.Tree(CollectionViewLayout(style: style)) { 82 | content 83 | } 84 | } 85 | } 86 | 87 | /// A `_Variadic­View.Tree` with a `MUIListViewLayout` and list styles. 88 | fileprivate struct CollectionViewLayout: _VariadicView_UnaryViewRoot { 89 | 90 | // MARK: - Properties 91 | 92 | var style: CollectionStyle 93 | 94 | // MARK: - Initializers 95 | 96 | public init(style: CollectionStyle) { 97 | self.style = style 98 | } 99 | 100 | // MARK: - View Body 101 | 102 | @ViewBuilder 103 | public func body(children: _VariadicView.Children) -> some View { 104 | ScrollView { 105 | switch style { 106 | case .plain: 107 | plainStyle(children: children) 108 | 109 | case .inset: 110 | insetStyle(children: children) 111 | 112 | case .insetGrouped: 113 | insetGroupedStyle(children: children) 114 | } 115 | } 116 | } 117 | 118 | /// Returns a plain-style list with a vertical stack of content items. 119 | func plainStyle(children: _VariadicView.Children) -> some View { 120 | 121 | let last = children.last?.id 122 | 123 | return VStack(spacing: MaterialUIKit.configuration.verticalStackSpacing) { 124 | ForEach(children) { child in 125 | child 126 | .font(MaterialUIKit.configuration.h4) 127 | .fontWeightWithFallback(.regular) 128 | .foregroundStyle(.materialUIPrimaryTitle) 129 | .align(.leading) 130 | 131 | if child.id != last { 132 | Separator() 133 | } 134 | } 135 | } 136 | } 137 | 138 | /// Returns an inset-style list with rounded rectangles as background on individual list item. 139 | func insetStyle(children: _VariadicView.Children) -> some View { 140 | return VStack(spacing: MaterialUIKit.configuration.verticalStackSpacing) { 141 | ForEach(children) { child in 142 | child 143 | .font(MaterialUIKit.configuration.h4) 144 | .fontWeightWithFallback(.regular) 145 | .foregroundStyle(.materialUIPrimaryTitle) 146 | .align(.leading) 147 | .secondaryBackground() 148 | } 149 | } 150 | } 151 | 152 | /// Returns an inset-grouped-style list with rounded rectangles as background. 153 | func insetGroupedStyle(children: _VariadicView.Children) -> some View { 154 | let last = children.last?.id 155 | 156 | return VStack(spacing: MaterialUIKit.configuration.verticalStackSpacing) { 157 | ForEach(children) { child in 158 | child 159 | .font(MaterialUIKit.configuration.h4) 160 | .fontWeightWithFallback(.regular) 161 | .foregroundStyle(.materialUIPrimaryTitle) 162 | .align(.leading) 163 | 164 | if child.id != last { 165 | Separator() 166 | } 167 | } 168 | } 169 | .secondaryBackground() 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Navigation And Structure/Container.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Container.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 container that wraps and aligns its child elements. 12 | @available(iOS 15.0, *) 13 | public struct Container: View where Content: View { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | private let content: Content 18 | 19 | // MARK: - INITIALIZER 20 | 21 | /// Creates an empty container view. 22 | /// 23 | /// - Parameter content: The content to be wrapped in the container view. 24 | public init(@ViewBuilder content: () -> Content) { 25 | self.content = content() 26 | } 27 | 28 | // MARK: - VIEW BODY 29 | 30 | public var body: some View { 31 | ZStack { 32 | Color.materialUIPrimaryBackground.ignoresSafeArea(.all) 33 | 34 | VStack(spacing: MaterialUIKit.configuration.verticalStackSpacing) { 35 | content 36 | } 37 | .padding(MaterialUIKit.configuration.contentPadding) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Navigation And Structure/NavigationContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationContainer.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 container for managing navigation stack and layout. 14 | @available(iOS 15.0, *) 15 | public struct NavigationContainer: View where Content: View { 16 | 17 | // MARK: - PROPERTIES 18 | 19 | private let content: Content 20 | 21 | // MARK: - INITIALIZER 22 | 23 | /// Creates a navigation container with the specified content. 24 | /// 25 | /// - Parameter content: The content to be wrapped in the navigation container. 26 | public init(@ViewBuilder content: () -> Content) { 27 | self.content = content() 28 | } 29 | 30 | // MARK: - VIEW BODY 31 | 32 | public var body: some View { 33 | if #available(iOS 16.0, *) { 34 | NavigationStack { contentWrapper() } 35 | } else { 36 | NavigationView { contentWrapper() } 37 | } 38 | } 39 | 40 | /// A wrapper function that provides a custom-styled content view. 41 | private func contentWrapper() -> some View { 42 | ZStack { 43 | Color.materialUIPrimaryBackground.ignoresSafeArea(.all) 44 | 45 | TopAppBar { 46 | content 47 | .padding(MaterialUIKit.configuration.contentPadding) 48 | .padding(.bottom, -MaterialUIKit.configuration.contentPadding) 49 | } 50 | .navigationBarHidden(true) 51 | } 52 | } 53 | } 54 | 55 | extension View { 56 | 57 | /// Sets the title for the navigation container. 58 | /// 59 | /// - Parameter title: The title to be displayed in the navigation container. 60 | @available(iOS 15.0, *) 61 | public func navigationContainerTitle(_ title: String) -> some View { 62 | return self.preference(key: NavigationContainerTitlePreferenceKey.self, value: title) 63 | } 64 | 65 | /// Sets the style for the navigation container header. 66 | /// 67 | /// - Parameter style: The style of the navigation container header. 68 | @available(iOS 15.0, *) 69 | public func navigationContainerHeaderStyle(_ style: NavigationContainerHeaderStyle) -> some View { 70 | return self.preference(key: NavigationContainerHeaderStylePreferenceKey.self, value: style) 71 | } 72 | 73 | /// Sets the toolbar for the navigation container. 74 | /// 75 | /// - Parameter toolbar: The toolbar to be displayed in the navigation container. 76 | @available(iOS 15.0, *) 77 | public func navigationContainerToolbar(toolbar: () -> Toolbar) -> some View where Toolbar: View { 78 | return self.preference(key: NavigationContainerToolBarPreferenceKey.self, value: EquatableViewContainer(view: AnyView(toolbar()))) 79 | } 80 | 81 | /// Sets the visibility of the back button in the navigation container. 82 | /// 83 | /// - Parameter hidden: A Boolean value indicating whether the back button should be hidden. 84 | @available(iOS 15.0, *) 85 | public func navigationContainerBackButtonHidden(_ hidden: Bool) -> some View { 86 | return self.preference(key: NavigationContainerBackButtonHiddenPreferenceKey.self, value: !hidden) 87 | } 88 | 89 | /// Sets the properties for the navigation container’s top bar. 90 | /// 91 | /// - Parameters: 92 | /// - title: The title to be displayed in the navigation container. 93 | /// - backButtonHidden: A Boolean value indicating whether the back button should be hidden. 94 | /// - style: The style of the navigation container header. 95 | @available(iOS 15.0, *) 96 | public func navigationContainerTopBar(title: String, backButtonHidden: Bool, style: NavigationContainerHeaderStyle) -> some View { 97 | self 98 | .navigationContainerTitle(title) 99 | .navigationContainerBackButtonHidden(backButtonHidden) 100 | .navigationContainerHeaderStyle(style) 101 | } 102 | 103 | } 104 | 105 | // MARK: - FILE PRIVATE 106 | 107 | fileprivate struct ContainerHeader: View { 108 | 109 | // MARK: - Properties 110 | 111 | let toolbar: EquatableViewContainer 112 | let title: String 113 | let headerStyle: NavigationContainerHeaderStyle 114 | let showBackButton: Bool 115 | 116 | @Environment(\.dismiss) private var dismiss 117 | 118 | // MARK: - View Body 119 | 120 | var body: some View { 121 | switch headerStyle { 122 | case .large: 123 | largeHeader() 124 | case .inline: 125 | inlineHeader() 126 | } 127 | } 128 | 129 | /// Returns a navigation container header with large header style. 130 | private func largeHeader() -> some View { 131 | VStack(alignment: .leading, spacing: MaterialUIKit.configuration.verticalStackSpacing) { 132 | HStack { 133 | if showBackButton { 134 | Button { 135 | dismiss() 136 | hapticFeedback() 137 | } label: { 138 | Image(systemName: "arrow.left") 139 | .font(.title3) 140 | } 141 | .tint(.materialUIPrimaryTitle) 142 | } 143 | 144 | toolbar.view 145 | } 146 | .align(.leading) 147 | 148 | Text(title) 149 | .font(MaterialUIKit.configuration.hXL) 150 | .fontWeightWithFallback(.semibold) 151 | } 152 | .foregroundStyle(.materialUIPrimaryTitle) 153 | } 154 | 155 | /// Returns a navigation container header with inline header style. 156 | private func inlineHeader() -> some View { 157 | HStack(spacing: MaterialUIKit.configuration.horizontalStackSpacing) { 158 | if showBackButton { 159 | Button { 160 | dismiss() 161 | hapticFeedback() 162 | } label: { 163 | Image(systemName: "arrow.left") 164 | .font(.headline) 165 | } 166 | } 167 | 168 | Text(title) 169 | .font(MaterialUIKit.configuration.h3) 170 | .fontWeightWithFallback(.medium) 171 | 172 | Spacer() 173 | 174 | toolbar.view 175 | } 176 | .foregroundStyle(.materialUIPrimaryTitle) 177 | } 178 | } 179 | 180 | /// A container view that represents the navigation top bar and content of a screen. 181 | fileprivate struct TopAppBar: View where Content: View { 182 | 183 | // MARK: - PROPERTIES 184 | 185 | public let content: Content 186 | 187 | @State private var showBackButton: Bool = false 188 | @State private var headerStyle: NavigationContainerHeaderStyle = .large 189 | @State private var title: String = "" 190 | @State private var toolbar: EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView())) 191 | 192 | // MARK: - INITIALIZER 193 | 194 | public init(@ViewBuilder content: () -> Content) { 195 | self.content = content() 196 | } 197 | 198 | // MARK: - VIEW BODY 199 | 200 | public var body: some View { 201 | VStack(spacing: 0) { 202 | ContainerHeader( 203 | toolbar: toolbar, 204 | title: title, 205 | headerStyle: headerStyle, 206 | showBackButton: showBackButton 207 | ) 208 | .padding(.top, 10) 209 | .padding(.horizontal, MaterialUIKit.configuration.contentPadding) 210 | 211 | content 212 | .frame(maxWidth: .infinity, maxHeight: .infinity) 213 | } 214 | // Sets navigation title 215 | .onPreferenceChange(NavigationContainerTitlePreferenceKey.self) { value in 216 | self.title = value 217 | } 218 | // Sets the visiblity for back button 219 | .onPreferenceChange(NavigationContainerBackButtonHiddenPreferenceKey.self) { value in 220 | self.showBackButton = value 221 | } 222 | // Sets the navigation title style 223 | .onPreferenceChange(NavigationContainerHeaderStylePreferenceKey.self) { value in 224 | self.headerStyle = value 225 | } 226 | // Sets the toolbar 227 | .onPreferenceChange(NavigationContainerToolBarPreferenceKey.self) { value in 228 | toolbar = value 229 | } 230 | } 231 | } 232 | 233 | /// Sets the title for the navigation bar. 234 | fileprivate struct NavigationContainerTitlePreferenceKey: PreferenceKey { 235 | static var defaultValue: String = "" 236 | 237 | static func reduce(value: inout String, nextValue: () -> String) { 238 | value = nextValue() 239 | } 240 | } 241 | 242 | /// Sets the visibility of the back button in the navigation bar. 243 | fileprivate struct NavigationContainerBackButtonHiddenPreferenceKey: PreferenceKey { 244 | static var defaultValue: Bool = false 245 | 246 | static func reduce(value: inout Bool, nextValue: () -> Bool) { 247 | value = nextValue() 248 | } 249 | } 250 | 251 | /// Sets the style for the navigation bar header. 252 | fileprivate struct NavigationContainerHeaderStylePreferenceKey: PreferenceKey { 253 | static var defaultValue: NavigationContainerHeaderStyle = .large 254 | 255 | static func reduce(value: inout NavigationContainerHeaderStyle, nextValue: () -> NavigationContainerHeaderStyle) { 256 | value = nextValue() 257 | } 258 | } 259 | 260 | /// Sets the toolbar for the navigation bar. 261 | fileprivate struct NavigationContainerToolBarPreferenceKey: PreferenceKey { 262 | static var defaultValue: EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()) ) 263 | 264 | static func reduce(value: inout EquatableViewContainer, nextValue: () -> EquatableViewContainer) { 265 | value = nextValue() 266 | } 267 | } 268 | 269 | /// A container to hold an equatable view. 270 | fileprivate struct EquatableViewContainer: Equatable { 271 | let id = UUID().uuidString 272 | let view:AnyView 273 | 274 | static func == (lhs: EquatableViewContainer, rhs: EquatableViewContainer) -> Bool { 275 | return lhs.id == rhs.id 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Navigation And Structure/NavigationRoute.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationRoute.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 24/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Represents a Material UI-styled navigation route for transitioning between views. 12 | @available(iOS 15.0, *) 13 | public struct NavigationRoute: View where Label: View, Destination: View { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | private let destination: Destination 18 | private let label: Label 19 | 20 | // MARK: - INITIALIZER 21 | 22 | /// Creates a `NavigationRoute` with the specified destination and label. 23 | /// 24 | /// - Parameters: 25 | /// - destination: A closure that returns the view to navigate to. 26 | /// - label: A closure that returns the view used as the navigation trigger. 27 | public init(@ViewBuilder destination: () -> Destination, @ViewBuilder label: () -> Label) { 28 | self.destination = destination() 29 | self.label = label() 30 | } 31 | 32 | // MARK: - VIEW BODY 33 | 34 | public var body: some View { 35 | NavigationLink { 36 | destination 37 | .navigationBarHidden(true) 38 | } label: { 39 | label 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Navigation And Structure/NavigationRouteLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationContainerLabel.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 label for navigation routes, used to display route names in a navigation container. 12 | @available(iOS 15.0, *) 13 | public struct NavigationRouteLabel: View { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | private let systemImage: String 18 | private let titleKey: String 19 | 20 | // MARK: - INITIALIZERS 21 | 22 | /// Creates a `NavigationRouteLabel` instance with the specified system image and title. 23 | /// 24 | /// - Parameters: 25 | /// - systemImage: The name of the system image to be used as the label's icon. 26 | /// - titleKey: The key for the title text to be displayed. 27 | public init(systemImage: String, _ titleKey: String) { 28 | self.systemImage = systemImage 29 | self.titleKey = titleKey 30 | } 31 | 32 | // MARK: - VIEW BODY 33 | 34 | public var body: some View { 35 | HStack(spacing: MaterialUIKit.configuration.horizontalStackSpacing) { 36 | Image(systemName: systemImage) 37 | .font(.subheadline) 38 | .padding(10) 39 | .foregroundStyle(.materialUIHighlight) 40 | .background(.materialUITertiaryBackground) 41 | .clipShape(Circle()) 42 | 43 | Text(titleKey) 44 | .font(MaterialUIKit.configuration.h4) 45 | .fontWeightWithFallback(.regular) 46 | .foregroundStyle(.materialUIPrimaryTitle) 47 | 48 | Spacer() 49 | 50 | Image(systemName: "chevron.right") 51 | .foregroundStyle(.materialUISecondaryTitle) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Navigation And Structure/SegmentedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentedButton.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 segmented control for switching between options. 12 | @available(iOS 15.0, *) 13 | public struct SegmentedButton: View where Data: RandomAccessCollection, ID: Hashable, Content: View, Data.Element: Hashable, Data: Hashable { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | private var data: Data 18 | private var id: KeyPath 19 | private var content: (Data.Element) -> Content 20 | 21 | @Binding private var selection: Data.Element 22 | @Namespace private var namespace 23 | 24 | // MARK: - INITIALIZER 25 | 26 | /// Creates a segemented control with the given data, identifier, and content. 27 | /// 28 | /// - Parameters: 29 | /// - data: A collection of elements to display in the segmented control. 30 | /// - id: A key path to an `ID` property on each element to uniquely identify them. 31 | /// - selection: A binding to the currently selected element in the segmented control. 32 | /// - content: A closure that returns the content view for a given element. 33 | public init( 34 | _ data: Data, 35 | id: KeyPath, 36 | selection: Binding, 37 | @ViewBuilder _ content: @escaping (Data.Element) -> Content 38 | ) { 39 | self.data = data 40 | self.id = id 41 | self._selection = selection 42 | self.content = content 43 | } 44 | 45 | // MARK: - VIEW BODY 46 | 47 | public var body: some View { 48 | HStack(spacing: .zero) { 49 | ForEach(data, id: id) { item in 50 | ZStack { 51 | if item == selection { 52 | RoundedRectangle(cornerRadius: .zero) 53 | .foregroundStyle(.materialUIHighlight.opacity(0.2)) 54 | .matchedGeometryEffect(id: "selectedTabBackground", in: namespace) 55 | } 56 | 57 | content(item) 58 | .tag(item) 59 | .font(MaterialUIKit.configuration.h4) 60 | .foregroundStyle(.materialUIPrimaryTitle) 61 | .fontWeightWithFallback(item == selection ? .semibold : .regular) 62 | .padding(.vertical, MaterialUIKit.configuration.verticalPadding) 63 | .frame(maxWidth: .infinity) 64 | .onTapGesture { 65 | withMaterialAnimation { 66 | selection = item 67 | } 68 | } 69 | } 70 | 71 | if !isLastElement(data: data, item: item) { 72 | RoundedRectangle(cornerRadius: 0.5) 73 | .foregroundStyle(.materialUIOutline) 74 | .frame(width: MaterialUIKit.configuration.borderWidth) 75 | } 76 | } 77 | } 78 | .frame(maxWidth: .infinity, maxHeight: 40) 79 | .background(.materialUIPrimaryBackground) 80 | .cornerRadius(MaterialUIKit.configuration.cornerRadius) 81 | .stroke(background: .materialUIOutline) 82 | .hapticFeedbackOnChange(of: selection) 83 | } 84 | 85 | /// Checks if the current element is the last one in the collection. 86 | fileprivate func isLastElement(data: Data, item: Data.Element) -> Bool { 87 | return data.last == item 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Navigation And Structure/TabBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBar.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 container for bottom tab items, used to organize and switch between different views. 14 | @available(iOS 15.0, *) 15 | public struct TabBar: View where Content: View { 16 | 17 | // MARK: - PROPERTIES 18 | 19 | private let content: Content 20 | 21 | @Binding private var selection: TabBarItem 22 | @State private var tabs: [TabBarItem] = [] 23 | 24 | // MARK: - INITIALIZER 25 | 26 | /// Creates a container view with a bottom tab bar. 27 | /// 28 | /// - Parameters: 29 | /// - selection: Binding to the selected tab item. 30 | /// - content: The main content of the view. 31 | public init(selection: Binding, @ViewBuilder content: () -> Content) { 32 | self._selection = selection 33 | self.content = content() 34 | } 35 | 36 | // MARK: - View BODY 37 | 38 | public var body: some View { 39 | ZStack(alignment: .bottom) { 40 | content 41 | .frame(maxWidth: .infinity, maxHeight: .infinity) 42 | .ignoresSafeArea(edges: .bottom) 43 | 44 | TabBarContainer(tabs: tabs, selection: $selection, localSelection: selection) 45 | } 46 | .ignoresSafeArea(.keyboard) 47 | .onPreferenceChange(TabBarPreferenceKey.self) { value in 48 | self.tabs = value 49 | } 50 | .hapticFeedbackOnChange(of: selection) 51 | } 52 | } 53 | 54 | extension View { 55 | 56 | /// Sets up a tab bar item with the specified system image, title, and selection binding. 57 | /// 58 | /// - Parameters: 59 | /// - systemImage: The name of the system image for the tab item. 60 | /// - titleKey: The title text for the tab item. 61 | /// - selection: Binding to the selected tab item. 62 | public func tabBarItem(systemImage: String, titleKey: String, selection: Binding) -> some View { 63 | modifier( 64 | TabBarItemViewModifer( 65 | tab: TabBarItem( 66 | systemImage: systemImage, 67 | titleKey: titleKey 68 | ), 69 | selection: selection 70 | ) 71 | ) 72 | } 73 | } 74 | 75 | // MARK: - FILE PRIVATE 76 | 77 | /// A view that represents the MaterialUI-style tab bar. 78 | fileprivate struct TabBarContainer: View { 79 | 80 | // MARK: - PROPERTIES 81 | 82 | let tabs: [TabBarItem] 83 | @Binding var selection: TabBarItem 84 | @State var localSelection: TabBarItem 85 | 86 | @Namespace private var namespace 87 | 88 | // MARK: - VIEW BODY 89 | 90 | var body: some View { 91 | tabBar 92 | .onChangeWithFallback(of: selection) { oldValue, newValue in 93 | withMaterialAnimation { 94 | localSelection = newValue 95 | } 96 | } 97 | } 98 | 99 | /// Returns tab bar. 100 | fileprivate var tabBar: some View { 101 | HStack { 102 | ForEach(tabs, id: \.self) { tab in 103 | VStack(alignment: .center , spacing: 4) { 104 | Image(systemName: tab.systemImage) 105 | .padding(.horizontal, MaterialUIKit.configuration.horizontalPadding) 106 | .padding(.vertical, 5) 107 | .font(.headline) 108 | .foregroundStyle(localSelection == tab ? .materialUITonal : .materialUIPrimaryTitle) 109 | .background( 110 | ZStack { 111 | if localSelection == tab { 112 | RoundedRectangle(cornerRadius: .infinity) 113 | .fill(.materialUIAccent) 114 | .matchedGeometryEffect(id: "backgroundRectangle", in: namespace) 115 | } 116 | } 117 | ) 118 | 119 | Text(tab.titleKey) 120 | .font(.footnote) 121 | .foregroundStyle(.materialUIPrimaryTitle) 122 | } 123 | .frame(maxWidth: .infinity) 124 | .onTapGesture { 125 | selection = tab 126 | } 127 | } 128 | } 129 | .padding(.vertical, 10) 130 | .background( 131 | MaterialUIKit.configuration.colorScheme.secondaryBackground 132 | .ignoresSafeArea(edges: [.horizontal, .bottom]) 133 | ) 134 | } 135 | } 136 | 137 | /// A preference key to collect tab bar items for rendering. 138 | fileprivate struct TabBarPreferenceKey: PreferenceKey { 139 | static var defaultValue: [TabBarItem] = [] 140 | 141 | static func reduce(value: inout [TabBarItem], nextValue: () -> [TabBarItem]) { 142 | value += nextValue() 143 | } 144 | } 145 | 146 | 147 | /// A view modifier to handle the appearance of tab bar items. 148 | fileprivate struct TabBarItemViewModifer: ViewModifier { 149 | 150 | let tab: TabBarItem 151 | @Binding var selection: TabBarItem 152 | 153 | /// Applies the view modifier to handle the appearance of tab bar items. 154 | @ViewBuilder 155 | func body(content: Content) -> some View { 156 | if selection == tab { 157 | ZStack { 158 | MaterialUIKit.configuration.colorScheme.primaryBackground 159 | content 160 | } 161 | .opacity(1) 162 | .preference(key: TabBarPreferenceKey.self, value: [tab]) 163 | } else { 164 | Text("") 165 | .opacity(0) 166 | .preference(key: TabBarPreferenceKey.self, value: [tab]) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Presentation/ProgressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressBar.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 progress indicator for showing ongoing processes or loading states. 14 | @available(iOS 15.0, *) 15 | public struct ProgressBar: View { 16 | 17 | // MARK: - PROPERTIES 18 | 19 | private let lineWidth: CGFloat 20 | 21 | @State private var rotationAngle: Double = 0 22 | private let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect() 23 | 24 | // MARK: - INITIALIZER 25 | 26 | /// Creates a progress view with default line width of 2 points. 27 | public init() { 28 | lineWidth = 2 29 | } 30 | 31 | /// Creates a progress view with a custom line width. 32 | /// 33 | /// - Parameters: 34 | /// - lineWidth: Sets the stroke of progress view. 35 | public init(lineWidth: CGFloat) { 36 | self.lineWidth = lineWidth 37 | } 38 | 39 | // MARK: - VIEW BODY 40 | 41 | public var body: some View { 42 | MaterialProgressArc() 43 | .stroke(.materialUIAccent, lineWidth: lineWidth) 44 | .frame(width: 35, height: 35) 45 | .rotationEffect(Angle(degrees: rotationAngle)) 46 | .onReceive(timer) { _ in 47 | withMaterialAnimation { 48 | rotationAngle += 40 49 | } 50 | } 51 | } 52 | } 53 | 54 | // MARK: - FILE PRIVATE 55 | 56 | /// A custom shape for the `ProgressBar`. 57 | fileprivate struct MaterialProgressArc: Shape { 58 | func path(in rect: CGRect) -> Path { 59 | let radius = min(rect.width, rect.height) / 2 60 | let center = CGPoint(x: rect.midX, y: rect.midY) 61 | let startAngle: Angle = .degrees(0) 62 | let endAngle: Angle = .degrees(360 * 0.8) // 80% of the circle 63 | 64 | var path = Path() 65 | path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) 66 | 67 | return path 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Presentation/Separator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Separator.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 separator for dividing content or sections within a view. 12 | @available(iOS 15.0, *) 13 | public struct Separator: View { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | private let orientation: SeparatorOrientationStyle 18 | 19 | // MARK: - INITIALIZER 20 | 21 | /// Creates a separator with default horizontal orientation. 22 | public init() { 23 | self.orientation = .horizontal 24 | } 25 | 26 | /// Creates a separator with the specified orientation. 27 | /// 28 | /// - Parameter orientation: The orientation of the separator. 29 | public init(orientation: SeparatorOrientationStyle) { 30 | self.orientation = orientation 31 | } 32 | 33 | // MARK: - VIEW BODY 34 | 35 | public var body: some View { 36 | RoundedRectangle(cornerRadius: MaterialUIKit.configuration.cornerRadius) 37 | .frame( 38 | width: orientation == .vertical ? MaterialUIKit.configuration.borderWidth : nil, 39 | height: orientation == .horizontal ? MaterialUIKit.configuration.borderWidth : nil 40 | ) 41 | .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil) 42 | .padding(2) 43 | .foregroundStyle(.materialUISeparator) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Components/Search/SearchBox.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBox.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 search box for user input and search actions. 12 | @available(iOS 15.0, *) 13 | public struct SearchBox: View { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | private let placeholder: String 18 | private let action: () -> Void 19 | @Binding private var searchText: String 20 | @State private var showSearchButton: Bool = false 21 | 22 | // MARK: - INITIALIZERS 23 | 24 | /// Creates a search box with a default placeholder text. 25 | /// 26 | /// - Parameters: 27 | /// - searchText: A binding to the text that the user enters into the search bar. 28 | /// - action: A closure to execute when the user triggers the search action. 29 | public init(searchText: Binding, _ action: @escaping () -> Void) { 30 | self.placeholder = "Search" 31 | self._searchText = searchText 32 | self.action = action 33 | } 34 | 35 | /// Creates a search box with a custom placeholder text. 36 | /// 37 | /// - Parameters: 38 | /// - placeholder: The placeholder text to display when the search bar is empty. 39 | /// - searchText: A binding to the text that the user enters into the search bar. 40 | /// - action: A closure to execute when the user triggers the search action. 41 | public init(_ placeholder: String, searchText: Binding, _ action: @escaping () -> Void) { 42 | self.placeholder = placeholder 43 | self._searchText = searchText 44 | self.action = action 45 | } 46 | 47 | // MARK: - VIEW BODY 48 | 49 | public var body: some View { 50 | HStack { 51 | HStack(spacing: MaterialUIKit.configuration.horizontalStackSpacing) { 52 | Image(systemName: "magnifyingglass") 53 | .font(.headline) 54 | .foregroundStyle(.materialUISecondaryTitle) 55 | 56 | TextField(placeholder, text: $searchText) 57 | .keyboardType(.webSearch) 58 | .tint(.materialUIAccent) 59 | 60 | Spacer() 61 | 62 | if !(searchText.isEmpty) { 63 | Button { 64 | withMaterialAnimation { 65 | searchText = "" 66 | showSearchButton = false 67 | } 68 | } label: { 69 | Image(systemName: "xmark.circle.fill") 70 | .foregroundStyle(.materialUISecondaryTitle) 71 | } 72 | } 73 | } 74 | .padding(.vertical, MaterialUIKit.configuration.verticalPadding) 75 | .padding(.horizontal, MaterialUIKit.configuration.horizontalPadding) 76 | .background(.materialUISecondaryBackground) 77 | .cornerRadius(.infinity) 78 | .stroke(cornerRadius: .infinity) 79 | .tint(.materialUIAccent) 80 | 81 | if showSearchButton { 82 | Button("Search") { 83 | action() 84 | hapticFeedback() 85 | } 86 | .foregroundStyle(.materialUIAccent) 87 | .fontWeightWithFallback(.medium) 88 | .buttonStyle(ActionButtonAnimationStyle()) 89 | } 90 | } 91 | .onChangeWithFallback(of: searchText) { oldValue, newValue in 92 | withMaterialAnimation { 93 | if !(newValue.isEmpty) { 94 | showSearchButton = true 95 | } 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Configuration/MUIKitConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.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 | /// Configuration settings for MaterialUIKit. 12 | /// 13 | /// The `MUIKitConfiguration` struct provides a centralized way to define and manage the configuration settings for the MaterialUIKit components. 14 | /// This includes various design attributes such as padding, font styles, spacing, and color schemes, which are used to maintain a consistent and customizable look and feel across the UI components. 15 | @available(iOS 15.0, *) 16 | public struct MUIKitConfiguration { 17 | 18 | /// The padding applied inside UI elements to separate content from borders. 19 | /// 20 | /// Defaults to `20`. 21 | public var contentPadding: CGFloat 22 | 23 | /// The font style used for the extra large header (H-XL). 24 | /// 25 | /// Defaults to `.largeTitle`. 26 | public var hXL: Font 27 | 28 | /// The font style used for the largest header (H1). 29 | /// 30 | /// Defaults to `.title`. 31 | public var h1: Font 32 | 33 | /// The font style used for the second largest header (H2). 34 | /// 35 | /// Defaults to `.title2`. 36 | public var h2: Font 37 | 38 | /// The font style used for the third largest header (H3). 39 | /// 40 | /// Defaults to `.title3`. 41 | public var h3: Font 42 | 43 | /// The font style used for medium-sized headers (H4). 44 | /// 45 | /// Defaults to `.body`. 46 | public var h4: Font 47 | 48 | /// The font style used for smaller headers (H5). 49 | /// 50 | /// Defaults to `.subheadline`. 51 | public var h5: Font 52 | 53 | /// The font style used for the smallest headers (H6). 54 | /// 55 | /// Defaults to `.caption`. 56 | public var h6: Font 57 | 58 | /// The horizontal padding applied to UI elements. 59 | /// 60 | /// Defaults to `16`. 61 | public var horizontalPadding: CGFloat 62 | 63 | /// The horizontal padding applied to UI elements. 64 | /// 65 | /// Defaults to `16`. 66 | public var verticalPadding: CGFloat 67 | 68 | /// The corner radius applied to UI elements. 69 | /// 70 | /// Defaults to `20`. 71 | public var cornerRadius: CGFloat 72 | 73 | /// The vertical spacing applied between elements in a vertical layout. 74 | /// 75 | /// Defaults to `16`. 76 | public var verticalStackSpacing: CGFloat 77 | 78 | /// The horizontal spacing applied between elements in a horizontal layout. 79 | /// 80 | /// Defaults to `12`. 81 | public var horizontalStackSpacing: CGFloat 82 | 83 | /// The width of the border applied to UI elements. 84 | /// 85 | /// Defaults to `1.0. 86 | public var borderWidth: CGFloat 87 | 88 | /// The type of animation applied to UI elements. 89 | /// 90 | /// Defaults to `.spring()`. 91 | public var animationType: Animation 92 | 93 | /// The intensity of haptic feedback for interactive elements. 94 | /// 95 | /// Defaults to `0.8`. 96 | public var hapticFeedbackIntensity: CGFloat 97 | 98 | /// The intensity of haptic feedback for interactive elements. 99 | /// 100 | /// Defaults to `.light`. 101 | public var hapticFeedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle 102 | 103 | /// The color scheme applied to UI elements. 104 | /// 105 | /// Defaults to `MaterialClassic()`. 106 | public var colorScheme: MUIKitColorScheme 107 | 108 | /// Initializes a `MaterialUIKitConfiguration` instance with default values. 109 | public init() { 110 | self.contentPadding = 20 111 | self.hXL = .largeTitle 112 | self.h1 = .title 113 | self.h2 = .title2 114 | self.h3 = .title3 115 | self.h4 = .body 116 | self.h5 = .subheadline 117 | self.h6 = .caption 118 | self.horizontalPadding = 16 119 | self.verticalPadding = 16 120 | self.cornerRadius = 20 121 | self.verticalStackSpacing = 16 122 | self.horizontalStackSpacing = 12 123 | self.borderWidth = 1.0 124 | self.animationType = .spring(duration: 0.4) 125 | self.hapticFeedbackIntensity = 0.6 126 | self.hapticFeedbackStyle = .light 127 | self.colorScheme = MaterialClassic() 128 | } 129 | 130 | /// Initializes a `MUIKitConfiguration` instance with specified values for each property. 131 | public init( 132 | contentPadding: CGFloat, 133 | hXL: Font, 134 | h1: Font, 135 | h2: Font, 136 | h3: Font, 137 | h4: Font, 138 | h5: Font, 139 | h6: Font, 140 | horizontalPadding: CGFloat, 141 | verticalPadding: CGFloat, 142 | cornerRadius: CGFloat, 143 | verticalStackSpacing: CGFloat, 144 | horizontalStackSpacing: CGFloat, 145 | borderWidth: CGFloat, 146 | animationDuration: Double, 147 | animationType: Animation, 148 | hapticFeedbackIntensity: CGFloat, 149 | hapticFeedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle, 150 | colorScheme: MUIKitColorScheme 151 | ) { 152 | self.contentPadding = contentPadding 153 | self.hXL = hXL 154 | self.h1 = h1 155 | self.h2 = h2 156 | self.h3 = h3 157 | self.h4 = h4 158 | self.h5 = h5 159 | self.h6 = h6 160 | self.horizontalPadding = horizontalPadding 161 | self.verticalPadding = verticalPadding 162 | self.cornerRadius = cornerRadius 163 | self.verticalStackSpacing = verticalStackSpacing 164 | self.horizontalStackSpacing = horizontalStackSpacing 165 | self.borderWidth = borderWidth 166 | self.animationType = animationType 167 | self.hapticFeedbackIntensity = hapticFeedbackIntensity 168 | self.hapticFeedbackStyle = hapticFeedbackStyle 169 | self.colorScheme = colorScheme 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Configuration/MaterialUIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MaterialUIKit.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 | /// MaterialUIKit module containing configuration settings. 12 | /// 13 | /// This class provides a centralized place for configuring global settings for MaterialUIKit components. 14 | /// It includes options for setting layout, styling, and feedback properties that apply to all MaterialUIKit elements. 15 | @available(iOS 15.0, *) 16 | open class MaterialUIKit { 17 | 18 | /// A static instance of `MUIKitConfiguration` that holds the global configuration settings for MaterialUIKit components. 19 | /// 20 | /// Use this property to customize the appearance and behavior of MaterialUIKit components throughout your app. 21 | public static var configuration: MUIKitConfiguration = MUIKitConfiguration() 22 | } 23 | 24 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Environment Keys/CornerRadiusKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CornerRadiusKey.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 24/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Environment key for setting the corner radius. 12 | package struct CornerRadiusKey: EnvironmentKey { 13 | package static var defaultValue: CGFloat = MaterialUIKit.configuration.cornerRadius 14 | } 15 | 16 | package extension EnvironmentValues { 17 | var cornerRadius: CGFloat { 18 | get { self[CornerRadiusKey.self] } 19 | set { self[CornerRadiusKey.self] = newValue } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Environment Keys/FontKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontKey.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 24/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Environment key for setting the font. 12 | package struct FontKey: EnvironmentKey { 13 | package static var defaultValue: Font = MaterialUIKit.configuration.h4 14 | } 15 | 16 | package extension EnvironmentValues { 17 | var font: Font { 18 | get { self[FontKey.self] } 19 | set { self[FontKey.self] = newValue } 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Environment Keys/FontWeightKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FontWeightKey.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 24/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Environment key for setting the font weight. 12 | package struct FontWeightKey: EnvironmentKey { 13 | package static var defaultValue: Font.Weight = .medium 14 | } 15 | 16 | package extension EnvironmentValues { 17 | var fontWeight: Font.Weight { 18 | get { self[FontWeightKey.self] } 19 | set { self[FontWeightKey.self] = newValue } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Environment Keys/FrameSizeKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameSizeKey.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 24/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Environment key for setting the frame size. 12 | package struct FrameSizeKey: EnvironmentKey { 13 | package static var defaultValue: CGFloat = 20 14 | } 15 | 16 | package extension EnvironmentValues { 17 | var frameSize: CGFloat { 18 | get { self[FrameSizeKey.self] } 19 | set { self[FrameSizeKey.self] = newValue } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Internal/Color+UIColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+UIColors.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 7/01/24 7 | // 8 | 9 | import SwiftUI 10 | import UIKit 11 | 12 | /// Extension on `UIColor` providing a convenience initializer to create a color from a hex string. 13 | package extension UIColor { 14 | 15 | /// Initializes a color with a hex string and an optional alpha value. 16 | /// 17 | /// - Parameters: 18 | /// - hex: A hex string representing the color. 19 | /// - alpha: An optional alpha value (default is 1.0). 20 | convenience init(hex: String, alpha: CGFloat = 1.0) { 21 | var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines) 22 | hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "") 23 | 24 | var rgb: UInt64 = 0 25 | 26 | Scanner(string: hexSanitized).scanHexInt64(&rgb) 27 | 28 | let red = CGFloat((rgb & 0xFF0000) >> 16) / 255.0 29 | let green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 30 | let blue = CGFloat(rgb & 0x0000FF) / 255.0 31 | 32 | self.init(red: red, green: green, blue: blue, alpha: alpha) 33 | } 34 | } 35 | 36 | /// Extension on `Color` providing an initializer that dynamically selects a color based on the current user interface style. 37 | package extension Color { 38 | 39 | /// Initializes a color with light and dark hex strings. 40 | /// 41 | /// - Parameters: 42 | /// - lightHex: The hex string for the light mode color. 43 | /// - darkHex: The hex string for the dark mode color. 44 | init(lightHex: String, darkHex: String) { 45 | self = Color(UIColor { (traitCollection) -> UIColor in 46 | if traitCollection.userInterfaceStyle == .dark { 47 | return UIColor(hex: darkHex) 48 | } else { 49 | return UIColor(hex: lightHex) 50 | } 51 | }) 52 | } 53 | } 54 | 55 | 56 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Internal/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date.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 | /// An extension on `Date` to format dates in a specific format. 12 | package extension Date { 13 | 14 | /// Formats the date in the "E, MMM D" style. 15 | func formattedMUIDate() -> String { 16 | let dateFormatter = DateFormatter() 17 | dateFormatter.dateFormat = "E, MMM d" 18 | return dateFormatter.string(from: self) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Internal/StyledBackgroundModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StyledBackgroundModifiers.swift.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 | package extension View { 12 | 13 | /// Adds an elevated background and rounded corners. 14 | func elevatedStyledBackground() -> some View { 15 | return self 16 | .foregroundStyle(.materialUIHighlight) 17 | .contentPadding() 18 | .background(.materialUITertiaryBackground) 19 | .shadow(color: .black.opacity(0.15), radius: 1.5, x: 0, y: 1) 20 | .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1) 21 | } 22 | 23 | /// Adds a filled background and rounded corners. 24 | func filledStyledBackground() -> some View { 25 | return self 26 | .foregroundStyle(.materialUITonal) 27 | .contentPadding() 28 | .background(.materialUIAccent) 29 | } 30 | 31 | /// Adds a tonal background and rounded corners. 32 | func tonalStyledBackground() -> some View { 33 | return self 34 | .foregroundStyle(.materialUIHighlight) 35 | .contentPadding() 36 | .background(.materialUISecondaryBackground) 37 | } 38 | 39 | /// Adds a outline background and rounded corners. 40 | func outlineStyledBackground(cornerRadius: CGFloat? = nil) -> some View { 41 | return self 42 | .foregroundStyle(.materialUIAccent) 43 | .contentPadding() 44 | .overlay( 45 | RoundedRectangle(cornerRadius: cornerRadius ?? .infinity) 46 | .stroke(.materialUIAccent, lineWidth: MaterialUIKit.configuration.borderWidth + 0.5) 47 | ) 48 | } 49 | 50 | /// Adds a text-only button with no background. 51 | func textStyledBackground(_ padding: CGFloat? = nil) -> some View { 52 | return self 53 | .foregroundStyle(.materialUIAccent) 54 | .padding(padding ?? 10) 55 | } 56 | 57 | /// Adds elevated background and rounded corners, occupying full available width. 58 | func elevatedStretchedStyledBackground(cornerRadius: CGFloat? = nil) -> some View { 59 | return self 60 | .foregroundStyle(.materialUIHighlight) 61 | .extendedWidthPadded() 62 | .background(.materialUITertiaryBackground) 63 | .shadow(color: .black.opacity(0.15), radius: 1.5, x: 0, y: 1) 64 | .shadow(color: .black.opacity(0.3), radius: 1, x: 0, y: 1) 65 | } 66 | 67 | /// Adds a filled background and rounded corners, occupying full available width. 68 | func filledStretchedStyledBackground(cornerRadius: CGFloat? = nil) -> some View { 69 | return self 70 | .foregroundStyle(.materialUITonal) 71 | .extendedWidthPadded() 72 | .background(.materialUIAccent) 73 | } 74 | 75 | /// Adds a tonal background and rounded corners, occupying full available width. 76 | func tonalStretchedStyledBackground(cornerRadius: CGFloat? = nil) -> some View { 77 | return self 78 | .foregroundStyle(.materialUIHighlight) 79 | .extendedWidthPadded() 80 | .background(.materialUISecondaryBackground) 81 | } 82 | 83 | /// Adds an outlined border and rounded corners, occupying full available width. 84 | func outlineStretchedStyledBackground(cornerRadius: CGFloat? = nil) -> some View { 85 | return self 86 | .foregroundStyle(.materialUIAccent) 87 | .extendedWidthPadded() 88 | .overlay( 89 | RoundedRectangle(cornerRadius: cornerRadius ?? .infinity) 90 | .stroke(.materialUIAccent, lineWidth: MaterialUIKit.configuration.borderWidth + 0.5) 91 | .frame(maxWidth: .infinity) 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Internal/UINavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 24/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// An extension for `UINavigationController` to customize its behavior. 12 | extension UINavigationController { 13 | 14 | /// Overrides the `viewDidLoad` method to disable the interactive pop gesture recognizer's delegate. 15 | open override func viewDidLoad() { 16 | super.viewDidLoad() 17 | interactivePopGestureRecognizer?.delegate = nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Internal/View+FallbackModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+FallbackModifiers.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 20/01/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | package extension View { 12 | 13 | /// Adjusts font weight with fallback for older iOS versions. 14 | /// 15 | /// - Parameter weight: The font weight to apply of type `Font.Weight`. 16 | func fontWeightWithFallback(_ weight: Font.Weight) -> some View { 17 | if #available(iOS 16.0, *) { 18 | return self 19 | .fontWeight(weight) 20 | } else { 21 | return self 22 | .font(Font.headline.weight(weight)) 23 | } 24 | } 25 | 26 | /// Adds a modifier for this view that fires an action when a specific value changes with fallback for older iOS versions. 27 | /// 28 | /// - Parameters: 29 | /// - value: The value to check against when determining whether 30 | /// to run the closure. 31 | /// - initial: Whether the action should be run when this view initially 32 | /// appears. 33 | /// - action: A closure to run when the value changes. 34 | /// - oldValue: The old value that failed the comparison check (or the 35 | /// initial value when requested). 36 | /// - newValue: The new value that failed the comparison check. 37 | func onChangeWithFallback(of value: V, initial: Bool = false, _ action: @escaping (_ oldValue: V, _ newValue: V) -> Void) -> some View where V: Equatable { 38 | 39 | if #available(iOS 17.0, *) { 40 | return self 41 | .onChange(of: value) { oldValue, newValue in 42 | action(oldValue, newValue) 43 | } 44 | } else { 45 | return self 46 | .onChange(of: value, perform: { newValue in 47 | action(newValue, newValue) 48 | }) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Internal/ViewModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModifiers.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 09/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | package extension View { 12 | 13 | /// Applies a stroke to the view using the configuration settings for border width, color, and corner radius. 14 | /// 15 | /// - Parameter cornerRadius: The corner radius for the stroke. If `nil`, the radius from the configuration is used. 16 | func stroke(background: Color? = nil, cornerRadius: CGFloat? = nil) -> some View { 17 | self 18 | .padding(MaterialUIKit.configuration.borderWidth) 19 | .background(background ?? .materialUIOutline) 20 | .cornerRadius(cornerRadius ?? MaterialUIKit.configuration.cornerRadius) 21 | } 22 | 23 | /// Applies a material primary background with corner radius and margin to the view. 24 | /// 25 | /// - Parameter cornerRadius: The corner radius for the stroke. If `nil`, the radius from the configuration is used. 26 | func primaryBackground(cornerRadius: CGFloat? = nil) -> some View { 27 | self 28 | .padding(MaterialUIKit.configuration.contentPadding) 29 | .background(.materialUIPrimaryBackground) 30 | .cornerRadius(cornerRadius ?? MaterialUIKit.configuration.cornerRadius) 31 | } 32 | 33 | /// Applies a material secondary background with corner radius and margin to the view. 34 | /// 35 | /// - Parameter cornerRadius: The corner radius for the stroke. If `nil`, the radius from the configuration is used. 36 | func secondaryBackground(cornerRadius: CGFloat? = nil) -> some View { 37 | self 38 | .padding(MaterialUIKit.configuration.contentPadding) 39 | .background(.materialUISecondaryBackground) 40 | .cornerRadius(cornerRadius ?? MaterialUIKit.configuration.cornerRadius) 41 | } 42 | 43 | /// Applies a material tertiary background with corner radius and margin to the view. 44 | /// 45 | /// - Parameter cornerRadius: The corner radius for the stroke. If `nil`, the radius from the configuration is used. 46 | func tertiaryBackground(cornerRadius: CGFloat? = nil) -> some View { 47 | self 48 | .padding(MaterialUIKit.configuration.contentPadding) 49 | .background(.materialUITertiaryBackground) 50 | .cornerRadius(cornerRadius ?? MaterialUIKit.configuration.cornerRadius) 51 | } 52 | 53 | /// Aligns the view within its frame based on the specified alignment option. 54 | /// 55 | /// - Parameter at: The alignment option specifying how to align the view within its frame. 56 | @ViewBuilder 57 | func align(_ at: Alignment) -> some View { 58 | switch at { 59 | case .top: 60 | self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) 61 | case .bottom: 62 | self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) 63 | case .leading: 64 | self.frame(maxWidth: .infinity, alignment: .leading) 65 | case .trailing: 66 | self.frame(maxWidth: .infinity, alignment: .trailing) 67 | case .topLeading: 68 | self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 69 | case .topTrailing: 70 | self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) 71 | case .bottomLeading: 72 | self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomLeading) 73 | case .bottomTrailing: 74 | self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) 75 | case .center: 76 | self.frame(maxWidth: .infinity, alignment: .center) 77 | default: 78 | self.frame(maxWidth: .infinity, alignment: .center) 79 | } 80 | } 81 | 82 | /// Executes a closure with the configured animation type from MaterialUIKit. 83 | /// 84 | /// - Parameter action: The closure to execute with the configured animation. 85 | func withMaterialAnimation(_ action: @escaping () -> Void) { 86 | withAnimation(MaterialUIKit.configuration.animationType) { 87 | action() 88 | } 89 | } 90 | 91 | /// Adds a backdrop for modal presentation, with animation synchronization. 92 | /// 93 | /// - Parameters: 94 | /// - isPresented: Binding to control the modal presentation. 95 | /// - animationFlag: Binding to synchronize animation state. 96 | func modalBackdrop(isPresented: Binding, animationFlag: Binding) -> some View { 97 | self 98 | .frame(maxWidth: .infinity, maxHeight: .infinity) 99 | .background(Color.black.opacity(0.45)) 100 | .opacity(animationFlag.wrappedValue ? 1 : 0) 101 | .onChange(of: isPresented.wrappedValue) { _ in 102 | withMaterialAnimation { 103 | animationFlag.wrappedValue = isPresented.wrappedValue 104 | } 105 | } 106 | } 107 | 108 | /// Applies vertical and horizontal padding to the view using the values from `MaterialUIKit.configuration`. 109 | func contentPadding() -> some View { 110 | self 111 | .padding(.horizontal, 4) 112 | .padding(.vertical, MaterialUIKit.configuration.verticalPadding) 113 | .padding(.horizontal, MaterialUIKit.configuration.horizontalPadding) 114 | } 115 | 116 | /// Applies vertical and horizontal padding to the view and sets the width to the maximum available, using the values from `MaterialUIKit.configuration`. 117 | func extendedWidthPadded() -> some View { 118 | self 119 | .padding(.vertical, MaterialUIKit.configuration.verticalPadding) 120 | .padding(.horizontal, MaterialUIKit.configuration.horizontalPadding) 121 | .frame(maxWidth: .infinity) 122 | } 123 | 124 | /// Triggers haptic feedback according to the specified style and intensity in the configuration. 125 | func hapticFeedback() { 126 | switch MaterialUIKit.configuration.hapticFeedbackStyle { 127 | case .light: 128 | UIImpactFeedbackGenerator(style: .light) 129 | .impactOccurred(intensity: MaterialUIKit.configuration.hapticFeedbackIntensity) 130 | case .medium: 131 | UIImpactFeedbackGenerator(style: .medium) 132 | .impactOccurred(intensity: MaterialUIKit.configuration.hapticFeedbackIntensity) 133 | case .heavy: 134 | UIImpactFeedbackGenerator(style: .heavy) 135 | .impactOccurred(intensity: MaterialUIKit.configuration.hapticFeedbackIntensity) 136 | case .soft: 137 | UIImpactFeedbackGenerator(style: .soft) 138 | .impactOccurred(intensity: MaterialUIKit.configuration.hapticFeedbackIntensity) 139 | case .rigid: 140 | UIImpactFeedbackGenerator(style: .rigid) 141 | .impactOccurred(intensity: MaterialUIKit.configuration.hapticFeedbackIntensity) 142 | @unknown default: 143 | UIImpactFeedbackGenerator(style: .light) 144 | .impactOccurred(intensity: MaterialUIKit.configuration.hapticFeedbackIntensity) 145 | } 146 | } 147 | 148 | /// Applies haptic feedback when the specified value changes. 149 | /// 150 | /// - Parameter value: The value to observe for changes. The feedback is triggered when this value changes. 151 | func hapticFeedbackOnChange(of value: V) -> some View where V: Equatable { 152 | self 153 | .onChangeWithFallback(of: value) { oldValue, newValue in 154 | hapticFeedback() 155 | } 156 | } 157 | 158 | /// Conditionally applies a transformation to a view based on a given condition. 159 | /// 160 | /// - Parameters: 161 | /// - condition: A Boolean value that determines whether the transformation 162 | /// should be applied. If `true`, the content closure is executed; if `false`, 163 | /// the original view is returned unchanged. 164 | /// - content: A closure that defines the transformation to apply to the view 165 | /// if the condition is `true`. This closure takes the original view as a parameter 166 | /// and returns the modified or wrapped view. 167 | /// 168 | /// - Returns: The original view if the condition is `false`; otherwise, the result 169 | /// of the `content` closure. 170 | @ViewBuilder 171 | func `if`(_ condition: Bool, content: (Self) -> Content) -> some View { 172 | if condition { 173 | content(self) 174 | } else { 175 | self 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Public/Color+MUIColorScheme.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+MUIColorScheme.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 | /// A set of custom color definitions tailored for MaterialUIKit styling. 12 | /// 13 | /// This extension adds convenience static properties to the `Color` type, 14 | /// allowing easy access to colors defined in the current `MaterialUIKit` configuration. 15 | public extension Color { 16 | /// The accent color based on the current color scheme setting. 17 | static let materialUIAccent: Color = MaterialUIKit.configuration.colorScheme.accent 18 | 19 | /// The accent tonal color shade based on the current color scheme setting. 20 | static var materialUITonal: Color = MaterialUIKit.configuration.colorScheme.tonal 21 | 22 | /// The primary background color based on the current color scheme setting. 23 | static var materialUIPrimaryBackground: Color = MaterialUIKit.configuration.colorScheme.primaryBackground 24 | 25 | /// The secondary background color based on the current color scheme setting. 26 | static var materialUISecondaryBackground: Color = MaterialUIKit.configuration.colorScheme.secondaryBackground 27 | 28 | /// The tertiary background color based on the current color scheme setting. 29 | static var materialUITertiaryBackground: Color = MaterialUIKit.configuration.colorScheme.tertiaryBackground 30 | 31 | /// The accent title color based on the current color scheme setting. 32 | static var materialUIHighlight: Color = MaterialUIKit.configuration.colorScheme.highlight 33 | 34 | /// The primary title color based on the current color scheme setting. 35 | static var materialUIPrimaryTitle: Color = MaterialUIKit.configuration.colorScheme.primaryTitle 36 | 37 | /// The secondary title color based on the current color scheme setting. 38 | static var materialUISecondaryTitle: Color = MaterialUIKit.configuration.colorScheme.secondaryTitle 39 | 40 | /// The color used for indicating errors and alerts. 41 | static var materialUIOnError: Color = MaterialUIKit.configuration.colorScheme.onError 42 | 43 | /// The separator color based on the current color scheme setting. 44 | static var materialUISeparator: Color = MaterialUIKit.configuration.colorScheme.separator 45 | 46 | /// The color used for indicating inactive or disabled state. 47 | static var materialUIOnDisabled: Color = MaterialUIKit.configuration.colorScheme.onDisabled 48 | 49 | /// The color used for outlines and stroke. 50 | static var materialUIOutline: Color = MaterialUIKit.configuration.colorScheme.outline 51 | } 52 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Extensions/Public/ShapeStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapeStyle.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 | /// A set of custom color definitions tailored for MaterialUIKit styling. 12 | /// 13 | /// This extension adds convenience static properties to the `ShapeStyle` type, 14 | /// allowing easy access to colors defined in the current `MaterialUIKit` configuration. 15 | public extension ShapeStyle where Self == Color { 16 | /// The accent color based on the current color scheme setting. 17 | static var materialUIAccent: Color { 18 | MaterialUIKit.configuration.colorScheme.accent 19 | } 20 | 21 | /// The accent tonal color shade based on the current color scheme setting. 22 | static var materialUITonal: Color { 23 | MaterialUIKit.configuration.colorScheme.tonal 24 | } 25 | 26 | /// The primary background color based on the current color scheme setting. 27 | static var materialUIPrimaryBackground: Color { 28 | MaterialUIKit.configuration.colorScheme.primaryBackground 29 | } 30 | 31 | /// The secondary background color based on the current color scheme setting. 32 | static var materialUISecondaryBackground: Color { 33 | MaterialUIKit.configuration.colorScheme.secondaryBackground 34 | } 35 | 36 | /// The tertiary background color based on the current color scheme setting. 37 | static var materialUITertiaryBackground: Color { 38 | MaterialUIKit.configuration.colorScheme.tertiaryBackground 39 | } 40 | 41 | /// The accent title color based on the current color scheme setting. 42 | static var materialUIHighlight: Color { 43 | MaterialUIKit.configuration.colorScheme.highlight 44 | } 45 | 46 | /// The primary title color based on the current color scheme setting. 47 | static var materialUIPrimaryTitle: Color { 48 | MaterialUIKit.configuration.colorScheme.primaryTitle 49 | } 50 | 51 | /// The secondary title color based on the current color scheme setting. 52 | static var materialUISecondaryTitle: Color { 53 | MaterialUIKit.configuration.colorScheme.secondaryTitle 54 | } 55 | 56 | /// The color used for indicating errors and alerts. 57 | static var materialUIOnError: Color { 58 | MaterialUIKit.configuration.colorScheme.onError 59 | } 60 | 61 | /// The separator color based on the current color scheme setting. 62 | static var materialUISeparator: Color { 63 | MaterialUIKit.configuration.colorScheme.separator 64 | } 65 | 66 | /// The color used for indicating inactive or disabled state. 67 | static var materialUIOnDisabled: Color { 68 | MaterialUIKit.configuration.colorScheme.onDisabled 69 | } 70 | 71 | /// The color used for outlines and stroke. 72 | static var materialUIOutline: Color { 73 | MaterialUIKit.configuration.colorScheme.outline 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/ActionButtonAnimationStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionButtonAnimationStyle.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 | /// Custom button style for animating the appearance of a button during click event. 12 | package struct ActionButtonAnimationStyle: ButtonStyle { 13 | package func makeBody(configuration: Configuration) -> some View { 14 | configuration.label 15 | .opacity(configuration.isPressed ? 0.8 : 1.0) 16 | .scaleEffect(configuration.isPressed ? 0.97 : 1.0) 17 | .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/ActionButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionButtonStyle.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 05/07/24 7 | // 8 | 9 | import Foundation 10 | 11 | /// Represents different button styles for the ``ActionButton``. 12 | @available(iOS 15.0, *) 13 | @frozen public enum ActionButtonStyle { 14 | /// An elevated background and rounded corners. 15 | case elevated 16 | 17 | /// A filled background and rounded corners. 18 | case filled 19 | 20 | /// A tonal background and rounded corners. 21 | case tonal 22 | 23 | /// An outlined border and rounded corners. 24 | case outline 25 | 26 | /// A text-only button with no background. 27 | case text 28 | 29 | /// An elevated background and rounded corners, occupying full available width. 30 | case elevatedStretched 31 | 32 | /// A filled background and rounded corners, occupying full available width. 33 | case filledStretched 34 | 35 | /// A tonal background and rounded corners, occupying full available width. 36 | case tonalStretched 37 | 38 | /// An outlined border and rounded corners, occupying full available width. 39 | case outlineStretched 40 | } 41 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/CheckboxStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckboxStyle.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 08/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Visual representation of the Material design styled checkbox. 12 | package struct CheckboxStyle: ToggleStyle { 13 | package func makeBody(configuration: Configuration) -> some View { 14 | HStack { 15 | configuration.label 16 | .align(.leading) 17 | 18 | Image(systemName: configuration.isOn ? "checkmark.square.fill" : "square") 19 | .font(.title2) 20 | .foregroundStyle(configuration.isOn ? .materialUIAccent : .materialUIOnDisabled) 21 | .onTapGesture { 22 | withAnimation(MaterialUIKit.configuration.animationType) { 23 | configuration.isOn.toggle() 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/CollectionStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionStyle.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 | /// Represents different styles for ``Collection``. 12 | @available(iOS 15.0, *) 13 | @frozen public enum CollectionStyle { 14 | /// A plain-style list. 15 | case plain 16 | 17 | /// A list style with inset cells. 18 | case inset 19 | 20 | /// A grouped list style with inset cells. 21 | case insetGrouped 22 | } 23 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/IconButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IconButtonStyle.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 05/07/2024 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Represens different styles for the ``IconButton``. 12 | @available(iOS 15.0, *) 13 | @frozen public enum IconButtonStyle { 14 | /// A circular button with an elevated style. 15 | case elevated 16 | 17 | /// A circular button with a filled style. 18 | case filled 19 | 20 | /// A circular button with a tonal style. 21 | case tonal 22 | } 23 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/NavigationContainerHeaderStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationContainerHeaderStyle.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 | /// Represents different styles for navigation container headers. 12 | @available(iOS 15.0, *) 13 | @frozen public enum NavigationContainerHeaderStyle { 14 | /// A large-style navigation bar header. 15 | case large 16 | 17 | /// An inline-style navigation bar header. 18 | case inline 19 | } 20 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/SeparatorOrientationStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SeparatorOrientationStyle.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 | /// Represents the orientation of the separator. 12 | @available(iOS 15.0, *) 13 | @frozen public enum SeparatorOrientationStyle { 14 | /// A horizontal separator. 15 | case horizontal 16 | 17 | /// A vertical separator. 18 | case vertical 19 | } 20 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/SwitchStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwitchStyle.swift 3 | // MaterialUIKit: https://github.com/aumChauhan/MaterialUIKit.git 4 | // 5 | // Author: Aum Chauhan 6 | // Created On: 13/07/24 7 | // 8 | 9 | import SwiftUI 10 | 11 | /// Visual representation of the Material Design styled toggle switch. 12 | package struct SwitchStyle: ToggleStyle { 13 | package func makeBody(configuration: Configuration) -> some View { 14 | HStack { 15 | configuration.label 16 | 17 | Spacer() 18 | 19 | RoundedRectangle(cornerRadius: MaterialUIKit.configuration.cornerRadius) 20 | .foregroundStyle(configuration.isOn ? .materialUIAccent : .materialUIPrimaryBackground) 21 | .frame(width: 42, height: 24) 22 | .overlay(alignment: configuration.isOn ? .trailing : .leading) { 23 | Circle() 24 | .frame(width: configuration.isOn ? 18 : 14) 25 | .foregroundStyle(configuration.isOn ? .materialUITonal : .materialUIOnDisabled) 26 | .padding(.horizontal, configuration.isOn ? 2 : 4) 27 | } 28 | .overlay( 29 | RoundedRectangle(cornerRadius: .infinity) 30 | .stroke(.materialUIOutline, lineWidth: MaterialUIKit.configuration.borderWidth) 31 | .frame(maxWidth: .infinity) 32 | ) 33 | .onTapGesture { 34 | withAnimation(MaterialUIKit.configuration.animationType) { 35 | configuration.isOn.toggle() 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/MaterialUIKit/Styles & Types/TabBarItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarItem.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 | /// A model representing an item in the MaterialUI-style tab bar. 12 | @available(iOS 15.0, *) 13 | public struct TabBarItem: Hashable { 14 | 15 | // MARK: - PROPERTIES 16 | 17 | let systemImage: String 18 | let titleKey: String 19 | 20 | // MARK: - INITIALIZERS 21 | 22 | public init(systemImage: String, titleKey: String) { 23 | self.systemImage = systemImage 24 | self.titleKey = titleKey 25 | } 26 | } 27 | --------------------------------------------------------------------------------