diff --git a/Sources/StylableScrollView/BackButton.swift b/Sources/StylableScrollView/BackButton.swift index a8cb340..776b3d4 100644 --- a/Sources/StylableScrollView/BackButton.swift +++ b/Sources/StylableScrollView/BackButton.swift @@ -14,20 +14,25 @@ public struct BackButton: View { @Environment(\.presentationMode) public var presentationMode: Binding - + @State private var hasBeenShownAtLeastOnce: Bool = false public var body: some View { (presentationMode.wrappedValue.isPresented || hasBeenShownAtLeastOnce) ? - Button(action: { self.presentationMode.wrappedValue.dismiss() }) { - Image(systemName: "chevron.left") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(height: 20, alignment: .leading) - .foregroundColor(color) - .padding(.horizontal, 16) - .font(Font.body.bold()) - } + Button( + action: { + self.presentationMode.wrappedValue.dismiss() + }, + label: { + Image(systemName: "chevron.left") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 20, alignment: .leading) + .foregroundColor(color) + .padding(.horizontal, 16) + .font(Font.body.bold()) + } + ) .onAppear { self.hasBeenShownAtLeastOnce = true } diff --git a/Sources/StylableScrollView/Extensions/Color.swift b/Sources/StylableScrollView/Extensions/Color.swift index 7febd31..99418c9 100644 --- a/Sources/StylableScrollView/Extensions/Color.swift +++ b/Sources/StylableScrollView/Extensions/Color.swift @@ -28,6 +28,14 @@ public extension Color { return scheme == .light ? Color(red: 0.95, green: 0.95, blue: 0.97) : Color(red: 0.11, green: 0.11, blue: 0.12) } + /// `Color.systemGray6` multi-platform equivalent. + /// + /// - Parameters: + /// - scheme: The current color scheme. + static func label(for scheme: ColorScheme) -> Color { + return scheme == .light ? Color.black : Color.white + } + /// A multi-platform solution for obtaining the window's background color. static var backgroundColor: Color { diff --git a/Sources/StylableScrollView/Extensions/Modifiers.swift b/Sources/StylableScrollView/Extensions/Modifiers.swift index 56f8109..0f48344 100644 --- a/Sources/StylableScrollView/Extensions/Modifiers.swift +++ b/Sources/StylableScrollView/Extensions/Modifiers.swift @@ -18,7 +18,7 @@ import Foundation import SwiftUI -// MARK: - SCROLLVIEW STYLE +// MARK: - SCROLLVIEW STYLE @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public extension View { @@ -53,9 +53,10 @@ public extension View { /// ) /// } /// - /// ![A stretchable scroll view with a big text talking about French footballer Kylian Mbappé. On top, a stretchable header can be seen.](StretchableHeader-kmbappe.png) + /// ![A stretchable scroll view with a big text talking about French footballer + /// Kylian Mbappé. On top, a stretchable header can be seen.](StretchableHeader-kmbappe.png) /// - @inlinable @ViewBuilder func scrollViewStyle(_ style: S) -> some View where S : ScrollViewStyle { + @inlinable @ViewBuilder func scrollViewStyle(_ style: S) -> some View where S: ScrollViewStyle { self.environment(\.scrollViewStyle, AnyScrollViewStyle(style)) @@ -63,7 +64,7 @@ public extension View { } -// MARK: - STRETCHABLE HEADER +// MARK: - STRETCHABLE HEADER @available(iOS 15.0, macOS 12, *) @available(tvOS, unavailable) @available(watchOS, unavailable) @@ -84,19 +85,24 @@ public extension ScrollView { /// - header: The header image that will be used for the sticky header. /// - title: The title that appears on top of the header. /// - navBar: The contents of the navigationBar that appears when scrolling. + /// - leadingElements: The navigation bar elements that will appear on the leading side. + /// - trailingElements: The navigation bar elements that will appear on the trailing side. /// - @ViewBuilder @inlinable func stretchableHeader( + @ViewBuilder @inlinable func stretchableHeader( header: Image, title: () -> Title, - navBar: () -> NavBar - ) -> some View where Title: View, NavBar: View { + navBar: () -> NavBar, + leadingElements: @escaping (NavigationBarProxy) -> LeadingElements, + trailingElements: @escaping (NavigationBarProxy) -> TrailingElements + ) -> some View where Title: View, NavBar: View, LeadingElements: View, TrailingElements: View { StylableScrollView( .vertical, showIndicators: false, content: { self - }) + } + ) .scrollViewStyle( StretchableScrollViewStyle( header: { @@ -105,7 +111,12 @@ public extension ScrollView { .aspectRatio(contentMode: .fill) }, title: title, - navBarContent: navBar + navBarContent: navBar, + leadingElements: { + leadingElements($0) + }, trailingElements: { + trailingElements($0) + } ) ) @@ -126,24 +137,34 @@ public extension ScrollView { /// - header: The view that will be used for the sticky header. /// - title: The title that appears on top of the header. /// - navBar: The contents of the navigationBar that appears when scrolling. + /// - leadingElements: The navigation bar elements that will appear on the leading side. + /// - trailingElements: The navigation bar elements that will appear on the trailing side. /// - @ViewBuilder @inlinable func stretchableHeader( + @ViewBuilder @inlinable func stretchableHeader( header: () -> Header, title: () -> Title, - navBar: () -> NavBar - ) -> some View where Header: View, Title: View, NavBar: View { + navBar: () -> NavBar, + leadingElements: @escaping (NavigationBarProxy) -> LeadingElements, + trailingElements: @escaping (NavigationBarProxy) -> TrailingElements + ) -> some View where Header: View, Title: View, NavBar: View, LeadingElements: View, TrailingElements: View { StylableScrollView( .vertical, showIndicators: false, content: { self - }) + } + ) .scrollViewStyle( StretchableScrollViewStyle( header: header, title: title, - navBarContent: navBar + navBarContent: navBar, + leadingElements: { + leadingElements($0) + }, trailingElements: { + trailingElements($0) + } ) ) @@ -151,7 +172,7 @@ public extension ScrollView { } -// MARK: - SIZE MODIFIERS +// MARK: - SIZE MODIFIERS @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public extension View { @@ -175,7 +196,7 @@ public extension View { } -// MARK: - NAVIGATION BAR ELEMENTS +// MARK: - NAVIGATION BAR ELEMENTS extension View { /// Creates a fake navigation bar item for ``StretchableScrollViewStyle``'s fake navigation bar using @@ -184,7 +205,8 @@ extension View { /// - Parameters: /// - axis: The horizontal edge the element will appear at. /// - content: The element view - /// - affectedByColorChanges: Whether the element is affected by the changes in color when shown in a navigation Bar. Defaults to `true`. + /// - affectedByColorChanges: Whether the element is affected by + /// the changes in color when shown in a navigation Bar. Defaults to `true`. /// /// Use this modifier to set a specific navigation bar element for /// ``StretchableScrollViewStyle`` instances within a view: @@ -202,9 +224,22 @@ extension View { @available(iOS 15.0, macOS 12.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) - @ViewBuilder public func navigationBarElement(axis: HorizontalEdge, _ content: () -> Content) -> some View { - - self.preference( + @available(*, deprecated, message: "Please pass your elements as arguments to the ScrollViewStyle.") + public func navigationBarElement(axis: HorizontalEdge, _ content: () -> Content) -> some View { + + guard Constants.MAJOR < 1 else { + fatalError(".navigationBarElement(axis:,_:) is obsolete and is not working anymore.") + } + + /* + * This function passes the navigation bar elements as preference items to the scroll view style, + * but they do not update, even if a state variable is changed. + * + * For this reason, this function is deprecated and should be deleted before the stable release, + * unless we find a way to make it work as desired. + */ + #warning("TODO: We should remove this function before the stable release (X.y.z | X > 0).") + return self.preference( key: Preferences.NavigationBar.Elements.Key.self, value: [ Preferences.NavigationBar.Elements.Data( @@ -215,4 +250,5 @@ extension View { ) } + } diff --git a/Sources/StylableScrollView/Configuration/Configuration.swift b/Sources/StylableScrollView/Namespaces/Configuration.swift similarity index 100% rename from Sources/StylableScrollView/Configuration/Configuration.swift rename to Sources/StylableScrollView/Namespaces/Configuration.swift diff --git a/Sources/StylableScrollView/Namespaces/Constants.swift b/Sources/StylableScrollView/Namespaces/Constants.swift new file mode 100644 index 0000000..40aa83f --- /dev/null +++ b/Sources/StylableScrollView/Namespaces/Constants.swift @@ -0,0 +1,14 @@ +// +// File.swift +// +// +// Created by Alex Modroño Vara on 26/12/21. +// + +import Foundation + +struct Constants { + static let MAJOR = 0 + static let MINOR = 1 + static let PATCH = 0 +} diff --git a/Sources/StylableScrollView/Configuration/Environment.swift b/Sources/StylableScrollView/Namespaces/Environment.swift similarity index 100% rename from Sources/StylableScrollView/Configuration/Environment.swift rename to Sources/StylableScrollView/Namespaces/Environment.swift diff --git a/Sources/StylableScrollView/Namespaces/NavigationBarProxy.swift b/Sources/StylableScrollView/Namespaces/NavigationBarProxy.swift new file mode 100644 index 0000000..7fdd7a9 --- /dev/null +++ b/Sources/StylableScrollView/Namespaces/NavigationBarProxy.swift @@ -0,0 +1,38 @@ +/* +* THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS +* NON-VIOLENT PUBLIC LICENSE v4 ("LICENSE"). THE WORK IS PROTECTED BY +* COPYRIGHT AND ALL OTHER APPLICABLE LAWS. ANY USE OF THE WORK OTHER THAN +* AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. BY +* EXERCISING ANY RIGHTS TO THE WORK PROVIDED IN THIS LICENSE, YOU AGREE +* TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE +* MAY BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +* CONTAINED HERE IN AS CONSIDERATION FOR ACCEPTING THE TERMS AND +* CONDITIONS OF THIS LICENSE AND FOR AGREEING TO BE BOUND BY THE TERMS +* AND CONDITIONS OF THIS LICENSE. +* +* StylableScrollView.swift +* +* Created by Alex Modroño Vara on 26/12/21. +* +*/ +import Foundation +import SwiftUI + +// MARK: - NAVIGATION BAR +// +/// A proxy for access to several aspects of a Navigation Bar Element, +/// such as whether the navigation bar has changed display mode, the +/// preferred color for navigation bar elements, and more. +@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) +public struct NavigationBarProxy { + + public enum Mode { + case large + case inline + } + + public var mode: Mode + + public var elementsColor: Color + +} diff --git a/Sources/StylableScrollView/Configuration/Preferences.swift b/Sources/StylableScrollView/Namespaces/Preferences.swift similarity index 98% rename from Sources/StylableScrollView/Configuration/Preferences.swift rename to Sources/StylableScrollView/Namespaces/Preferences.swift index 906cafe..0be8469 100644 --- a/Sources/StylableScrollView/Configuration/Preferences.swift +++ b/Sources/StylableScrollView/Namespaces/Preferences.swift @@ -18,6 +18,9 @@ import Foundation import SwiftUI +// MARK: – SWIFTLINT +// swiftlint:disable nesting + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct Preferences { @@ -26,11 +29,11 @@ public struct Preferences { /// Allows us to read the minY of the header from ancestor views. public struct Key: PreferenceKey { - + public typealias Value = Data public static var defaultValue: Data = Data.init(minY: 0) - + public static func reduce(value: inout Data, nextValue: () -> Data) { value = nextValue() } @@ -74,7 +77,7 @@ public struct Preferences { public typealias Value = [Data] public static var defaultValue: [Data] = [] - + public static func reduce(value: inout [Data], nextValue: () -> [Data]) { value.append(contentsOf: nextValue()) } diff --git a/Sources/StylableScrollView/ScrollViewStyle.swift b/Sources/StylableScrollView/ScrollViewStyle.swift index df8b0b4..9558316 100644 --- a/Sources/StylableScrollView/ScrollViewStyle.swift +++ b/Sources/StylableScrollView/ScrollViewStyle.swift @@ -29,7 +29,7 @@ import SwiftUI public protocol ScrollViewStyle { /// A view that represents the body of a ScrollView. - associatedtype Body : View + associatedtype Body: View /// Creates a view that represents the body of a ScrollView. /// diff --git a/Sources/StylableScrollView/StylableScrollView.swift b/Sources/StylableScrollView/StylableScrollView.swift index 4a9fbc9..7cc2184 100644 --- a/Sources/StylableScrollView/StylableScrollView.swift +++ b/Sources/StylableScrollView/StylableScrollView.swift @@ -70,14 +70,18 @@ import SwiftUI /// ) /// } /// -/// ![A stretchable scroll view with a big text talking about French footballer Kylian Mbappé. On top, a stretchable header can be seen.](StretchableHeader-kmbappe.png) +/// ![A stretchable scroll view with a big text talking about +/// French footballer Kylian Mbappé. On top, a stretchable header can be seen.](StretchableHeader-kmbappe.png) /// @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) public struct StylableScrollView: View { + /// The actual version of StylableScrollView that is being used. + public let version: String = "\(Constants.MAJOR).\(Constants.MINOR).\(Constants.PATCH)" + /// The style that is being used. @Environment(\.scrollViewStyle) private var style - + /// The configuration for the ScrollView private var configuration: ScrollViewStyleConfiguration @@ -93,7 +97,7 @@ public struct StylableScrollView: View { } -public extension StylableScrollView where Content : View { +public extension StylableScrollView where Content: View { /// Creates an instance of an ``StylableScrollView`` that is scrollable in a specific /// axis, and can show indicators while scrolling, using the body that you define. @@ -104,7 +108,11 @@ public extension StylableScrollView where Content : View { /// component of the content offset, in a way that's suitable for the platform. /// - content: The scroll view's content. /// - init(_ axes: Axis.Set = .vertical, showIndicators: Bool = true, content: () -> Content) { + init( + _ axes: Axis.Set = .vertical, + showIndicators: Bool = true, + content: () -> Content + ) { self.init( ScrollViewStyleConfiguration( diff --git a/Sources/StylableScrollView/Styles/AnyScrollViewStyle.swift b/Sources/StylableScrollView/Styles/AnyScrollViewStyle.swift index 5a4fdf5..b6961f4 100644 --- a/Sources/StylableScrollView/Styles/AnyScrollViewStyle.swift +++ b/Sources/StylableScrollView/Styles/AnyScrollViewStyle.swift @@ -21,11 +21,11 @@ import SwiftUI /// A type-erased ``ScrollViewStyle``. public struct AnyScrollViewStyle: ScrollViewStyle { private let styleMakeBody: (ScrollViewStyle.Configuration) -> AnyView - + public init(_ style: S) { self.styleMakeBody = style.makeTypeErasedBody } - + public func makeBody(configuration: ScrollViewStyle.Configuration) -> AnyView { self.styleMakeBody(configuration) } diff --git a/Sources/StylableScrollView/Styles/StretchableScrollViewStyle.swift b/Sources/StylableScrollView/Styles/StretchableScrollViewStyle.swift index e14c20f..4d102cd 100644 --- a/Sources/StylableScrollView/Styles/StretchableScrollViewStyle.swift +++ b/Sources/StylableScrollView/Styles/StretchableScrollViewStyle.swift @@ -23,41 +23,86 @@ import SwiftUI @available(iOS 15.0, macOS 12.0, *) @available(tvOS, unavailable) @available(watchOS, unavailable) -public struct StretchableScrollViewStyle: ScrollViewStyle where Header: View, Title: View, Content: View { +// swiftlint:disable:next line_length +public struct StretchableScrollViewStyle: ScrollViewStyle where Header: View, Title: View, Content: View, LE: View, TE: View { private let header: Header private let title: Title private let navBarContent: Content + private let trailingElements: (NavigationBarProxy) -> TE + private let leadingElements: (NavigationBarProxy) -> LE public let showBackButton: Bool private var headerHeight: CGFloat - public init(size headerHeight: CGFloat = 200, header: () -> Header, title: () -> Title, navBarContent: () -> Content, _ showBackButton: Bool = true) { + public init( + size headerHeight: CGFloat = 200, + header: () -> Header, + title: () -> Title, + navBarContent: () -> Content, + _ showBackButton: Bool = true, + leadingElements: @escaping (NavigationBarProxy) -> LE, + trailingElements: @escaping (NavigationBarProxy) -> TE + ) { self.headerHeight = headerHeight self.header = header() self.title = title() self.navBarContent = navBarContent() self.showBackButton = showBackButton + self.leadingElements = leadingElements + self.trailingElements = trailingElements } - private struct view: View { + private class Manager: ObservableObject { - typealias navBarPreferenceKey = Preferences.NavigationBar.Elements.Key - typealias navBarPreferenceData = Preferences.NavigationBar.Elements.Data + @Environment(\.colorScheme) var colorScheme + + @Published var minY: CGFloat = 0 { + didSet { + self.materialOpacity = Double(-minY > 80 ? -(minY + 80) / 30 : 0) + } + } + @Published var navigationBarProxy: NavigationBarProxy = NavigationBarProxy( + mode: .large, + elementsColor: Color.white + ) + + /// The opacity of the material view that is used as the background of the fake navigation bar. + @Published var materialOpacity: CGFloat = 0 { + didSet { + self.navigationBarProxy.mode = materialOpacity < 0.3 ? .large : .inline + + if self.colorScheme == .light { + + self.navigationBarProxy.elementsColor = self.navigationBarProxy.mode == .large ? + Color.backgroundColor : Color.backgroundColorInverted(for: self.colorScheme) + + } + + } + } + + } + + private struct BodyView: View { + + @ObservedObject var manager: Manager = Manager() - @State var minY: CGFloat = 0 @State var leadingNavBarElements: [AnyView] = [] @State var trailingNavBarElements: [AnyView] = [] + let trailingElements: (NavigationBarProxy) -> TE + let leadingElements: (NavigationBarProxy) -> LE + @Environment(\.colorScheme) var colorScheme @Environment(\.layoutDirection) var layoutDirection: LayoutDirection var configuration: ScrollViewStyleConfiguration - + let header: Header let title: Title let navBarContent: Content @@ -75,22 +120,7 @@ public struct StretchableScrollViewStyle: ScrollViewStyl configuration.content } .onPreferenceChange(Preferences.Header.Key.self) { preference in - self.minY = preference.minY - } - .onPreferenceChange(navBarPreferenceKey.self) { preferences in - - for p in preferences { - - if p.axis == .leading { - self.leadingNavBarElements.append(p.element) - } - - if p.axis == .trailing { - self.trailingNavBarElements.append(p.element) - } - - } - + self.manager.minY = preference.minY } } ) @@ -99,21 +129,20 @@ public struct StretchableScrollViewStyle: ScrollViewStyl if self.showBackButton { BackButton( - color: navBarElementColor( - for: materialOpacity( - for: self.minY - ), - self.colorScheme - ) + color: self.manager.navigationBarProxy.elementsColor ) .shadow(radius: 10) } - getNavBarElements(for: self.layoutDirection == .leftToRight ? .leading : .trailing) + self.leadingElements( + self.manager.navigationBarProxy + ) Spacer() - getNavBarElements(for: self.layoutDirection == .leftToRight ? .trailing : .leading) + self.trailingElements( + self.manager.navigationBarProxy + ) } .padding(.top, 5) @@ -122,46 +151,6 @@ public struct StretchableScrollViewStyle: ScrollViewStyl ) } - @ViewBuilder private func getNavBarElements(for horizontalEdge: HorizontalEdge) -> some View { - - HStack { - if horizontalEdge == .leading { - if self.leadingNavBarElements.count > 0 { - ForEach( - 0 ..< self.leadingNavBarElements.count - ) { index in - self.leadingNavBarElements[index] - } - } - } else { - if self.trailingNavBarElements.count > 0 { - ForEach( - 0 ..< self.trailingNavBarElements.count - ) { index in - self.trailingNavBarElements[index] - } - } - } - } - .foregroundColor( - navBarElementColor( - for: materialOpacity( - for: self.minY - ), - self.colorScheme - ) - ) - - } - - private func navBarElementColor(for materialOpacity: Double, _ scheme: ColorScheme) -> Color { - if scheme == .light { - return materialOpacity < 0.3 ? Color.backgroundColor : Color.backgroundColorInverted(for: self.colorScheme) - } else { - return Color.white - } - } - public var headerView: some View { GeometryReader(content: { proxy -> AnyView in @@ -189,8 +178,7 @@ public struct StretchableScrollViewStyle: ScrollViewStyl .background(.ultraThickMaterial) .overlay( Divider() - .foregroundColor(.gray) - , alignment: .bottom + .foregroundColor(.gray), alignment: .bottom ) } .opacity(materialOpacity(for: minY)) @@ -212,7 +200,7 @@ public struct StretchableScrollViewStyle: ScrollViewStyl .offset(y: minY > 0 ? -minY : 0) .zIndex(0.8) .opacity(materialOpacity(for: minY) > 0.3 ? 0 : 1) - .animation(.easeIn(duration: 0.2)), + .animation(.easeIn(duration: 0.2), value: materialOpacity(for: minY)), alignment: .bottomLeading ) .preference( @@ -247,7 +235,7 @@ public struct StretchableScrollViewStyle: ScrollViewStyl private func getHeightForHeader(for minY: CGFloat, _ isOptional: Bool = true) -> CGFloat? { return minY > 0 ? headerHeight + minY : isOptional ? nil : headerHeight } - + } /// Creates a view that represents a body that is scrollable in the direction of the given @@ -264,7 +252,9 @@ public struct StretchableScrollViewStyle: ScrollViewStyl NSLog("Configuration.axes was passed as %d, but .vertical will be used instead.", configuration.axes.rawValue) } - return StretchableScrollViewStyle.view( + return StretchableScrollViewStyle.BodyView( + trailingElements: self.trailingElements, + leadingElements: self.leadingElements, configuration: configuration, header: self.header, title: self.title,