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