From 17023325f8375a004bb44a5ff9e04f795a393e3a Mon Sep 17 00:00:00 2001 From: Lucas Zischka <63545066+LucasMucGH@users.noreply.github.com> Date: Tue, 28 Dec 2021 22:23:31 +0100 Subject: [PATCH] New Year Update (#38) - Update Copyright - Update swift-tools-version - Update deprecated code (real fix for #19, replaces #20) - Add `.absolutePositionValue` option (closes #37) - Add `BottomSheetPositionAbsolute` - Use explicit animations (fixes #31) - Hide examples in ReadMe - Implement and fix `.appleScrollBehavior` (closes #37) --- BottomSheetSwiftUI.podspec | 2 +- CHANGELOG.md | 11 + Package.swift | 2 +- README.md | 55 +++-- Sources/BottomSheet/BottomSheet.swift | 6 +- Sources/BottomSheet/BottomSheetArray.swift | 6 +- Sources/BottomSheet/BottomSheetPosition.swift | 42 +++- Sources/BottomSheet/BottomSheetView.swift | 208 ++++++++++++------ .../BottomSheet/Helper/BindingExtension.swift | 19 ++ .../Helper/UIApplicationExtension.swift | 14 ++ .../{ => Helper}/ViewExtension.swift | 3 +- .../HelperViews/BSScrollView.swift | 65 ++++++ .../BottomSheet/HelperViews/EffectView.swift | 2 +- .../HelperViews/RoundedCorner.swift | 2 +- 14 files changed, 344 insertions(+), 93 deletions(-) create mode 100644 Sources/BottomSheet/Helper/BindingExtension.swift create mode 100644 Sources/BottomSheet/Helper/UIApplicationExtension.swift rename Sources/BottomSheet/{ => Helper}/ViewExtension.swift (96%) create mode 100644 Sources/BottomSheet/HelperViews/BSScrollView.swift diff --git a/BottomSheetSwiftUI.podspec b/BottomSheetSwiftUI.podspec index ec190d366..2607fbf03 100644 --- a/BottomSheetSwiftUI.podspec +++ b/BottomSheetSwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BottomSheetSwiftUI' - s.version = '2.4.0' + s.version = '2.5.0' s.summary = 'A sliding Sheet from the bottom of the Screen with 3 States build with SwiftUI.' s.homepage = 'https://github.com/LucasMucGH/BottomSheet' diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d1a23dd..3ef52d0dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ BottomSheet Changelog ================== +#### v2.5.0 +- Update Copyright +- Update swift-tools-version +- Update deprecated code (real fix for #19, replaces #20) +- Add `.absolutePositionValue` option #37 +- Add `BottomSheetPositionAbsolute` +- Use explicit animations #31 +- Hide examples in ReadMe +- Update and fix `.appleScrollBehavior` #37 +- Code clean up + #### v2.4.0 - Add option to enable shadow - Add pod install diff --git a/Package.swift b/Package.swift index 98350ef48..79254eef8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/README.md b/README.md index a3f5a253b..21bb31b6a 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ struct ContentView: View { ### Options +`.absolutePositionValue` Allows absolute values in pixels to be used as BottomSheetPosition values. + `.allowContentDrag` Allows the BottomSheet to move when dragging the mainContent. - Do not use if the mainContent is packed into a ScrollView. @@ -173,15 +175,15 @@ struct ContentView: View { ## Custom States - You can create your own custom BottomSheetPosition enum: - The enum must be conforming to `CGFloat` and `CaseIterable` - The case and enum name doesnt matter - The case/state with `rawValue == 0` is hiding the BottomSheet - - The value can be anythig between `0` and `1` (`x <= 1`, `x >= 0`) - - The value is the height of the BottomSheet propotional to the screen height (`1 == 100% == full screen`) + - The value can be anythig between `0` and `1` (`x <= 1`, `x >= 0`) or anything above `0` (`x >= 0`) when using the`.absolutePositionValue` option + - The value is the height of the BottomSheet propotional to the screen height (`1 == 100% == full screen`) or the height of the BottomSheet in pixel (`1 == 1px`) when using the`.absolutePositionValue` option - The lowest value (greater than 0) automaticly gets the `.bottom` behavior. To prevent this please use the option `.noBottomPosition` +This BottomSheetPosition uses relative values. ```swift import SwiftUI @@ -190,6 +192,13 @@ enum CustomBottomSheetPosition: CGFloat, CaseIterable { } ``` +This BottomSheetPositionAbsolute uses absolute values and requires the the`.absolutePositionValue` option. +```swift +public enum BottomSheetPositionAbsolute: CGFloat, CaseIterable { + case top = 750, middle = 300, bottom = 100, hidden = 0 +} +``` + ## Examples **PLEASE NOTE:** When installed via Cocoapods, please keep in mind that the pod is called `BottomSheetSwiftUI` and not `BottomSheet`; so please use `import BottomSheetSwiftUI` instead. @@ -198,22 +207,26 @@ enum CustomBottomSheetPosition: CGFloat, CaseIterable { This BottomSheet shows additional information about a book. You can close it by swiping it away, by tapping on the background or the close button. -It also uses a custom `enum` for the states, since only the states `.middle`, `.bottom` and `.hidden` should exist. +The drag indicator is hidden. +It uses a custom `enum` for the states with absolute values, since only the states `.middle`, `.bottom` and `.hidden` should exist with a predefined absolute height. +
+Source Code + ```swift import SwiftUI import BottomSheet -//The custom BottomSheetPosition enum. +//The custom BottomSheetPosition enum with absolute values. enum BookBottomSheetPosition: CGFloat, CaseIterable { - case middle = 0.4, bottom = 0.125, hidden = 0 + case middle = 300, bottom = 100, hidden = 0 } struct BookDetailView: View { - @State private var bottomSheetPosition: BookBottomSheetPosition = .middle + @State var bottomSheetPosition: BookBottomSheetPosition = .middle let backgroundColors: [Color] = [Color(red: 0.2, green: 0.85, blue: 0.7), Color(red: 0.13, green: 0.55, blue: 0.45)] let readMoreColors: [Color] = [Color(red: 0.70, green: 0.22, blue: 0.22), Color(red: 1, green: 0.32, blue: 0.32)] @@ -224,7 +237,7 @@ struct BookDetailView: View { LinearGradient(gradient: Gradient(colors: self.backgroundColors), startPoint: .topLeading, endPoint: .bottomTrailing) .edgesIgnoringSafeArea(.all) - .bottomSheet(bottomSheetPosition: self.$bottomSheetPosition, options: [.allowContentDrag, .showCloseButton(), .swipeToDismiss, .tapToDissmiss], headerContent: { + .bottomSheet(bottomSheetPosition: self.$bottomSheetPosition, options: [.noDragIndicator, .allowContentDrag, .showCloseButton(), .swipeToDismiss, .tapToDissmiss, .absolutePositionValue], headerContent: { //The name of the book as the heading and the author as the subtitle with a divider. VStack(alignment: .leading) { Text("Wuthering Heights") @@ -279,27 +292,36 @@ struct BookButton: ButtonStyle { } } ``` +
### Word Search View This BottomSheet shows nouns which can be filtered by searching. -It adopts the scrolling behavior of apple, so that you can only scroll the `ScrollView` in the `.top` position. +It adopts the scrolling behavior of apple, so that you can only scroll the `ScrollView` in the `.top` position. The higher the BottomSheet is dragged, the more blurry the background becomes (with the BlurEffect .dark) to move the focus to the BottomSheet. +
+Source Code + ```swift import SwiftUI import BottomSheet struct WordSearchView: View { - @State private var bottomSheetPosition: BottomSheetPosition = .middle + @State var bottomSheetPosition: BottomSheetPosition = .middle + @State var searchText: String = "" - @State private var searchText: String = "" let backgroundColors: [Color] = [Color(red: 0.28, green: 0.28, blue: 0.53), Color(red: 1, green: 0.69, blue: 0.26)] let words: [String] = ["birthday", "pancake", "expansion", "brick", "bushes", "coal", "calendar", "home", "pig", "bath", "reading", "cellar", "knot", "year", "ink"] + var filteredWords: [String] { + self.words.filter({ $0.contains(self.searchText.lowercased()) || self.searchText.isEmpty }) + } + + var body: some View { //A green gradient as a background that ignores the safe area. LinearGradient(gradient: Gradient(colors: self.backgroundColors), startPoint: .topLeading, endPoint: .bottomTrailing) @@ -322,7 +344,7 @@ struct WordSearchView: View { } }) { //The list of nouns that will be filtered by the searchText. - ForEach(self.words.filter({ $0.contains(self.searchText.lowercased()) || self.searchText.isEmpty}), id: \.self) { word in + ForEach(self.filteredWords, id: \.self) { word in Text(word) .font(.title) .padding([.leading, .bottom]) @@ -330,12 +352,13 @@ struct WordSearchView: View { } .frame(maxWidth: .infinity, alignment: .leading) .transition(.opacity) - .animation(.easeInOut) + .animation(.easeInOut, value: self.filteredWords) .padding(.top) } } } ``` +
### Artist Songs View @@ -344,13 +367,16 @@ It has a custom animation and color for the drag indicator and the background, a +
+Source Code + ```swift import SwiftUI import BottomSheet struct ArtistSongsView: View { - @State private var bottomSheetPosition: BottomSheetPosition = .middle + @State var bottomSheetPosition: BottomSheetPosition = .middle let backgroundColors: [Color] = [Color(red: 0.17, green: 0.17, blue: 0.33), Color(red: 0.80, green: 0.38, blue: 0.2)] let songs: [String] = ["One Dance (feat. Wizkid & Kyla)", "God's Plan", "SICKO MODE", "In My Feelings", "Work (feat. Drake)", "Nice For What", "Hotline Bling", "Too Good (feat. Rihanna)", "Life Is Good (feat. Drake)"] @@ -374,6 +400,7 @@ struct ArtistSongsView: View { } } ``` +
## Contributing diff --git a/Sources/BottomSheet/BottomSheet.swift b/Sources/BottomSheet/BottomSheet.swift index ea670e809..38b8e2112 100644 --- a/Sources/BottomSheet/BottomSheet.swift +++ b/Sources/BottomSheet/BottomSheet.swift @@ -2,7 +2,7 @@ // BottomSheet.swift // // Created by Lucas Zischka. -// Copyright © 2021 Lucas Zischka. All rights reserved. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. // import SwiftUI @@ -14,6 +14,8 @@ public struct BottomSheet { return lhs.rawValue == rhs.rawValue } + ///Allows absolute values in pixels to be used as BottomSheetPosition values. + case absolutePositionValue ///Allows the BottomSheet to move when dragging the mainContent. Do not use if the mainContent is packed into a ScrollView. case allowContentDrag ///Sets the animation for opening and closing the BottomSheet. @@ -62,6 +64,8 @@ public struct BottomSheet { */ public var rawValue: String { switch self { + case .absolutePositionValue: + return "absolutePositionValue" case .allowContentDrag: return "allowContentDrag" case .animation: diff --git a/Sources/BottomSheet/BottomSheetArray.swift b/Sources/BottomSheet/BottomSheetArray.swift index 8dc713ab5..138e4d031 100644 --- a/Sources/BottomSheet/BottomSheetArray.swift +++ b/Sources/BottomSheet/BottomSheetArray.swift @@ -2,12 +2,16 @@ // BottomSheetArray.swift // // Created by Lucas Zischka. -// Copyright © 2021 Lucas Zischka. All rights reserved. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. // import SwiftUI internal extension Array where Element == BottomSheet.Options { + var absolutePositionValue: Bool { + return self.contains(BottomSheet.Options.absolutePositionValue) + } + var allowContentDrag: Bool { return self.contains(BottomSheet.Options.allowContentDrag) } diff --git a/Sources/BottomSheet/BottomSheetPosition.swift b/Sources/BottomSheet/BottomSheetPosition.swift index 0cc42cc1e..3fd9cb81f 100644 --- a/Sources/BottomSheet/BottomSheetPosition.swift +++ b/Sources/BottomSheet/BottomSheetPosition.swift @@ -2,29 +2,55 @@ // BottomSheetPosition.swift // // Created by Lucas Zischka. -// Copyright © 2021 Lucas Zischka. All rights reserved. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. // import SwiftUI /** - The default BottomSheetPosition; it has the following cases and values: `top = 0.975`, `middle = 0.4`, `bottom = 0.125`, `hidden = 0` + The default BottomSheetPosition; it has the following cases and values: `top = 0.975`, `middle = 0.4`, `bottom = 0.125`, `hidden = 0`. + + This BottomSheetPosition uses relative values. For absolute values in pixels, see BottomSheetPositionAbsolute. You can create your own custom BottomSheetPosition enum: - The enum must be conforming to `CGFloat` and `CaseIterable` - The case and enum name doesnt matter - The case/state with `rawValue == 0` is hiding the BottomSheet - - The value can be anythig between `0` and `1` (`x <= 1`, `x >= 0`) - - The value is the height of the BottomSheet propotional to the screen height (`1 == 100% == full screen`) + - The value can be anythig between `0` and `1` (`x <= 1`, `x >= 0`) or anything above `0` (`x >= 0`) when using the`.absolutePositionValue` option + - The value is the height of the BottomSheet propotional to the screen height (`1 == 100% == full screen`) or the height of the BottomSheet in pixel (`1 == 1px`) when using the`.absolutePositionValue` option - The lowest value (greater than 0) automaticly gets the `.bottom` behavior. To prevent this please use the option `.noBottomPosition` */ public enum BottomSheetPosition: CGFloat, CaseIterable { - //The state where the height of the BottomSheet is 97.5% + ///The state where the height of the BottomSheet is 97.5% case top = 0.975 - //The state where the height of the BottomSheet is 40% + ///The state where the height of the BottomSheet is 40% case middle = 0.4 - //The state where the height of the BottomSheet is 12.5% and the `mainContent` is hidden + ///The state where the height of the BottomSheet is 12.5% and the `mainContent` is hidden case bottom = 0.125 - //The state where the BottomSheet is hidden + ///The state where the BottomSheet is hidden + case hidden = 0 +} + +/** + The default BottomSheetPositionAbsolute; it has the following cases and values: `top = 750`, `middle = 300`, `bottom = 100`, `hidden = 0`. + + This BottomSheetPositionAbsolute uses absolute values and requires the the`.absolutePositionValue` option. For relative values in pixels, see BottomSheetPosition. + + You can create your own custom BottomSheetPosition enum: + - The enum must be conforming to `CGFloat` and `CaseIterable` + - The case and enum name doesnt matter + - The case/state with `rawValue == 0` is hiding the BottomSheet + - The value can be anythig between `0` and `1` (`x <= 1`, `x >= 0`) or anything above `0` (`x >= 0`) when using the`.absolutePositionValue` option + - The value is the height of the BottomSheet propotional to the screen height (`1 == 100% == full screen`) or the height of the BottomSheet in pixel (`1 == 1px`) when using the`.absolutePositionValue` option + - The lowest value (greater than 0) automaticly gets the `.bottom` behavior. To prevent this please use the option `.noBottomPosition` + */ +public enum BottomSheetPositionAbsolute: CGFloat, CaseIterable { + ///The state where the height of the BottomSheet is 750px + case top = 750 + ///The state where the height of the BottomSheet is 300px + case middle = 300 + ///The state where the height of the BottomSheet is 100px and the `mainContent` is hidden + case bottom = 100 + ///The state where the BottomSheet is hidden case hidden = 0 } diff --git a/Sources/BottomSheet/BottomSheetView.swift b/Sources/BottomSheet/BottomSheetView.swift index ba5f97da1..0739a2c9b 100644 --- a/Sources/BottomSheet/BottomSheetView.swift +++ b/Sources/BottomSheet/BottomSheetView.swift @@ -2,15 +2,16 @@ // BottomSheetView.swift // // Created by Lucas Zischka. -// Copyright © 2021 Lucas Zischka. All rights reserved. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. // import SwiftUI +import Combine -@available(iOSApplicationExtension, unavailable) internal struct BottomSheetView: View where bottomSheetPositionEnum.RawValue == CGFloat, bottomSheetPositionEnum: CaseIterable { @State private var translation: CGFloat = 0 + @State private var isScrollEnabled: Bool = true @Binding private var bottomSheetPosition: bottomSheetPositionEnum private let options: [BottomSheet.Options] @@ -45,13 +46,12 @@ internal struct BottomSheetView 0 { + self.translation = offset.y + self.endEditing() + + let height: CGFloat = (self.translation * 2) / geometry.size.height + self.switchPosition(with: height) + } + }) { self.mainContent } - .disabled(!self.isTopPosition) } else { self.mainContent } @@ -118,14 +127,16 @@ internal struct BottomSheetView Double { + withAnimation(.linear) { + if self.options.backgroundBlur { + if self.options.absolutePositionValue { + return Double((self.bottomSheetPosition.rawValue - self.translation) / geometry.size.height) + } else { + return Double((self.bottomSheetPosition.rawValue * geometry.size.height - self.translation) / geometry.size.height) + } + } else { + return 0 + } + } + } + + private func headerContentPadding(geometry: GeometryProxy) -> CGFloat { + withAnimation(self.options.animation) { + if self.isBottomPosition { + return geometry.safeAreaInsets.bottom + 25 + } else if self.headerContent == nil { + return 20 + } else { + return 0 + } + } + } + + private func frameHeightValue(geometry: GeometryProxy) -> Double { + withAnimation(self.options.animation) { + if self.options.absolutePositionValue { + return min(max(self.bottomSheetPosition.rawValue - self.translation, 0), geometry.size.height * 1.05) + } else { + return min(max((geometry.size.height * self.bottomSheetPosition.rawValue) - self.translation, 0), geometry.size.height * 1.05) + } + } + } + + private func offsetYValue(geometry: GeometryProxy) -> Double { + withAnimation(self.options.animation) { + if self.isHiddenPosition { + return max(geometry.size.height + geometry.safeAreaInsets.bottom, geometry.size.height * -0.05) + } else if self.isBottomPosition { + if self.options.absolutePositionValue { + return max(geometry.size.height - self.bottomSheetPosition.rawValue + self.translation + geometry.safeAreaInsets.bottom, geometry.size.height * -0.05) + } else { + return max(geometry.size.height - (geometry.size.height * self.bottomSheetPosition.rawValue) + self.translation + geometry.safeAreaInsets.bottom, geometry.size.height * -0.05) + } + } else { + if self.options.absolutePositionValue { + return max(geometry.size.height - self.bottomSheetPosition.rawValue + self.translation, geometry.size.height * -0.05) + } else { + return max(geometry.size.height - (geometry.size.height * self.bottomSheetPosition.rawValue) + self.translation, geometry.size.height * -0.05) + } + } } } private func endEditing() -> Void { - UIApplication.shared.windows.filter{$0.isKeyWindow}.first?.endEditing(true) + UIApplication.shared.endEditing() } private func tapToDismiss() -> Void { @@ -191,58 +268,64 @@ internal struct BottomSheetView Void { - if let hiddenPosition = bottomSheetPositionEnum(rawValue: 0) { - self.bottomSheetPosition = hiddenPosition + withAnimation(self.options.animation) { + if let hiddenPosition = bottomSheetPositionEnum(rawValue: 0) { + self.bottomSheetPosition = hiddenPosition + } + + self.endEditing() } - - self.endEditing() } private func switchPositionIndicator() -> Void { - if !self.isHiddenPosition { - - if let currentIndex = self.allCases.firstIndex(where: { $0 == self.bottomSheetPosition }), self.allCases.count > 1 { - if currentIndex == self.allCases.endIndex - 1 { - if self.allCases[currentIndex - 1].rawValue != 0 { - self.bottomSheetPosition = self.allCases[currentIndex - 1] + withAnimation(self.options.animation) { + if !self.isHiddenPosition { + + if let currentIndex = self.allCases.firstIndex(where: { $0 == self.bottomSheetPosition }), self.allCases.count > 1 { + if currentIndex == self.allCases.endIndex - 1 { + if self.allCases[currentIndex - 1].rawValue != 0 { + self.bottomSheetPosition = self.allCases[currentIndex - 1] + } + } else { + self.bottomSheetPosition = self.allCases[currentIndex + 1] } - } else { - self.bottomSheetPosition = self.allCases[currentIndex + 1] } + } + self.endEditing() } - - self.endEditing() } private func switchPosition(with height: CGFloat) -> Void { - if !self.isHiddenPosition { - - if let currentIndex = self.allCases.firstIndex(where: { $0 == self.bottomSheetPosition }), self.allCases.count > 1 { - if height <= -0.1 && height > -0.3 { - if currentIndex < self.allCases.endIndex - 1 { - self.bottomSheetPosition = self.allCases[currentIndex + 1] - } - } else if height <= -0.3 { - self.bottomSheetPosition = self.allCases[self.allCases.endIndex - 1] - } else if height >= 0.1 && height < 0.3 { - if currentIndex > self.allCases.startIndex && (self.allCases[currentIndex - 1].rawValue != 0 || (self.allCases[currentIndex - 1].rawValue == 0 && self.options.swipeToDismiss)) { - self.bottomSheetPosition = self.allCases[currentIndex - 1] - } - } else if height >= 0.3 { - if (self.allCases[self.allCases.startIndex].rawValue == 0 && self.options.swipeToDismiss) || self.allCases[self.allCases.startIndex].rawValue != 0 { - self.bottomSheetPosition = self.allCases[self.allCases.startIndex] - } else { - self.bottomSheetPosition = self.allCases[self.allCases.startIndex + 1] + withAnimation(self.options.animation) { + if !self.isHiddenPosition { + + if let currentIndex = self.allCases.firstIndex(where: { $0 == self.bottomSheetPosition }), self.allCases.count > 1 { + if height <= -0.1 && height > -0.3 { + if currentIndex < self.allCases.endIndex - 1 { + self.bottomSheetPosition = self.allCases[currentIndex + 1] + } + } else if height <= -0.3 { + self.bottomSheetPosition = self.allCases[self.allCases.endIndex - 1] + } else if height >= 0.1 && height < 0.3 { + if currentIndex > self.allCases.startIndex && (self.allCases[currentIndex - 1].rawValue != 0 || (self.allCases[currentIndex - 1].rawValue == 0 && self.options.swipeToDismiss)) { + self.bottomSheetPosition = self.allCases[currentIndex - 1] + } + } else if height >= 0.3 { + if (self.allCases[self.allCases.startIndex].rawValue == 0 && self.options.swipeToDismiss) || self.allCases[self.allCases.startIndex].rawValue != 0 { + self.bottomSheetPosition = self.allCases[self.allCases.startIndex] + } else { + self.bottomSheetPosition = self.allCases[self.allCases.startIndex + 1] + } } } + } + self.translation = 0 + self.endEditing() } - - self.translation = 0 - self.endEditing() } @@ -254,14 +337,13 @@ internal struct BottomSheetView>>, _PaddingLayout> { init(bottomSheetPosition: Binding, options: [BottomSheet.Options], title: String?, @ViewBuilder content: () -> mContent) { if title == nil { self.init(bottomSheetPosition: bottomSheetPosition, options: options, headerContent: { return nil }, mainContent: content) } else { self.init(bottomSheetPosition: bottomSheetPosition, options: options, headerContent: { return Text(title!) - .font(.title).bold().lineLimit(1).padding(.bottom) as? hContent }, mainContent: content) + .font(.title).bold().lineLimit(1).padding(.bottom) as? hContent }, mainContent: content) } } } diff --git a/Sources/BottomSheet/Helper/BindingExtension.swift b/Sources/BottomSheet/Helper/BindingExtension.swift new file mode 100644 index 000000000..42397b56c --- /dev/null +++ b/Sources/BottomSheet/Helper/BindingExtension.swift @@ -0,0 +1,19 @@ +// +// BindingExtension.swift +// +// Created by Lucas Zischka. +// Copyright © 2021 Lucas Zischka. All rights reserved. +// + +import SwiftUI + +internal extension Binding { + /// Wrapper to listen to didSet of Binding + func didSet(_ didSet: @escaping ((newValue: Value, oldValue: Value)) -> Void) -> Binding { + return .init(get: { self.wrappedValue }, set: { newValue in + let oldValue = self.wrappedValue + self.wrappedValue = newValue + didSet((newValue, oldValue)) + }) + } +} diff --git a/Sources/BottomSheet/Helper/UIApplicationExtension.swift b/Sources/BottomSheet/Helper/UIApplicationExtension.swift new file mode 100644 index 000000000..f262e57f3 --- /dev/null +++ b/Sources/BottomSheet/Helper/UIApplicationExtension.swift @@ -0,0 +1,14 @@ +// +// UIApplicationExtension.swift +// +// Created by Lucas Zischka. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. +// + +import UIKit + +internal extension UIApplication { + func endEditing() { + sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} diff --git a/Sources/BottomSheet/ViewExtension.swift b/Sources/BottomSheet/Helper/ViewExtension.swift similarity index 96% rename from Sources/BottomSheet/ViewExtension.swift rename to Sources/BottomSheet/Helper/ViewExtension.swift index 770884394..19edee89d 100644 --- a/Sources/BottomSheet/ViewExtension.swift +++ b/Sources/BottomSheet/Helper/ViewExtension.swift @@ -2,12 +2,11 @@ // ViewExtension.swift // // Created by Lucas Zischka. -// Copyright © 2021 Lucas Zischka. All rights reserved. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. // import SwiftUI -@available(iOSApplicationExtension, unavailable) public extension View { /** A modifer to add a BottomSheet to your view. diff --git a/Sources/BottomSheet/HelperViews/BSScrollView.swift b/Sources/BottomSheet/HelperViews/BSScrollView.swift new file mode 100644 index 000000000..368d86226 --- /dev/null +++ b/Sources/BottomSheet/HelperViews/BSScrollView.swift @@ -0,0 +1,65 @@ +// +// BSScrollView.swift +// +// Created by Lucas Zischka. +// Copyright © 2021 Lucas Zischka. All rights reserved. +// + +import SwiftUI + +internal struct BSScrollView: View { + + @Binding private var isScrollEnabled: Bool + + private let defaultAxes: Axis.Set + private let showsIndicators: Bool + private let onOffsetChange: (CGPoint) -> Void + private let content: Content + + private var axes: Axis.Set { + return self.isScrollEnabled ? self.defaultAxes : [] + } + + + internal var body: some View { + ScrollView(self.axes, showsIndicators: self.showsIndicators) { + GeometryReader { geometry in + Color.clear + .preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scrollView")).origin + ) + } + .frame(width: 0, height: 0) + self.content + //.padding(.top, -8) + .frame(maxWidth: .infinity, alignment: .top) + } + .coordinateSpace(name: "scrollView") + .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: self.onOffsetChange) + } + + + internal init(axes: Axis.Set = .vertical, showsIndicators: Bool = true, isScrollEnabled: Binding = .constant(true), onOffsetChange: @escaping (CGPoint) -> Void = { _ in }, @ViewBuilder content: () -> Content) { + self.defaultAxes = axes + self.showsIndicators = showsIndicators + self._isScrollEnabled = isScrollEnabled + self.onOffsetChange = onOffsetChange + self.content = content() + } +} + +private struct ScrollOffsetPreferenceKey: PreferenceKey { + static var defaultValue: CGPoint = .zero + + static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {} +} + +struct BSScrollView_Previews: PreviewProvider { + static var previews: some View { + BSScrollView { + ForEach(0...100, id: \.self) { i in + Text("\(i)") + } + } + + } +} diff --git a/Sources/BottomSheet/HelperViews/EffectView.swift b/Sources/BottomSheet/HelperViews/EffectView.swift index bf7accb09..ce3e80db9 100644 --- a/Sources/BottomSheet/HelperViews/EffectView.swift +++ b/Sources/BottomSheet/HelperViews/EffectView.swift @@ -2,7 +2,7 @@ // EffectView.swift // // Created by Lucas Zischka. -// Copyright © 2021 Lucas Zischka. All rights reserved. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. // import SwiftUI diff --git a/Sources/BottomSheet/HelperViews/RoundedCorner.swift b/Sources/BottomSheet/HelperViews/RoundedCorner.swift index 5abe52a0f..46fc252b2 100644 --- a/Sources/BottomSheet/HelperViews/RoundedCorner.swift +++ b/Sources/BottomSheet/HelperViews/RoundedCorner.swift @@ -2,7 +2,7 @@ // RoundedCorner.swift // // Created by Lucas Zischka. -// Copyright © 2021 Lucas Zischka. All rights reserved. +// Copyright © 2021-2022 Lucas Zischka. All rights reserved. // import SwiftUI