diff --git a/CHANGELOG.md b/CHANGELOG.md index 85986377..2026682e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [0.12.0](https://github.com/Orange-OpenSource/ods-ios/compare/0.12.0...0.11.0) - 2023-04-14 + +- [DemoApp/SDK] Add Bottom Sheet component ([#325](https://github.com/Orange-OpenSource/ods-ios/issues/325)) +- [SDK] Accessibility issues on Slider (Bug [#385](https://github.com/Orange-OpenSource/ods-ios/issues/385)) +- [Build] Update Build scripts to prepare upload on internal portal ([#383](https://github.com/Orange-OpenSource/ods-ios/issues/383)) +- [DemoApp] Add animation on Bottom Sheet when oppening and closing, automatically open it when appears ([#377](https://github.com/Orange-OpenSource/ods-ios/issues/377)) +- [DemoApp] Customization bottom sheet title uniformity ([#378](https://github.com/Orange-OpenSource/ods-ios/issues/378)) +- [DemoApp] Lists icon not displaying (Bug [#375](https://github.com/Orange-OpenSource/ods-ios/issues/375)) +- [SDK] Value is not computed well if Slider configured with step less than 1 (Bug [#313](https://github.com/Orange-OpenSource/ods-ios/issues/313)) +- [DemoApp] Update About module illustrations with B&W images ([#371](https://github.com/Orange-OpenSource/ods-ios/issues/371)) ## [0.11.2](https://github.com/Orange-OpenSource/ods-ios/compare/0.11.2...0.10.0) - 2023-03-27 diff --git a/Gemfile.lock b/Gemfile.lock index bb01d659..50c500e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.5) + CFPropertyList (3.0.6) rexml activesupport (6.1.6) concurrent-ruby (~> 1.0, >= 1.0.2) @@ -9,24 +9,24 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.4) + public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.2.0) - aws-partitions (1.687.0) - aws-sdk-core (3.168.4) + aws-partitions (1.745.0) + aws-sdk-core (3.171.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.61.0) + aws-sdk-kms (1.63.0) aws-sdk-core (~> 3, >= 3.165.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.117.2) + aws-sdk-s3 (1.120.1) aws-sdk-core (~> 3, >= 3.165.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.4) @@ -86,8 +86,8 @@ GEM escape (0.0.4) ethon (0.15.0) ffi (>= 1.15.0) - excon (0.96.0) - faraday (1.10.2) + excon (0.99.0) + faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -160,9 +160,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.32.0) - google-apis-core (>= 0.9.1, < 2.a) - google-apis-core (0.9.3) + google-apis-androidpublisher_v3 (0.38.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.0) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -171,10 +171,10 @@ GEM retriable (>= 2.0, < 4.a) rexml webrick - google-apis-iamcredentials_v1 (0.16.0) - google-apis-core (>= 0.9.1, < 2.a) - google-apis-playcustomapp_v1 (0.12.0) - google-apis-core (>= 0.9.1, < 2.a) + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.19.0) google-apis-core (>= 0.9.0, < 2.a) google-cloud-core (1.6.0) @@ -182,7 +182,7 @@ GEM google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.0) + google-cloud-errors (1.3.1) google-cloud-storage (1.44.0) addressable (~> 2.8) digest-crc (~> 0.4) @@ -191,7 +191,7 @@ GEM google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.3.0) + googleauth (1.5.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -205,8 +205,8 @@ GEM i18n (1.10.0) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.6.1) - jwt (2.6.0) + json (2.6.3) + jwt (2.7.0) memoist (0.16.2) mini_magick (4.12.0) mini_mime (1.1.2) @@ -220,7 +220,7 @@ GEM netrc (0.11.0) optparse (0.1.1) os (1.1.4) - plist (3.6.0) + plist (3.7.0) public_suffix (4.0.7) rake (13.0.6) representable (3.2.0) @@ -239,7 +239,7 @@ GEM faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simctl (1.6.8) + simctl (1.6.10) CFPropertyList naturally terminal-notifier (2.0.0) @@ -259,9 +259,9 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (1.8.0) - webrick (1.7.0) + webrick (1.8.1) word_wrap (1.0.0) - xcodeproj (1.21.0) + xcodeproj (1.22.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -278,6 +278,7 @@ PLATFORMS arm64-darwin-21 x86_64-darwin-19 x86_64-darwin-20 + x86_64-darwin-21 DEPENDENCIES cocoapods (= 1.11.3) diff --git a/InnovationCupTheme/Sources/InnovationCupTheme/InnovationCupTheme.swift b/InnovationCupTheme/Sources/InnovationCupTheme/InnovationCupTheme.swift index 13d5960f..2034578c 100644 --- a/InnovationCupTheme/Sources/InnovationCupTheme/InnovationCupTheme.swift +++ b/InnovationCupTheme/Sources/InnovationCupTheme/InnovationCupTheme.swift @@ -77,6 +77,9 @@ public struct InnovationCupThemeFactory { theme.componentColors.functionalInfo = InnovationCupThemeColors.functionalInfo.colorDecription.color theme.componentColors.functionalAlert = InnovationCupThemeColors.functionalAlert.colorDecription.color + // Bottom sheet + theme.componentColors.bottomSheetHeaderBackground = InnovationCupThemeColors.tabBarItem.colorDecription.color + // Fonts: use the default ones // theme.font = { style in } diff --git a/NOTICE.txt b/NOTICE.txt index bc4c0cab..d50e0bde 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -69,11 +69,10 @@ OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/Tab OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/Text edit menu.imageset/Text edit menu.png OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/Typography.imageset/Typography.svg -OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIIcIceCream.imageset/iconsCommunicationDIIcIceCream.svg -OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCafe.imageset/iconsCommunicationDIcCafe.svg -OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCookingPot.imageset/iconsCommunicationDIcCookingPot.svg -OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationRUIcRestaurant.imageset/iconsCommunicationRUIcRestaurant.svg -OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIIcIceCream.imageset/iconsCommunicationDIIcIceCream.svg -OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCafe.imageset/iconsCommunicationDIcCafe.svg +OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/IceCream.imageset/iconsCommunicationDIIcIceCream.svg +OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Cafe.imageset/iconsCommunicationDIcCafe.svg +OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/CookingPot.imageset/iconsCommunicationDIcCookingPot.svg +OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Restaurant.imageset/iconsCommunicationRUIcRestaurant.svg +OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/IceCream.imageset/iconsCommunicationDIIcIceCream.svg +OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Cafe.imageset/iconsCommunicationDIcCafe.svg End of the parts list under Orange SA Copyright - diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/BottomSheedHeader.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/BottomSheedHeader.swift new file mode 100644 index 00000000..9e1eceb2 --- /dev/null +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/BottomSheedHeader.swift @@ -0,0 +1,138 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import SwiftUI + +struct BottomSheedHeader: View { + + // ======================= + // MARK: Stored Properties + // ======================= + + let title: String + let subtitle: String? + let icon: Image? + let applyRotation: Bool + + // ========== + // MARK: Body + // ========== + + var body: some View { + VStack(spacing: 0) { + VStack(spacing: ODSSpacing.none) { + RoundedRectangle(cornerRadius: 4) + .frame(width: 55, height: 4, alignment: .center) + .padding(.top, ODSSpacing.s) + .padding(.bottom, ODSSpacing.xs) + + VStack(spacing: ODSSpacing.none) { + HStack(spacing: ODSSpacing.xs) { + icon? + .foregroundColor(.primary) + .accessibility(hidden: true) + .odsFont(.headline) + .animation(.linear, value: applyRotation) + .rotationEffect(.degrees(applyRotation ? 180 : 0)) + + VStack(alignment: .leading, spacing: ODSSpacing.none) { + Text(title) + .odsFont(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + + if let subtitle = self.subtitle { + Text(subtitle) + .odsFont(.subhead) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + } + .padding(.leading, ODSSpacing.s) + .padding(.trailing, ODSSpacing.m) + .padding(.bottom, ODSSpacing.s) + } + } + .background(Color(.systemGray6)) + .padding(.bottom, 10) + .cornerRadius(10) + .shadow(color: Color(UIColor.systemGray), radius: 4) + .padding(.bottom, -10) + .padding(.top, 10) + .mask(Rectangle().padding(.top, -40)) + + Divider() + } + } +} + +#if DEBUG +struct HeaderPreviewProvider_Previews: PreviewProvider { + + struct AnimatinoExample: View { + @State var applyRotation = false + + var body: some View { + BottomSheedHeader(title: "Rotation: \(applyRotation ? "Yes" : "No")", subtitle: nil, icon: Image(systemName: "chevron.up"), applyRotation: applyRotation) + .onTapGesture { + applyRotation.toggle() + } + + ODSButton(text: LocalizedStringKey(applyRotation ? "Remove Rotation" : "Apply Rotation"), emphasis: .highest) { + applyRotation.toggle() + } + } + } + + static var previews: some View { + VStack(spacing: 50) { + VStack { + Text("Title and Subtile") + .odsFont(.title2) + .frame(maxWidth: .infinity, alignment: .leading) + BottomSheedHeader(title: "Title", subtitle: "Subtitle", icon: nil, applyRotation: false) + } + + VStack { + Text("Title and icon (without rotation)") + .odsFont(.title2) + .frame(maxWidth: .infinity, alignment: .leading) + BottomSheedHeader(title: "Title", subtitle: nil, icon: Image(systemName: "chevron.down"), applyRotation: false) + } + + VStack { + Text("Title and icon (with rotation)") + .odsFont(.title2) + .frame(maxWidth: .infinity, alignment: .leading) + BottomSheedHeader(title: "Title", subtitle: nil, icon: Image(systemName: "chevron.down"), applyRotation: true) + } + + VStack { + Text("Title and icon (animated rotation)") + .odsFont(.title2) + .frame(maxWidth: .infinity, alignment: .leading) + AnimatinoExample() + } + } + .padding(.horizontal, 16) + } +} +#endif diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetExpandingModifier.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetExpandingModifier.swift new file mode 100644 index 00000000..509ee97a --- /dev/null +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetExpandingModifier.swift @@ -0,0 +1,104 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import BottomSheet +import SwiftUI + +struct ODSBottomSheetExpandingModifier: ViewModifier where ContentView: View { + + // ======================= + // MARK: Stored Properties + // ======================= + + private let title: String + private let subtitle: String? + private let icon: Image? + private let mainContent: () -> ContentView + private var bottomSheetSize: Binding + @State private var bottomSheetPosition: BottomSheetPosition + + // ================= + // MARK: Initializer + // ================= + + init(title: String, + subtile: String? = nil, + icon: Image? = nil, + bottomSheetSize: Binding, + @ViewBuilder content: @escaping () -> ContentView) { + self.title = title + self.subtitle = subtile + self.icon = icon + self.mainContent = content + self.bottomSheetSize = bottomSheetSize + self.bottomSheetPosition = self.bottomSheetSize.wrappedValue.position + } + + // ========== + // MARK: Body + // ========== + + func body(content: Content) -> some View { + content + .bottomSheet( + bottomSheetPosition: $bottomSheetPosition, + switchablePositions: ODSBottomSheetSize.allCases.map { $0.position }, + headerContent: { + BottomSheedHeader(title: title, subtitle: subtitle, icon: icon, applyRotation: false) + .onTapGesture { + switch self.bottomSheetSize.wrappedValue { + case .small: + self.bottomSheetPosition = ODSBottomSheetSize.medium.position + case .medium: + self.bottomSheetPosition = ODSBottomSheetSize.large.position + case .large: + self.bottomSheetPosition = ODSBottomSheetSize.small.position + default: + break + } + } + }, + mainContent: mainContent + ) + .showDragIndicator(false) + .enableAppleScrollBehavior(true) + .enableContentDrag(true) + .enableTapToDismiss(true) + .onDismiss { + bottomSheetPosition = ODSBottomSheetSize.small.position + } + .customBackground(self.background) + .onChange(of: bottomSheetPosition) { newValue in + bottomSheetSize.wrappedValue = ODSBottomSheetSize(from: newValue) + } + + } + + // ===================== + // MARK: Private Helpers + // ===================== + + @ViewBuilder + private var background: some View { + Color(.clear) + } +} diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetSize+extension.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetSize+extension.swift new file mode 100644 index 00000000..47f0aaf1 --- /dev/null +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetSize+extension.swift @@ -0,0 +1,79 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +import BottomSheet + +extension ODSBottomSheetSize { + var position: BottomSheetPosition { + switch self { + case .hidden: + return .hidden + case .small: + return .dynamicBottom + case .medium: + return .relative(0.5) + case .large: + return .relativeTop(0.975) + } + } + + public init(from position: BottomSheetPosition) { + switch position { + case .hidden: + self = .hidden + case .dynamicBottom: + self = .small + case .relative(let ratio) where ratio == 0.5: + self = .medium + default: + self = .large + } + } +} + +#if DEBUG +extension BottomSheetPosition { + public var description: String { + switch self { + case .absolute(let s): + return "absolute (\(s))" + case .hidden: + return "Hidden" + case .dynamicBottom: + return "dynamicBottom" + case .dynamic: + return "dynamic" + case .dynamicTop: + return "dynamicTop" + case .relativeBottom(let s): + return "dynamicBottom(\(s))" + case .relative(let s): + return "relative(\(s))" + case .relativeTop(let s): + return "relativeTop(\(s))" + case .absoluteBottom(let s): + return "absoluteBottom(\(s))" + case .absoluteTop(let s): + return "absoluteTop(\(s))" + } + } +} +#endif diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetStandardModifier.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetStandardModifier.swift new file mode 100644 index 00000000..a2b1a41a --- /dev/null +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/Internal/ODSBottomSheetStandardModifier.swift @@ -0,0 +1,220 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import SwiftUI + +struct ODSBottomSheetStandardModifier: ViewModifier where BottomSheetContent: View { + + // ======================= + // MARK: Stored Properties + // ======================= + let isOpen: Binding + let headerConfig: ODSBottomSheetStandardHeaderConfig + @ViewBuilder let bottomSheetContent: () -> BottomSheetContent + @State private var bottomSheetHeaderSize = CGSize() + + // ========== + // MARK: Body + // ========== + + func body(content: Content) -> some View { + ZStack { + content + ODSBottomSheetStandard(isOpen: isOpen, headerSize: $bottomSheetHeaderSize, + headerConfig: headerConfig, content: bottomSheetContent) + } + } +} + +/// Used to configure the header of the bottom sheet __ODSBottomSheetStandard__. +struct ODSBottomSheetStandardHeaderConfig { + + // ======================= + // MARK: Stored Properties + // ======================= + + fileprivate let title: String + fileprivate let subtitle: String? + fileprivate let icon: Image? + fileprivate let animateIcon: Bool + + // ================== + // MARK: Initializers + // ================== + + /// Initilize the header of the __ODSBottomSheetStandard__ with title only. + /// - Parameters: + /// - title: The title of the bottom sheet + init(title: String) { + self.init(title: title, subtitle: nil, icon: nil, animateIcon: false) + } + + /// Initilize the header of the __ODSBottomSheetStandard__ with title and subtitle. + /// - Parameters: + /// - title: The title of the bottom sheet + /// - subtitle: The additional subtitle + init(title: String, subtitle: String) { + self.init(title: title, subtitle: subtitle, icon: nil, animateIcon: false) + } + + /// Initilize the header of the __ODSBottomSheetStandard__ with title and subtitle. + /// - Parameters: + /// - title: The title of the bottom sheet + /// - icon: The additional icon added near to the title + /// - animateIcon: To animate (ration to 180 degrees) when sheet is opening. + init(title: String, icon: Image, animateIcon: Bool = true) { + self.init(title: title, subtitle: nil, icon: icon, animateIcon: animateIcon) + } + + private init(title: String, subtitle: String?, icon: Image?, animateIcon: Bool) { + self.title = title + self.subtitle = subtitle + self.icon = icon + self.animateIcon = animateIcon + } +} + +/// The standard bottom sheet must be used only with a "simple, basic" content. If a more complex content must be added +/// prefer the __ odsBottomSheetExpanding__ modifiers. +/// +/// The view of standard bottom sheet can be used out of the __odsBottomSheetStandard__ modifiers. +/// +/// To do so, use it in a `ZStack` like this: +/// +/// struct YourView: View { +/// @State var isShowingSheet: Bool = false +/// var body: some View { +/// ZStack { +/// // Main content goes here. +/// ScrollView { +/// Text("Bottom sheet is \(isShowingSheet ? "Opened": "Closed")") +/// } +/// ODSBottomSheetStandard(isOpen: $isShowingSheet, headerConfig: ODSBottomSheetStandardHeaderConfig(title: "Customize")) { +/// // Bottom sheet content goes here +/// } +/// } +/// } +/// } +/// +struct ODSBottomSheetStandard: View where Content: View { + + // ======================= + // MARK: Stored Properties + // ======================= + + private let headerConfig: ODSBottomSheetStandardHeaderConfig + private let content: Content + private let isOpen: Binding + private let headerSize: Binding? + + // ================= + // MARK: Initializer + // ================= + + /// Initilize the bottom sheet view. + /// + /// - Parameters: + /// - isOpen: A binding to a Boolean value that determines whether to open the sheet + /// - headerSize: A binding to a `CGSize` value that provide the size of the header. + /// Nice to get its height to add padding at the bottom of the main conent view to avoid bottom sheet to overlap it. + /// - headerConfig: The header configuration. + /// - content: A closure that returns the content of the bottom sheet. + /// + // swiftlint:disable multiline_parameters_brackets + init(isOpen: Binding, + headerSize: Binding? = nil, + headerConfig: ODSBottomSheetStandardHeaderConfig, + @ViewBuilder content: @escaping () -> Content) { + self.isOpen = isOpen + self.headerSize = headerSize + self.headerConfig = headerConfig + self.content = content() + } + + // ========== + // MARK: Body + // ========== + + var body: some View { + VStack(spacing: ODSSpacing.none) { + Spacer() + + VStack(spacing: ODSSpacing.none) { + BottomSheedHeader(title: headerConfig.title, + subtitle: headerConfig.subtitle, + icon: headerConfig.icon, + applyRotation: applyRotation) + .onTapGesture { + withAnimation(Animation.linear) { + isOpen.wrappedValue.toggle() + } + } + .readSize { size in + headerSize?.wrappedValue = size + } + + if isOpen.wrappedValue { + content + .background(Color(UIColor.systemBackground)) + .transition(.asymmetric(insertion: .move(edge: .bottom), removal: .move(edge: .bottom))) + } + } + } + } + + // ============== + // MARK: Helpers + // ============== + + private var applyRotation: Bool { + headerConfig.animateIcon && self.isOpen.wrappedValue + } +} + +#if DEBUG +struct StandardBottomSheetPreviewProvider_Previews: PreviewProvider { + + struct BottomSheet: View { + @State var isOpen: Bool + + var body: some View { + ODSBottomSheetStandard(isOpen: $isOpen, + headerConfig: ODSBottomSheetStandardHeaderConfig(title: "Recipes", icon: Image(systemName: "chevron.up"))) { + VStack { + ODSListStandardItem(model: ODSListStandardItemModel(title: "Summer Salad", leadingIcon: ODSListItemLeadingIcon.icon(Image(systemName: "sun.max")))) + + ODSListStandardItem(model: ODSListStandardItemModel(title: "Bocoli Soup", leadingIcon: ODSListItemLeadingIcon.icon(Image(systemName: "heart")))) + + ODSListStandardItem(model: ODSListStandardItemModel(title: "Pesto farfalle", leadingIcon: ODSListItemLeadingIcon.icon(Image(systemName: "music.note")))) + + ODSListStandardItem(model: ODSListStandardItemModel(title: "Fig Sponge Cake", leadingIcon: ODSListItemLeadingIcon.icon(Image(systemName: "star")))) + } + .padding(.horizontal, 16) + } + } + } + + static var previews: some View { + BottomSheet(isOpen: true) + } +} +#endif diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/ODSBottomSheetExpanding.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/ODSBottomSheetExpanding.swift new file mode 100644 index 00000000..4c5e0b1d --- /dev/null +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/ODSBottomSheetExpanding.swift @@ -0,0 +1,131 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import SwiftUI + +// swiftlint:disbale comment_spacing multiline_parameters_brackets vertical_parameter_alignment +public enum ODSBottomSheetSize: String, CaseIterable { + case hidden /// The state where the BottomSheet is hidden + case small /// The state where the BottomSheet is closed (`mainContent` is hidden) + case medium /// The state where the height of the BottomSheet is 50% of the screen + case large /// The state where the height of the BottomSheet is 90% of the screen +} + +extension View { + /// + /// ODS Sheet Bottom. + /// + /// Bottom sheets are surfaces anchored to the bottom of the screen that present users supplemental content. + /// It is useful for requesting a specific information or enabling a simple task related to the current context + /// of the current view or more globaly the application context. + /// + /// Unlike the standard bottom sheet __odsBottomSheetExpanding__ proposes open and close states, the expanding bottom sheet supports + /// multiple sizes or detents (small, medium and large). So a more complex and scrollable content can be proposed. + /// + /// The sheet expands when the user scrolls its contents, drags up or down the grabber (in header). A user can also tap the grabber to cycle + /// through the available sizes. + /// + /// This modifier adds a bottom sheet containing only title in header and a complex content. + /// It opens the sheet in the size provided by the initial value of a binding to a __ODSBottomSheetSize__ value. + /// The value is updated, when the sheet is expanded or closed (by swiping the content, tapping on header, tapping on dimming area, ...) + /// + /// The example below shows how to present a bottom sheet: + /// + /// struct BottomSheetPresentation: View { + /// @State private var bottomSheetSize: ODSBottomSheetSize = .large + /// + /// var body: some View { + /// VStack { + /// // Main content goes here. + /// Text("Bottom sheet size \(bottomSheetSize.rawValue)") + /// } + /// .odsBottomSheetExpanding(title: "Customize", bottomSheetSize: $bottomSheetSize) { + /// // Bottom sheet content goes here + /// } + /// } + /// } + /// + /// - Parameters: + /// - title: The title added in the header. + /// - bottomSheetSize: A binding to __ODSBottomSheetSize__ value that determines + /// the size of the sheet that you create in the modifier's `content` closure. + /// - content: A closure that returns the content of the bottom sheet. + /// + public func odsBottomSheetExpanding ( + title: String, + bottomSheetSize: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(ODSBottomSheetExpandingModifier( + title: title, + subtile: nil, + icon: nil, + bottomSheetSize: bottomSheetSize, + content: content)) + } + + /// This modifier adds a bottom sheet containing title and subtitle in header and a complex content. + /// + /// - Parameters: + /// - title: The title added in the header. + /// - subtitle: The additionnal subtitle added in the header. + /// - bottomSheetSize: A binding to __ODSBottomSheetSize__ value that determines + /// the size of the sheet that you create in the modifier's `content` closure. + /// - content: A closure that returns the content of the bottom sheet. + /// + public func odsBottomSheetExpanding ( + title: String, + subtitle: String, + bottomSheetSize: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(ODSBottomSheetExpandingModifier( + title: title, + subtile: subtitle, + icon: nil, + bottomSheetSize: bottomSheetSize, + content: content)) + } + + /// This modifier adds a bottom sheet containing title and icon in header and a complex content. + /// + /// - Parameters: + /// - title: The title added in the header. + /// - icon: The additional icon added near to the title in header. + /// - bottomSheetSize: A binding to __ODSBottomSheetSize__ value that determines + /// the size of the sheet that you create in the modifier's `content` closure. + /// - content: A closure that returns the content of the bottom sheet. + /// + public func odsBottomSheetExpanding ( + title: String, + icon: Image, + bottomSheetSize: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(ODSBottomSheetExpandingModifier( + title: title, + subtile: nil, + icon: icon, + bottomSheetSize: bottomSheetSize, + content: content)) + } +} diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/ODSBottomSheetStandard.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/ODSBottomSheetStandard.swift new file mode 100644 index 00000000..c69c2d88 --- /dev/null +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/BottomSheet/ODSBottomSheetStandard.swift @@ -0,0 +1,120 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import SwiftUI + +extension View { + + /// + /// ODS Sheet Bottom. + /// + /// Bottom sheets are surfaces anchored to the bottom of the screen that present users supplemental content. + /// It is useful for requesting a specific information or enabling a simple task related to the current context + /// of the current view or more globaly the application context. + /// + /// The standard bottom sheet must be used only with a "simple, basic" content. If a more complex content must be added + /// prefer the __ odsBottomSheetExpanding__ modifiers. + /// + /// + /// This modifier adds a bottom sheet containing only title in header. It opens the sheet + /// when a binding to a Boolean value that you provide is true. + /// + /// The example below shows how to present a bottom sheet: + /// + /// struct BottomSheetPresentation: View { + /// @State private var isOpen = false + /// + /// var body: some View { + /// VStack { + /// // Main content goes here. + /// Text("Bottom sheet is \(isOpen ? "Opened": "Closed")") + /// } + /// .odsBottomSheetStandard(isOpen: $isOpen, title: "Customize") { + /// // Bottom sheet content goes here + /// } + /// } + /// } + /// + /// - Parameters: + /// - isOpen: A binding to a Boolean value that determines whether + /// to open the sheet that you create in the modifier's + /// `content` closure. + /// - title: The title added in the header. + /// - content: A closure that returns the content of the bottom sheet. + /// + public func odsBottomSheetStandard ( + isOpen: Binding, + title: String, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(ODSBottomSheetStandardModifier( + isOpen: isOpen, + headerConfig: ODSBottomSheetStandardHeaderConfig(title: title), + bottomSheetContent: content)) + } + + /// This modifier adds a bottom sheet containing title and subtitle in header. + /// + /// - Parameters: + /// - isOpen: A binding to a Boolean value that determines whether + /// to open the sheet that you create in the modifier's + /// `content` closure. + /// - title: The title added in the header. + /// - subtitle: Add a subtitle in header. + /// - content: A closure that returns the content of the bottom sheet. + /// + public func odsBottomSheetStandard ( + isOpen: Binding, + title: String, + subtitle: String, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(ODSBottomSheetStandardModifier( + isOpen: isOpen, + headerConfig: ODSBottomSheetStandardHeaderConfig(title: title, subtitle: subtitle), + bottomSheetContent: content)) + } + + /// This modifier adds a bottom sheet containing title and icon in header. + /// + /// - Parameters: + /// - isOpen: A binding to a Boolean value that determines whether + /// to open the sheet that you create in the modifier's + /// `content` closure. + /// - title: The title added in the header. + /// - icon: Add a icon in header before title. + /// - animateIcon: To animate (ration to 180 degrees) when sheet is opening. + /// - content: A closure that returns the content of the bottom sheet. + /// + public func odsBottomSheetStandard ( + isOpen: Binding, + title: String, + icon: Image, + annimateIcon: Bool = true, + @ViewBuilder content: @escaping () -> Content + ) -> some View { + self.modifier(ODSBottomSheetStandardModifier( + isOpen: isOpen, + headerConfig: ODSBottomSheetStandardHeaderConfig(title: title, icon: icon, animateIcon: annimateIcon), + bottomSheetContent: content)) + } +} diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Buttons/Internal/ODSButtonStyles.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Buttons/Internal/ODSButtonStyles.swift index 9d6c2e46..13ebe0d1 100644 --- a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Buttons/Internal/ODSButtonStyles.swift +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Buttons/Internal/ODSButtonStyles.swift @@ -29,7 +29,7 @@ import SwiftUI struct ODSButtonStyleModifier: ViewModifier { let emphasis: ODSButton.Emphasis @Environment(\.theme) var theme - + @ViewBuilder func body(content: Content) -> some View { switch emphasis { diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Cards/ODSCardSmall.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Cards/ODSCardSmall.swift index 53f1cee8..f47a9131 100644 --- a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Cards/ODSCardSmall.swift +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Cards/ODSCardSmall.swift @@ -93,7 +93,7 @@ public struct ODSCardSmall: View { .accessibilityHidden(true) .frame(maxHeight: 100) .clipped() - + VStack(alignment: .leading, spacing: ODSSpacing.xs) { Text(model.title) .lineLimit(1) diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Lists/ODSListItemModels.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Lists/ODSListItemModels.swift index aed47c35..723f051c 100644 --- a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Lists/ODSListItemModels.swift +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Lists/ODSListItemModels.swift @@ -172,4 +172,3 @@ public class ODSListSelectionItemModel: ODSListItemModel, ObservableObject { id = UUID() } } - diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Slider/ODSSliderView.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Slider/ODSSliderView.swift index 8c3715d4..f10e1824 100644 --- a/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Slider/ODSSliderView.swift +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Components/Slider/ODSSliderView.swift @@ -29,95 +29,332 @@ import SwiftUI /// Based on the native `Slider`, the `ODSSLider` offers to the user the possibility /// to type direcly on the slider's track to get a value. /// -public struct ODSSlider: View where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint, ValueLabel: View { - @Binding var value: V - public let range: ClosedRange - let step: V.Stride - let minimumValueLabel: () -> ValueLabel - let maximumValueLabel: () -> ValueLabel - - /// Creates an unlabeled slider to select a value from a given range, subject to a - /// step increment. + +// MARK: - Initializers with labels. +public struct ODSSlider where V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint, Label: View, ValueLabel: View { + + // ======================= + // MARK: Stored properties + // ======================= + + @Binding private var value: V + private let range: ClosedRange + private let label: Label + private let minimumValueLabel: ValueLabel + private let maximumValueLabel: ValueLabel + private let onEditingChanged: (Bool) -> Void + private let step: V.Stride? + + private let values: [V]? + @State private var isEditing: Bool { + didSet { + onEditingChanged(isEditing) + } + } + + // ================== + // MARK: Initializers + // ================== + + /// Creates a slider to select a value from a given range, subject to a + /// step increment, which displays the provided labels. /// /// - Parameters: - /// - value: The selected value within `range`. - /// - range: The range of the valid values. - /// - step: The distance between each valid value. default 1 + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - step: The distance between each valid value. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, SwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - minimumValueLabel: A view that describes `bounds.lowerBound`. + /// - maximumValueLabel: A view that describes `bounds.upperBound`. + /// - onEditingChanged: A callback for when editing begins and ends. /// /// The `value` of the created instance is equal to the position of - /// the given value within `range`. + /// the given value within `bounds`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. /// - public init(value: Binding, range: ClosedRange, step: V.Stride = 1) where ValueLabel == EmptyView { + /// - Remark: Accessibilty recommendation: + /// We recommand to not set information on `minimumValueLabel` and `maximumValueLabel` view using `.accessibilityHidden(true)` + /// + public init(value: Binding, in bounds: ClosedRange = 0...1, @ViewBuilder label: () -> Label, @ViewBuilder minimumValueLabel: () -> ValueLabel, @ViewBuilder maximumValueLabel: () -> ValueLabel, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { + _value = value - self.range = range - self.step = step - maximumValueLabel = { - EmptyView() - } - minimumValueLabel = { - EmptyView() - } + self.range = bounds + self.step = nil + self.onEditingChanged = onEditingChanged + self.label = label() + self.minimumValueLabel = minimumValueLabel() + self.maximumValueLabel = maximumValueLabel() + + self.values = nil + self._isEditing = State(initialValue: false) } /// Creates a slider to select a value from a given range, subject to a - /// step increment. which displays the provided labels. + /// step increment, which displays the provided labels. /// /// - Parameters: - /// - value: The selected value within `range`. - /// - range: The range of the valid values. - /// - step: The distance between each valid value. default 1 - /// - minimumValueLabel: A view that describes `range.lowerBound`. - /// - maximumValueLabel: A view that describes `range.lowerBound`. + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. + /// - step: The distance between each valid value. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, SwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - minimumValueLabel: A view that describes `bounds.lowerBound`. + /// - maximumValueLabel: A view that describes `bounds.upperBound`. + /// - onEditingChanged: A callback for when editing begins and ends. /// /// The `value` of the created instance is equal to the position of - /// the given value within `range`. + /// the given value within `bounds`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + /// + /// - Remark: Accessibilty recommendation: + /// We recommand to not set information on `minimumValueLabel` and `maximumValueLabel` view using `.accessibilityHidden(true)` /// - public init(value: Binding, range: ClosedRange, step: V.Stride = 1, @ViewBuilder minimumLabelView: @escaping () -> ValueLabel, @ViewBuilder maximumLabelView: @escaping () -> ValueLabel) { + public init(value: Binding, in bounds: ClosedRange, step: V.Stride = 1, @ViewBuilder label: () -> Label, @ViewBuilder minimumValueLabel: () -> ValueLabel, @ViewBuilder maximumValueLabel: () -> ValueLabel, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { + _value = value - self.range = range + self.range = bounds self.step = step - minimumValueLabel = minimumLabelView - maximumValueLabel = maximumLabelView + self.onEditingChanged = onEditingChanged + self.label = label() + self.minimumValueLabel = minimumValueLabel() + self.maximumValueLabel = maximumValueLabel() + + self.values = Array(stride(from: range.lowerBound, through: range.upperBound, by: step)) + self._isEditing = State(initialValue: false) + } +} + +// MARK: - Initializers with label, without value labels. +extension ODSSlider where ValueLabel == EmptyView { + + /// Creates a slider to select a value from a given range, which displays + /// the provided label. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, SwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + public init(value: Binding, in bounds: ClosedRange = 0...1, @ViewBuilder label: () -> Label, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { + self.init( + value: value, + in: bounds, + label: label, + minimumValueLabel: { EmptyView() }, + maximumValueLabel: { EmptyView() }, + onEditingChanged: onEditingChanged) + } + + /// Creates a slider to select a value from a given range, subject to a + /// step increment. which displays the provided label. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - step: The distance between each valid value. + /// - label: A `View` that describes the purpose of the instance. Not all + /// slider styles show the label, but even in those cases, SwiftUI + /// uses the label for accessibility. For example, VoiceOver uses the + /// label to identify the purpose of the slider. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + public init(value: Binding, in bounds: ClosedRange, step: V.Stride = 1, @ViewBuilder label: () -> Label, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { + self.init( + value: value, + in: bounds, + step: step, + label: label, + minimumValueLabel: { EmptyView() }, + maximumValueLabel: { EmptyView() }, + onEditingChanged: onEditingChanged) + } +} + +// MARK: - Initializers without labels. +extension ODSSlider where ValueLabel == EmptyView, Label == EmptyView { + + /// Creates a slider to select a value from a given range. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. Defaults to `0...1`. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`, mapped into `0...1`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + public init(value: Binding, in bounds: ClosedRange = 0...1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { + self.init( + value: value, + in: bounds, + label: { EmptyView() }, + minimumValueLabel: { EmptyView() }, + maximumValueLabel: { EmptyView() }, + onEditingChanged: onEditingChanged) } + /// Creates a slider to select a value from a given range, subject to a + /// step increment. + /// + /// - Parameters: + /// - value: The selected value within `bounds`. + /// - bounds: The range of the valid values. + /// - onEditingChanged: A callback for when editing begins and ends. + /// + /// The `value` of the created instance is equal to the position of + /// the given value within `bounds`. + /// + /// The slider calls `onEditingChanged` when editing begins and ends. For + /// example, on iOS, editing begins when the user starts to drag the thumb + /// along the slider's track. + public init(value: Binding, in bounds: ClosedRange, step: V.Stride = 1, onEditingChanged: @escaping (Bool) -> Void = { _ in }) { + self.init( + value: value, + in: bounds, + step: step, + label: { EmptyView() }, + minimumValueLabel: { EmptyView() }, + maximumValueLabel: { EmptyView() }, + onEditingChanged: onEditingChanged) + } +} + +// MARK: - View implementation. +extension ODSSlider: View { + + // ========== + // MARK: Body + // ========== + public var body: some View { VStack { HStack(alignment: .center) { - minimumValueLabel() + minimumValueLabel GeometryReader { geometry in - Slider( - value: $value, - in: range, - step: step) - .gesture(DragGesture(minimumDistance: 0).onChanged { value in - let percent = min(max(0, Float(value.location.x / geometry.size.width * 1)), 1) - let newValue = self.range.lowerBound + round(V(Double(percent)) * (self.range.upperBound - self.range.lowerBound)) - let rounded = round(V.Stride(newValue) / step) * step - self.$value.wrappedValue = V(rounded) - }) - .frame( - width: geometry.size.width, - height: geometry.size.height, - alignment: .center) + slider + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + let newValue = computeNewValue(for: value.location.x, in: geometry.size.width) + if !isEditing { + if newValue != self.$value.wrappedValue { + self.isEditing = true + } + } + + self.$value.wrappedValue = newValue + } + .onEnded { value in + self.$value.wrappedValue = computeNewValue(for: value.location.x, in: geometry.size.width) + self.isEditing = false + } + ) + .frame( + width: geometry.size.width, + height: geometry.size.height, + alignment: .center) } - maximumValueLabel() + maximumValueLabel } } } + + // ===================== + // MARK: private helpers + // ===================== + @ViewBuilder + var slider: some View { + if let step = self.step { + Slider(value: $value, in: range, step: step) { + self.label + } + } else { + Slider(value: $value, in: range) { + self.label + } + } + } + + private func computeNewValue(for xPosition: Double, in globalWidth: Double) -> V { + if xPosition >= globalWidth { + return range.upperBound + } else { + if xPosition <= 0 { + return range.lowerBound + } else { + let percent = xPosition / globalWidth + let computedValue = (V(percent) * (self.range.upperBound - self.range.lowerBound)) + self.range.lowerBound + + // Adjust newValue according to step + return adjustNewValue(from: computedValue) + } + } + } + + private func adjustNewValue(from computedValue: V) -> V { + guard let values = self.values else { + return computedValue + } + + var newValue = computedValue + var distance: V.Stride = .infinity + + for value in values { + let newDistance = value.distance(to: computedValue) + if abs(newDistance) < abs(distance) { + distance = newDistance + newValue = value + } else { + return newValue + } + } + + return newValue + } } #if DEBUG +// MARK: - Previews. struct ODSSlider_Previews: PreviewProvider { static var previews: some View { - VStack { ODSSlider( value: .constant(50), - range: 0 ... 100.0) + in: 0 ... 100.0) .padding([.leading, .trailing], ODSSpacing.s) } } @@ -126,15 +363,16 @@ struct ODSSlider_Previews: PreviewProvider { struct ODSSlider_Previews_with_label: PreviewProvider { static var previews: some View { - VStack { - ODSSlider( - value: .constant(50), - range: 0 ... 100.0) - { + ODSSlider(value: .constant(50), + in: 0 ... 100.0) { + Text("Spead") + } minimumValueLabel: { Image(systemName: "speaker.wave.1.fill") - } maximumLabelView: { + } maximumValueLabel: { Image(systemName: "speaker.wave.3.fill") + } onEditingChanged: { isEditing in + print("isEditing(\(isEditing))") } .padding([.leading, .trailing], ODSSpacing.s) } diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/Modifiers/ODSTabBarModifier.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/Modifiers/ODSTabBarModifier.swift index 143c66fd..137a9d74 100644 --- a/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/Modifiers/ODSTabBarModifier.swift +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/Modifiers/ODSTabBarModifier.swift @@ -54,15 +54,16 @@ extension View { itemAppearance.normal.badgeBackgroundColor = uiBadgeColor itemAppearance.selected.badgeBackgroundColor = uiBadgeColor } - + let appearance = UITabBarAppearance() appearance.configureWithOpaqueBackground() + appearance.configureWithTransparentBackground() appearance.backgroundColor = backgroundColor?.uiColor - + appearance.stackedLayoutAppearance = itemAppearance appearance.inlineLayoutAppearance = itemAppearance appearance.compactInlineLayoutAppearance = itemAppearance - + UITabBar.appearance().standardAppearance = appearance UITabBar.appearance().scrollEdgeAppearance = appearance } diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/ODSComponentsColors.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/ODSComponentsColors.swift index bb3e9dd3..847a131c 100644 --- a/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/ODSComponentsColors.swift +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/ODSComponentsColors.swift @@ -53,6 +53,9 @@ public struct ODSComponentColors { public var functionalAlert: Color public var functionalInfo: Color + // Bottom sheet + public var bottomSheetHeaderBackground: Color + // ================== // MARK: Initializers // ================== @@ -80,5 +83,8 @@ public struct ODSComponentColors { self.functionalPositive = .green self.functionalInfo = .blue self.functionalAlert = .yellow + + // Bottom sheet + self.bottomSheetHeaderBackground = Color(UIColor.systemGray6) } } diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/View/ODSThemeableView.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/View/ODSThemeableView.swift index 4aa1f088..e4f65a38 100644 --- a/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/View/ODSThemeableView.swift +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Theme/View/ODSThemeableView.swift @@ -52,18 +52,18 @@ import SwiftUI /// public struct ODSThemeableView: View where Content: View { - + // ======================= // MARK: Stored Properties // ======================= private let theme: ODSTheme private let content: () -> Content - + // ================== // MARK: Initializers // ================== - + /// Creates an instance with the theme to be applied. /// /// - Parameters: @@ -76,7 +76,7 @@ public struct ODSThemeableView: View where Content: View { self.content = content self.navigationBarColors(for: theme) } - + public var body: some View { content() .accentColor(theme.componentColors.accent) @@ -86,3 +86,4 @@ public struct ODSThemeableView: View where Content: View { .toolBarColors(for: theme) } } + diff --git a/OrangeDesignSystem/Sources/OrangeDesignSystem/Utils/TabBar+readSize.swift b/OrangeDesignSystem/Sources/OrangeDesignSystem/Utils/TabBar+readSize.swift new file mode 100644 index 00000000..b19d4a40 --- /dev/null +++ b/OrangeDesignSystem/Sources/OrangeDesignSystem/Utils/TabBar+readSize.swift @@ -0,0 +1,85 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// + +import SwiftUI + +extension View { + func readTabBarHeight( _ onChange: @escaping (CGFloat) -> Void) -> some View { + self.configureTabBar { controller, _ in + onChange(controller.tabBar.bounds.height) + } + } + + func configureTabBar(configurator: @escaping (UITabBarController, Bool) -> Void) -> some View { + modifier(TabBarConfigurationViewModifier(configurator: configurator)) + } +} + +struct TabBarConfigurationViewModifier: ViewModifier { + let configurator: (UITabBarController, Bool) -> Void + + func body(content: Content) -> some View { + content + .background(TabBarConfigurator(configurator: configurator)) + } +} + +struct TabBarConfigurator: UIViewControllerRepresentable { + let configurator: (UITabBarController, Bool) -> Void + + func makeUIViewController(context: Context) -> TabBarConfigurationViewController { + TabBarConfigurationViewController(configurator: configurator) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + } +} + +class TabBarConfigurationViewController: UIViewController { + let configurator: (UITabBarController, Bool) -> Void + + init(configurator: @escaping (UITabBarController, Bool) -> Void) { + self.configurator = configurator + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let tabBarController = self.tabBarController { + configurator(tabBarController, true) + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if let tabBarController = self.tabBarController { + configurator(tabBarController, false) + } + } +} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo.xcodeproj/project.pbxproj b/OrangeDesignSystemDemo/OrangeDesignSystemDemo.xcodeproj/project.pbxproj index c3132f50..9729412e 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo.xcodeproj/project.pbxproj +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo.xcodeproj/project.pbxproj @@ -19,7 +19,10 @@ 07387C6328F062D900D8721F /* ListComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07387C6228F062D900D8721F /* ListComponent.swift */; }; 07387C6928F0641400D8721F /* StandardListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07387C6828F0641400D8721F /* StandardListModel.swift */; }; 0752EC2828EDBB540029A7BE /* ChipsComponentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0752EC2728EDBB540029A7BE /* ChipsComponentModel.swift */; }; - 0752EC2A28EDD7A80029A7BE /* BottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0752EC2928EDD7A80029A7BE /* BottomSheet.swift */; }; + 07535BAA29D31A440012F298 /* BottomSheetStandardVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07535BA929D31A440012F298 /* BottomSheetStandardVariant.swift */; }; + 07535BAF29D713320012F298 /* BottomSheetExpandingVariantOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07535BAD29D713320012F298 /* BottomSheetExpandingVariantOptions.swift */; }; + 07535BB029D713320012F298 /* BottomSheetExpandingVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07535BAE29D713320012F298 /* BottomSheetExpandingVariant.swift */; }; + 07535BCF29DC57E60012F298 /* CustomizableVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07535BCE29DC57E60012F298 /* CustomizableVariant.swift */; }; 075462FA28F474CC002E2E40 /* ButtonsComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075462F728F474CC002E2E40 /* ButtonsComponent.swift */; }; 075462FC28F474CC002E2E40 /* EmphasisAndFunctionnalVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075462F928F474CC002E2E40 /* EmphasisAndFunctionnalVariant.swift */; }; 075E5D6D29378735009A801B /* RecipesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 075E5D6C29378735009A801B /* RecipesLoader.swift */; }; @@ -65,6 +68,7 @@ 07F141C0298BC213007C8575 /* CardHorizontalVariant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F141BF298BC213007C8575 /* CardHorizontalVariant.swift */; }; 07F141C2298C0644007C8575 /* RecipeBookModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F141C1298C0644007C8575 /* RecipeBookModel.swift */; }; 07F141C4298CF4CB007C8575 /* RecipesBook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F141C3298CF4CB007C8575 /* RecipesBook.swift */; }; + 07F141C829928B96007C8575 /* BottomSheetComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F141C629928B96007C8575 /* BottomSheetComponent.swift */; }; 07F343862943458D0043335A /* ProgressIndicatorComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F343852943458C0043335A /* ProgressIndicatorComponent.swift */; }; C88E9E08333D56BAEA28A60E /* Pods_OrangeDesignSystemDemoTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E816C9278060A8072007BFD0 /* Pods_OrangeDesignSystemDemoTests.framework */; }; F90D9768280030A6006D29FC /* TypographyPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90D9765280030A6006D29FC /* TypographyPage.swift */; }; @@ -124,7 +128,10 @@ 07387C6228F062D900D8721F /* ListComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListComponent.swift; sourceTree = ""; }; 07387C6828F0641400D8721F /* StandardListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardListModel.swift; sourceTree = ""; }; 0752EC2728EDBB540029A7BE /* ChipsComponentModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChipsComponentModel.swift; sourceTree = ""; }; - 0752EC2928EDD7A80029A7BE /* BottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheet.swift; sourceTree = ""; }; + 07535BA929D31A440012F298 /* BottomSheetStandardVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetStandardVariant.swift; sourceTree = ""; }; + 07535BAD29D713320012F298 /* BottomSheetExpandingVariantOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BottomSheetExpandingVariantOptions.swift; path = Expanding/BottomSheetExpandingVariantOptions.swift; sourceTree = ""; }; + 07535BAE29D713320012F298 /* BottomSheetExpandingVariant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BottomSheetExpandingVariant.swift; path = Expanding/BottomSheetExpandingVariant.swift; sourceTree = ""; }; + 07535BCE29DC57E60012F298 /* CustomizableVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizableVariant.swift; sourceTree = ""; }; 075462F728F474CC002E2E40 /* ButtonsComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonsComponent.swift; sourceTree = ""; }; 075462F928F474CC002E2E40 /* EmphasisAndFunctionnalVariant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmphasisAndFunctionnalVariant.swift; sourceTree = ""; }; 075E5D6C29378735009A801B /* RecipesLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipesLoader.swift; sourceTree = ""; }; @@ -167,6 +174,7 @@ 07F141BF298BC213007C8575 /* CardHorizontalVariant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardHorizontalVariant.swift; sourceTree = ""; }; 07F141C1298C0644007C8575 /* RecipeBookModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeBookModel.swift; sourceTree = ""; }; 07F141C3298CF4CB007C8575 /* RecipesBook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipesBook.swift; sourceTree = ""; }; + 07F141C629928B96007C8575 /* BottomSheetComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomSheetComponent.swift; sourceTree = ""; }; 07F343852943458C0043335A /* ProgressIndicatorComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressIndicatorComponent.swift; sourceTree = ""; }; 2ACFE972C59B1460F410852D /* Pods-OrangeDesignSystemDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OrangeDesignSystemDemo.debug.xcconfig"; path = "Target Support Files/Pods-OrangeDesignSystemDemo/Pods-OrangeDesignSystemDemo.debug.xcconfig"; sourceTree = ""; }; 759CBF84E701D1BE3D68D365 /* Pods_OrangeDesignSystemDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OrangeDesignSystemDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -323,6 +331,23 @@ path = Chips; sourceTree = ""; }; + 07535BAB29D31ACA0012F298 /* Extending */ = { + isa = PBXGroup; + children = ( + 07535BAE29D713320012F298 /* BottomSheetExpandingVariant.swift */, + 07535BAD29D713320012F298 /* BottomSheetExpandingVariantOptions.swift */, + ); + name = Extending; + sourceTree = ""; + }; + 07535BAC29D31AD60012F298 /* Standard */ = { + isa = PBXGroup; + children = ( + 07535BA929D31A440012F298 /* BottomSheetStandardVariant.swift */, + ); + path = Standard; + sourceTree = ""; + }; 075462F628F474CC002E2E40 /* Buttons */ = { isa = PBXGroup; children = ( @@ -364,6 +389,7 @@ 07C8233D28AE68B2003B2C3A /* Pages */ = { isa = PBXGroup; children = ( + 07F141C529928B96007C8575 /* BottomSheet */, 07D3AEDE296813DA00B36C3F /* ToolBar */, 07CC452A2923CF9C008BE71F /* Banners */, 075462F628F474CC002E2E40 /* Buttons */, @@ -394,8 +420,8 @@ isa = PBXGroup; children = ( 07C8235828AE690F003B2C3A /* ComponentPage.swift */, - 0752EC2928EDD7A80029A7BE /* BottomSheet.swift */, 07387C5C28F0068B00D8721F /* Component.swift */, + 07535BCE29DC57E60012F298 /* CustomizableVariant.swift */, ); name = Template; path = OrangeDesignSystemDemo/Views/Components/Template; @@ -441,6 +467,16 @@ path = Cards; sourceTree = ""; }; + 07F141C529928B96007C8575 /* BottomSheet */ = { + isa = PBXGroup; + children = ( + 07535BAC29D31AD60012F298 /* Standard */, + 07535BAB29D31ACA0012F298 /* Extending */, + 07F141C629928B96007C8575 /* BottomSheetComponent.swift */, + ); + path = BottomSheet; + sourceTree = ""; + }; 174EE48FFD62B367205CFF78 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -920,6 +956,7 @@ F90D978728005A4A006D29FC /* ComponentList.swift in Sources */, 07387C6928F0641400D8721F /* StandardListModel.swift in Sources */, 0789AB992934C35B00796B82 /* SliderComponent.swift in Sources */, + 07535BAA29D31A440012F298 /* BottomSheetStandardVariant.swift in Sources */, 07973C54287C7470004397D7 /* ODSColors+extension.swift in Sources */, F90D977428003104006D29FC /* ODSDemoAboutView.swift in Sources */, F96A3E19280451660086B9BF /* ColorDetail.swift in Sources */, @@ -937,11 +974,14 @@ F90D978528003299006D29FC /* AboutConfigDemo.swift in Sources */, F90D9768280030A6006D29FC /* TypographyPage.swift in Sources */, F96A3E17280451660086B9BF /* ColorsPage.swift in Sources */, + 07F141C829928B96007C8575 /* BottomSheetComponent.swift in Sources */, 07C8234F28AE68B2003B2C3A /* TextFieldComponent.swift in Sources */, 071D3EF128884D8200DFD1C9 /* SpacingsPage.swift in Sources */, + 07535BCF29DC57E60012F298 /* CustomizableVariant.swift in Sources */, 07C8236828AE823F003B2C3A /* ThemeSelectionView.swift in Sources */, F90D9768280030A6006D29FC /* TypographyPage.swift in Sources */, 07387C5F28F02F4F00D8721F /* SelectionList.swift in Sources */, + 07535BAF29D713320012F298 /* BottomSheetExpandingVariantOptions.swift in Sources */, F96A3E17280451660086B9BF /* ColorsPage.swift in Sources */, 07C8234F28AE68B2003B2C3A /* TextFieldComponent.swift in Sources */, 075E612C299AA988004CE0A6 /* ColorsGuideline.swift in Sources */, @@ -953,7 +993,6 @@ 07C8235028AE68B2003B2C3A /* ChipsComponent.swift in Sources */, 07081296293E29A7002E38BB /* ProgressBarVariant.swift in Sources */, 07000FB7292B7B0700CE537A /* NavigatinBarModifiers.swift in Sources */, - 0752EC2A28EDD7A80029A7BE /* BottomSheet.swift in Sources */, 07C52F2028D37A2B0067CFC0 /* CardExampleData.swift in Sources */, 075E612329968A56004CE0A6 /* SecureVariant.swift in Sources */, 072DE105296DCE3E00229FCF /* ToastView.swift in Sources */, @@ -964,6 +1003,7 @@ F96A3E18280451660086B9BF /* ColorUsage.swift in Sources */, 075E5D6D29378735009A801B /* RecipesLoader.swift in Sources */, F90D977528003104006D29FC /* ODSDemoAboutConfig.swift in Sources */, + 07535BB029D713320012F298 /* BottomSheetExpandingVariant.swift in Sources */, 07C8235928AE690F003B2C3A /* ComponentPage.swift in Sources */, 07F141C2298C0644007C8575 /* RecipeBookModel.swift in Sources */, 07387C6328F062D900D8721F /* ListComponent.swift in Sources */, @@ -1158,7 +1198,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.11.2; + MARKETING_VERSION = 0.12.0; PRODUCT_BUNDLE_IDENTIFIER = "soft.cocoa.ods-ios-demo.dev"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1193,7 +1233,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.11.2; + MARKETING_VERSION = 0.12.0; PRODUCT_BUNDLE_IDENTIFIER = "soft.cocoa.ods-ios-demo.dev"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OrangeDesignSystemDemo/OrangeDesignSystemDemo.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..df102642 --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "BottomSheet", + "repositoryURL": "https://github.com/lucaszischka/BottomSheet", + "state": { + "branch": null, + "revision": "4c9ef84552712e0117c37d4893270fdc28fb9288", + "version": "3.1.0" + } + } + ] + }, + "version": 1 +} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCafe.imageset/Contents.json b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Cafe.imageset/Contents.json similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCafe.imageset/Contents.json rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Cafe.imageset/Contents.json diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCafe.imageset/iconsCommunicationDIcCafe.svg b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Cafe.imageset/iconsCommunicationDIcCafe.svg similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCafe.imageset/iconsCommunicationDIcCafe.svg rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Cafe.imageset/iconsCommunicationDIcCafe.svg diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCookingPot.imageset/Contents.json b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/CookingPot.imageset/Contents.json similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCookingPot.imageset/Contents.json rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/CookingPot.imageset/Contents.json diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCookingPot.imageset/iconsCommunicationDIcCookingPot.svg b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/CookingPot.imageset/iconsCommunicationDIcCookingPot.svg similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIcCookingPot.imageset/iconsCommunicationDIcCookingPot.svg rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/CookingPot.imageset/iconsCommunicationDIcCookingPot.svg diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIIcIceCream.imageset/Contents.json b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/IceCream.imageset/Contents.json similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIIcIceCream.imageset/Contents.json rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/IceCream.imageset/Contents.json diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIIcIceCream.imageset/iconsCommunicationDIIcIceCream.svg b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/IceCream.imageset/iconsCommunicationDIIcIceCream.svg similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationDIIcIceCream.imageset/iconsCommunicationDIIcIceCream.svg rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/IceCream.imageset/iconsCommunicationDIIcIceCream.svg diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationRUIcRestaurant.imageset/Contents.json b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Restaurant.imageset/Contents.json similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationRUIcRestaurant.imageset/Contents.json rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Restaurant.imageset/Contents.json diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationRUIcRestaurant.imageset/iconsCommunicationRUIcRestaurant.svg b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Restaurant.imageset/iconsCommunicationRUIcRestaurant.svg similarity index 100% rename from OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/iconsCommunicationRUIcRestaurant.imageset/iconsCommunicationRUIcRestaurant.svg rename to OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/Recipes/Restaurant.imageset/iconsCommunicationRUIcRestaurant.svg diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/AboutImage_generic.png b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/AboutImage_generic.png new file mode 100644 index 00000000..eef8c253 Binary files /dev/null and b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/AboutImage_generic.png differ diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/AboutImage_generic.svg b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/AboutImage_generic.svg deleted file mode 100644 index 696bbd0f..00000000 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/AboutImage_generic.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - Image - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/Contents.json b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/Contents.json index 4adf4f7d..b73e2676 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/Contents.json +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/AboutImage_generic.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "AboutImage_generic.svg", + "filename" : "AboutImage_generic.png", "idiom" : "universal" } ], diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/BottomSheet_generic.imageset/BottomSheet_generic.png b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/BottomSheet_generic.imageset/BottomSheet_generic.png new file mode 100644 index 00000000..4a053f8b Binary files /dev/null and b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/BottomSheet_generic.imageset/BottomSheet_generic.png differ diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/BottomSheet_generic.imageset/Contents.json b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/BottomSheet_generic.imageset/Contents.json new file mode 100644 index 00000000..5a4598f6 --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Generic/BottomSheet_generic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "BottomSheet_generic.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/BottomSheet.imageset/Contents.json b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/BottomSheet.imageset/Contents.json new file mode 100644 index 00000000..db609ce3 --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/BottomSheet.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bottomSheet.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/BottomSheet.imageset/bottomSheet.png b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/BottomSheet.imageset/bottomSheet.png new file mode 100644 index 00000000..4a053f8b Binary files /dev/null and b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Assets.xcassets/thumbs/Orange/BottomSheet.imageset/bottomSheet.png differ diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/OrangeDesignSystemDemoApp.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/OrangeDesignSystemDemoApp.swift index 3509144c..50563e8c 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/OrangeDesignSystemDemoApp.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/OrangeDesignSystemDemoApp.swift @@ -27,7 +27,7 @@ import SwiftUI @main struct ods_ios_swiftUI_demoApp: App { @StateObject var themeProvider = ThemeProvider() - + var body: some Scene { WindowGroup { ODSThemeableView(theme: themeProvider.currentTheme) { diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Themes/ThemeSelectionView.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Themes/ThemeSelectionView.swift index 692e507f..3b4ba22b 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Themes/ThemeSelectionView.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Themes/ThemeSelectionView.swift @@ -141,6 +141,7 @@ struct HotSwhitchIndicatorModifier: ViewModifier { self.hotSwitchWarningIndicator = hotSwitchWarningIndicator } + @ViewBuilder func body(content: Content) -> some View { content .alert("Warning", isPresented: $hotSwitchWarningIndicator.showAlert) { diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/About/ODSDemoAboutConfig.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/About/ODSDemoAboutConfig.swift index 62cebd0a..137b32fd 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/About/ODSDemoAboutConfig.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/About/ODSDemoAboutConfig.swift @@ -25,19 +25,26 @@ import OrangeDesignSystem import SwiftUI public final class ODSDemoAboutConfig: NSObject { + + // ======================= + // MARK: Stored Properties + // ======================= public static let instance = ODSDemoAboutConfig() - let applicationInformation: ApplicationInformation + // ================= + // MARK: Initializer + // ================= + override private init() { applicationInformation = ApplicationInformation( - name: "Orange Design System", + name: "Orange Design System Demo", version: Bundle.main.marketingVersion, buildNumber: Bundle.main.buildNumber, buildType: Bundle.main.buildType, description: "In this app you'll find implemented code examples of the guidelines, components and modules, for the themes of the Orange Design System.", - imageHeader: Image("AboutImage", bundle: Bundle.main)) + imageHeader: ThemeProvider().imageFromResources("AboutImage")) } public func configure() { diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/ComponentList.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/ComponentList.swift index 54b0da72..40dd64e3 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/ComponentList.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/ComponentList.swift @@ -46,6 +46,7 @@ struct ComponentsList: View { // Remark: Components are automatically displayed sorted by their name let components: [Component] = [ BannerComponent(), + BottomSheetComponent(), ButtonComponent(), CardComponent(), ChipsComponent(), diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Banners/BannerComponent.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Banners/BannerComponent.swift index 7900a447..5198c754 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Banners/BannerComponent.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Banners/BannerComponent.swift @@ -62,17 +62,15 @@ struct BannerVariant: View { // ========== var body: some View { - ZStack { - + CustomizableVariant { BannerVariantContent(model: model) - - BottomSheet { - BannerVariantOptions(model: model) - } + } options: { + + BannerVariantOptions(model: model) } } } - + struct BannerVariantContent: View { // ======================= diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/BottomSheetComponent.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/BottomSheetComponent.swift new file mode 100644 index 00000000..bd92b74f --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/BottomSheetComponent.swift @@ -0,0 +1,56 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// + +import OrangeDesignSystem +import SwiftUI +import BottomSheet + +struct BottomSheetComponent: Component { + let title: String + let imageName: String + let description: String + let variants: AnyView + + init() { + title = "Sheets: Bottom" + imageName = "BottomSheet" + description = "By default, a sheet is modal, presenting a focused experience that prevents users from interacting with the parent view, until they dismiss the sheet. A modal sheet is useful for requesting a specific information or enabling a simple task." + variants = AnyView(BottomSheetVariants()) + } +} + +struct BottomSheetVariants: View { + var body: some View { + VariantEntryItem(text: "Expanding", technicalElement: ".odsBottomSheetExpanding()") { + ExpandingBottomSheetVariantHome(model: BottomSheetVariantModel()) + .navigationTitle("Expanding") + + } + + VariantEntryItem(text: "Standard", technicalElement: ".odsBottomSheetStandard()") { + StandardBottomSheetVariant() + .navigationTitle("Standard") + } + } +} + diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Expanding/BottomSheetExpandingVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Expanding/BottomSheetExpandingVariant.swift new file mode 100644 index 00000000..8a03e61c --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Expanding/BottomSheetExpandingVariant.swift @@ -0,0 +1,206 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// + +import SwiftUI +import OrangeDesignSystem + +struct BottomSheetVariant: View { + + // ====================== + // MARK: Store properties + // ====================== + + @ObservedObject var model: BottomSheetVariantModel + + // ========== + // MARK: Body + // ========== + + var body: some View { + PageContent(model: model) + .modifier(BottomSheetModifier(model: model)) + + } +} + +fileprivate struct BottomSheetModifier: ViewModifier { + + // ====================== + // MARK: Store properties + // ====================== + + @ObservedObject var model: BottomSheetVariantModel + + // ========== + // MARK: Body + // ========== + @ViewBuilder + func body(content: Content) -> some View { + if model.showSubtitle { + content + .odsBottomSheetExpanding(title: "Recipes", + subtitle: "French products", + bottomSheetSize: $model.bottomSheetSize, + content: bottomSheetContent) + } else { + if model.showIcon { + content + .odsBottomSheetExpanding(title: "Recipes", + icon: Image("Heart_19371"), + bottomSheetSize: $model.bottomSheetSize, + content: bottomSheetContent) + } else { + content + .odsBottomSheetExpanding(title: "Recipes", + bottomSheetSize: $model.bottomSheetSize, + content: bottomSheetContent) + } + } + } + + private func bottomSheetContent() -> some View { + BottonSheetContent(model: model) + .background(Color(UIColor.systemBackground)) + + } +} + +fileprivate struct BottonSheetContent: View { + + // ====================== + // MARK: Store properties + // ====================== + + @ObservedObject var model: BottomSheetVariantModel + + // ========== + // MARK: Body + // ========== + + var body: some View { + if model.contentType == .tutorial { + tutorialPage() + } else { + examplePage() + } + } + + // ============= + // MARK: Helpers + // ============= + @ViewBuilder + private func tutorialPage() -> some View { + TutorialText(message: model.tutorialTextOnBottomSheetContent) + } + + private func examplePage() -> some View { +// List { + ForEach(RecipeBook.shared.recipes, id: \.title) { recipe in + let listItemModel = + ODSListStandardItemModel(title: recipe.title, leadingIcon: .icon(Image(recipe.iconName))) + + ODSListStandardItem(model: listItemModel) + .padding(.horizontal, ODSSpacing.s) + .listRowSeparator(Visibility.visible) + .listRowInsets(EdgeInsets()) + .onTapGesture { + model.selectedRecipe = recipe + } + } +// } +// .listStyle(.plain) + } +} + + +fileprivate struct PageContent: View { + + // ====================== + // MARK: Store properties + // ====================== + + @ObservedObject var model: BottomSheetVariantModel + + // ========== + // MARK: Body + // ========== + + var body: some View { + ScrollView { + if model.contentType == .example { + exemplePage() + } else { + tutorialPage() + } + } + } + + // ==================== + // MARK: Computed value + // ==================== + + @ViewBuilder + private func exemplePage() -> some View { + if let recipe = model.selectedRecipe { + let cardModel = + ODSCardVerticalImageFirstModel(title: recipe.title, + subtitle: recipe.subtitle, + imageSource: .asyncImage(recipe.url, Image("ods_empty", bundle: Bundle.ods)), + supportingText: recipe.description) + ODSCardVerticalImageFirst(model: cardModel) { + ODSButton(text: "Start preparing", emphasis: .highest, action: {}) + } + .padding(.horizontal, ODSSpacing.s) + } else { + EmptyView() + } + } + + @ViewBuilder + private func tutorialPage() -> some View { + TutorialText(message: model.tutorialTextOnPageContent) + } +} + +struct TutorialText: View { + + // ====================== + // MARK: Store properties + // ====================== + + let message: String? + + // ========== + // MARK: Body + // ========== + + var body: some View { + if let message = message { + Text(message) + .fixedSize(horizontal: false, vertical: true) + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity, alignment: .center) + .padding(.all, ODSSpacing.m) + } + } +} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Expanding/BottomSheetExpandingVariantOptions.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Expanding/BottomSheetExpandingVariantOptions.swift new file mode 100644 index 00000000..66c234d7 --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Expanding/BottomSheetExpandingVariantOptions.swift @@ -0,0 +1,245 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// + +import SwiftUI +import OrangeDesignSystem + +class BottomSheetVariantModel: ObservableObject { + + // ====================== + // MARK: Store properties + // ====================== + + @Published var bottomSheetSize: ODSBottomSheetSize + @Published var contentType: ContentType + @Published var showSubtitle: Bool { + didSet { if showSubtitle { showIcon = false } } + } + @Published var showIcon: Bool { + didSet { if showIcon { showSubtitle = false } } + } + + @Published var selectedRecipe: Recipe? + + + // ================= + // MARK: Initializer + // ================= + + init() { + self.bottomSheetSize = .medium + self.showSubtitle = false + self.showIcon = false + self.contentType = .tutorial + self.selectedRecipe = RecipeBook.shared.recipes[0] + } + + // ============== + // MARK: Tutorial + // ============== + + var tutorialTextOnPageContent: String? { + switch bottomSheetSize { + case .hidden, .large: + return nil + case .small: + return + """ + To open the bottom sheet :\n + Drag the component up + """ + case .medium: + return + """ + To open or close the bottom sheet :\n + Drag the handle up or down\n + Scroll the content\n + Tap the dimming area + """ + } + } + + var tutorialTextOnBottomSheetContent: String? { + switch bottomSheetSize { + case .hidden, .small, .medium: + return nil + case .large: + return + """ + To close the bottom sheet :\n + Drag the handle down\n + Scroll the content\n + Tap the dimming area + """ + } + } +} + +struct ExpandingBottomSheetVariantHome: View { + + // ====================== + // MARK: Store properties + // ====================== + + @ObservedObject private var model: BottomSheetVariantModel + @State private var showBottomSheet = false + + // ================= + // MARK: Initializer + // ================= + + init(model: BottomSheetVariantModel) { + self.model = model + } + + // ========== + // MARK: Body + // ========== + + var body: some View { + ScrollView { + VStack(spacing: ODSSpacing.m) { + Text("Customize the bottom sheet before opening sheet to see it.") + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, ODSSpacing.m) + + ExpandingBottomSheetVariantOptions(model: model) + + ODSButton(text: "See the component", emphasis: .highest, variableWidth: false) { + showBottomSheet = true + } + .multilineTextAlignment(.center) + .padding(.horizontal, ODSSpacing.m) + .padding(.top, ODSSpacing.m) + + Spacer() + + // First solution: Show Variant in navigation +// NavigationLink( +// destination: +// BottomSheetVariant(model: model) +// .navigationBarTitle("Sheet: Bottom tutorial", displayMode: .inline), +// isActive: $showBottomSheet, +// label: { EmptyView() } +// ) + } + // Second solution: Show Variant in full screen + .fullScreenCover(isPresented: $showBottomSheet) { + NavigationView { + BottomSheetVariant(model: model) + .navigationBarTitle("Sheet: Bottom \(model.contentType.rawValue)", displayMode: .inline) + .navigationbarMenuForThemeSelection() + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + showBottomSheet = false + } label: { + Text("Close") + } + } + } + } + } + .padding(.vertical, ODSSpacing.m) + } + } +} + + +struct ExpandingBottomSheetVariantOptions: View { + + // ====================== + // MARK: Store properties + // ====================== + + @ObservedObject var model: BottomSheetVariantModel + + // ========== + // MARK: Body + // ========== + + var body: some View { + VStack(spacing: ODSSpacing.m) { + Group { + ODSChipPicker(title: "Detent", + selection: $model.bottomSheetSize, + chips: ODSBottomSheetSize.chips) + + ODSChipPicker(title: "Content", + selection: $model.contentType, + chips: ContentType.chips) + + Toggle("Subtitle", isOn: $model.showSubtitle) + .padding(.horizontal, ODSSpacing.m) + .disabled(model.showIcon) + + Toggle("Icon", isOn: $model.showIcon) + .padding(.horizontal, ODSSpacing.m) + .disabled(model.showSubtitle) + } + } + .odsFont(.bodyRegular) + } +} + + +// MARK: - Internal type and extension + +enum ContentType: String, CaseIterable { + case tutorial + case example + + var chip: ODSChip { + ODSChip(self, text: self.rawValue.capitalized) + } + + static var chips: [ODSChip] { + Self.allCases.map { $0.chip } + } +} + +extension ODSBottomSheetSize { + var description: String { + switch self { + case .small: + return "Small" + case .medium: + return "Medium" + case .large: + return "Large" + case .hidden: + return "Hidden" + } + } + + var chip: ODSChip { + ODSChip(self, text: self.description) + } + + static var chips: [ODSChip] { + Self.allCases + .filter { $0 != .hidden } + .map { $0.chip } + } +} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Standard/BottomSheetStandardVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Standard/BottomSheetStandardVariant.swift new file mode 100644 index 00000000..a583103c --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/BottomSheet/Standard/BottomSheetStandardVariant.swift @@ -0,0 +1,107 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// + +import OrangeDesignSystem +import SwiftUI + +struct StandardBottomSheetVariant: View { + + // ======================= + // MARK: Stored properties + // ======================= + + @State var selectedRecipe: Recipe? = RecipesListSelection.receipes.first + @State var isOpen: Bool = false + + // ========== + // MARK: Body + // ========== + + var body: some View { + pageContent + .odsBottomSheetStandard(isOpen: $isOpen, title: "Recipes", icon: Image(systemName: "chevron.down")) { + RecipesListSelection(selectedRecipe: $selectedRecipe) + } + } + + // ============ + // MARK: Helper + // ============ + + @ViewBuilder + private var pageContent: some View { + if let recipe = selectedRecipe { + let cardModel = ODSCardVerticalImageFirstModel(title: recipe.title, + subtitle: recipe.subtitle, + imageSource: .asyncImage(recipe.url, Image("ods_empty", bundle: Bundle.ods)), + supportingText: recipe.description) + + ScrollView { + ODSCardVerticalImageFirst(model: cardModel).padding(.horizontal, ODSSpacing.s) + } + } else { + EmptyView() + } + } +} + +struct RecipesListSelection: View { + + // ======================= + // MARK: Stored properties + // ======================= + + private let selectedRecipe: Binding + private let listItemModels: [ODSListStandardItemModel] + fileprivate static let receipes: [Recipe] = Array(RecipeBook.shared.recipes.prefix(4)) + + // ================= + // MARK: Initializer + // ================= + + init(selectedRecipe: Binding) { + self.selectedRecipe = selectedRecipe + self.selectedRecipe.wrappedValue = Self.receipes.first + self.listItemModels = Self.receipes.map { recipe in + ODSListStandardItemModel(title: recipe.title, leadingIcon: .icon(Image(recipe.iconName))) + } + } + + // ========== + // MARK: Body + // ========== + + var body: some View { + VStack(spacing: 0) { + ForEach(self.listItemModels, id: \.title) { listItemModel in + ODSListStandardItem(model: listItemModel) + .padding(.horizontal, ODSSpacing.s) + .listRowSeparator(Visibility.visible) + .listRowInsets(EdgeInsets()) + .onTapGesture { + selectedRecipe.wrappedValue = Self.receipes.first { $0.title == listItemModel.title } + } + } + } + } +} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/ButtonsComponent.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/ButtonsComponent.swift index 594f0929..263102b1 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/ButtonsComponent.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/ButtonsComponent.swift @@ -87,7 +87,7 @@ struct CommonButtonVariant: View where Variant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { ScrollView { VStack(spacing: ODSSpacing.m) { contentView(model) @@ -96,11 +96,10 @@ struct CommonButtonVariant: View where Variant: View { .padding(.horizontal, ODSSpacing.m) } .padding(.bottom, 55) - - BottomSheet(showContent: false) { - EmphasisAndFunctionalVariantOptions(model: model) - } - } - .background(Color("componentBackground2")) + .background(Color("componentBackground2")) + } options: { + EmphasisAndFunctionalVariantOptions(model: model) + } } } + diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/IconVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/IconVariant.swift index f80528c6..b3de8c5f 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/IconVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Buttons/IconVariant.swift @@ -39,40 +39,42 @@ struct IconVariant: View { // ========== var body: some View { - ZStack { - ScrollView { - VStack(spacing: ODSSpacing.m) { - Text("Plain buttons are the most ubiquitous compoent found troughout applications. Consisting a icon, they are the most simple button style.") - .odsFont(.bodyRegular) - .frame(maxWidth: .infinity, alignment: .leading) - - VariantsTitle().frame(maxWidth: .infinity, alignment: .leading) + CustomizableVariant { + variant + } options: { + IconVariantOptions(model: model) + } + } + + var variant: some View { + ScrollView { + VStack(spacing: ODSSpacing.m) { + Text("Plain buttons are the most ubiquitous compoent found troughout applications. Consisting a icon, they are the most simple button style.") + .odsFont(.bodyRegular) + .frame(maxWidth: .infinity, alignment: .leading) + + VariantsTitle().frame(maxWidth: .infinity, alignment: .leading) + + VStack(alignment: .center, spacing: ODSSpacing.l) { + VStack(alignment: .center, spacing: ODSSpacing.s) { + Text("Icon (add)").odsFont(.headline).frame(maxWidth: .infinity, alignment: .leading) + + ODSIconButton(image: Image("Add")) {} + .disabled(model.showDisabled) + } - VStack(alignment: .center, spacing: ODSSpacing.l) { - VStack(alignment: .center, spacing: ODSSpacing.s) { - Text("Icon (add)").odsFont(.headline).frame(maxWidth: .infinity, alignment: .leading) - - ODSIconButton(image: Image("Add")) {} - .disabled(model.showDisabled) - } - - VStack(alignment: .center, spacing: ODSSpacing.s) { - Text("Icon (info)").odsFont(.headline).frame(maxWidth: .infinity, alignment: .leading) - - ODSIconButton(image: Image(systemName: "info.circle")) {} - .disabled(model.showDisabled) - } + VStack(alignment: .center, spacing: ODSSpacing.s) { + Text("Icon (info)").odsFont(.headline).frame(maxWidth: .infinity, alignment: .leading) + + ODSIconButton(image: Image(systemName: "info.circle")) {} + .disabled(model.showDisabled) } } - .padding(.top, ODSSpacing.m) - .padding(.horizontal, ODSSpacing.m) - } - .padding(.bottom, 55) - - BottomSheet(showContent: false) { - IconVariantOptions(model: model) } + .padding(.top, ODSSpacing.m) + .padding(.horizontal, ODSSpacing.m) } + .padding(.bottom, 55) .background(Color("componentBackground2")) } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardHorizontalVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardHorizontalVariant.swift index 5cd723d3..a1509515 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardHorizontalVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardHorizontalVariant.swift @@ -122,7 +122,7 @@ struct CardHorizontalVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { ScrollView { ODSCardHorizontal(model: model.cardModel) { if let text = model.button1Text { @@ -146,10 +146,8 @@ struct CardHorizontalVariant: View { .alert(model.alertText, isPresented: $model.showAlert) { Button("close", role: .cancel) {} } - - BottomSheet(showContent: false) { - CardHorizontalVariantOptions(model: model) - } + } options: { + CardHorizontalVariantOptions(model: model) } } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardSmallVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardSmallVariant.swift index 3996a22c..4bb18d3d 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardSmallVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardSmallVariant.swift @@ -64,7 +64,7 @@ struct CardSmallVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { ScrollView { LazyVGrid(columns: columns, spacing: ODSSpacing.none) { ForEach(model.cardSmallModels) { model in @@ -77,13 +77,11 @@ struct CardSmallVariant: View { } .padding(.horizontal, ODSSpacing.m) .padding(.top, ODSSpacing.m) - .alert("Card container clicked", isPresented: $showAlert) { - Button("close", role: .cancel) {} - } - - BottomSheet { - CardSmallVariantOptions(model: model) - } + } options: { + CardSmallVariantOptions(model: model) + } + .alert("Card container clicked", isPresented: $showAlert) { + Button("close", role: .cancel) {} } } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalHeaderFirstVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalHeaderFirstVariant.swift index 6ca57d1e..b6817d76 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalHeaderFirstVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalHeaderFirstVariant.swift @@ -95,7 +95,7 @@ struct CardVerticalHeaderFirstVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { ScrollView { ODSCardVerticalHeaderFirst(model: model.cardModel) { if let text = model.button1Text { @@ -119,10 +119,8 @@ struct CardVerticalHeaderFirstVariant: View { .alert(model.alertText, isPresented: $model.showAlert) { Button("close", role: .cancel) {} } - - BottomSheet(showContent: false) { - CardVerticalHeaderFirstVariantOptions(model: model) - } + } options: { + CardVerticalHeaderFirstVariantOptions(model: model) } } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalImageFirstVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalImageFirstVariant.swift index c5a83498..8f10d2ff 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalImageFirstVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Cards/CardVerticalImageFirstVariant.swift @@ -90,7 +90,7 @@ struct CardVerticalImageFirstVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { // Card demonstrator ScrollView { ODSCardVerticalImageFirst(model: model.cardModel) { @@ -115,10 +115,8 @@ struct CardVerticalImageFirstVariant: View { .alert(model.alertText, isPresented: $model.showAlert) { Button("close", role: .cancel) {} } - - BottomSheet(showContent: false) { - CardVerticalImageFirstVariantOptions(model: model) - } + } options: { + CardVerticalImageFirstVariantOptions(model: model) } } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/SelectionVariant/SelectionList.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/SelectionVariant/SelectionList.swift index df726e1f..bc9b37c2 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/SelectionVariant/SelectionList.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/SelectionVariant/SelectionList.swift @@ -38,13 +38,10 @@ struct SelectionListVariant: View { // ========== var body: some View { - ZStack { - + CustomizableVariant { SelectionListVariantInner(model: model) - - BottomSheet { + } options: { SelectionListVariantOptions(model: model) - } } } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/StandardVariant/StandardList.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/StandardVariant/StandardList.swift index 25b1896b..a61ca8c7 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/StandardVariant/StandardList.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Lists/StandardVariant/StandardList.swift @@ -37,13 +37,10 @@ struct StandardListVariant: View { // ========== var body: some View { - ZStack { - + CustomizableVariant { StandardListVariantInner(model: model) - - BottomSheet { - StandardListVariantOptions(model: model) - } + } options: { + StandardListVariantOptions(model: model) } } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/NavigationBar/NavigationBarComponent.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/NavigationBar/NavigationBarComponent.swift index cf58fa37..0cd6ec72 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/NavigationBar/NavigationBarComponent.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/NavigationBar/NavigationBarComponent.swift @@ -63,12 +63,10 @@ struct NavigationBarVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { NavigationBarVariantContent(model: model) - - BottomSheet { - NavigationBarVariantOptions(model: model) - } + } options: { + NavigationBarVariantOptions(model: model) } } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ActivityIndicatorVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ActivityIndicatorVariant.swift index aeed2f63..6a061dbd 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ActivityIndicatorVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ActivityIndicatorVariant.swift @@ -37,7 +37,7 @@ struct ActivityIndicatorVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { VStack { ProgressView { if model.showLabel { @@ -47,9 +47,7 @@ struct ActivityIndicatorVariant: View { Spacer() } .padding(.all, ODSSpacing.m) - } - - BottomSheet { + } options: { ActivityIndicatorVariantOptions(model: model) } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ProgressBarVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ProgressBarVariant.swift index f2b75974..f4e643c1 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ProgressBarVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/ProgressIndicator/ProgressBarVariant.swift @@ -40,40 +40,46 @@ struct ProgressBarVariant: View { // ========== var body: some View { - ZStack { - VStack { - ProgressView(value: secondsElapsed, total: maxSeconds) { - if model.showLabel { - Label(title: {Text("Downloading...")}, icon: { - if model.showIconInLabel { - Image(systemName: "tray.and.arrow.down") - } - }) - } - } currentValueLabel: { - if model.showCurrentValue { - let percent = String(format: "%.0f", secondsElapsed) - Text("\(percent) %") - .frame(maxWidth: .infinity, alignment: .trailing) - } + CustomizableVariant { + variant + } options: { + ProgressBarVariantOptions(model: model) + } + } + + // ==================== + // MARK: Private helper + // ==================== + + var variant: some View { + VStack { + ProgressView(value: secondsElapsed, total: maxSeconds) { + if model.showLabel { + Label(title: {Text("Downloading...")}, icon: { + if model.showIconInLabel { + Image(systemName: "tray.and.arrow.down") + } + }) } - .tint(theme.componentColors.accent) - .onReceive(timer) { _ in - if secondsElapsed < maxSeconds { - secondsElapsed += 1 - } else { - secondsElapsed = 0 - } + } currentValueLabel: { + if model.showCurrentValue { + let percent = String(format: "%.0f", secondsElapsed) + Text("\(percent) %") + .frame(maxWidth: .infinity, alignment: .trailing) } - - Spacer() } - .padding(.all, ODSSpacing.m) - - BottomSheet() { - ProgressBarVariantOptions(model: model) + .tint(theme.componentColors.accent) + .onReceive(timer) { _ in + if secondsElapsed < maxSeconds { + secondsElapsed += 1 + } else { + secondsElapsed = 0 + } } + + Spacer() } + .padding(.all, ODSSpacing.m) } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Sliders/SlidersVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Sliders/SlidersVariant.swift index 203ec743..71f46b88 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Sliders/SlidersVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/Sliders/SlidersVariant.swift @@ -31,57 +31,66 @@ struct SliderVariant: View { // ====================== @ObservedObject var model: SliderVariantModel - @State private var value = 50.0 - let range = 0 ... 100.0 + @State private var value = 5.0 + private let range = 0 ... 10.0 // ========== // MARK: Body // ========== var body: some View { - ZStack { - ScrollView { - VStack { - if model.showValue { - Text(String(format: "%.0f", value)) - .odsFont(.bodyRegular) - .frame(maxWidth: .infinity, alignment: .leading) - .accessibilityHidden(true) + CustomizableVariant { + variant + } options: { + SliderVariantOptions(model: model) + } + } + + + // ===================== + // MARK: Private Helpers + // ===================== + var variant: some View { + ScrollView { + VStack { + if model.showValue { + Text(String(format: "%.2f", value)) + .odsFont(.bodyRegular) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityHidden(true) + } + + if model.stepped { + ODSSlider(value: $value, in: range, step: 0.5) { + Text("Volume") + } minimumValueLabel: { + SliderLabel(show: model.showSideIcons, systemName: "speaker.wave.1.fill") + } maximumValueLabel: { + SliderLabel(show: model.showSideIcons, systemName: "speaker.wave.3.fill") + } onEditingChanged: { isEditing in + print("isEdition: \(isEditing)") } - - ODSSlider(value: $value, range: range, step: step) { + } else { + ODSSlider(value: $value, in: range) { + Text("Volume") + } minimumValueLabel: { SliderLabel(show: model.showSideIcons, systemName: "speaker.wave.1.fill") - } maximumLabelView: { + } maximumValueLabel: { SliderLabel(show: model.showSideIcons, systemName: "speaker.wave.3.fill") + } onEditingChanged: { isEditing in + print("isEdition: \(isEditing)") } - .accessibilityLabel(Text("Volume")) } - .padding(.horizontal, ODSSpacing.m) - .padding(.top, ODSSpacing.m) - } - - BottomSheet(showContent: false) { - SliderVariantOptions(model: model) } + .padding(.horizontal, ODSSpacing.m) + .padding(.top, ODSSpacing.m) } } - // ===================== - // MARK: Private Helpers - // ===================== - - private var step: Double { - return model.stepped ? 5.0 : 1.0 - } - - struct SliderLabel: View { - let show: Bool - let systemName: String - - var body: some View { - if show { - Image(systemName: systemName).accessibilityHidden(true) - } + @ViewBuilder + func SliderLabel(show: Bool, systemName: String) -> some View { + if show { + Image(systemName: systemName).accessibilityHidden(true) } } } @@ -123,7 +132,7 @@ struct SliderVariantOptions: View { VStack(spacing: ODSSpacing.m) { Toggle("Side icons", isOn: $model.showSideIcons) Toggle("Display value", isOn: $model.showValue) - Toggle("Stepped", isOn: $model.stepped) + Toggle("Stepped (0.5)", isOn: $model.stepped) } .odsFont(.bodyRegular) .padding(.vertical, ODSSpacing.m) diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TabBar/TabBarVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TabBar/TabBarVariant.swift index 778356e7..1a1eb3ce 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TabBar/TabBarVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TabBar/TabBarVariant.swift @@ -51,7 +51,7 @@ struct TabBarVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { GeometryReader { reader in VStack(alignment: .center, spacing: 0) { VStack { @@ -72,13 +72,11 @@ struct TabBarVariant: View { .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in spacerHeight = Self.computeSpacerHeight() } - - BottomSheet { - TabBarVariantOptions(model: model) - } + } options: { + TabBarVariantOptions(model: model) } } - + // ============= // MARK: Helpers // ============= diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TextFields/CapitalizedTextInputsVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TextFields/CapitalizedTextInputsVariant.swift index 6df98cde..df77bbc0 100644 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TextFields/CapitalizedTextInputsVariant.swift +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Pages/TextFields/CapitalizedTextInputsVariant.swift @@ -53,7 +53,7 @@ private struct CapitalizedTextInputsVariant: View { // ========== var body: some View { - ZStack { + CustomizableVariant { VStack { textField .textInputAutocapitalization(model.selectedCapitalizationType.textInputAutocapitalization) @@ -68,10 +68,8 @@ private struct CapitalizedTextInputsVariant: View { Spacer() } - - BottomSheet(showContent: false) { - CapitalizedTextInputsVariantOptions(model: model) - } + } options: { + CapitalizedTextInputsVariantOptions(model: model) } } diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Template/BottomSheet.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Template/BottomSheet.swift deleted file mode 100644 index 5ff4c94b..00000000 --- a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Template/BottomSheet.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// MIT License -// Copyright (c) 2021 Orange -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the Software), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -// - -import SwiftUI -import OrangeDesignSystem - -// MARK: Button sheet with header and content -struct BottomSheet: View where ContentView: View { - @State var showContent: Bool = true - let contentView: () -> ContentView - - init(showContent: Bool = true, @ViewBuilder contentView: @escaping () -> ContentView) { - self.showContent = showContent - self.contentView = contentView - } - - var body: some View { - VStack(spacing: ODSSpacing.none) { - Spacer() - - VStack(spacing: ODSSpacing.none) { - BottomSheedHeader(showContent: $showContent) - .background(Color(.systemGray6)) - - if showContent { - contentView() - } - } - .background(Color(UIColor.systemBackground)) - } - .cornerRadius(10) - .shadow(radius: 8) - } -} - -struct BottomSheedHeader: View { - - @Binding var showContent: Bool - - var body: some View { - VStack(spacing: ODSSpacing.none) { - RoundedRectangle(cornerRadius: 4) - .frame(width: 55, height: 4, alignment: .center) - .padding(.top, ODSSpacing.s) - .padding(.bottom, ODSSpacing.xs) - - Button { - showContent.toggle() - } label: { - VStack(spacing: ODSSpacing.none) { - HStack(spacing: ODSSpacing.m) { - let imageName = showContent ? "chevron.down" : "chevron.up" - - Image(systemName: imageName) - .foregroundColor(.primary) - .accessibility(hidden: true) - - Text("Settings") - .odsFont(.headline) - .foregroundColor(.primary) - Spacer() - } - .padding(.all, ODSSpacing.s) - - Divider() - } - } - } - } -} diff --git a/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Template/CustomizableVariant.swift b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Template/CustomizableVariant.swift new file mode 100644 index 00000000..9cf88426 --- /dev/null +++ b/OrangeDesignSystemDemo/OrangeDesignSystemDemo/Views/Components/Template/CustomizableVariant.swift @@ -0,0 +1,62 @@ +// +// MIT License +// Copyright (c) 2021 Orange +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the Software), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// + +import SwiftUI +import OrangeDesignSystem + +struct CustomizableVariant: View where Variant: View, Options: View { + + // ======================= + // MARK: Stored Properties + // ======================= + + @State var isOpen = false + let variant: () -> Variant + let options: () -> Options + + // ================= + // MARK: Initializer + // ================= + + init(@ViewBuilder variant: @escaping () -> Variant, + @ViewBuilder options: @escaping () -> Options) { + self.variant = variant + self.options = options + } + + // ========== + // MARK: Body + // ========== + + var body: some View { + variant() + .task { + withAnimation(Animation.linear.delay(0.5)) { + self.isOpen = true + } + } + .odsBottomSheetStandard(isOpen: $isOpen, title: "Customize", + icon: Image(systemName: "chevron.down"), annimateIcon: true, + content: self.options) + } +} diff --git a/OrangeDesignSystemDemo/fastlane/Fastfile b/OrangeDesignSystemDemo/fastlane/Fastfile index 3387695c..b3c807be 100644 --- a/OrangeDesignSystemDemo/fastlane/Fastfile +++ b/OrangeDesignSystemDemo/fastlane/Fastfile @@ -43,7 +43,7 @@ platform :ios do # BUILD DEBUG APP # ------------------------------------------------------------ desc "BUILD DEBUG APP" - lane :build do + lane :buildDebugApp do cocoapods( clean_install: true ) @@ -71,40 +71,42 @@ platform :ios do set_info_plist_value(path: "#{Dir.pwd}/../OrangeDesignSystemDemo/Info.plist", key: "ODSBuildType", value: "This is a QUALIF version") - build_and_upload + build_and_upload(upload: true) end # ------------------------------------------------------------ # BUILD & UPLOAD TO TESTFLIGHT PROD APP # ------------------------------------------------------------ - desc "BUILD & UPLOAD TO TESTFLIGHT PROD APP" - lane :prod do + desc "BUILD & UPLOAD TO TESTFLIGHT (if set in options: upload) PROD APP" + lane :prod do |options| puts "This is a dumb 'puts' to ensure the 'Appfile' is read!" - build_and_upload + build_and_upload(options) end - # ----------------------------------------------------------------------- # PRIVATE LANE BUILD & UPLOAD (DEV / QUALIF / PROD is set by main lane) # ----------------------------------------------------------------------- - desc "PRIVATE LANE BUILD & UPLOAD (DEV / QUALIF / PROD is set by main lane)" - private_lane :build_and_upload do - TESTFLIGHT_GROUPS = ENV['TESTFLIGHT_GROUPS'] + private_lane :build_and_upload do |options| + build + + if options[:upload] + upload + else + puts ">> Upload to testflight no requested" + end + end + # ----------------------------------------------------------------------- + # PRIVATE LANE BUILD (DEV / QUALIF / PROD is set by main lane) + # ----------------------------------------------------------------------- + desc "PRIVATE LANE BUILD (DEV / QUALIF / PROD is set by main lane)" + private_lane :build do update_app_identifier( xcodeproj: "#{ODS_PROJECT}", plist_path: "#{ODS_SCHEME}/Info.plist", app_identifier: CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier) ) - api_key = app_store_connect_api_key( - key_id: APPLE_KEY_ID, - issuer_id: APPLE_ISSUER_ID, - key_content: APPLE_KEY_CONTENT, - duration: 500, - in_house: false - ) - increment cocoapods( @@ -123,8 +125,24 @@ platform :ios do export_method: 'app-store', xcargs: '-allowProvisioningUpdates' ) - - + end + + # ----------------------------------------------------------------------- + # PRIVATE LANE UPLOAD TO TESTFLIGHT (DEV / QUALIF / PROD is set by main lane) + # ----------------------------------------------------------------------- + desc "PRIVATE LANE UPLOAD TO TESTFLIGHT" + private_lane :upload do + + api_key = app_store_connect_api_key( + key_id: APPLE_KEY_ID, + issuer_id: APPLE_ISSUER_ID, + key_content: APPLE_KEY_CONTENT, + duration: 500, + in_house: false + ) + + TESTFLIGHT_GROUPS = ENV['TESTFLIGHT_GROUPS'] + version = get_app_version puts version @@ -145,6 +163,10 @@ platform :ios do ) end + # ------- + # Helpers + # ------- + # Get version set in the Xcode project def get_app_version version = get_version_number( xcodeproj: ODS_PROJECT, @@ -153,7 +175,8 @@ platform :ios do return version end - + # Read release note in section associated to the current version + # If empty, try within the Unreleased section def read_current_release_notes version = get_app_version diff --git a/OrangeDesignSystemDemo/fastlane/README.md b/OrangeDesignSystemDemo/fastlane/README.md index 59a5f9ba..3b360cc0 100644 --- a/OrangeDesignSystemDemo/fastlane/README.md +++ b/OrangeDesignSystemDemo/fastlane/README.md @@ -31,10 +31,10 @@ UPDATE BUILD NUMBER WITH TIMESTAMP READ AND SET NEXT RELEASE NOTE IN CHANLOG -### ios build +### ios buildDebugApp ```sh -[bundle exec] fastlane ios build +[bundle exec] fastlane ios buildDebugApp ``` BUILD DEBUG APP @@ -53,7 +53,7 @@ BUILD & UPLOAD TO TESTFLIGHT QUALIF APP [bundle exec] fastlane ios prod ``` -BUILD & UPLOAD TO TESTFLIGHT PROD APP +BUILD & UPLOAD TO TESTFLIGHT (if set in options: upload) PROD APP ---- diff --git a/OrangeTheme/Sources/OrangeTheme/OrangeTheme.swift b/OrangeTheme/Sources/OrangeTheme/OrangeTheme.swift index 08c5a978..910a83bb 100644 --- a/OrangeTheme/Sources/OrangeTheme/OrangeTheme.swift +++ b/OrangeTheme/Sources/OrangeTheme/OrangeTheme.swift @@ -207,6 +207,9 @@ public struct OrangeThemeFactory { theme.componentColors.functionalInfo = OrangeColors.functionalInfo.colorDecription.color theme.componentColors.functionalAlert = OrangeColors.functionalAlert.colorDecription.color + // Bottom sheet + theme.componentColors.bottomSheetHeaderBackground = OrangeColors.componentBackground.colorDecription.color + theme.font = { style in switch style { case .largeTitle: diff --git a/Package.swift b/Package.swift index 949397e8..6c64cc9e 100644 --- a/Package.swift +++ b/Package.swift @@ -24,14 +24,14 @@ let package = Package( dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), - + .package(url: "https://github.com/lucaszischka/BottomSheet", .exact("3.1.0")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "OrangeDesignSystem", - dependencies: [], + dependencies: ["BottomSheet"], path: "OrangeDesignSystem/Sources"), .target( name: "OrangeTheme", diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md index 5307cb50..1658d973 100644 --- a/THIRD-PARTY.md +++ b/THIRD-PARTY.md @@ -19,3 +19,6 @@ You may download the source code on the following website: https://github.com/re Parma is distributed under the terms and conditions ot the MIT License (http://opensource.org/licenses/MIT) You may download the source code on the following website: https://github.com/dasautoooo/Parma +### BottomSheet +BottomSheet is distributed under the terms and conditions ot the MIT License (http://opensource.org/licenses/MIT) +You may download the source code on the following website: https://github.com/lucaszischka/BottomSheet diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index f2c45188..7334e3ea 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -78,6 +78,7 @@ PLATFORMS arm64-darwin-21 x86_64-darwin-19 x86_64-darwin-20 + x86_64-darwin-21 DEPENDENCIES jekyll (~> 4.2.1) diff --git a/docs/_data/data_menus.yml b/docs/_data/data_menus.yml index 31e5fe9a..625f03d6 100644 --- a/docs/_data/data_menus.yml +++ b/docs/_data/data_menus.yml @@ -36,6 +36,8 @@ toc2: url: components/progressIndicator_docs - page: Slider url: components/slider_docs + - page: Sheets - Bottom + url: components/sheetsBottom_docs - page: Text fields url: components/textInput_docs diff --git a/docs/components/sheetsBottom.md b/docs/components/sheetsBottom.md new file mode 100644 index 00000000..2f8ddc00 --- /dev/null +++ b/docs/components/sheetsBottom.md @@ -0,0 +1,86 @@ +--- +layout: detail +title: Bottom sheets +description: Bottom Sheets are surfaces anchored to the bottom of the screen that present users supplement content. +--- + +--- + +**Page Summary** + +* [Specifications references](#specifications-references) +* [Accessibility](#accessibility) +* [Variants](#variants) + * [Standard](#standard) + * [Expanding](#expanding) + +--- + +## Specifications references + +- [Design System Manager - Bottom sheets](https://system.design.orange.com/0c1af118d/p/3347ca-sheets-bottom/b/83b619) + +## Accessibility + +Please follow [accessibility criteria for development](https://a11y-guidelines.orange.com/en/mobile/ios/) + +## Variants + +Bottom sheets are surfaces anchored to the bottom of the screen that present users supplemental content. +It is useful for requesting a specific information or enabling a simple task related to the current context +of the current view or more globaly the application context. + +### Standard + +The standard bottom sheet must be used only with a "simple, basic" content. If a more complex content (scrollable) must be added prefer the Expanding variant. + +It defines two states: +- **closed**: The content is hidden +- **opened**: The content is visible (above the main view) + +A taps on the header, opens or closes the bottom sheet. + +```swift +struct BottomSheetPresentation: View { + @State private var isOpen = false + + var body: some View { + VStack { + // Main content goes here. + Text("Bottom sheet is \(isOpen ? "Opened": "Closed")") + } + .odsBottomSheetStandard(isOpen: $isOpen, title: "Customize") { + // Bottom sheet content goes here + } + } +} +``` + +### Expanding + +The type of bottom must be used if the content is more complex and perhaps need to be scrollable. + +It defines three size: +- **small**: (closed) The content is hidden, only the header is visible +- **medium**: (parcially opened) The content is parcially visible (half screen above the main view) but not scrollable +- **large**: (opened) The content is visible and scrollable + +User can resize by tapping on dimming area (close), drag the content, or tap on the header to cycle through the available sizes. + +```swift + struct BottomSheetPresentation: View { + @State private var bottomSheetSize: ODSBottomSheetSize = .large + var body: some View { + VStack { + // Main content goes here. + Text("Bottom sheet size \(bottomSheetSize.rawValue)") + } + .odsBottomSheetExpanding(title: "Customize", bottomSheetSize: $bottomSheetSize) { + // Bottom sheet content goes here + } + } + } +``` + +**Remark**: In order to compute the resizing when user scrolls the content, the bottom sheet automatically adds the provided content is a scrollView. + diff --git a/docs/components/sheetsBottom_docs.md b/docs/components/sheetsBottom_docs.md new file mode 100644 index 00000000..42eeca14 --- /dev/null +++ b/docs/components/sheetsBottom_docs.md @@ -0,0 +1,4 @@ +--- +layout: main +content_page: sheetsBottom.md +--- diff --git a/docs/components/slider.md b/docs/components/slider.md index f8b2d8f0..a3a08968 100644 --- a/docs/components/slider.md +++ b/docs/components/slider.md @@ -30,17 +30,7 @@ Please follow [accessibility criteria for development](https://a11y-guidelines.o As the `ODSSlider` is based on the native `Slider`, Voice Over is able to vocalize However, if you want to set a description you need to add it using `.accessibilityLabel` on the `ODSSlider`. -We recommand to not set information on `minimumValueLabel` and `maximumValueLabel` view using `.accessibilityHidden(true)`. You can do it like this: - -```swift -ODSSlider(value: $value, - range: range, - step: step) { - Text("0").accessibilityHidden(true) -} maximumLabelView: { - Text("100").accessibilityHidden(true) -} -``` +We recommand to not set information on `minimumValueLabel` and `maximumValueLabel` view using `.accessibilityHidden(true)` ## Variants @@ -54,52 +44,65 @@ Unlabelled sliders allow users to make easy selections that do not require any d struct UnlabeledSlider: View { @State private var value = 50.0 - let range = 0 ... 100.0 var body: some View { - Text("Unlabeled slider") - .odsFont(.title2) - VStack(alignment: .center) { - ODSSlider(value: $value, range: range) - } - .padding(.horizontal, ODSSpacing.s) + ODSSlider(value: $value, in: 0 ... 100) } } ``` ### Labeled slider (with images) +We recommand to not set information on `minimumValueLabel` and `maximumValueLabel` view using `.accessibilityHidden(true)`. You can do it like this: + ```swift -ODSSlider(value: $value, in: 0 ... 100) { - Image(systemName: "speaker.wave.1.fill") -} maximumValueLabel: { - Image(systemName: "speaker.wave.3.fill") +struct LabeledSlider: View { + + @State private var value = 50.0 + + var body: some View { + ODSSlider(value: $value, in: 0 ... 100) { + Text("Volume") + } minimumValueLabel: { + Image(systemName: "speaker.wave.1.fill").accessibilityHidden(true) + } maximumValueLabel: { + Image(systemName: "speaker.wave.3.fill").accessibilityHidden(true) + } + } } ``` ### Labeled slider (with text) +We recommand to not set information on `minimumValueLabel` and `maximumValueLabel` view using `.accessibilityHidden(true)`. You can do it like this: + ```swift ODSSlider(value: $value, in: 0 ... 100) { - Text(" 0") + Text("Volume") +} minimumValueLabel: { + Text("0").accessibilityHidden(true) } maximumValueLabel: { - Text("100") + Text("100").accessibilityHidden(true) } ``` ### Stepped slider (with text and value display) +We recommand to not set information on `minimumValueLabel` and `maximumValueLabel` view using `.accessibilityHidden(true)`. You can do it like this: + ```swift -Text("Stepped slider").odsFont(.title2) -Text("Value : \(Int(value))").odsFont(.bodyRegular) -VStack(alignment: .center) { - ODSSlider( - value: $value, - in: 0 ... 100, - step: 10) { - Text(" 0") - } maximumValueLabel: { - Text("100") +struct SteppedSlider: View { + + @State private var value = 50.0 + + var body: some View { + ODSSlider(value: $value, in: 0 ... 100.0, step: 0.5) { + Text("Volume") + } minimumValueLabel: { + Image(systemName: "speaker.wave.1.fill").accessibilityHidden(true) + } maximumValueLabel: { + Image(systemName: "speaker.wave.3.fill").accessibilityHidden(true) + } } } ```