diff --git a/.github/workflows/build-example.yml b/.github/workflows/build-example.yml deleted file mode 100644 index 8412c19..0000000 --- a/.github/workflows/build-example.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Build Example - -on: - workflow_dispatch: - workflow_call: - -concurrency: - group: ${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer - PROJECT_DIR: Example - PROJECT_NAME: Example.xcodeproj - iOSSCHEME: Example (iOS) - OSXSCHEME: Example - TVSCHEME: Example - WATCHSCHEME: Example - PACKAGE_NAME: OversizeUI - -jobs: - - build-iOS-example: - name: Build iOS examples - runs-on: macOS-latest - strategy: - matrix: - iosDestination: ['platform=iOS Simulator,OS=15.0,name=iPhone 8','platform=iOS Simulator,OS=15.5,name=iPhone X','platform=iOS Simulator,OS=16.0,name=iPhone 14'] - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build iOS - run: | - xcodebuild clean build -project "${{ env.PROJECT_DIR }}/${{ env.PROJECT_NAME }}" -scheme "${{ env.iOSSCHEME }}" | xcpretty - env: - destination: ${{ matrix.iosDestination }} - - build-macOS-example: - name: Build macOS examples - runs-on: macOS-latest - strategy: - matrix: - macOSDestination: ["platform=macOS,arch=x86_64"] - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build macOS - run: | - xcodebuild clean build -project "${{ env.PROJECT_DIR }}/${{ env.PROJECT_NAME }}" -scheme "${{ env.OSXSCHEME }}" | xcpretty - env: - destination: ${{ matrix.macOSDestination }} - - build-tvOS-example: - name: Build tvOS examples - runs-on: macOS-latest - strategy: - matrix: - tvOSDestination: ["platform=tvOS Simulator,name=Apple TV 4K"] - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build tvOS - run: | - xcodebuild clean build -project "${{ env.PROJECT_DIR }}/${{ env.PROJECT_NAME }}" -scheme "${{ env.TVSCHEME }}" | xcpretty - env: - destination: ${{ matrix.tvOSDestination }} - - build-watchOS-example: - name: Build watchOS examples - runs-on: macOS-latest - strategy: - matrix: - watchOSdestination: ['platform=watchOS Simulator,name=Apple Watch Series 5 - 44mm'] - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build watchOS - run: | - xcodebuild clean build -project "${{ env.PROJECT_DIR }}/${{ env.PROJECT_NAME }}" -scheme "${{ env.WATCHSCHEME }}" | xcpretty - env: - destination: ${{ matrix.watchOSdestination }} - diff --git a/.github/workflows/build-swiftpm.yml b/.github/workflows/build-swiftpm.yml deleted file mode 100644 index 1a226b9..0000000 --- a/.github/workflows/build-swiftpm.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Build Example - -on: - workflow_dispatch: - workflow_call: - -concurrency: - group: ${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -env: - PACKAGE_NAME: OversizeUI - -jobs: - - swiftpm: - name: Build SwiftPM - runs-on: macOS-latest - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build Package - run: xcodebuild clean build -skipPackagePluginValidation -scheme ${{ env.PACKAGE_NAME }} -destination 'platform=iOS Simulator,name=iPhone 8,OS=16.2' | xcpretty && exit ${PIPESTATUS[0]} diff --git a/.github/workflows/bump.yml b/.github/workflows/bump.yml deleted file mode 100644 index 68d71ff..0000000 --- a/.github/workflows/bump.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: Bump version -on: - workflow_dispatch: - workflow_call: - -jobs: - - tag: - name: Create tag - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: '0' - - - name: Bump version and push tag - uses: anothrNick/github-tag-action@master - env: - GITHUB_TOKEN: ${{ secrets.ACTIONS_TOKEN }} - WITH_V: false diff --git a/.github/workflows/ci-pull-request.yml b/.github/workflows/ci-pull-request.yml index 7f1603c..773a7ca 100644 --- a/.github/workflows/ci-pull-request.yml +++ b/.github/workflows/ci-pull-request.yml @@ -4,13 +4,60 @@ on: branches: - 'main' workflow_dispatch: + jobs: build-swiftpm: name: Build SwiftPM - uses: ./.github/workflows/build-swiftpm.yml + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm.yml@main + with: + package: OversizeUI secrets: inherit - build-example: - name: Build Examples + + build-iOS-example: + name: Build iOS example needs: build-swiftpm - uses: ./.github/workflows/build-example.yml + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + strategy: + matrix: + destination: ['platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2', 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (6th generation),OS=17.2'] + with: + path: Example/Example + scheme: Example (iOS) + destination: ${{ matrix.destination }} + secrets: inherit + + build-macOS-example: + name: Build macOS examples + needs: build-swiftpm + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + with: + path: Example/Example + scheme: Example (macOS) + destination: platform=macOS,arch=arm64 + secrets: inherit + + build-tvOS-example: + name: Build tvOS examples + needs: build-swiftpm + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + strategy: + matrix: + destination: ['platform=tvOS Simulator,name=Apple TV 4K (3rd generation) (at 1080p),OS=17.2', 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation) (at 1080p),OS=16.4'] + with: + path: Example/Example + scheme: Example (tvOS) + destination: ${{ matrix.destination }} + secrets: inherit + + build-watchOS-example: + name: Build watchOS examples + needs: build-swiftpm + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + strategy: + matrix: + destination: ['platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm),OS=10.2', 'platform=watchOS Simulator,name=Apple Watch Series 8 (45mm),OS=9.4'] + with: + path: Example/Example + scheme: Example (watchOS) + destination: ${{ matrix.destination }} secrets: inherit diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 5176617..6c38a5b 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -6,23 +6,71 @@ on: branches: - main workflow_dispatch: + jobs: - build-swiftpm: name: Build SwiftPM - uses: ./.github/workflows/build-swiftpm.yml + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm.yml@main + strategy: + matrix: + packages: [OversizeUI] + with: + package: ${{ matrix.packages }} + secrets: inherit + + build-iOS-example: + name: Build iOS example + needs: build-swiftpm + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + strategy: + matrix: + destination: ['platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2', 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (6th generation),OS=17.2'] + with: + path: Example/Example + scheme: Example (iOS) + destination: ${{ matrix.destination }} + secrets: inherit + + build-macOS-example: + name: Build macOS examples + needs: build-swiftpm + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + with: + path: Example/Example + scheme: Example (macOS) + destination: platform=macOS,arch=arm64 + secrets: inherit + + build-tvOS-example: + name: Build tvOS examples + needs: build-swiftpm + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + strategy: + matrix: + destination: ['platform=tvOS Simulator,name=Apple TV 4K (3rd generation) (at 1080p),OS=17.2', 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation) (at 1080p),OS=16.4'] + with: + path: Example/Example + scheme: Example (tvOS) + destination: ${{ matrix.destination }} secrets: inherit - build-example: - name: Build Examples + build-watchOS-example: + name: Build watchOS examples needs: build-swiftpm - uses: ./.github/workflows/build-example.yml + uses: oversizedev/GithubWorkflows/.github/workflows/build-app.yml@main + strategy: + matrix: + destination: ['platform=watchOS Simulator,name=Apple Watch Ultra 2 (49mm),OS=10.2', 'platform=watchOS Simulator,name=Apple Watch Series 8 (45mm),OS=9.4'] + with: + path: Example/Example + scheme: Example (watchOS) + destination: ${{ matrix.destination }} secrets: inherit bump: name: Bump version - needs: [build-swiftpm, build-example] - uses: ./.github/workflows/bump.yml + needs: [build-swiftpm, build-iOS-example, build-macOS-example, build-tvOS-example, build-watchOS-example] + uses: oversizedev/GithubWorkflows/.github/workflows/bump.yml@main secrets: inherit publish-docc: diff --git a/.gitignore b/.gitignore index 6745e57..3b9f5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /*.xcodeproj xcuserdata/ DerivedData/ -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +/.swiftpm /.idea/ /Package.resolved +/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm diff --git a/.swiftformat b/.swiftformat index effc95a..ed5d06d 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,3 +1,3 @@ ---swiftversion 5.7 +--swiftversion 5.9 --disable preferKeyPath --ifdef no-indent \ No newline at end of file diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index 54782e3..0000000 --- a/.swiftpm/xcode/package.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded - - - diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 3267907..d445862 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -811,6 +811,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ER582ZK85C; IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = romanov.cc.Example; @@ -825,6 +826,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ER582ZK85C; IPHONEOS_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = romanov.cc.Example; @@ -843,6 +845,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; IBSC_MODULE = Example_WatchKit_Extension; INFOPLIST_FILE = "Example WatchKit App/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -869,6 +872,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; IBSC_MODULE = Example_WatchKit_Extension; INFOPLIST_FILE = "Example WatchKit App/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -893,6 +897,7 @@ ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Example WatchKit Extension/Preview Content\""; + DEVELOPMENT_TEAM = ER582ZK85C; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = "Example WatchKit Extension/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -916,6 +921,7 @@ ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"Example WatchKit Extension/Preview Content\""; + DEVELOPMENT_TEAM = ER582ZK85C; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = "Example WatchKit Extension/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -940,6 +946,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; INFOPLIST_FILE = ExampleTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -962,6 +969,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; INFOPLIST_FILE = ExampleTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -983,6 +991,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; INFOPLIST_FILE = ExampleUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1003,6 +1012,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; INFOPLIST_FILE = ExampleUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1025,6 +1035,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = tvOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1045,6 +1056,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ER582ZK85C; ENABLE_PREVIEWS = YES; INFOPLIST_FILE = tvOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1234,7 +1246,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = romanov.cc.Example; PRODUCT_NAME = Example; SDKROOT = macosx; @@ -1259,7 +1271,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 12.0; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = romanov.cc.Example; PRODUCT_NAME = Example; SDKROOT = macosx; diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/OversizeUI.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example (macOS).xcscheme similarity index 52% rename from .swiftpm/xcode/xcshareddata/xcschemes/OversizeUI.xcscheme rename to Example/Example.xcodeproj/xcshareddata/xcschemes/Example (macOS).xcscheme index 31c65c8..60aa8e4 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/OversizeUI.xcscheme +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example (macOS).xcscheme @@ -1,10 +1,11 @@ + LastUpgradeVersion = "1530" + version = "1.7"> + buildImplicitDependencies = "YES" + buildArchitectures = "Automatic"> - - - - + BlueprintIdentifier = "84D9F9EF26BDE4EB00B2FCA2" + BuildableName = "Example.app" + BlueprintName = "Example (macOS)" + ReferencedContainer = "container:Example.xcodeproj"> @@ -40,19 +27,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - + shouldUseLaunchSchemeArgsEnv = "YES" + shouldAutocreateTestPlan = "YES"> + + + + - + + BlueprintIdentifier = "84D9F9EF26BDE4EB00B2FCA2" + BuildableName = "Example.app" + BlueprintName = "Example (macOS)" + ReferencedContainer = "container:Example.xcodeproj"> - + diff --git a/Example/Shared/DemoPages/SurfaceDemo.swift b/Example/Shared/DemoPages/SurfaceDemo.swift index 7ff06d1..ad4636e 100644 --- a/Example/Shared/DemoPages/SurfaceDemo.swift +++ b/Example/Shared/DemoPages/SurfaceDemo.swift @@ -36,7 +36,7 @@ struct SurfaceDemo: View { Spacer() }} .controlRadius(.zero) - .surfaceContentInsets(.zero) + .surfaceContentMargins(.zero) .elevation(.z2) Surface { HStack { diff --git a/Package.swift b/Package.swift index 8c32425..0aefc86 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.7 +// swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. // swiftlint:disable all @@ -9,15 +9,18 @@ let package = Package( defaultLocalization: "en", platforms: [ .iOS(.v15), - .macOS(.v12), + .macOS(.v13), .tvOS(.v15), .watchOS(.v9), ], products: [ - .library(name: "OversizeUI", targets: ["OversizeUI"]), + .library( + name: "OversizeUI", + targets: ["OversizeUI"] + ), ], dependencies: [ - .package(url: "https://github.com/SwiftGen/SwiftGenPlugin", from: "6.6.2"), + .package(url: "https://github.com/SwiftGen/SwiftGenPlugin", .upToNextMajor(from: "6.6.2")), ], targets: [ .target( diff --git a/Sources/OversizeUI/Controls/Avatar/Avatar.swift b/Sources/OversizeUI/Controls/Avatar/Avatar.swift index bc36704..00cac67 100644 --- a/Sources/OversizeUI/Controls/Avatar/Avatar.swift +++ b/Sources/OversizeUI/Controls/Avatar/Avatar.swift @@ -22,8 +22,10 @@ public enum AvatarBackgroundType { /// ``` /// public struct Avatar: View { - @available(tvOS, unavailable) + + #if !os(tvOS) @Environment(\.controlSize) var controlSize: ControlSize + #endif /// The first name text of the avatar. let firstName: String? @@ -152,8 +154,10 @@ public struct Avatar: View { .foregroundColor(onBackgroundColor) } - @available(tvOS, unavailable) private var avatarTextFont: Font { + #if os(tvOS) + return .largeTitle + #else switch controlSize { case .mini: return .caption @@ -161,15 +165,18 @@ public struct Avatar: View { return .subheadline case .regular: return .title3 - case .large: + case .large, .extraLarge: return .largeTitle @unknown default: return .title2 } + #endif } - @available(tvOS, unavailable) private var avatarTextSpace: CGFloat { + #if os(tvOS) + return 2 + #else switch controlSize { case .mini: return 0 @@ -177,15 +184,18 @@ public struct Avatar: View { return 1 case .regular: return 2 - case .large: + case .large, .extraLarge: return 2 @unknown default: return 0 } + #endif } - @available(tvOS, unavailable) private var avatarSize: CGFloat { + #if os(tvOS) + return Space.xLarge.rawValue + #else switch controlSize { case .mini: return Space.medium.rawValue @@ -193,11 +203,12 @@ public struct Avatar: View { return Space.large.rawValue case .regular: return Space.xLarge.rawValue - case .large: + case .large, .extraLarge: return Space.xxxLarge.rawValue @unknown default: return Space.xLarge.rawValue } + #endif } } diff --git a/Sources/OversizeUI/Controls/Badge/Bage.swift b/Sources/OversizeUI/Controls/Badge/Bage.swift index 69fffbf..320ddf8 100644 --- a/Sources/OversizeUI/Controls/Badge/Bage.swift +++ b/Sources/OversizeUI/Controls/Badge/Bage.swift @@ -21,10 +21,10 @@ public struct Bage: View { HStack { label .foregroundColor(color) - .caption() + .caption(.medium) } .padding(.vertical, .xxxSmall) - .padding(.horizontal, 6) + .padding(.horizontal, .xxSmall) .background( RoundedRectangle(cornerRadius: controlRadius, style: .continuous) diff --git a/Sources/OversizeUI/Controls/Button/Button.swift b/Sources/OversizeUI/Controls/Button/Button.swift index eb75eee..01927f6 100644 --- a/Sources/OversizeUI/Controls/Button/Button.swift +++ b/Sources/OversizeUI/Controls/Button/Button.swift @@ -20,8 +20,9 @@ public struct OversizeButtonStyle: ButtonStyle { @Environment(\.elevation) private var elevation: Elevation @Environment(\.controlBorderShape) var controlBorderShape: ControlBorderShape @Environment(\.isBordered) var isBordered: Bool - @available(tvOS, unavailable) + #if !os(tvOS) @Environment(\.controlSize) var controlSize: ControlSize + #endif private let type: ButtonType private let isInfinityWidth: Bool? @@ -98,7 +99,15 @@ public struct OversizeButtonStyle: ButtonStyle { private func foregroundColor(for role: ButtonRole?) -> Color { switch type { case .primary: - return Color.onPrimaryHighEmphasis + switch role { + case .some(.destructive), .some(.cancel): return Color.onPrimaryHighEmphasis + default: + if isAccent { + return Color.onPrimaryHighEmphasis + } else { + return Color.backgroundPrimary + } + } case .secondary, .quaternary: switch role { case .some(.destructive): return Color.error @@ -145,7 +154,7 @@ public struct OversizeButtonStyle: ButtonStyle { return .small case .regular: return .small - case .large: + case .large, .extraLarge: return .medium @unknown default: return .zero @@ -164,7 +173,7 @@ public struct OversizeButtonStyle: ButtonStyle { return .xxSmall case .regular: return .small - case .large: + case .large, .extraLarge: return .medium @unknown default: return .zero diff --git a/Sources/OversizeUI/Controls/ColorSelector/Styles/HorizontalColorSelectorStyle.swift b/Sources/OversizeUI/Controls/ColorSelector/Styles/HorizontalColorSelectorStyle.swift index e9d7208..78918b2 100644 --- a/Sources/OversizeUI/Controls/ColorSelector/Styles/HorizontalColorSelectorStyle.swift +++ b/Sources/OversizeUI/Controls/ColorSelector/Styles/HorizontalColorSelectorStyle.swift @@ -12,7 +12,7 @@ public struct HorizontalColorSelectorStyle: ColorSelectorStyle { HStack { configuration.label } - .padding(.horizontal) + .padding(.horizontal, .medium) } .padding(.vertical) } diff --git a/Sources/OversizeUI/Controls/DateField/DateField.swift b/Sources/OversizeUI/Controls/DateField/DateField.swift index 3277a21..0e6092e 100644 --- a/Sources/OversizeUI/Controls/DateField/DateField.swift +++ b/Sources/OversizeUI/Controls/DateField/DateField.swift @@ -70,7 +70,7 @@ public struct DateField: View { Text(selection.formatted(date: .long, time: .shortened)) } Spacer() - Image.Base.calendar + Image.Base.Calendar.fill.icon() } } } diff --git a/Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift b/Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift index eae54e1..219a640 100644 --- a/Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift +++ b/Sources/OversizeUI/Controls/DateField/DatePickerSheet.swift @@ -5,6 +5,10 @@ import SwiftUI +#if os(iOS) +@available(macOS, unavailable) +@available(watchOS, unavailable) +@available(tvOS, unavailable) public struct DatePickerSheet: View { @Environment(\.screenSize) var screenSize @Environment(\.dismiss) var dismiss @@ -68,3 +72,4 @@ public struct DatePickerSheet: View { return control } } +#endif diff --git a/Sources/OversizeUI/Controls/HUD/HUD.swift b/Sources/OversizeUI/Controls/HUD/HUD.swift index a0f2e71..eff6dc4 100644 --- a/Sources/OversizeUI/Controls/HUD/HUD.swift +++ b/Sources/OversizeUI/Controls/HUD/HUD.swift @@ -9,10 +9,8 @@ public struct HUD: View where Title: View, Icon: View { @Environment(\.screenSize) var screenSize private let text: String? - private let title: Title? private let icon: Icon? - private let isAutoHide: Bool @Binding private var isPresented: Bool diff --git a/Sources/OversizeUI/Controls/IconPicker/IconPicker.swift b/Sources/OversizeUI/Controls/IconPicker/IconPicker.swift index 599bf3c..2fbfc25 100644 --- a/Sources/OversizeUI/Controls/IconPicker/IconPicker.swift +++ b/Sources/OversizeUI/Controls/IconPicker/IconPicker.swift @@ -11,17 +11,17 @@ import SwiftUI public struct IconPicker: View { @Environment(\.theme) private var theme: ThemeSettings @Environment(\.horizontalSizeClass) var horizontalSizeClass - + private let label: String private let icons: [Image] @Binding private var selection: Image? @State private var showModal = false @State private var isSelected = false - + @State private var selectedIndex: Int? - + @State var offset = CGPoint(x: 0, y: 0) - + private var gridPadding: CGFloat { guard let sizeClass = horizontalSizeClass else { return 40 } switch sizeClass { @@ -31,7 +31,7 @@ public struct IconPicker: View { return 72 } } - + public init(_ label: String, _ icons: [Image], selection: Binding) @@ -40,71 +40,56 @@ public struct IconPicker: View { self.icons = icons _selection = selection } - + public var body: some View { Button { showModal.toggle() } label: { HStack(spacing: .xxSmall) { Text(label) - .headline() .onSurfaceHighEmphasisForegroundColor() + + Spacer() + if let image = selection { + image + } + IconDeprecated(.chevronDown, color: .onSurfaceHighEmphasis) } - Spacer() - if let image = selection { - image - } - IconDeprecated(.chevronDown, color: .onSurfaceHighEmphasis) } - .frame(minWidth: 0, maxWidth: .infinity) - .padding() - .background( - RoundedRectangle(cornerRadius: Radius.medium, - style: .continuous) - .fill(Color.surfaceSecondary) - .overlay( - RoundedRectangle(cornerRadius: Radius.medium, - style: .continuous) - .stroke(theme.borderTextFields - ? Color.border - : Color.surfaceSecondary, lineWidth: CGFloat(theme.borderSize)) - ) - ) + .buttonStyle(.field) .sheet(isPresented: $showModal) { modal } } - + private var modal: some View { PageView(label) { ScrollView { LazyVGrid(columns: [GridItem(.adaptive(minimum: gridPadding))]) { ForEach(icons.indices, id: \.self) { index in - Button(action: { - selectedIndex = index - - }, - label: { - if index == selectedIndex { - Group { - icons[index] - .resizable() - - .frame(width: 24, height: 24, alignment: .center) - } - .overlay( - RoundedRectangle(cornerRadius: Radius.medium, style: .continuous) - .strokeBorder(Color.border, lineWidth: 1) - .frame(width: 48, height: 48, alignment: .center) - ) - - } else { - icons[index] - .resizable() - .frame(width: 24, height: 24, alignment: .center) - } - }) - .padding(.vertical, horizontalSizeClass == .compact ? 12 : 20) + Button( + action: { selectedIndex = index }, + label: { + if index == selectedIndex { + Group { + icons[index] + .resizable() + .frame(width: 24, height: 24, alignment: .center) + } + .overlay( + RoundedRectangle(cornerRadius: Radius.medium, style: .continuous) + .strokeBorder(Color.border, lineWidth: 1) + .frame(width: 48, height: 48, alignment: .center) + ) + + } else { + icons[index] + .resizable() + .frame(width: 24, height: 24, alignment: .center) + } + } + ) + .padding(.vertical, horizontalSizeClass == .compact ? 12 : 20) } } .padding(.top, .medium) diff --git a/Sources/OversizeUI/Controls/KeyboardToolbar/KeyboardToolbar.swift b/Sources/OversizeUI/Controls/KeyboardToolbar/KeyboardToolbar.swift index b66ddfc..3e7a5ba 100644 --- a/Sources/OversizeUI/Controls/KeyboardToolbar/KeyboardToolbar.swift +++ b/Sources/OversizeUI/Controls/KeyboardToolbar/KeyboardToolbar.swift @@ -24,7 +24,9 @@ public struct KeyboardToolbar: View where A: View { actions .buttonStyle(.quaternary) .controlBorderShape(.capsule) + #if !os(tvOS) .controlSize(.mini) + #endif } } @@ -39,7 +41,9 @@ public struct KeyboardToolbar: View where A: View { .buttonStyle(.quaternary) .controlBorderShape(.capsule) .accent() + #if !os(tvOS) .controlSize(.mini) + #endif } } .padding(.horizontal, .small) diff --git a/Sources/OversizeUI/Controls/Loader/LoaderOverlayView.swift b/Sources/OversizeUI/Controls/Loader/LoaderOverlayView.swift index cc9d37d..2d09d66 100644 --- a/Sources/OversizeUI/Controls/Loader/LoaderOverlayView.swift +++ b/Sources/OversizeUI/Controls/Loader/LoaderOverlayView.swift @@ -10,6 +10,7 @@ public enum LoaderOverlayType { case spiner } +#if !os(watchOS) public struct LoaderOverlayView: View { private var loaderType: LoaderOverlayType private let showText: Bool @@ -145,3 +146,4 @@ struct LoaderOverlayView_Previews: PreviewProvider { .loader(isPresented: .constant(true)) } } +#endif diff --git a/Sources/OversizeUI/Controls/Menu/BarButtonMenuStyle.swift b/Sources/OversizeUI/Controls/Menu/BarButtonMenuStyle.swift index a5e8aa5..d755346 100644 --- a/Sources/OversizeUI/Controls/Menu/BarButtonMenuStyle.swift +++ b/Sources/OversizeUI/Controls/Menu/BarButtonMenuStyle.swift @@ -5,6 +5,7 @@ import SwiftUI +#if !os(tvOS) @available(watchOS, unavailable) @available(tvOS, unavailable) public struct BarButtonMenuStyle: MenuStyle { @@ -40,3 +41,4 @@ public extension MenuStyle where Self == BarButtonMenuStyle { BarButtonMenuStyle() } } +#endif diff --git a/Sources/OversizeUI/Controls/PageView/Page.swift b/Sources/OversizeUI/Controls/PageView/Page.swift new file mode 100644 index 0000000..1cf1990 --- /dev/null +++ b/Sources/OversizeUI/Controls/PageView/Page.swift @@ -0,0 +1,1122 @@ +import SwiftUI + +@available(iOS 16.0, *) +public struct Page: View + where Content: View, Header: View, LeadingBar: View, TrailingBar: View, TopToolbar: View, TitleLabel: View +{ + public typealias ScrollAction = (_ offset: CGPoint, _ headerVisibleRatio: CGFloat) -> Void + + private let title: String? + private let content: Content + private var header: Header? + private var leadingBar: LeadingBar? + private var trailingBar: TrailingBar? + private var topToolbar: TopToolbar? + private var titleLabel: TitleLabel? + + private var isLargeTitle = false + + @State + private var offset = CGPoint.zero + + private var visibleRatio: CGFloat { + (calcHeaderHeight + offset.y) / calcHeaderHeight + } + + @State + private var isShowSearchBar: Bool = false + + @Environment(\.screenSize) private var screenSize + + private let onScroll: ScrollAction? + + let custumHeaderHeight: CGFloat + + @State + private var navigationBarHeight: CGFloat = 0 + + private let headerMinHeight: CGFloat? + + var calcHeaderHeight: CGFloat { + isFocusSearchBar ? (navigationBarHeight - screenSize.safeAreaTop) + 20 : (isLargeTitle && !isFocusSearchBar ? 60 : 0) + (isShowSearchBar ? 57 : 0) + custumHeaderHeight + } + + /// Search + @Binding private var searchQuery: String + @Binding private var displaySearchBar: Bool + @FocusState private var focusStateSearchBar: Bool + @State private var isFocusSearchBar: Bool = false + private var isEnableSearchBar: Bool = false + private var prompt: String = .init() + private var searchCancelButton: PageViewSearchButtonType = .icon + @Namespace private var searchAnimation + + /// Background + private var backgroundColor: Color = .backgroundPrimary + private var backgroundLinerGradient: LinearGradient? + + public init( + _ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header + ) { + self.title = title + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.onScroll = onScroll + self.content = content() + self.header = header() + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } + + public var body: some View { + ZStack(alignment: .top) { + scrollView + navbarOverlay + } + .prefersNavigationBarHidden() + #if os(iOS) || os(macOS) + .toolbar { + if let title { + ToolbarItem(placement: .principal) { + Text(title) + .font(.headline) + .opacity(isLargeTitle ? 1 - visibleRatio : 1) // .opacity(isLargeTitle ? visibleRatio > 0 ? 0 : -5 * visibleRatio : 1) + } + } + } + #endif + #if !os(tvOS) + .toolbarBackground(.hidden) + #endif + .onChange(of: focusStateSearchBar, perform: onChangeFocusSearchBar) + .onChange(of: displaySearchBar, perform: onChangeDisplaySearchBar) + #if os(iOS) + .toolbar(isFocusSearchBar ? .hidden : .automatic, for: .navigationBar) + .navigationBarTitleDisplayMode(.inline) + #endif + } + + var scrollView: some View { + GeometryReader { proxy in + ScrollViewWithOffsetTracking( + showsIndicators: false, + onScroll: handleOffset + ) { + VStack(spacing: 0) { + scrollHeader + .opacity(isFocusSearchBar ? 0 : 1) + content + .frame(maxHeight: .infinity) + } + } + .onAppear { + if screenSize.safeAreaTop == 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + navigationBarHeight = proxy.safeAreaInsets.top + } + } else { + DispatchQueue.main.async { + navigationBarHeight = proxy.safeAreaInsets.top + } + } + } + .background(background.ignoresSafeArea()) + + } + } + + @ViewBuilder + var scrollHeader: some View { + ScrollViewHeader(content: pageHeader) + .frame(height: calcHeaderHeight) + } + + func handleOffset(_ scrollOffset: CGPoint) { + offset = scrollOffset + if isEnableSearchBar, scrollOffset.y > 95, !isShowSearchBar { + withAnimation(.easeOut(duration: 0.30)) { + isShowSearchBar = true + } + } + onScroll?(offset, visibleRatio) + } + + @ViewBuilder + var navbarOverlay: some View { + if isStickyHeaderVisible || isFocusSearchBar { + Color.clear + .frame(height: isFocusSearchBar ? navigationBarHeight + 20 : navigationBarHeight) + .overlay(alignment: .bottom) { + if isFocusSearchBar { + searchHeader + } else { + scrollHeader + } + } + .ignoresSafeArea(edges: .top) + .frame(height: headerMinHeight) + } + } + + func pageHeader() -> some View { + ZStack(alignment: .bottomLeading) { + if let header { + header + } else { + Rectangle() + .fill(Color.surfacePrimary.opacity(visibleRatio > 0 ? 0 : -5 * visibleRatio)) + //.fill(Material.bar.opacity(visibleRatio > 0 ? 0 : -5 * visibleRatio)) + .overlay(alignment: .bottom) { + Divider().opacity(visibleRatio > 0 ? 0 : -5 * visibleRatio) + } + } + + VStack(alignment: .leading, spacing: .xxSmall) { + if isLargeTitle, !isFocusSearchBar, let title { + Text(title) + .largeTitle() + .opacity(visibleRatio) + } + + if isEnableSearchBar, !isFocusSearchBar { + searchBarButton + } + } + .padding(.horizontal, .medium) + } + } + + private var isStickyHeaderVisible: Bool { + guard let headerMinHeight else { return visibleRatio <= 0 } + return offset.y < -headerMinHeight + } + + private var searchBarButton: some View { + HStack(spacing: .zero) { + Button { + onChangeFocusSearchBar(true) + displaySearchBar = true + focusStateSearchBar = true + } label: { + Text(searchQuery.isEmpty ? "Search" : searchQuery) + } + .buttonStyle(SeartchTextFieldButtonStyle(height: isShowSearchBar ? 45 : min(max(offset.y / 2, 0), 45))) + .matchedGeometryEffect(id: "PageSearchBar", in: searchAnimation) + } + .padding(.bottom, isShowSearchBar ? 12 : min(max(offset.y / 2, 0), 12)) + .opacity(isFocusSearchBar ? 1 : (isShowSearchBar ? visibleRatio : visibleRatio - 1)) + } + + private var searchHeader: some View { + ZStack(alignment: .bottomLeading) { + Rectangle() + #if os(iOS) || os(macOS) + .fill(Material.bar) + #endif + .overlay(alignment: .bottom) { + Divider().opacity(visibleRatio > 0 ? 0 : -5 * visibleRatio) + } + + HStack(spacing: .zero) { + TextField("Search", text: $searchQuery) + .textFieldStyle(SeartchTextFieldStyle()) + .focused($focusStateSearchBar) + .submitLabel(.search) + .matchedGeometryEffect(id: "PageSearchBar", in: searchAnimation) + + if isFocusSearchBar { + Button("Cancel") { + focusStateSearchBar = false + displaySearchBar = false + } + .buttonStyle(.quaternary(infinityWidth: false)) + #if !os(tvOS) + .controlSize(.mini) + #endif + .offset(x: 8) + } + } + .padding(.horizontal, .small) + .padding(.bottom, 12) + } + } + + public enum PageViewSearchButtonType { + case none, icon, label(String = "Cancel") + } + + public func largeTitle(_ isLargeTitle: Bool = true) -> Page { + var control = self + control.isLargeTitle = isLargeTitle + return control + } + + private var searchCancelButtonPadding: Space { + switch searchCancelButton { + case .icon, .none: + return .xSmall + case .label: + return .xxSmall + } + } + + @ViewBuilder + private var searchCancelButtonView: some View { + switch searchCancelButton { + case .none: + EmptyView() + case .icon: + IconDeprecated(.xMini, color: .onSurfaceMediumEmphasis) + .background { + Circle() + .fill(Color.backgroundTertiary) + .frame(width: 36, height: 36) + } + case let .label(text): + Text(text) + .subheadline(.bold) + .foregroundColor(.onSurfaceHighEmphasis) + .padding(.horizontal, .xSmall) + .background { + RoundedRectangle(cornerRadius: .small, style: .continuous) + .fill(Color.backgroundTertiary) + .frame(height: 40) + } + } + } + + func onChangeFocusSearchBar(_ state: Bool) { + withAnimation(.easeIn(duration: 0.20)) { + isFocusSearchBar = state + } + } + + func onChangeDisplaySearchBar(_ state: Bool) { + onChangeFocusSearchBar(state) + focusStateSearchBar = state + } + + public func searchable(text: Binding, prompt: String = "Search", cancelButton: Page.PageViewSearchButtonType = .label(), isSearch: Binding) -> Page { + var control = self + control._searchQuery = text + control._displaySearchBar = isSearch + control.prompt = prompt + control.isEnableSearchBar = true + control.searchCancelButton = cancelButton + return control + } + + public func searchable(text: Binding, prompt: String = "Search") -> Page { + var control = self + control._searchQuery = text + control._displaySearchBar = .constant(true) + control.prompt = prompt + control.isShowSearchBar = true + control.searchCancelButton = .none + return control + } + + public func leadingBar(@ViewBuilder leadingBar: @escaping () -> LeadingBar) -> Page { + var control = self + control.leadingBar = leadingBar() + return control + } + + public func trailingBar(@ViewBuilder trailingBar: @escaping () -> TrailingBar) -> Page { + var control = self + control.trailingBar = trailingBar() + return control + } + + public func topToolbar(@ViewBuilder topToolbar: @escaping () -> TopToolbar) -> Page { + var control = self + control.topToolbar = topToolbar() + return control + } + + public func titleLabel(@ViewBuilder titleLabel: @escaping () -> TitleLabel) -> Page { + var control = self + control.titleLabel = titleLabel() + return control + } + + public func bottomToolbar(style: PageViewBottomType = .shadow, @ViewBuilder bottomToolbar: @escaping () -> some View) -> some View { + VStack(spacing: .zero) { + overlay( + Group { + if style == .gradient { + VStack { + Spacer() + LinearGradient(colors: [backgroundColor.opacity(0), Color.surfacePrimary.opacity(1)], + startPoint: .top, + endPoint: .bottom) + .frame(height: 60) + } + } + if style == .none { + VStack { + Spacer() + bottomToolbar() + } + } + }) + if style != .none { + HStack { + Spacer() + bottomToolbar() + Spacer() + } + .background(Color.surfacePrimary.shadowElevaton(style == .shadow ? .z2 : .z0)) + } + } + } +} + +@available(iOS 16.0, *) +public extension Page { + enum PageViewBottomType { + case shadow, gradient, none + } +} + +@available(iOS 16.0, *) +public extension Page { + @ViewBuilder + internal var background: some View { + if let backgroundLinerGradient { + backgroundLinerGradient + } else { + backgroundColor + } + } + + func backgroundColor(_ backgroundColor: Color = .backgroundSecondary) -> Page { + var control = self + control.backgroundColor = backgroundColor + return control + } + + func backgroundSecondary() -> Page { + var control = self + control.backgroundColor = .backgroundSecondary + return control + } + + func backgroundLinerGradient(_ gradient: LinearGradient) -> Page { + var control = self + control.backgroundLinerGradient = gradient + return control + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView, TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, LeadingBar == EmptyView, TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + trailingBar = nil + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, LeadingBar == EmptyView, TopToolbar == EmptyView, TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + trailingBar = nil + topToolbar = nil + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView, TopToolbar == EmptyView, TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + topToolbar = nil + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, TopToolbar == EmptyView, TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + topToolbar = nil + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TopToolbar == EmptyView, TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + topToolbar = nil + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, LeadingBar == EmptyView, TopToolbar == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + trailingBar = nil + topToolbar = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, TopToolbar == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + topToolbar = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView, TopToolbar == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + topToolbar = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TopToolbar == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + topToolbar = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TitleLabel == EmptyView, Header == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + titleLabel = nil + header = nil + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView, TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, LeadingBar == EmptyView, TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + trailingBar = nil + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, LeadingBar == EmptyView, TopToolbar == EmptyView, TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + trailingBar = nil + topToolbar = nil + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView, TopToolbar == EmptyView, TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + topToolbar = nil + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, TopToolbar == EmptyView, TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + topToolbar = nil + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TopToolbar == EmptyView, TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + topToolbar = nil + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, LeadingBar == EmptyView, TopToolbar == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + trailingBar = nil + topToolbar = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView, TopToolbar == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + topToolbar = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView, TopToolbar == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + topToolbar = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TrailingBar == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + trailingBar = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where LeadingBar == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + leadingBar = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TopToolbar == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + topToolbar = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +@available(iOS 16.0, *) +public extension Page where TitleLabel == EmptyView { + init(_ title: String? = nil, + headerHeight: CGFloat = 0, + headerMinHeight: CGFloat? = nil, + onScroll: ScrollAction? = nil, + @ViewBuilder content: () -> Content, + @ViewBuilder header: () -> Header) + { + self.title = title + self.onScroll = onScroll + custumHeaderHeight = headerHeight + self.headerMinHeight = headerMinHeight + self.content = content() + titleLabel = nil + self.header = header() + // smallHeader = nil + _searchQuery = .constant(.init()) + _displaySearchBar = .constant(false) + } +} + +struct SeartchTextFieldStyle: TextFieldStyle { + func _body(configuration: TextField) -> some View { + HStack(spacing: .xxSmall) { + Image.Base.search + .renderingMode(.template) + .onSurfaceDisabledForegroundColor() + + configuration + } + .onSurfaceHighEmphasisForegroundColor() + .callout(.semibold) + .padding(.horizontal, 12) + .padding(.vertical, .xSmall) + .background( + RoundedRectangle( + cornerRadius: .medium, + style: .continuous + ) + .fill(Color.onSurfaceHighEmphasis.opacity(0.07)) + ) + .submitLabel(.search) + } +} + +struct SeartchTextFieldButtonStyle: ButtonStyle { + let height: CGFloat + + init(height: CGFloat = 45) { + self.height = height + } + + func makeBody(configuration: Self.Configuration) -> some View { + let opacity = ((height - 20) * 4) * 0.01 + + HStack(spacing: .xxSmall) { + Image.Base.search + .renderingMode(.template) + .foregroundColor(Color.onSurfaceDisabled.opacity(height > 20 ? opacity : 0)) + + configuration.label + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(Color.onSurfaceDisabled.opacity(height > 20 ? opacity : 0)) + } + + .callout(.semibold) + .padding(.horizontal, 12) + .frame(height: height) + .background( + RoundedRectangle( + cornerRadius: .medium, + style: .continuous + ) + .fill(Color.onSurfaceHighEmphasis.opacity(0.07)) + ) + .submitLabel(.search) + } +} + +private extension View { + @ViewBuilder + func prefersNavigationBarHidden() -> some View { + #if os(watchOS) + self + #else + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, *) { + self.toolbarBackground(.hidden) + } else { + self + } + #endif + } +} diff --git a/Sources/OversizeUI/Controls/PriceField/PriceField.swift b/Sources/OversizeUI/Controls/PriceField/PriceField.swift index dbf1f66..3d1a48b 100644 --- a/Sources/OversizeUI/Controls/PriceField/PriceField.swift +++ b/Sources/OversizeUI/Controls/PriceField/PriceField.swift @@ -5,33 +5,116 @@ import Foundation import SwiftUI +#if canImport(UIKit) +import UIKit +#endif @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) public struct PriceField: View { + private var formatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.usesGroupingSeparator = true + formatter.groupingSeparator = " " + formatter.decimalSeparator = "." + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + return formatter + } + + private var strValue2: String { value.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted).joined() } // value.filter { !$0.isWhitespace } } + private var doubleValue2: Double { .init((Double(strValue2) ?? 0) / 100.0) ?? 0 } + + private var strValue3: String { value.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted).joined() } // value.filter { !$0.isWhitespace } } + private var doubleValue3: Double { .init((Double(strValue3) ?? 0) * 100) ?? 0 } + + private var strValue: String { value.filter { !$0.isWhitespace } } + private var doubleValue: Double { .init(strValue) ?? 0 } + private var formattedValue: String { + let value = doubleValue + + return formatter.string(for: value) ?? "" + } + + private var formattedValue2: String { + let value = doubleValue2 + + return formatter.string(for: value) ?? "" + } + @Binding private var amount: Decimal private let currency: Locale.Currency + @State private var value: String = "" + @State private var active: Bool = false + + @State private var zapMode: Bool = false + public init(amount: Binding, currency: Locale.Currency) { _amount = amount self.currency = currency + + let allLocalCurrencies = Locale.availableIdentifiers.compactMap { Locale(identifier: $0) } + dispalySymbol = allLocalCurrencies.first { $0.currency == currency }?.currencySymbol ?? currency.identifier } public var body: some View { - #if os(iOS) - TextField( - "0", - value: $amount, - format: .currency(code: currency.identifier) - ) - .keyboardType(.decimalPad) - .textFieldStyle(.default) - #else - TextField( - "0", - value: $amount, - format: .currency(code: currency.identifier) - ) - .textFieldStyle(.default) - #endif + ZStack { + TextField("", text: $value, onEditingChanged: { + active = $0 + + if !$0 { + validate() + } + }) + .textFieldStyle(.default) + #if os(iOS) + .keyboardType(.decimalPad) + #endif + + HStack(spacing: 8) { + Text(value) + .lineLimit(1) + .headline(.medium) + .opacity(0) + .padding(.leading, .xSmall) + + Text(dispalySymbol) + + .onChange(of: currency) { _ in + validate() + } + + Spacer() + } + } + .onAppear(perform: validate) + .onChange(of: value) { format(text: $0) } } + + private func validate() { + value = formattedValue + } + + func format(text: String) { + if !zapMode, text.last == "." || text.last == "," { + let stringValue: String = text.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted).joined() + let doubleValue: Double = .init((Double(stringValue) ?? 0) * 100.0) ?? 0 + value = formatter.string(for: doubleValue) ?? "" + value.append(",") + zapMode = true + } else if zapMode { + let stringValue: String = text.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted).joined() + let doubleValue: Double = .init((Double(stringValue) ?? 0) / 100.0) ?? 0 + value = formatter.string(for: doubleValue) ?? "" + } else if text == "0,0" || text == "0.0" { + value = "0" + zapMode = false + } else { + let doubleValue: Double = .init((text.filter { !$0.isWhitespace })) ?? 0 + value = formatter.string(for: doubleValue) ?? "" + } + } + + let dispalySymbol: String } diff --git a/Sources/OversizeUI/Controls/Row/Row.swift b/Sources/OversizeUI/Controls/Row/Row.swift index 4771f1d..0526e0d 100644 --- a/Sources/OversizeUI/Controls/Row/Row.swift +++ b/Sources/OversizeUI/Controls/Row/Row.swift @@ -39,6 +39,8 @@ public struct Row: View where LeadingLabel: View, T private var сlearButtonStyle: RowClearIconStyle = .default private var сlearAction: (() -> Void)? + + private var leadingSpace: Space = .small private var isShowSubtitle: Bool { (subtitle?.isEmpty) != nil @@ -102,7 +104,8 @@ public struct Row: View where LeadingLabel: View, T .scaledToFill() .frame(width: leadingSize?.width, height: leadingSize?.height) .cornerRadius(leadingRadius ?? 0) - .padding(.trailing, .small) + .padding(.trailing, leadingSpace) + if textAlignment == .trailing || textAlignment == .center { Spacer() @@ -214,6 +217,12 @@ public extension Row { control.сlearAction = action return control } + + func leadingContentMargin(_ margin: Space = .small) -> Row { + var control = self + control.leadingSpace = margin + return control + } @available(*, deprecated, message: "Use leading: {} and tralling: {}") func rowLeading(_ leading: RowLeadingType?) -> Row { diff --git a/Sources/OversizeUI/Controls/Row/RowButtonStyle.swift b/Sources/OversizeUI/Controls/Row/RowButtonStyle.swift index df22a44..6a5033a 100644 --- a/Sources/OversizeUI/Controls/Row/RowButtonStyle.swift +++ b/Sources/OversizeUI/Controls/Row/RowButtonStyle.swift @@ -9,7 +9,7 @@ public struct RowActionButtonStyle: ButtonStyle { public init() {} public func makeBody(configuration: Self.Configuration) -> some View { configuration.label - .background(configuration.isPressed ? Color.surfaceSecondary : Color.clear) + .background(configuration.isPressed ? Color.surfaceSecondary.opacity(0.7) : Color.clear) .contentShape(Rectangle()) } } diff --git a/Sources/OversizeUI/Controls/ScrollView/ScrollViewWithOffset.swift b/Sources/OversizeUI/Controls/ScrollView/ScrollViewWithOffset.swift new file mode 100644 index 0000000..4c87040 --- /dev/null +++ b/Sources/OversizeUI/Controls/ScrollView/ScrollViewWithOffset.swift @@ -0,0 +1,80 @@ +// +// Copyright © 2021 Alexander Romanov +// ScrollViewOffset.swift, created on 06.04.2021 +// + +import SwiftUI + +public struct ScrollViewWithOffsetTracking: View { + public init( + _ axes: Axis.Set = .vertical, + showsIndicators: Bool = true, + cordinateSpaceName: String = "ScrollView", + onScroll: ScrollAction? = nil, + @ViewBuilder content: @escaping () -> Content + ) { + self.axes = axes + self.showsIndicators = showsIndicators + self.cordinateSpaceName = cordinateSpaceName + self.onScroll = onScroll ?? { _ in } + self.content = content + } + + public typealias ScrollAction = (_ offset: CGPoint) -> Void + + private let axes: Axis.Set + private let showsIndicators: Bool + private let cordinateSpaceName: String + private let onScroll: ScrollAction + private let content: () -> Content + + public var body: some View { + ScrollView(axes, showsIndicators: showsIndicators) { + ScrollViewOffsetTracker { + content() + } + }.withOffsetTracking( + coordinateSpaceName: cordinateSpaceName, + action: onScroll + ) + } +} + +struct ScrollViewOffsetTracker: View { + private let cordinateSpaceName: String + + init( + cordinateSpaceName: String = "ScrollView", + @ViewBuilder content: @escaping () -> Content + ) { + self.content = content + self.cordinateSpaceName = cordinateSpaceName + } + + private var content: () -> Content + + var body: some View { + ZStack(alignment: .top) { + GeometryReader { geo in + Color.clear + .preference( + key: ScrollOffsetPreferenceKey.self, + value: geo.frame(in: .named(cordinateSpaceName)).origin + ) + } + .frame(height: 0) + + content() + } + } +} + +private extension ScrollView { + func withOffsetTracking( + coordinateSpaceName: String, + action: @escaping (_ offset: CGPoint) -> Void + ) -> some View { + coordinateSpace(name: coordinateSpaceName) + .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: action) + } +} diff --git a/Sources/OversizeUI/Controls/ScrollView/ScrollViewWithStickyHeader.swift b/Sources/OversizeUI/Controls/ScrollView/ScrollViewWithStickyHeader.swift new file mode 100644 index 0000000..d66930a --- /dev/null +++ b/Sources/OversizeUI/Controls/ScrollView/ScrollViewWithStickyHeader.swift @@ -0,0 +1,36 @@ +//// +//// Copyright © 2023 Alexander Romanov +//// File.swift, created on 26.11.2023 +//// +// + +import SwiftUI + +public struct ScrollViewHeader: View { + public init( + @ViewBuilder content: @escaping () -> Content + ) { + self.content = content + } + + private let content: () -> Content + + public var body: some View { + GeometryReader { geo in + content() + .stretchable(in: geo) + } + } +} + +private extension View { + @ViewBuilder + func stretchable(in geo: GeometryProxy) -> some View { + let width = geo.size.width + let height = geo.size.height + let minY = geo.frame(in: .global).minY + let useStandard = minY <= 0 + frame(width: width, height: height + (useStandard ? 0 : minY)) + .offset(y: useStandard ? 0 : -minY) + } +} diff --git a/Sources/OversizeUI/Controls/SegmentedControl/SegmentedControl.swift b/Sources/OversizeUI/Controls/SegmentedControl/SegmentedControl.swift index ad28024..2d0e059 100644 --- a/Sources/OversizeUI/Controls/SegmentedControl/SegmentedControl.swift +++ b/Sources/OversizeUI/Controls/SegmentedControl/SegmentedControl.swift @@ -10,18 +10,18 @@ public struct SegmentedPickerSelector: V @Environment(\.segmentedControlStyle) private var style @Environment(\.controlRadius) var controlRadius: Radius @Environment(\.segmentedPickerMargins) var controlPadding: EdgeSpaceInsets - + public typealias Data = [Element] - + @State private var frames: [CGRect] @State private var selectedIndex: Data.Index? = 0 @Binding private var selection: Data.Element - + private let data: Data private let selectionView: () -> Selection? private let content: (Data.Element, Bool) -> Content private let action: (() -> Void)? - + public init(_ data: Data, selection: Binding, @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, @@ -36,7 +36,7 @@ public struct SegmentedPickerSelector: V _frames = State(wrappedValue: Array(repeating: .zero, count: data.count)) } - + public var body: some View { style .makeBody( @@ -56,7 +56,7 @@ public struct SegmentedPickerSelector: V } } } - + @ViewBuilder private func getSegmentedControl() -> some View { switch style.isEquallySpacing { @@ -66,74 +66,88 @@ public struct SegmentedPickerSelector: V leadingSegmentedControl } } - + private var equallSegmentedControl: some View { - ZStack(alignment: Alignment(horizontal: .horizontalCenterAlignment, - vertical: .center)) - { + ZStack(alignment: Alignment( + horizontal: .horizontalCenterAlignment, + vertical: .center + )){ if let selectedIndex { HStack(spacing: 0) { - Spacer() selectionView() - Spacer() + .contentShape(Rectangle()) + .frame( + maxWidth: .infinity, + alignment: .center + ) + } - .frame(width: frames[selectedIndex].width, - height: frames[selectedIndex].height) + .frame( + width: frames[selectedIndex].width, + height: frames[selectedIndex].height, + alignment: .center + ) .alignmentGuide(.horizontalCenterAlignment) { dimensions in dimensions[HorizontalAlignment.center] } .background(getSelection(selectionStyle: style.seletionStyle)) } - + HStack(spacing: 0) { ForEach(data.indices, id: \.self) { index in - Button(action: { - selectedIndex = index - selection = data[index] - action?() - }, - label: { - HStack(spacing: 0) { - Spacer() - - content(data[index], - selectedIndex == index) - .body(.semibold) - .foregroundColor(style.seletionStyle == .accentSurface && selectedIndex == index ? Color.onPrimaryHighEmphasis : Color.onSurfaceHighEmphasis) - .multilineTextAlignment(.center) - Spacer() - } - .padding(.leading, controlPadding.leading) - .padding(.trailing, controlPadding.trailing) - .padding(.top, - controlPadding.top != Space.zero || controlPadding.top != Space.xxSmall - ? controlPadding.top.rawValue - Space.xxSmall.rawValue - : Space.zero.rawValue) - .padding(.bottom, - controlPadding.bottom != Space.zero || controlPadding.bottom != Space.xxSmall - ? controlPadding.bottom.rawValue - Space.xxSmall.rawValue - : Space.zero.rawValue) - .background(selectedIndex != index - ? getUnselection(unselectionStyle: style.unseletionStyle) - : nil) - }) - - .buttonStyle(PlainButtonStyle()) - .background(GeometryReader { proxy in - Color.clear.onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { frames[index] = proxy.frame(in: .global) } } - }) - .alignmentGuide(.horizontalCenterAlignment, - isActive: selectedIndex == index) - { dimensions in + Button( + action: { + selectedIndex = index + selection = data[index] + action?() + }, + label: { + HStack(spacing: 0) { + content(data[index], + selectedIndex == index) + .body(.semibold) + .foregroundColor(style.seletionStyle == .accentSurface && selectedIndex == index ? Color.onPrimaryHighEmphasis : Color.onSurfaceHighEmphasis) + .multilineTextAlignment(.center) + .contentShape(Rectangle()) + .frame( + maxWidth: .infinity, + alignment: .center + ) + + } + .padding(.leading, controlPadding.leading) + .padding(.trailing, controlPadding.trailing) + .padding(.top, + controlPadding.top != Space.zero || controlPadding.top != Space.xxSmall + ? controlPadding.top.rawValue - Space.xxSmall.rawValue + : Space.zero.rawValue) + .padding(.bottom, + controlPadding.bottom != Space.zero || controlPadding.bottom != Space.xxSmall + ? controlPadding.bottom.rawValue - Space.xxSmall.rawValue + : Space.zero.rawValue) + .background(selectedIndex != index + ? getUnselection(unselectionStyle: style.unseletionStyle) + : nil) + }) + + .buttonStyle(PlainButtonStyle()) + .background(GeometryReader { proxy in + Color.clear.onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { frames[index] = proxy.frame(in: .global) } } + }) + .alignmentGuide( + .horizontalCenterAlignment, + isActive: selectedIndex == index + ) { dimensions in dimensions[HorizontalAlignment.center] } .padding(.trailing, style.unseletionStyle == .surface ? Space.xSmall.rawValue : 0) + } } } .animation(.easeInOut(duration: 0.3), value: selection) } - + private var leadingSegmentedControl: some View { ZStack(alignment: Alignment(horizontal: .horizontalCenterAlignment, vertical: .center)) @@ -141,6 +155,7 @@ public struct SegmentedPickerSelector: V if let selectedIndex { HStack { selectionView() + .contentShape(Rectangle()) } .frame(width: frames[selectedIndex].width, height: frames[selectedIndex].height) @@ -149,39 +164,41 @@ public struct SegmentedPickerSelector: V } .background(getSelection(selectionStyle: style.seletionStyle)) } - + HStack(spacing: 0) { ForEach(data.indices, id: \.self) { index in - Button(action: { - selectedIndex = index - selection = data[index] - action?() - }, - label: { content(data[index], selectedIndex == index) - .body(.semibold) - .foregroundColor(style.seletionStyle == .accentSurface && selectedIndex == index ? Color.onPrimaryHighEmphasis : Color.onSurfaceHighEmphasis) - .multilineTextAlignment(.center) - .padding(.leading, controlPadding.leading) - .padding(.trailing, controlPadding.trailing) - .padding(.top, - controlPadding.top != Space.zero || controlPadding.top != Space.xxSmall - ? controlPadding.top.rawValue - Space.xxSmall.rawValue - : Space.zero.rawValue) - .padding(.bottom, - controlPadding.bottom != Space.zero || controlPadding.bottom != Space.xxSmall - ? controlPadding.bottom.rawValue - Space.xxSmall.rawValue - : Space.zero.rawValue) - .background(selectedIndex != index - ? getUnselection(unselectionStyle: style.unseletionStyle) - : nil) - }) - .buttonStyle(PlainButtonStyle()) - .background(GeometryReader { proxy in - Color.clear.onAppear { frames[index] = proxy.frame(in: .global) } - }) - .alignmentGuide(.horizontalCenterAlignment, - isActive: selectedIndex == index) - { dimensions in + Button( + action: { + selectedIndex = index + selection = data[index] + action?() + }, + label: { content(data[index], selectedIndex == index) + .body(.semibold) + .foregroundColor(style.seletionStyle == .accentSurface && selectedIndex == index ? Color.onPrimaryHighEmphasis : Color.onSurfaceHighEmphasis) + .multilineTextAlignment(.center) + .padding(.leading, controlPadding.leading) + .padding(.trailing, controlPadding.trailing) + .padding(.top, + controlPadding.top != Space.zero || controlPadding.top != Space.xxSmall + ? controlPadding.top.rawValue - Space.xxSmall.rawValue + : Space.zero.rawValue) + .padding(.bottom, + controlPadding.bottom != Space.zero || controlPadding.bottom != Space.xxSmall + ? controlPadding.bottom.rawValue - Space.xxSmall.rawValue + : Space.zero.rawValue) + .background(selectedIndex != index + ? getUnselection(unselectionStyle: style.unseletionStyle) + : nil) + }) + .buttonStyle(PlainButtonStyle()) + .background(GeometryReader { proxy in + Color.clear.onAppear { frames[index] = proxy.frame(in: .global) } + }) + .alignmentGuide( + .horizontalCenterAlignment, + isActive: selectedIndex == index + ) { dimensions in dimensions[HorizontalAlignment.center] } .padding(.trailing, style.unseletionStyle == .surface ? Space.xSmall.rawValue : 0) @@ -190,45 +207,45 @@ public struct SegmentedPickerSelector: V } .animation(.easeInOut(duration: 0.3), value: selection) } - + @ViewBuilder private func getSelection(selectionStyle: SegmentedControlSeletionStyle) -> some View { switch selectionStyle { case .shadowSurface: - + RoundedRectangle(cornerRadius: style.isShowBackground ? controlRadius.rawValue - 4 : controlRadius.rawValue, style: .continuous) - .fill(Color.surfacePrimary) - .overlay( - RoundedRectangle(cornerRadius: style.isShowBackground - ? controlRadius.rawValue - 4 - : controlRadius.rawValue, - style: .continuous) - .stroke(theme.borderControls - ? Color.border - : Color.surfaceSecondary, lineWidth: CGFloat(theme.borderSize)) - ) - .shadowElevaton(.z2) + .fill(Color.surfacePrimary) + .overlay( + RoundedRectangle(cornerRadius: style.isShowBackground + ? controlRadius.rawValue - 4 + : controlRadius.rawValue, + style: .continuous) + .stroke(theme.borderControls + ? Color.border + : Color.surfaceSecondary, lineWidth: CGFloat(theme.borderSize)) + ) + .shadowElevaton(.z2) case .graySurface: - + if style.unseletionStyle == .clean { RoundedRectangle(cornerRadius: controlRadius, style: .continuous) - .fill(Color.surfaceSecondary) - .overlay( - RoundedRectangle(cornerRadius: controlRadius, - style: .continuous) - .stroke(theme.borderControls - ? Color.border - : Color.surfaceSecondary, lineWidth: CGFloat(theme.borderSize)) - ) - + .fill(Color.surfaceSecondary) + .overlay( + RoundedRectangle(cornerRadius: controlRadius, + style: .continuous) + .stroke(theme.borderControls + ? Color.border + : Color.surfaceSecondary, lineWidth: CGFloat(theme.borderSize)) + ) + } else { RoundedRectangle(cornerRadius: style.isShowBackground - ? controlRadius.rawValue - 4 - : controlRadius.rawValue, - style: .continuous) - .strokeBorder(Color.onSurfaceMediumEmphasis, lineWidth: 2) + ? controlRadius.rawValue - 4 + : controlRadius.rawValue, + style: .continuous) + .strokeBorder(Color.onSurfaceMediumEmphasis, lineWidth: 2) } case .accentSurface: RoundedRectangle( @@ -238,24 +255,24 @@ public struct SegmentedPickerSelector: V .fill(Color.accent) } } - + @ViewBuilder private func getUnselection(unselectionStyle: SegmentedControlUnseletionStyle) -> some View { switch unselectionStyle { case .clean: EmptyView() case .surface: - + RoundedRectangle(cornerRadius: controlRadius, style: .continuous) - .fill(Color.surfaceSecondary) - .overlay( - RoundedRectangle(cornerRadius: controlRadius, - style: .continuous) - .stroke(theme.borderControls - ? Color.border - : Color.surfaceSecondary, lineWidth: CGFloat(theme.borderSize)) - ) + .fill(Color.surfaceSecondary) + .overlay( + RoundedRectangle(cornerRadius: controlRadius, + style: .continuous) + .stroke(theme.borderControls + ? Color.border + : Color.surfaceSecondary, lineWidth: CGFloat(theme.borderSize)) + ) } } } diff --git a/Sources/OversizeUI/Controls/Select/MultiSelectPicker.swift b/Sources/OversizeUI/Controls/Select/MultiSelectPicker.swift new file mode 100644 index 0000000..576b3b5 --- /dev/null +++ b/Sources/OversizeUI/Controls/Select/MultiSelectPicker.swift @@ -0,0 +1,172 @@ +// +// Copyright © 2024 Alexander Romanov +// MultiSelectPicker.swift, created on 03.03.2024 +// + +import SwiftUI + +// swiftlint:disable all +@available(iOS 16.0, *) +public struct MultiSelectPicker: View + where + Content: View, + Actions: View, + ContentUnavailable: View +{ + @Environment(\.dismiss) private var dismiss + @Environment(\.multiSelectStyle) private var multiSelectStyle + @Environment(\.theme) private var theme: ThemeSettings + public typealias Data = [Element] + + @Binding private var selection: Data + private let data: Data + private let title: String + private let content: (Data.Element, Bool) -> Content + private let contentUnavailable: ContentUnavailable? + @State var selectedIndexes: [Int] = [] + let actions: Group? + + public init( + _ title: String, + _ data: Data, + selection: Binding, + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder actions: @escaping () -> Actions, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { + self.title = title + self.data = data + self.content = content + self.actions = Group { actions() } + self.contentUnavailable = contentUnavailable() + _selection = selection + } + + public var body: some View { + Page(title) { + if data.isEmpty, let contentUnavailable { + contentUnavailable + } else { + pageContent(data, selectStyle: multiSelectStyle) + } + + } + .backgroundSecondary() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + dismiss() + } label: { + Image.Base.close.icon() + } + } + + ToolbarItem(placement: .confirmationAction) { + actions + } + } + .onAppear { + defaultSelect() + } + } + + private func defaultSelect() { + if !selection.isEmpty { + for dataIndex in 0 ..< data.count { + let dataItem = data[dataIndex] + for selectIndex in 0 ..< selection.count where dataItem == selection[selectIndex] { + selectedIndexes.append(dataIndex) + } + } + } + } + + @ViewBuilder + private func pageContent(_ data: Data, selectStyle: MultiSelectStyle) -> some View { + switch selectStyle { + case .default: + rowsList(data) + case .section: + SectionView { + rowsList(data) + } + .sectionContentCompactRowMargins() + } + } + + private func rowsList(_ data: Data) -> some View { + 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) + } + let selectionItems = selectedIndexes.compactMap { data[$0] } + selection = selectionItems + } + ), label: { + content(data[index], isSelected) + }) + } + } + } +} + +@available(iOS 16.0, *) +public extension MultiSelectPicker where ContentUnavailable == Never { + init( + _ title: String, + _ data: Data, + selection: Binding, + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder actions: @escaping () -> Actions + ) { + self.title = title + self.data = data + self.content = content + self.actions = Group { actions() } + contentUnavailable = nil + _selection = selection + } +} + +@available(iOS 16.0, *) +public extension MultiSelectPicker where Actions == Never { + init( + _ title: String, + _ data: Data, + selection: Binding, + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { + self.title = title + self.data = data + self.content = content + actions = nil + self.contentUnavailable = contentUnavailable() + _selection = selection + } +} + +@available(iOS 16.0, *) +public extension MultiSelectPicker where ContentUnavailable == Never, Actions == Never { + init( + _ title: String, + _ data: Data, + selection: Binding, + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content + ) { + self.title = title + self.data = data + self.content = content + actions = nil + contentUnavailable = nil + _selection = selection + } +} diff --git a/Sources/OversizeUI/Controls/Select/SelectPicker.swift b/Sources/OversizeUI/Controls/Select/SelectPicker.swift new file mode 100644 index 0000000..c82e1d9 --- /dev/null +++ b/Sources/OversizeUI/Controls/Select/SelectPicker.swift @@ -0,0 +1,186 @@ +// +// Copyright © 2024 Alexander Romanov +// SelectPicker.swift, created on 29.02.2024 +// + +import SwiftUI + +// swiftlint:disable all +@available(iOS 16.0, *) +public struct SelectPicker: View + where + Content: View, + Actions: View +{ + @Environment(\.dismiss) private var dismiss + @Environment(\.theme) private var theme: ThemeSettings + @Environment(\.selectStyle) private var selectStyle + public typealias Data = [Element] + + @Binding private var selection: Data.Element + private let data: Data + private let title: String? + private let content: (Data.Element, Bool) -> Content + private let contentUnavailable: ContentUnavailable? + @State private var selectedIndex: Data.Index? = nil + let actions: Group? + + public init( + _ title: String? = nil, + _ data: Data, + selection: Binding, + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder actions: @escaping () -> Actions, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { + self.title = title + self.data = data + self.content = content + self.contentUnavailable = contentUnavailable() + self.actions = Group { actions() } + _selection = selection + } + + public var body: some View { + Page(title) { + if data.isEmpty, let contentUnavailable { + contentUnavailable + } else { + pageContent(data, selectStyle: selectStyle) + } + + } + .backgroundSecondary() + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button { + dismiss() + } label: { + Image.Base.close.icon() + } + } + + ToolbarItem(placement: .confirmationAction) { + actions + } + } + .onAppear { + defaultSelect() + } + } + + @ViewBuilder + private func pageContent(_ data: Data, selectStyle: SelectStyle) -> some View { + switch selectStyle { + case .default: + rowsList(data) + case .section: + SectionView { + rowsList(data) + } + .sectionContentCompactRowMargins() + case .wheel: + #if os(iOS) + SectionView { + wheelList(data) + } + #else + EmptyView() + #endif + } + } + + #if os(iOS) + private func wheelList(_ data: Data) -> some View { + Picker("", selection: $selection) { + ForEach(data, id: \.self) { item in + content(item, selection == item) + } + } + .labelsHidden() + .pickerStyle(.wheel) + } + #endif + + private func rowsList(_ data: Data) -> some View { + LazyVStack(alignment: .leading, spacing: .zero) { + ForEach(data.indices, id: \.self) { index in + Radio(isOn: index == selectedIndex) { + selectedIndex = index + selection = data[index] + dismiss() + } label: { + content(data[index], + selectedIndex == index) + .headline() + .onSurfaceHighEmphasisForegroundColor() + } + } + } + } + + private func defaultSelect() { + var index = 0 + for dataValue in data { + if selection == dataValue { + selectedIndex = index + } + index += 1 + } + } +} + +@available(iOS 16.0, *) +public extension SelectPicker where ContentUnavailable == Never { + init( + _ title: String? = nil, + _ data: Data, + selection: Binding, + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder actions: @escaping () -> Actions + ) { + self.title = title + self.data = data + self.content = content + self.actions = Group { actions() } + contentUnavailable = nil + _selection = selection + } +} + +@available(iOS 16.0, *) +public extension SelectPicker where Actions == Never { + init( + _ title: String? = nil, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content, + @ViewBuilder contentUnavailable: () -> ContentUnavailable + ) { + self.title = title + self.data = data + self.content = content + actions = nil + self.contentUnavailable = contentUnavailable() + _selection = selection + } +} + +@available(iOS 16.0, *) +public extension SelectPicker where ContentUnavailable == Never, Actions == Never { + init( + _ title: String? = nil, + _ data: Data, + selection: Binding, + activeModal: Binding = .constant(nil), + @ViewBuilder content: @escaping (Data.Element, Bool) -> Content + ) { + self.title = title + self.data = data + self.content = content + actions = nil + contentUnavailable = nil + _selection = selection + } +} diff --git a/Sources/OversizeUI/Controls/Surface/MaterialSurface.swift b/Sources/OversizeUI/Controls/Surface/MaterialSurface.swift index 0da7080..2ace2af 100644 --- a/Sources/OversizeUI/Controls/Surface/MaterialSurface.swift +++ b/Sources/OversizeUI/Controls/Surface/MaterialSurface.swift @@ -5,6 +5,7 @@ import SwiftUI +#if os(iOS) @available(macOS, unavailable) @available(watchOS, unavailable) @available(tvOS, unavailable) @@ -78,3 +79,4 @@ public struct MaterialSurface: View { return control } } +#endif diff --git a/Sources/OversizeUI/Controls/Surface/Surface.swift b/Sources/OversizeUI/Controls/Surface/Surface.swift index ce5fbdf..a0f1291 100644 --- a/Sources/OversizeUI/Controls/Surface/Surface.swift +++ b/Sources/OversizeUI/Controls/Surface/Surface.swift @@ -14,13 +14,11 @@ public enum SurfaceStyle { // swiftlint:disable opening_brace public struct Surface: View { - @Environment(\.elevation) private var elevation: Elevation @Environment(\.theme) private var theme: ThemeSettings + @Environment(\.isAccent) private var isAccent: Bool @Environment(\.surfaceRadius) var surfaceRadius: Radius @Environment(\.surfaceContentMargins) var contentInsets: EdgeSpaceInsets - @Environment(\.isAccent) private var isAccent: Bool - - let forceContentInsets: EdgeSpaceInsets? + @Environment(\.surfaceElevation) private var elevation: Elevation private enum Constants { /// Colors @@ -35,10 +33,13 @@ public struct Surface: View { private var backgroundColor: Color? private var border: Color? private var borderWidth: CGFloat? + private let forceContentInsets: EdgeSpaceInsets? + private var isSurfaceClipped: Bool = false - public init(action: (() -> Void)? = nil, - @ViewBuilder label: () -> Label) - { + public init( + action: (() -> Void)? = nil, + @ViewBuilder label: () -> Label + ) { self.label = label() self.action = action forceContentInsets = nil @@ -76,9 +77,11 @@ public struct Surface: View { ) .shadowElevaton(elevation) ) - .clipShape( - RoundedRectangle(cornerRadius: surfaceRadius, style: .continuous) - ) + .if(isSurfaceClipped) { view in + view.clipShape( + RoundedRectangle(cornerRadius: surfaceRadius, style: .continuous) + ) + } } private var strokeBorderColor: Color { @@ -143,6 +146,12 @@ public struct Surface: View { control.backgroundColor = color return control } + + public func surfaceClip(_ surfaceClipped: Bool = true) -> Surface { + var control = self + control.isSurfaceClipped = surfaceClipped + return control + } } public struct SurfaceButtonStyle: ButtonStyle { @@ -197,7 +206,7 @@ public extension View { } } -public extension Surface where Label == VStack, Row)>> { + public extension Surface where Label == VStack, Row)>> { init(action: (() -> Void)? = nil, @ViewBuilder label: () -> Label) { @@ -205,9 +214,9 @@ public extension Surface where Label == VStack, self.action = action forceContentInsets = .init(horizontal: .zero, vertical: .small) } -} + } -public extension Surface where Label == Row { + public extension Surface where Label == Row { init(action: (() -> Void)? = nil, @ViewBuilder label: () -> Label) { @@ -215,9 +224,9 @@ public extension Surface where Label == Row { self.action = action forceContentInsets = .init(horizontal: .zero, vertical: .small) } -} + } -public extension Surface where Label == Row { + public extension Surface where Label == Row { init(action: (() -> Void)? = nil, @ViewBuilder label: () -> Label) { @@ -225,7 +234,7 @@ public extension Surface where Label == Row { self.action = action forceContentInsets = .init(horizontal: .zero, vertical: .small) } -} + } struct Surface_Previews: PreviewProvider { static var previews: some View { diff --git a/Sources/OversizeUI/Controls/Toast/Snackbar.swift b/Sources/OversizeUI/Controls/Toast/Snackbar.swift index b7ed239..e3a6c46 100644 --- a/Sources/OversizeUI/Controls/Toast/Snackbar.swift +++ b/Sources/OversizeUI/Controls/Toast/Snackbar.swift @@ -48,7 +48,9 @@ public struct Snackbar: View where Label: View, Actions: View { HStack(spacing: .xxSmall) { actions .buttonStyle(.quaternary) + #if !os(tvOS) .controlSize(.mini) + #endif .accent() } } diff --git a/Sources/OversizeUI/Controls/URLField/URLField.swift b/Sources/OversizeUI/Controls/URLField/URLField.swift index 3e3473c..1bc6c93 100644 --- a/Sources/OversizeUI/Controls/URLField/URLField.swift +++ b/Sources/OversizeUI/Controls/URLField/URLField.swift @@ -23,20 +23,29 @@ public struct URLField: View { } public var body: some View { - if #available(iOS 16.0, *) { - TextField(title, value: $url, format: .url) - .keyboardType(.URL) - .textContentType(.URL) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - } else { - TextField(title, text: $urlString) - .keyboardType(.URL) - .textContentType(.URL) - .textInputAutocapitalization(.never) - .autocorrectionDisabled() - .fieldHelper(.constant("Invalid URL"), style: $textFieldHelper) - } + TextField(title, text: $urlString, onEditingChanged: { state in + if state { + textFieldHelper = .none + } else if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { + textFieldHelper = .none + self.url = url + } else { + textFieldHelper = .errorText + urlString = "" + } + }, onCommit: { + if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) { + textFieldHelper = .none + self.url = url + } else { + textFieldHelper = .errorText + } + }) + .keyboardType(.URL) + .textContentType(.URL) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .fieldHelper(.constant("Invalid URL"), style: $textFieldHelper) } } #endif diff --git a/Sources/OversizeUI/Core/EnvironmentKeys/MultiSelectStyleEnvironment.swift b/Sources/OversizeUI/Core/EnvironmentKeys/MultiSelectStyleEnvironment.swift new file mode 100644 index 0000000..0ba24a0 --- /dev/null +++ b/Sources/OversizeUI/Core/EnvironmentKeys/MultiSelectStyleEnvironment.swift @@ -0,0 +1,26 @@ +// +// Copyright © 2024 Alexander Romanov +// MultiSelectStyleEnvironment.swift, created on 03.03.2024 +// + +import SwiftUI + +public enum MultiSelectStyle { + case `default`, section +} +private struct MultiSelectStyleKey: EnvironmentKey { + public static var defaultValue: MultiSelectStyle = .default +} + +public extension EnvironmentValues { + var multiSelectStyle: MultiSelectStyle { + get { self[MultiSelectStyleKey.self] } + set { self[MultiSelectStyleKey.self] = newValue } + } +} + +public extension View { + func multiSelectStyle(_ multiSelectStyle: MultiSelectStyle) -> some View { + environment(\.multiSelectStyle, multiSelectStyle) + } +} diff --git a/Sources/OversizeUI/Core/EnvironmentKeys/SelectPickerStyleEnvironment.swift b/Sources/OversizeUI/Core/EnvironmentKeys/SelectPickerStyleEnvironment.swift new file mode 100644 index 0000000..7ea08e5 --- /dev/null +++ b/Sources/OversizeUI/Core/EnvironmentKeys/SelectPickerStyleEnvironment.swift @@ -0,0 +1,29 @@ +// +// Copyright © 2024 Alexander Romanov +// SelectStyleEnvironment.swift, created on 01.03.2024 +// + +import SwiftUI + +public enum SelectStyle { + case `default` + case section + @available(macOS, unavailable) + case wheel +} +private struct SelectStyleKey: EnvironmentKey { + public static var defaultValue: SelectStyle = .default +} + +public extension EnvironmentValues { + var selectStyle: SelectStyle { + get { self[SelectStyleKey.self] } + set { self[SelectStyleKey.self] = newValue } + } +} + +public extension View { + func selectStyle(_ selectStyle: SelectStyle) -> some View { + environment(\.selectStyle, selectStyle) + } +} diff --git a/Sources/OversizeUI/Core/EnvironmentKeys/SurfaceElevationEnvironment.swift b/Sources/OversizeUI/Core/EnvironmentKeys/SurfaceElevationEnvironment.swift new file mode 100644 index 0000000..1dae333 --- /dev/null +++ b/Sources/OversizeUI/Core/EnvironmentKeys/SurfaceElevationEnvironment.swift @@ -0,0 +1,23 @@ +// +// Copyright © 2023 Alexander Romanov +// SurfaceElevationEnvironment.swift, created on 27.12.2023 +// + +import SwiftUI + +private struct SurfaceElevationStateKey: EnvironmentKey { + public static var defaultValue: Elevation = .z0 +} + +public extension EnvironmentValues { + var surfaceElevation: Elevation { + get { self[SurfaceElevationStateKey.self] } + set { self[SurfaceElevationStateKey.self] = newValue } + } +} + +public extension View { + func surfaceElevation(_ elevation: Elevation = .z0) -> some View { + environment(\.surfaceElevation, elevation) + } +} diff --git a/Sources/OversizeUI/Core/PreferenceKeys/ScrollOffsetPreferenceKey.swift b/Sources/OversizeUI/Core/PreferenceKeys/ScrollOffsetPreferenceKey.swift index 0da7c3e..612c37c 100644 --- a/Sources/OversizeUI/Core/PreferenceKeys/ScrollOffsetPreferenceKey.swift +++ b/Sources/OversizeUI/Core/PreferenceKeys/ScrollOffsetPreferenceKey.swift @@ -6,8 +6,6 @@ import SwiftUI public struct ScrollOffsetPreferenceKey: PreferenceKey { - public static var defaultValue: [CGFloat] = [0] - public static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) { - value.append(contentsOf: nextValue()) - } + public static var defaultValue: CGPoint = .zero + public static func reduce(value _: inout CGPoint, nextValue _: () -> CGPoint) {} } diff --git a/Sources/OversizeUI/Core/Typography.swift b/Sources/OversizeUI/Core/Typography.swift index 3b792b7..ad9011b 100644 --- a/Sources/OversizeUI/Core/Typography.swift +++ b/Sources/OversizeUI/Core/Typography.swift @@ -91,7 +91,7 @@ public struct Typography: ViewModifier { } else { switch fontStyle { case .largeTitle, .title: - return isBold ?? true ? .heavy : .regular + return isBold ?? true ? .bold : .regular case .headline: return isBold ?? true ? .bold : .semibold default: diff --git a/Sources/OversizeUI/Core/ViewModifier/Debug/ColorOverlay.swift b/Sources/OversizeUI/Core/ViewModifier/Debug/ColorOverlay.swift new file mode 100644 index 0000000..cc4622e --- /dev/null +++ b/Sources/OversizeUI/Core/ViewModifier/Debug/ColorOverlay.swift @@ -0,0 +1,30 @@ +// +// Copyright © 2023 Alexander Romanov +// DebugOverlayModifier.swift, created on 24.05.2023 +// + +import SwiftUI + +public extension Color { + static func random(randomOpacity: Bool = false) -> Color { + Color( + red: .random(in: 0 ... 1), + green: .random(in: 0 ... 1), + blue: .random(in: 0 ... 1), + opacity: randomOpacity ? .random(in: 0 ... 1) : 1 + ) + } +} + +public struct DebugOverlayModifier: ViewModifier { + public func body(content: Content) -> some View { + content + .overlay(Color.random(randomOpacity: true)) + } +} + +public extension View { + func debugOverlay() -> some View { + modifier(DebugOverlayModifier()) + } +} diff --git a/Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift b/Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift index 0f415ae..266ad6f 100644 --- a/Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift +++ b/Sources/OversizeUI/Core/ViewModifier/HalfSheet/HalfSheet.swift @@ -42,7 +42,6 @@ public enum Detents: Hashable { // swiftlint:disable line_length #if os(iOS) - public struct SheetModifier: ViewModifier { public let detents: [Detents] public func body(content: Content) -> some View { @@ -56,7 +55,7 @@ public extension View { @_disfavoredOverload func presentationDetents(_ detents: [Detents]) -> some View { Group { - if #available(iOS 16, *) { + if #available(iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0, *) { let suiDetents: Set = Set(detents.compactMap { $0.convertToSUI() }) self.presentationDetents(suiDetents) } else { diff --git a/Sources/OversizeUI/Deprecated/ButtonLegacy.swift b/Sources/OversizeUI/Deprecated/ButtonLegacy.swift index 87866db..de2ee23 100644 --- a/Sources/OversizeUI/Deprecated/ButtonLegacy.swift +++ b/Sources/OversizeUI/Deprecated/ButtonLegacy.swift @@ -142,7 +142,6 @@ public struct ButtonStyleExtended: ButtonStyle { ) .opacity(configuration.isPressed ? 0.9 : 1) .scaleEffect(configuration.isPressed ? 0.95 : 1) - .shadowElevaton(shadow ? .z2 : .z0, color: Color.primary) } } diff --git a/Sources/OversizeUI/Deprecated/IconsNames.swift b/Sources/OversizeUI/Deprecated/IconsNames.swift index 294e5b3..cf300f1 100644 --- a/Sources/OversizeUI/Deprecated/IconsNames.swift +++ b/Sources/OversizeUI/Deprecated/IconsNames.swift @@ -4,6 +4,7 @@ // import Foundation + // swiftlint:disable type_body_length identifier_name public enum IconsNames: String, CaseIterable { case none diff --git a/Sources/OversizeUI/Controls/NavigationBar/ModalNavigationBar.swift b/Sources/OversizeUI/Deprecated/NavigationBar/ModalNavigationBar.swift similarity index 100% rename from Sources/OversizeUI/Controls/NavigationBar/ModalNavigationBar.swift rename to Sources/OversizeUI/Deprecated/NavigationBar/ModalNavigationBar.swift diff --git a/Sources/OversizeUI/Controls/PageView/PageView.swift b/Sources/OversizeUI/Deprecated/PageView.swift similarity index 99% rename from Sources/OversizeUI/Controls/PageView/PageView.swift rename to Sources/OversizeUI/Deprecated/PageView.swift index cf370ae..912e4b4 100644 --- a/Sources/OversizeUI/Controls/PageView/PageView.swift +++ b/Sources/OversizeUI/Deprecated/PageView.swift @@ -59,7 +59,7 @@ public struct PageView } .coordinateSpace(name: "Page") } - + @ViewBuilder var header: some View { if title != nil || leadingBar != nil || trailingBar != nil || topToolbar != nil || titleLabel != nil { diff --git a/Sources/OversizeUI/Controls/ScrollView/ScrollViewOffset.swift b/Sources/OversizeUI/Deprecated/ScrollViewOffset.swift similarity index 100% rename from Sources/OversizeUI/Controls/ScrollView/ScrollViewOffset.swift rename to Sources/OversizeUI/Deprecated/ScrollViewOffset.swift diff --git a/Sources/OversizeUI/Extensions/View/View+ScrollViewOffset.swift b/Sources/OversizeUI/Deprecated/View+ScrollViewOffset.swift similarity index 100% rename from Sources/OversizeUI/Extensions/View/View+ScrollViewOffset.swift rename to Sources/OversizeUI/Deprecated/View+ScrollViewOffset.swift diff --git a/Sources/OversizeUI/Core/PreferenceKeys/ViewOffsetKey.swift b/Sources/OversizeUI/Deprecated/ViewOffsetKey.swift similarity index 100% rename from Sources/OversizeUI/Core/PreferenceKeys/ViewOffsetKey.swift rename to Sources/OversizeUI/Deprecated/ViewOffsetKey.swift diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/Contents.json index 78cc7fc..0eb9612 100644 --- a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/Contents.json +++ b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "Plus.svg", + "filename" : "plus.pdf", "idiom" : "universal" } ], @@ -10,6 +10,6 @@ "version" : 1 }, "properties" : { - "preserves-vector-representation" : true + "template-rendering-intent" : "template" } } diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/plus.pdf b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/plus.pdf new file mode 100644 index 0000000..b89ccd9 Binary files /dev/null and b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/plus.pdf differ diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Mini.imageset/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Mini.imageset/Contents.json new file mode 100644 index 0000000..a88436c --- /dev/null +++ b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Mini.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "plus-mini.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Mini.imageset/plus-mini.svg b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Mini.imageset/plus-mini.svg new file mode 100644 index 0000000..d014c8a --- /dev/null +++ b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Mini.imageset/plus-mini.svg @@ -0,0 +1,3 @@ + + + diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Fill.imageset/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square.imageset/Contents.json similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Fill.imageset/Contents.json rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square.imageset/Contents.json diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/Plus.svg b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square.imageset/Plus.svg similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus.imageset/Plus.svg rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square.imageset/Plus.svg diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/Contents.json similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone/Contents.json rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/Contents.json diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone.imageset/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/Fill.imageset/Contents.json similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone.imageset/Contents.json rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/Fill.imageset/Contents.json diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Fill.imageset/Plus.svg b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/Fill.imageset/Plus.svg similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Fill.imageset/Plus.svg rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/Fill.imageset/Plus.svg diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone/Fill.imageset/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone.imageset/Contents.json similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone/Fill.imageset/Contents.json rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone.imageset/Contents.json diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone.imageset/Plus.svg b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone.imageset/Plus.svg similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone.imageset/Plus.svg rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone.imageset/Plus.svg diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Fill.imageset/Contents.json b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Fill.imageset/Contents.json new file mode 100644 index 0000000..78cc7fc --- /dev/null +++ b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Fill.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Plus.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone/Fill.imageset/Plus.svg b/Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Fill.imageset/Plus.svg similarity index 100% rename from Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/TwoTone/Fill.imageset/Plus.svg rename to Sources/OversizeUI/Resources/Icons.xcassets/Base/Plus/Square/TwoTone/Fill.imageset/Plus.svg diff --git a/Sources/OversizeUI/Shapes/AnyShape.swift b/Sources/OversizeUI/Shapes/AnyShape.swift index 9438a72..cf59ea7 100644 --- a/Sources/OversizeUI/Shapes/AnyShape.swift +++ b/Sources/OversizeUI/Shapes/AnyShape.swift @@ -6,9 +6,9 @@ import SwiftUI public struct AnyShape: Shape { - public var make: (CGRect, inout Path) -> Void - - public init(_ make: @escaping (CGRect, inout Path) -> Void) { + public var make: @Sendable (CGRect, inout Path) -> Void + + public init(_ make: @escaping @Sendable (CGRect, inout Path) -> Void) { self.make = make }