diff --git a/Sources/OversizeUI/Controls/HUD/HUD.swift b/Sources/OversizeUI/Controls/HUD/HUD.swift index 0f31dbb..a0f2e71 100644 --- a/Sources/OversizeUI/Controls/HUD/HUD.swift +++ b/Sources/OversizeUI/Controls/HUD/HUD.swift @@ -1,6 +1,6 @@ // // Copyright © 2023 Alexander Romanov -// File.swift, created on 22.05.2023 +// HUD.swift, created on 22.05.2023 // import SwiftUI diff --git a/Sources/OversizeUI/Controls/PageView/PageView.swift b/Sources/OversizeUI/Controls/PageView/PageView.swift index 10e485e..6e75b82 100644 --- a/Sources/OversizeUI/Controls/PageView/PageView.swift +++ b/Sources/OversizeUI/Controls/PageView/PageView.swift @@ -237,7 +237,7 @@ public struct PageView return control } - public func bottomToolbar(style: PageViewBottomType = .shadow, ignoreSafeArea: Bool = true, @ViewBuilder bottomToolbar: @escaping () -> some View) -> some View { + public func bottomToolbar(style: PageViewBottomType = .shadow, @ViewBuilder bottomToolbar: @escaping () -> some View) -> some View { VStack(spacing: .zero) { overlay( Group { @@ -266,7 +266,6 @@ public struct PageView .background(Color.surfacePrimary.shadowElevaton(style == .shadow ? .z2 : .z0)) } } - .ignoresSafeArea(edges: ignoreSafeArea ? .bottom : .top) } } diff --git a/Sources/OversizeUI/Controls/Row/Row.swift b/Sources/OversizeUI/Controls/Row/Row.swift index 8d7c67a..3c4e81d 100644 --- a/Sources/OversizeUI/Controls/Row/Row.swift +++ b/Sources/OversizeUI/Controls/Row/Row.swift @@ -5,20 +5,6 @@ import SwiftUI -public protocol RowLeadingContentProtocol: View {} - -public struct RowLeadingContent: View { // where Content: View { - private let content: Content - - public init(@ViewBuilder content: () -> Content) { - self.content = content() - } - - public var body: some View { - content - } -} - public enum RowClearIconStyle { case `default`, onSurface } diff --git a/Sources/OversizeUI/Controls/SectionView/SectionView.swift b/Sources/OversizeUI/Controls/SectionView/SectionView.swift index e141526..008ed42 100644 --- a/Sources/OversizeUI/Controls/SectionView/SectionView.swift +++ b/Sources/OversizeUI/Controls/SectionView/SectionView.swift @@ -73,7 +73,7 @@ public struct SectionView: View { } .padding(.horizontal, surfaceHorizontalPadding) } - // .padding(.vertical, surfaceVerticalPaddingSize) + .padding(.vertical, surfaceVerticalPaddingSize) } private var titleView: some View { @@ -190,14 +190,14 @@ public struct SectionView: View { } } -// private var surfaceVerticalPaddingSize: CGFloat { -// switch style { -// case .default: -// return Space.small.rawValue -// case .smallIndent, .edgeToEdge: -// return 2 -// } -// } + private var surfaceVerticalPaddingSize: CGFloat { + switch style { + case .default: + return Space.small.rawValue + case .smallIndent, .edgeToEdge: + return 2 + } + } } public extension SectionView { diff --git a/Sources/OversizeUI/Controls/Select/MultiSelect.swift b/Sources/OversizeUI/Controls/Select/MultiSelect.swift index 6406580..ec9ba88 100644 --- a/Sources/OversizeUI/Controls/Select/MultiSelect.swift +++ b/Sources/OversizeUI/Controls/Select/MultiSelect.swift @@ -6,10 +6,12 @@ import SwiftUI // swiftlint:disable all -public struct MultiSelect: View +public struct MultiSelect: View where Content: View, - Selection: View + Selection: View, + Actions: View, + ContentUnavailable: View { @Environment(\.theme) private var theme: ThemeSettings public typealias Data = [Element] @@ -18,27 +20,41 @@ public struct MultiSelect: View private let data: Data private let label: String private let content: (Data.Element, Bool) -> Content + private let contentUnavailable: ContentUnavailable? private let selectionView: (Data) -> Selection - @State private var showModal = false + @State private var showModal: Bool = false + @Binding private var showModalBinding: Bool? @State var selectedIndexes: [Int] = [] + let actions: Group? - public init(_ label: String, - _ data: Data, - selection: Binding, - @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, - @ViewBuilder selectionView: @escaping (Data) -> Selection) - { + public init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data) -> Selection, + @ViewBuilder actions: @escaping () -> Actions, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { self.label = label self.data = data self.content = content self.selectionView = selectionView + self.actions = Group { actions() } + self.contentUnavailable = contentUnavailable() + _showModalBinding = activeModal _selection = selection } public var body: some View { ZStack { Button { - showModal.toggle() + if showModalBinding != nil { + showModalBinding?.toggle() + } else { + showModal.toggle() + } } label: { if selectedIndexes.isEmpty { Text(label) @@ -77,6 +93,11 @@ public struct MultiSelect: View modal #endif } + .onChange(of: showModalBinding) { state in + if let state { + showModal = state + } + } } .onAppear { if !selection.isEmpty { @@ -92,28 +113,95 @@ public struct MultiSelect: View private var modal: some View { PageView(label) { - LazyVStack(alignment: .leading, spacing: .zero) { - ForEach(data.indices, id: \.self) { index in - let isSelected = selectedIndexes.contains(index) + if data.isEmpty, let contentUnavailable { + contentUnavailable + } else { + LazyVStack(alignment: .leading, spacing: .zero) { + ForEach(data.indices, id: \.self) { index in + let isSelected = selectedIndexes.contains(index) - Checkbox(isOn: - Binding(get: { - isSelected - }, set: { _ in - if isSelected, let elementIndex = selectedIndexes.firstIndex(of: index) { - selectedIndexes.remove(at: elementIndex) - } else { - selectedIndexes.append(index) + Checkbox(isOn: Binding( + get: { isSelected }, + set: { _ in + if isSelected, let elementIndex = selectedIndexes.firstIndex(of: index) { + selectedIndexes.remove(at: elementIndex) + } else { + selectedIndexes.append(index) + } + let selectionItems = selectedIndexes.compactMap { data[$0] } + selection = selectionItems } - let selectionItems = selectedIndexes.compactMap { data[$0] } - selection = selectionItems - }), label: { + ), label: { content(data[index], isSelected) }) + } } } } .leadingBar { BarButton(.close) } + .trailingBar { actions } + } +} + +public extension MultiSelect where ContentUnavailable == Never { + init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data) -> Selection, + @ViewBuilder actions: @escaping () -> Actions + ) { + self.label = label + self.data = data + self.content = content + self.selectionView = selectionView + self.actions = Group { actions() } + contentUnavailable = nil + _showModalBinding = activeModal + _selection = selection + } +} + +public extension MultiSelect where Actions == Never { + init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data) -> Selection, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { + self.label = label + self.data = data + self.content = content + self.selectionView = selectionView + actions = nil + self.contentUnavailable = contentUnavailable() + _showModalBinding = activeModal + _selection = selection + } +} + +public extension MultiSelect where ContentUnavailable == Never, Actions == Never { + init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data) -> Selection + ) { + self.label = label + self.data = data + self.content = content + self.selectionView = selectionView + actions = nil + contentUnavailable = nil + _showModalBinding = activeModal + _selection = selection } } diff --git a/Sources/OversizeUI/Controls/Select/Select.swift b/Sources/OversizeUI/Controls/Select/Select.swift index 4673d0a..5f35453 100644 --- a/Sources/OversizeUI/Controls/Select/Select.swift +++ b/Sources/OversizeUI/Controls/Select/Select.swift @@ -6,40 +6,55 @@ import SwiftUI // swiftlint:disable all -public struct Select: View +public struct Select: View where Content: View, + Actions: View, Selection: View { @Environment(\.theme) private var theme: ThemeSettings public typealias Data = [Element] @Binding private var selection: Data.Element + @Binding private var showModalBinding: Bool? private let data: Data private let label: String private let content: (Data.Element, Bool) -> Content + private let contentUnavailable: ContentUnavailable? @State private var selectedIndex: Data.Index? = 0 private let selectionView: (Data.Element) -> Selection - @State private var showModal = false + @State private var showModal: Bool = false @State private var isSelected = false + let actions: Group? - public init(_ label: String, - _ data: Data, - selection: Binding, - @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, - @ViewBuilder selectionView: @escaping (Data.Element) -> Selection) - { + public init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data.Element) -> Selection, + @ViewBuilder actions: @escaping () -> Actions, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { self.label = label self.data = data self.content = content self.selectionView = selectionView + self.contentUnavailable = contentUnavailable() + self.actions = Group { actions() } + _showModalBinding = activeModal _selection = selection } public var body: some View { ZStack { Button { - showModal.toggle() + if showModalBinding != nil { + showModalBinding?.toggle() + } else { + showModal.toggle() + } } label: { if isSelected, let index = selectedIndex { selectionView(data[index]) @@ -79,30 +94,100 @@ public struct Select: View modal #endif } + .onChange(of: showModalBinding) { state in + if let state { + showModal = state + } + } } } private var modal: some View { PageView(label) { - LazyVStack(alignment: .leading, spacing: .zero) { - ForEach(data.indices, id: \.self) { index in - Button(action: { - selectedIndex = index - selection = data[index] - isSelected = true - showModal.toggle() - }, - label: { - content(data[index], - selectedIndex == index) - .headline() - .onSurfaceHighEmphasisForegroundColor() - - }) + if data.isEmpty, let contentUnavailable { + contentUnavailable + } else { + LazyVStack(alignment: .leading, spacing: .zero) { + ForEach(data.indices, id: \.self) { index in + Radio(isOn: index == selectedIndex) { + selectedIndex = index + selection = data[index] + isSelected = true + showModal.toggle() + } label: { + content(data[index], + selectedIndex == index) + .headline() + .onSurfaceHighEmphasisForegroundColor() + } + } } } } .leadingBar { BarButton(.close) } + .trailingBar { actions } + } +} + +public extension Select where ContentUnavailable == Never { + init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data.Element) -> Selection, + @ViewBuilder actions: @escaping () -> Actions + ) { + self.label = label + self.data = data + self.content = content + self.selectionView = selectionView + self.actions = Group { actions() } + contentUnavailable = nil + _showModalBinding = activeModal + _selection = selection + } +} + +public extension Select where Actions == Never { + init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data.Element) -> Selection, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { + self.label = label + self.data = data + self.content = content + self.selectionView = selectionView + actions = nil + self.contentUnavailable = contentUnavailable() + _showModalBinding = activeModal + _selection = selection + } +} + +public extension Select where ContentUnavailable == Never, Actions == Never { + init( + _ label: String, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder selectionView: @escaping (Data.Element) -> Selection + ) { + self.label = label + self.data = data + self.content = content + self.selectionView = selectionView + actions = nil + contentUnavailable = nil + _showModalBinding = activeModal + _selection = selection } } diff --git a/Sources/OversizeUI/Controls/Surface/Surface.swift b/Sources/OversizeUI/Controls/Surface/Surface.swift index 6babfa4..5f17f63 100644 --- a/Sources/OversizeUI/Controls/Surface/Surface.swift +++ b/Sources/OversizeUI/Controls/Surface/Surface.swift @@ -217,6 +217,16 @@ public extension Surface where Label == Row { } } +public extension Surface where Label == Row { + init(action: (() -> Void)? = nil, + @ViewBuilder label: () -> Label) + { + self.label = label() + self.action = action + forceContentInsets = .init(horizontal: .zero, vertical: .small) + } +} + struct Surface_Previews: PreviewProvider { static var previews: some View { VStack { diff --git a/Sources/OversizeUI/Core/EnvironmentKeys/SectionTitleInsetsEnvironment.swift b/Sources/OversizeUI/Core/EnvironmentKeys/SectionTitleInsetsEnvironment.swift index 1cdc83f..1622c11 100644 --- a/Sources/OversizeUI/Core/EnvironmentKeys/SectionTitleInsetsEnvironment.swift +++ b/Sources/OversizeUI/Core/EnvironmentKeys/SectionTitleInsetsEnvironment.swift @@ -25,4 +25,9 @@ public extension View { environment(\.sectionTitleInsets, .init(top: .zero, leading: .medium, bottom: .zero, trailing: .medium)) .environment(\.surfaceContentInsets, .init(top: .medium, leading: .zero, bottom: .medium, trailing: .zero)) } + + func sectionContentCompactRowInsets() -> some View { + environment(\.sectionTitleInsets, .init(top: .zero, leading: .medium, bottom: .zero, trailing: .medium)) + .environment(\.surfaceContentInsets, .init(top: .xxSmall, leading: .zero, bottom: .xxSmall, trailing: .zero)) + } } diff --git a/Sources/OversizeUI/Extensions/View/View+Available.swift b/Sources/OversizeUI/Extensions/View/View+Available.swift index 7aec189..abbacbe 100644 --- a/Sources/OversizeUI/Extensions/View/View+Available.swift +++ b/Sources/OversizeUI/Extensions/View/View+Available.swift @@ -5,6 +5,14 @@ import SwiftUI +public enum PresentationContentInteraction { + case automatic, resizes, scrolls +} + +public enum PresentationAdaptation { + case automatic, none, popover, sheet, fullScreenCover +} + public extension View { @available(tvOS, unavailable) @_disfavoredOverload @@ -16,7 +24,7 @@ public extension View { self } } - + @_disfavoredOverload @ViewBuilder func presentationDragIndicator(_ visibility: Visibility) -> some View { @@ -26,4 +34,35 @@ public extension View { self } } + + @_disfavoredOverload + @ViewBuilder + func presentationContentInteraction(_ behavior: PresentationContentInteraction) -> some View { + if #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) { + self.presentationContentInteraction(behavior == .automatic ? .automatic : behavior == .resizes ? .resizes : .scrolls) + } else { + self + } + } + + @_disfavoredOverload + @ViewBuilder + func presentationCompactAdaptation(_ adaptation: PresentationAdaptation) -> some View { + if #available(iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4, *) { + self.presentationCompactAdaptation( + adaptation == .automatic ? .automatic : adaptation == .none ? .none : adaptation == .popover ? .popover : adaptation == .sheet ? .sheet : .fullScreenCover) + } else { + self + } + } + + @_disfavoredOverload + @ViewBuilder + func scrollDisabled(_ disabled: Bool) -> some View { + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { + self.scrollDisabled(disabled) + } else { + self + } + } }