├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── Package.swift
├── README.md
└── Sources
└── SwiftUIExtension
├── ButtonStyles
└── SideBarActionButtonStyle.swift
├── Compability
├── SymbolEffect.swift
├── TextCompability.swift
├── TransitionBlurReplace.swift
├── ViewCompability.swift
├── WidgetConfigurationCompability.swift
└── WidgetViewCompability.swift
├── Extensions
├── ColorExtension.swift
├── ImageExtension.swift
├── NavigationTransitionExtension.swift
├── PreviewDevice.swift
├── SafeArea.swift
├── SizeTracker.swift
├── View+Fade.swift
├── View+Haptic.swift
├── View+Higlight.swift
├── View+If.swift
├── View+Layout.swift
├── View+Notifications.swift
├── View+Rotation.swift
├── View+TabItem.swift
├── ViewExtension.swift
└── Views+Vibrant.swift
├── Layout.swift
├── LayoutGuides
├── Archived
│ ├── LayoutGuidesEnvironmentOLD.swift
│ ├── LayoutGuidesOLD.swift
│ └── LayoutGuidesObserverViewOLD.swift
├── LayoutGuides.swift
└── LayoutGuidesObserverView.swift
└── Views
├── ActivityView.swift
├── AdaptiveStack.swift
├── CustomBlurView.swift
├── ExpandableSection.swift
├── FadeBlurView.swift
├── FixedSpacer.swift
├── Mimicrate
├── LargeButton.swift
├── MimicrateCloseButton.swift
├── ModalHeaderView.swift
├── ModalSheet
│ ├── ModalSheet.swift
│ ├── ModalSheetContentView.swift
│ └── ModalSheetModifier.swift
├── NativeSection.swift
├── NavigationBarCloseButton.swift
└── ScrollWithBottomContent.swift
├── SelectableStack.swift
└── SelectableVStack.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # macOS Files
2 | .DS_Store
3 | .Trashes
4 |
5 | # Swift Package Manager
6 | .swiftpm
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | hello@ivanvorobei.io.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Here provided info about contribution process and recommendations.
4 |
5 | ## Codestyle
6 |
7 | ### Marks
8 |
9 | For clean struct code good is using marks.
10 |
11 | ```swift
12 | class Example {
13 |
14 | // MARK: - Init
15 |
16 | init() {}
17 | }
18 | ```
19 |
20 | Here you find all which using in project:
21 |
22 | - // MARK: - Init
23 | - // MARK: - Lifecycle
24 | - // MARK: - Layout
25 | - // MARK: - Public
26 | - // MARK: - Private
27 | - // MARK: - Internal
28 | - // MARK: - Models
29 | - // MARK: - Ovveride
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Sparrow Code
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.5
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "SwiftUIExtension",
7 | platforms: [
8 | .iOS(.v15),
9 | .watchOS(.v8),
10 | .macOS(.v12)
11 | ],
12 | products: [
13 | .library(
14 | name: "SwiftUIExtension",
15 | targets: ["SwiftUIExtension"]
16 | )
17 | ],
18 | dependencies: [
19 | .package(url: "https://github.com/sparrowcode/SwiftBoost", .upToNextMajor(from: "4.0.16")),
20 | .package(url: "https://github.com/siteline/swiftui-introspect", .upToNextMajor(from: "1.3.0"))
21 | ],
22 | targets: [
23 | .target(
24 | name: "SwiftUIExtension",
25 | dependencies: [
26 | .product(name: "SwiftBoost", package: "SwiftBoost"),
27 | .product(name: "SwiftUIIntrospect", package: "swiftui-introspect")
28 | ],
29 | swiftSettings: [
30 | .define("SWIFTUIEXTENSION_SPM")
31 | ]
32 | )
33 | ],
34 | swiftLanguageVersions: [.v5]
35 | )
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUIExtension
2 |
3 | Collection of SwiftUI extensions to boost development process.
4 |
5 |
6 | ### iOS Dev Community
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/ButtonStyles/SideBarActionButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension ButtonStyle where Self == SideBarActionButtonStyle {
4 |
5 | static var sideBarAction: Self { SideBarActionButtonStyle() }
6 | }
7 |
8 | public struct SideBarActionButtonStyle: ButtonStyle {
9 |
10 | public func makeBody(configuration: Configuration) -> some View {
11 | configuration.label
12 | .foregroundColor(.accentColor)
13 | .opacity(configuration.isPressed ? 0.4 : 1)
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Compability/SymbolEffect.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | public func symbolEffectCompability(_ model: SymbolEffectCompability) -> some View {
6 | if #available(iOS 17, watchOS 10.0, *) {
7 | return self.symbolEffect(.pulse)
8 | } else {
9 | return self
10 | }
11 | }
12 | }
13 |
14 | public enum SymbolEffectCompability {
15 |
16 | case pulse
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Compability/TextCompability.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension Text {
4 |
5 | public func fontDesignCompability(_ design: Font.Design?) -> Text {
6 | if #available(iOS 16.1, macOS 13.0, tvOS 16.1, watchOS 9.1, *) {
7 | return self.fontDesign(design)
8 | } else {
9 | return self
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Compability/TransitionBlurReplace.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | public func transitionBlurReplaceCombability() -> some View {
6 | modifier(TransitionBlurReplace())
7 | }
8 | }
9 |
10 | struct TransitionBlurReplace: ViewModifier {
11 |
12 | func body(content: Content) -> some View {
13 | if #available(iOS 18.0, macOS 14.0, watchOS 10.0, *) {
14 | content
15 | .transition(.blurReplace.combined(with: .opacity))
16 | } else {
17 | content
18 | .transition(.opacity)
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Compability/ViewCompability.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftUIIntrospect
3 |
4 | extension View {
5 |
6 | public func scrollTargetBehaviorCompability() -> some View {
7 | if #available(iOS 17.0, watchOS 10.0, macOS 14.0, *) {
8 | return self.scrollTargetBehavior(.viewAligned)
9 | } else {
10 | return self
11 | }
12 | }
13 |
14 | public func baselineOffsetCompability(_ offeset: CGFloat) -> some View {
15 | if #available(iOS 16.0, watchOS 9.0, macOS 13.0, *) {
16 | return self.baselineOffset(offeset)
17 | } else {
18 | return self
19 | }
20 | }
21 |
22 | /*public func fontWeightCompability(_ weight: Font.Weight) -> some View {
23 | if #available(iOS 16.0, watchOS 9.0, macOS 13.0, *) {
24 | return self.fontWeight(weight)
25 | } else {
26 | return self
27 | }
28 | }*/
29 |
30 | public func invalidatableContentCompability() -> some View {
31 | if #available(iOS 17.0, watchOS 10.0, macOS 14.0, *) {
32 | return self.invalidatableContent()
33 | } else {
34 | return self
35 | }
36 | }
37 |
38 | public func scrollTargetLayoutCompability(isEnabled: Bool = true) -> some View {
39 | if #available(iOS 17.0, watchOS 10.0, macOS 14.0, *) {
40 | return self.scrollTargetLayout(isEnabled: isEnabled)
41 | } else {
42 | return self
43 | }
44 | }
45 |
46 | public func scrollClipDisabledCompability(_ disabled: Bool = true) -> some View {
47 | if #available(iOS 17.0, watchOS 10.0, macOS 14.0, *) {
48 | return self.scrollClipDisabled(disabled)
49 | } else {
50 | #if !os(watchOS)
51 | return self.introspect(.scrollView, on: .iOS(.v15, .v16)) { scrollView in
52 | scrollView.clipsToBounds = false
53 | }
54 | #else
55 | return self
56 | #endif
57 | }
58 | }
59 |
60 | public func fontDesignCompability(_ design: Font.Design?) -> some View {
61 | if #available(iOS 16.1, macOS 13.0, tvOS 16.1, watchOS 9.1, *) {
62 | return self.fontDesign(design)
63 | } else {
64 | return self
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Compability/WidgetConfigurationCompability.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | #if canImport(WidgetKit)
4 | import WidgetKit
5 |
6 | @available(watchOS 9.0, macOS 11.0, *)
7 | extension WidgetConfiguration {
8 |
9 | public func contentMarginsSafeDisabled() -> some WidgetConfiguration {
10 | if #available(iOS 15.0, macOS 12.0, *) {
11 | return self.contentMarginsDisabled()
12 | } else {
13 | return self
14 | }
15 | }
16 | }
17 | #endif
18 |
19 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Compability/WidgetViewCompability.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | #if canImport(WidgetKit)
4 | import WidgetKit
5 |
6 | extension EnvironmentValues {
7 |
8 | public var widgetContentMarginsCompability: EdgeInsets {
9 | get {
10 | if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) {
11 | return self.widgetContentMargins
12 | } else {
13 | return .init(top: 16, leading: 16, bottom: 16, trailing: 16)
14 | }
15 | }
16 | }
17 | }
18 |
19 | extension View {
20 |
21 | public func containerBackgroundForWidgetCompability(background: @escaping () -> Background) -> some View where Background: View {
22 | modifier(ContainerBackgroundForWidgetCompabilityModifier(background: background))
23 | }
24 |
25 | public func containerBackgroundForWidget(@ViewBuilder background: @escaping () -> Background) -> some View where Background: View {
26 | modifier(ContainerBackgroundForWidgetModifier(background: background))
27 | }
28 | }
29 |
30 | struct ContainerBackgroundForWidgetCompabilityModifier: ViewModifier where Background: View {
31 |
32 | let background: () -> Background
33 |
34 | func body(content: Content) -> some View {
35 | if #available(iOS 17.0, iOSApplicationExtension 17.0, watchOS 10.0, watchOSApplicationExtension 10.0, macOS 14.0, *) {
36 | content
37 | .containerBackground(for: .widget) {
38 | background()
39 | }
40 | } else {
41 | content
42 | }
43 | }
44 | }
45 |
46 | struct ContainerBackgroundForWidgetModifier: ViewModifier where Background: View {
47 |
48 | let background: () -> Background
49 |
50 | func body(content: Content) -> some View {
51 | if #available(iOS 17.0, iOSApplicationExtension 17.0, watchOS 10.0, watchOSApplicationExtension 10.0, macOS 14.0, *) {
52 | content
53 | .containerBackground(for: .widget) {
54 | background()
55 | }
56 | } else {
57 | ZStack {
58 | background()
59 | content
60 | .padding()
61 | }
62 | }
63 | }
64 | }
65 |
66 | #endif
67 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/ColorExtension.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftBoost
3 |
4 | extension Color {
5 |
6 | public init(hex string: String) {
7 | var string: String = string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
8 | if string.hasPrefix("#") {
9 | _ = string.removeFirst()
10 | }
11 |
12 | // Double the last value if incomplete hex
13 | if !string.count.isMultiple(of: 2), let last = string.last {
14 | string.append(last)
15 | }
16 |
17 | // Fix invalid values
18 | if string.count > 8 {
19 | string = String(string.prefix(8))
20 | }
21 |
22 | // Scanner creation
23 | let scanner = Scanner(string: string)
24 |
25 | var color: UInt64 = 0
26 | scanner.scanHexInt64(&color)
27 |
28 | if string.count == 2 {
29 | let mask = 0xFF
30 |
31 | let g = Int(color) & mask
32 |
33 | let gray = Double(g) / 255.0
34 |
35 | self.init(.sRGB, red: gray, green: gray, blue: gray, opacity: 1)
36 |
37 | } else if string.count == 4 {
38 | let mask = 0x00FF
39 |
40 | let g = Int(color >> 8) & mask
41 | let a = Int(color) & mask
42 |
43 | let gray = Double(g) / 255.0
44 | let alpha = Double(a) / 255.0
45 |
46 | self.init(.sRGB, red: gray, green: gray, blue: gray, opacity: alpha)
47 |
48 | } else if string.count == 6 {
49 | let mask = 0x0000FF
50 | let r = Int(color >> 16) & mask
51 | let g = Int(color >> 8) & mask
52 | let b = Int(color) & mask
53 |
54 | let red = Double(r) / 255.0
55 | let green = Double(g) / 255.0
56 | let blue = Double(b) / 255.0
57 |
58 | self.init(.sRGB, red: red, green: green, blue: blue, opacity: 1)
59 |
60 | } else if string.count == 8 {
61 | let mask = 0x000000FF
62 | let r = Int(color >> 24) & mask
63 | let g = Int(color >> 16) & mask
64 | let b = Int(color >> 8) & mask
65 | let a = Int(color) & mask
66 |
67 | let red = Double(r) / 255.0
68 | let green = Double(g) / 255.0
69 | let blue = Double(b) / 255.0
70 | let alpha = Double(a) / 255.0
71 |
72 | self.init(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
73 |
74 | } else {
75 | self.init(.sRGB, red: 1, green: 1, blue: 1, opacity: 1)
76 | }
77 | }
78 | }
79 |
80 |
81 | extension View {
82 |
83 | #if canImport(UIKit)
84 | @available(iOS 15.0, watchOS 8.0, *)
85 | public func foregroundColor(_ color: UIColor) -> some View {
86 | self.foregroundColor(.init(uiColor: color))
87 | }
88 | #endif
89 | }
90 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/ImageExtension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | extension Image {
5 |
6 | public static func load(url: URL, completion: @escaping (Image?) -> Void) {
7 | DispatchQueue.global(qos: .default).async {
8 | if let data = try? Data(contentsOf: url) {
9 |
10 | #if canImport(UIKit)
11 | let songArtwork = UIImage(data: data) ?? UIImage()
12 | completion(Image(uiImage: songArtwork))
13 | #endif
14 |
15 | #if canImport(AppKit) && !targetEnvironment(macCatalyst)
16 | let songArtwork = NSImage(data: data) ?? NSImage()
17 | completion(Image(nsImage: songArtwork))
18 | #endif
19 |
20 | completion(nil)
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/NavigationTransitionExtension.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | public func matchedTransitionSourceCompability(id: some Hashable, in namespace: Namespace.ID) -> some View {
6 | if #available(iOS 18.0, macOS 14.0, visionOS 2.0, watchOS 11.0, *) {
7 | return self.matchedTransitionSource(id: id, in: namespace)
8 | } else {
9 | return self
10 | }
11 | }
12 |
13 | public func navigationTransitionZoom(id: some Hashable, in namespace: Namespace.ID) -> some View {
14 | if #available(iOS 18.0, macOS 14.0, visionOS 2.0, watchOS 11.0, *) {
15 | return self.navigationTransition(.zoom(sourceID: id, in: namespace))
16 | } else {
17 | return self
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/PreviewDevice.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension PreviewDevice {
4 |
5 | public static var iPhone16Pro: PreviewDevice {
6 | PreviewDevice(rawValue: "iPhone 16 Pro")
7 | }
8 |
9 | public static var iPadPro11: PreviewDevice {
10 | PreviewDevice(rawValue: "iPad Pro 11-inch (M4)")
11 | }
12 |
13 | public static var visionPro: PreviewDevice {
14 | PreviewDevice(rawValue: "Apple Vision Pro")
15 | }
16 |
17 | public static var mac: PreviewDevice {
18 | PreviewDevice(rawValue: "Mac")
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/SafeArea.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | /**
6 | Default value for `safeAreaInset` none zero — this wrapper drop all spaces.
7 | */
8 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
9 | public func safeAreaInsetNoneSpaced(edge: VerticalEdge, @ViewBuilder content: () -> Content) -> some View {
10 | self
11 | .safeAreaInset(edge: edge, spacing: .zero) {
12 | content()
13 | }
14 | }
15 | }
16 |
17 | // MARK: - Environment
18 |
19 | #if os(iOS)
20 | extension EnvironmentValues {
21 |
22 | public var safeAreaInsets: EdgeInsets {
23 | self[SafeAreaInsetsKey.self]
24 | }
25 | }
26 |
27 | private struct SafeAreaInsetsKey: EnvironmentKey {
28 |
29 | static var defaultValue: EdgeInsets {
30 | (UIApplication.shared.rootController?.view.window?.safeAreaInsets ?? .zero).insets
31 | //(UIApplication.shared.windows.first(where: { $0.isKeyWindow })?.safeAreaInsets ?? .zero).insets
32 | }
33 | }
34 |
35 | private extension UIEdgeInsets {
36 |
37 | var insets: EdgeInsets {
38 | EdgeInsets(top: top, leading: left, bottom: bottom, trailing: right)
39 | }
40 | }
41 | #endif
42 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/SizeTracker.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct SizeTracker: ViewModifier {
4 |
5 | @Binding var size: CGSize
6 |
7 | public func body(content: Content) -> some View {
8 | content.background(
9 | GeometryReader { geometry in
10 | Color.clear
11 | .onAppear {
12 | self.size = geometry.size
13 | }
14 | .onChange(of: geometry.size) { newSize in
15 | self.size = newSize
16 | }
17 | }
18 | )
19 | }
20 | }
21 |
22 | extension View {
23 |
24 | public func sizeChanged(_ size: Binding) -> some View {
25 | self.modifier(SizeTracker(size: size))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+Fade.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | public func fade(top: CGFloat? = nil, bottom: CGFloat? = nil) -> some View {
6 | modifier(VerticalFade(top: top, bottom: bottom))
7 | }
8 | }
9 |
10 | struct VerticalFade: ViewModifier {
11 |
12 | let top: CGFloat?
13 | let bottom: CGFloat?
14 |
15 | func body(content: Content) -> some View {
16 | content
17 | .mask {
18 | VStack(spacing: .zero) {
19 |
20 | if let top {
21 | // Top Fade
22 | LinearGradient(colors: [Color.black.opacity(0), Color.black], startPoint: .top, endPoint: .bottom)
23 | .frame(height: top)
24 | }
25 |
26 | // Middle
27 | Rectangle()
28 | .fill(Color.black)
29 | .ignoresSafeArea()
30 |
31 | if let bottom {
32 | // Bottom Fade
33 | LinearGradient(colors: [Color.black, Color.black.opacity(0)], startPoint: .top, endPoint: .bottom)
34 | .frame(height: bottom)
35 | }
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+Haptic.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | public func sensoryFeedbackCompability(_ feedback: SensoryFeedbackCompability, trigger: T) -> some View where T : Equatable {
6 | self.modifier(SensoryFeedbackModifier(feedback, trigger: trigger))
7 | }
8 | }
9 |
10 | struct SensoryFeedbackModifier: ViewModifier {
11 |
12 | let feedback: SensoryFeedbackCompability
13 | let trigger: T
14 |
15 | init(_ feedback: SensoryFeedbackCompability, trigger: T) {
16 | self.feedback = feedback
17 | self.trigger = trigger
18 | }
19 |
20 | func body(content: Content) -> some View {
21 | if #available(iOS 17.0, watchOS 10.0, *) {
22 | content
23 | .sensoryFeedback(.selection, trigger: trigger)
24 | } else {
25 | content
26 | }
27 | }
28 | }
29 |
30 | public enum SensoryFeedbackCompability {
31 |
32 | case selection
33 | }
34 |
35 | /*
36 | struct SensoryFeedbackModifier: ViewModifier {
37 |
38 | let selectedElement: AnyHasherable
39 |
40 | @ViewBuilder
41 | func body(content: Content) -> some View {
42 | if #available(iOS 17, *) {
43 | content.sensoryFeedback(.selection, trigger: selectedElement)
44 | } else {
45 | content
46 | }
47 | }
48 | }
49 |
50 | extension View {
51 | func applySensoryFeedback(selectedElement: AnyHasherable) -> some View {
52 | self.modifier(SensoryFeedbackModifier(selectedElement: selectedElement))
53 | }
54 | }
55 | */
56 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+Higlight.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftBoost
3 |
4 | extension View {
5 |
6 | public func higlight(clipShape: S) -> some View where S : Shape {
7 | self.modifier(HiglightModifier(clipShape: clipShape))
8 | }
9 | }
10 |
11 | struct HiglightModifier: ViewModifier where S : Shape {
12 |
13 | @State private var isAnimatingHiglight = false
14 | @State private var isAnimatingScale = false
15 |
16 | private let clipShape: S
17 |
18 | private var higlightDuration: TimeInterval = 1
19 | private var calmDuration: TimeInterval = 3
20 | private var changeAppearanceDuration: TimeInterval = 0.26
21 |
22 | private var blinkWidth: CGFloat { 18 }
23 | private var scale: CGFloat = 0.94
24 |
25 | init(clipShape: S) {
26 | self.clipShape = clipShape
27 | }
28 |
29 | func body(content: Content) -> some View {
30 | content
31 | .overlay {
32 | GeometryReader { proxy in
33 | Rectangle()
34 | .blur(radius: 6)
35 | .vibrant(0.7)
36 | .frame(width: blinkWidth)
37 | .frame(height: proxy.size.height * 2)
38 | .rotationEffect(.degrees(45))
39 | .position(
40 | x: isAnimatingHiglight ? (proxy.size.width + proxy.size.height) : (0 - proxy.size.height),
41 | y: proxy.size.height / 2
42 | )
43 | .clipShape(clipShape)
44 | .animation(isAnimatingHiglight ? .interpolatingSpring(duration: higlightDuration) : .none, value: isAnimatingHiglight)
45 | }
46 | }
47 | .scaleEffect(isAnimatingScale ? scale : 1)
48 | .animation(.interpolatingSpring(duration: changeAppearanceDuration), value: isAnimatingScale)
49 | .onAppear {
50 | run()
51 | }
52 | }
53 |
54 | private func run() {
55 |
56 | isAnimatingHiglight = true
57 | isAnimatingScale = true
58 |
59 | delay(higlightDuration / 2) {
60 | isAnimatingScale = false
61 | }
62 |
63 | delay(higlightDuration) {
64 | isAnimatingHiglight = false
65 | delay(calmDuration) {
66 | self.run()
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+If.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | @ViewBuilder public func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View {
6 | if condition {
7 | transform(self)
8 | } else {
9 | self
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+Layout.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | #warning("todo: to del")
4 | /*extension View {
5 |
6 | public func horizontalSystemPadding(to view: HorizontalSystemPadding.PaddingView) -> some View {
7 |
8 | modifier(HorizontalSystemPadding(paddingView: view))
9 | }
10 |
11 | /*public func readableMargins() -> some View {
12 | self
13 | .padding(.horizontal)
14 | .frame(maxWidth: 414)
15 | }*/
16 | }
17 |
18 | public struct HorizontalSystemPadding: ViewModifier {
19 |
20 | let paddingView: PaddingView
21 |
22 | var value: CGFloat {
23 | switch horizontalSizeClass {
24 | case .compact:
25 | return 16
26 | case .regular:
27 | #if os(visionOS)
28 | return 24
29 | #else
30 | return 20
31 | #endif
32 | default:
33 | return 16
34 | }
35 | }
36 |
37 | @Environment(\.horizontalSizeClass) var horizontalSizeClass
38 |
39 | public func body(content: Content) -> some View {
40 | /*switch paddingView {
41 | case .scroll:
42 | if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) {
43 | content
44 | .contentMargins(.horizontal, value, for: .scrollContent)
45 | } else {
46 | content
47 | }
48 | case .view:
49 | content.padding(.horizontal, value)
50 | }*/
51 | content
52 | }
53 |
54 | public enum PaddingView {
55 |
56 | case scroll
57 | case view
58 | }
59 | }
60 | */
61 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+Notifications.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Combine
3 |
4 | extension View {
5 |
6 | public func onReceive(
7 | _ name: Notification.Name,
8 | perform action: @escaping (NotificationCenter.Publisher.Output) -> Void) -> some View {
9 | self.onReceive(NotificationCenter.default.publisher(for: name), perform: action)
10 | }
11 |
12 | public func onReceive(
13 | _ names: Notification.Name...,
14 | center: NotificationCenter = .default,
15 | object: AnyObject? = nil,
16 | perform action: @escaping (Notification) -> Void
17 | ) -> some View {
18 |
19 | let mergedPublisher = names.map { name in
20 | center.publisher(for: name, object: object)
21 | }.reduce(Empty().eraseToAnyPublisher()) { merged, publisher in
22 | merged.merge(with: publisher).eraseToAnyPublisher()
23 | }
24 |
25 | return self.onReceive(mergedPublisher, perform: action)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+Rotation.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import SwiftUI
3 |
4 | extension View {
5 |
6 | public func onRotate(perform action: @escaping (UIDeviceOrientation) -> Void) -> some View {
7 | self.modifier(DeviceRotationViewModifier(action: action))
8 | }
9 | }
10 |
11 | struct DeviceRotationViewModifier: ViewModifier {
12 |
13 | let action: (UIDeviceOrientation) -> Void
14 |
15 | func body(content: Content) -> some View {
16 | content
17 | .onAppear()
18 | .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
19 | action(UIDevice.current.orientation)
20 | }
21 | }
22 | }
23 | #endif
24 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/View+TabItem.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | public func disableSwipeForTabItem() -> some View {
6 | self.gesture(DragGesture())
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/ViewExtension.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | public func clipShapeWithBorder(_ content: S, width: CGFloat = 1, cornerRadius: CGFloat, style: RoundedCornerStyle = .continuous) -> some View where S : ShapeStyle {
6 |
7 | let roundedRect = RoundedRectangle(cornerRadius: cornerRadius, style: style)
8 |
9 | return self
10 | .clipShape(roundedRect)
11 | .overlay(roundedRect.strokeBorder(content, lineWidth: width))
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Extensions/Views+Vibrant.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 |
5 | func vibrant(_ intensity: CGFloat) -> some View {
6 | self.modifier(VibrantModifier(intensity: intensity))
7 | }
8 | }
9 |
10 | struct VibrantModifier: ViewModifier {
11 |
12 | let intensity: CGFloat
13 |
14 | init(intensity: CGFloat) {
15 | self.intensity = intensity
16 | }
17 |
18 | func body(content: Content) -> some View {
19 | content
20 | .foregroundColor(.white.opacity(1 - intensity))
21 | .blendMode(.plusLighter)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Layout.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum Spaces {
4 |
5 | public static var step: CGFloat = 4
6 |
7 | public static var default_half: CGFloat { self.default / 2 } // 8
8 | public static var default_less: CGFloat { step * 3 } // 12
9 | public static var `default`: CGFloat { step * 4 } // 16
10 | public static var default_more: CGFloat { step * 6 } // 24
11 | public static var default_double: CGFloat { self.default * 2 } // 32
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/LayoutGuides/Archived/LayoutGuidesEnvironmentOLD.swift:
--------------------------------------------------------------------------------
1 | /*import SwiftUI
2 |
3 | private struct LayoutMarginsGuidesKey: EnvironmentKey {
4 | static var defaultValue: EdgeInsets { .init() }
5 | }
6 |
7 | private struct ReadableContentGuidesKey: EnvironmentKey {
8 | static var defaultValue: EdgeInsets { .init() }
9 | }
10 |
11 | extension EnvironmentValues {
12 |
13 | public var layoutMarginsInsets: EdgeInsets {
14 | get { self[LayoutMarginsGuidesKey.self] }
15 | set { self[LayoutMarginsGuidesKey.self] = newValue }
16 | }
17 |
18 | public var readableContentInsets: EdgeInsets {
19 | get { self[ReadableContentGuidesKey.self] }
20 | set { self[ReadableContentGuidesKey.self] = newValue }
21 | }
22 | }
23 | */
24 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/LayoutGuides/Archived/LayoutGuidesOLD.swift:
--------------------------------------------------------------------------------
1 | /*import SwiftUI
2 |
3 | extension View {
4 |
5 | public func fitToReadableContentWidth(alignment: Alignment = .center) -> some View {
6 | self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .readableContent))
7 | }
8 |
9 | public func fitToLayoutMarginsWidth(alignment: Alignment = .center) -> some View {
10 | self.modifier(FitLayoutGuidesWidth(alignment: alignment, kind: .layoutMargins))
11 | }
12 |
13 | public func measureLayoutGuides() -> some View {
14 | self.modifier(LayoutGuidesModifier())
15 | }
16 | }
17 |
18 | public struct WithLayoutMargins: View where Content: View {
19 |
20 | let content: (EdgeInsets) -> Content
21 |
22 | public init(@ViewBuilder content: @escaping (EdgeInsets) -> Content) {
23 | self.content = content
24 | }
25 |
26 | public init(@ViewBuilder content: @escaping () -> Content) {
27 | self.content = { _ in content() }
28 | }
29 |
30 | public var body: some View {
31 | InsetContent(content: content)
32 | .measureLayoutGuides()
33 | }
34 |
35 | private struct InsetContent: View {
36 |
37 | let content: (EdgeInsets) -> Content
38 |
39 | @Environment(\.layoutMarginsInsets) var layoutMarginsInsets
40 |
41 | var body: some View {
42 | content(layoutMarginsInsets)
43 | }
44 | }
45 | }
46 |
47 | // MARK: - Private
48 |
49 | internal struct FitLayoutGuidesWidth: ViewModifier {
50 |
51 | enum Kind {
52 | case layoutMargins
53 | case readableContent
54 | }
55 |
56 | let alignment: Alignment
57 | let kind: Kind
58 |
59 | func body(content: Content) -> some View {
60 | switch kind {
61 | case .layoutMargins:
62 | content.modifier(InsetLayoutMargins(alignment: alignment))
63 | .measureLayoutGuides()
64 | case .readableContent:
65 | content.modifier(InsetReadableContent(alignment: alignment))
66 | .measureLayoutGuides()
67 | }
68 | }
69 |
70 | private struct InsetReadableContent: ViewModifier {
71 |
72 | let alignment: Alignment
73 | @Environment(\.readableContentInsets) var readableContentInsets
74 |
75 | func body(content: Content) -> some View {
76 | content
77 | .frame(maxWidth: .infinity, alignment: alignment)
78 | .padding(.leading, readableContentInsets.leading)
79 | .padding(.trailing, readableContentInsets.trailing)
80 | }
81 | }
82 |
83 | private struct InsetLayoutMargins: ViewModifier {
84 |
85 | let alignment: Alignment
86 | @Environment(\.layoutMarginsInsets) var layoutMarginsInsets
87 |
88 | func body(content: Content) -> some View {
89 | content
90 | .frame(maxWidth: .infinity, alignment: alignment)
91 | .padding(.leading, layoutMarginsInsets.leading)
92 | .padding(.trailing, layoutMarginsInsets.trailing)
93 | }
94 | }
95 | }
96 |
97 | internal struct LayoutGuidesModifier: ViewModifier {
98 |
99 | @State var layoutMarginsInsets: EdgeInsets = .init()
100 | @State var readableContentInsets: EdgeInsets = .init()
101 |
102 | func body(content: Content) -> some View {
103 | content
104 | #if os(iOS) || os(tvOS)
105 | .environment(\.layoutMarginsInsets, layoutMarginsInsets)
106 | .environment(\.readableContentInsets, readableContentInsets)
107 | .background(
108 | LayoutGuidesObserverView(
109 | onLayoutMarginsGuideChange: {
110 | layoutMarginsInsets = $0
111 | },
112 | onReadableContentGuideChange: {
113 | readableContentInsets = $0
114 | })
115 | )
116 | #endif
117 | }
118 | }
119 | */
120 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/LayoutGuides/Archived/LayoutGuidesObserverViewOLD.swift:
--------------------------------------------------------------------------------
1 | /*#if os(iOS) || os(tvOS)
2 | import UIKit
3 | import SwiftUI
4 |
5 | struct LayoutGuidesObserverView: UIViewRepresentable {
6 |
7 | let onLayoutMarginsGuideChange: (EdgeInsets) -> Void
8 | let onReadableContentGuideChange: (EdgeInsets) -> Void
9 |
10 | func makeUIView(context: Context) -> LayoutGuidesView {
11 | let uiView = LayoutGuidesView()
12 | uiView.onLayoutMarginsGuideChange = onLayoutMarginsGuideChange
13 | uiView.onReadableContentGuideChange = onReadableContentGuideChange
14 | return uiView
15 | }
16 |
17 | func updateUIView(_ uiView: LayoutGuidesView, context: Context) {
18 | uiView.onLayoutMarginsGuideChange = onLayoutMarginsGuideChange
19 | uiView.onReadableContentGuideChange = onReadableContentGuideChange
20 | }
21 |
22 | final class LayoutGuidesView: UIView {
23 | var onLayoutMarginsGuideChange: (EdgeInsets) -> Void = { _ in }
24 | var onReadableContentGuideChange: (EdgeInsets) -> Void = { _ in }
25 |
26 | override func layoutMarginsDidChange() {
27 | super.layoutMarginsDidChange()
28 | updateLayoutMargins()
29 | updateReadableContent()
30 | }
31 |
32 | override func layoutSubviews() {
33 | super.layoutSubviews()
34 | updateReadableContent()
35 | }
36 |
37 | override var frame: CGRect {
38 | didSet {
39 | self.updateReadableContent()
40 | }
41 | }
42 |
43 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
44 | super.traitCollectionDidChange(previousTraitCollection)
45 | if traitCollection.layoutDirection != previousTraitCollection?.layoutDirection {
46 | updateReadableContent()
47 | }
48 | }
49 |
50 | var previousLayoutMargins: EdgeInsets? = nil
51 | func updateLayoutMargins() {
52 | let edgeInsets = EdgeInsets(
53 | top: directionalLayoutMargins.top,
54 | leading: directionalLayoutMargins.leading,
55 | bottom: directionalLayoutMargins.bottom,
56 | trailing: directionalLayoutMargins.trailing
57 | )
58 | guard previousLayoutMargins != edgeInsets else { return }
59 | onLayoutMarginsGuideChange(edgeInsets)
60 | previousLayoutMargins = edgeInsets
61 | }
62 |
63 | var previousReadableContentGuide: EdgeInsets? = nil
64 | func updateReadableContent() {
65 | let isRightToLeft = traitCollection.layoutDirection == .rightToLeft
66 | let layoutFrame = readableContentGuide.layoutFrame
67 |
68 | let readableContentInsets =
69 | UIEdgeInsets(
70 | top: layoutFrame.minY - bounds.minY,
71 | left: layoutFrame.minX - bounds.minX,
72 | bottom: -(layoutFrame.maxY - bounds.maxY),
73 | right: -(layoutFrame.maxX - bounds.maxX)
74 | )
75 | let edgeInsets = EdgeInsets(
76 | top: readableContentInsets.top,
77 | leading: isRightToLeft ? readableContentInsets.right : readableContentInsets.left,
78 | bottom: readableContentInsets.bottom,
79 | trailing: isRightToLeft ? readableContentInsets.left : readableContentInsets.right
80 | )
81 | guard previousReadableContentGuide != edgeInsets else { return }
82 | onReadableContentGuideChange(edgeInsets)
83 | previousReadableContentGuide = edgeInsets
84 | }
85 | }
86 | }
87 | #endif
88 | */
89 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/LayoutGuides/LayoutGuides.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS) || os(tvOS)
2 | import SwiftUI
3 |
4 | extension View {
5 |
6 | public func fitGuide(_ guide: LayoutGuide, padding: LayoutPadding = LayoutPadding.view) -> some View {
7 | self.modifier(FitGuide(with: guide, to: padding))
8 | }
9 | }
10 |
11 | public enum LayoutGuide {
12 |
13 | case layoutMargings
14 | case readableMargins
15 | }
16 |
17 | public enum LayoutPadding {
18 |
19 | case scroll
20 | case view
21 | }
22 |
23 | // MARK: - Private
24 |
25 | internal struct FitGuide: ViewModifier {
26 |
27 | @State var layoutMargins: EdgeInsets? = nil
28 | @State var readableMargins: EdgeInsets? = nil
29 |
30 | private let guide: LayoutGuide
31 | private let padding: LayoutPadding
32 |
33 | init(with guide: LayoutGuide, to padding: LayoutPadding) {
34 | self.guide = guide
35 | self.padding = padding
36 | }
37 |
38 | func body(content: Content) -> some View {
39 | switch guide {
40 | case .layoutMargings:
41 |
42 | switch padding {
43 | case .scroll:
44 | if #available(iOS 17.0, *) {
45 | content
46 | .background(
47 | LayoutGuidesObserverView(
48 | layoutMarginsDidChanged: {
49 | layoutMargins = $0
50 | }
51 | )
52 | )
53 | .contentMargins(.leading, layoutMargins?.leading, for: .scrollContent)
54 | .contentMargins(.trailing, layoutMargins?.trailing, for: .scrollContent)
55 | } else {
56 | content
57 | }
58 | case .view:
59 | content
60 | .background(
61 | LayoutGuidesObserverView(
62 | layoutMarginsDidChanged: {
63 | layoutMargins = $0
64 | }
65 | )
66 | )
67 | .padding(.leading, layoutMargins?.leading)
68 | .padding(.trailing, layoutMargins?.trailing)
69 | }
70 |
71 | case .readableMargins:
72 |
73 | switch padding {
74 | case .scroll:
75 | if #available(iOS 17.0, *) {
76 | content
77 | .background(
78 | LayoutGuidesObserverView(
79 | readableMarginsDidChanged: {
80 | readableMargins = $0
81 | }
82 | )
83 | )
84 | .contentMargins(.leading, readableMargins?.leading, for: .scrollContent)
85 | .contentMargins(.trailing, readableMargins?.trailing, for: .scrollContent)
86 | } else {
87 | content
88 | }
89 | case .view:
90 | content
91 | .background(
92 | LayoutGuidesObserverView(
93 | readableMarginsDidChanged: {
94 | readableMargins = $0
95 | }
96 | )
97 | )
98 | .padding(.leading, readableMargins?.leading)
99 | .padding(.trailing, readableMargins?.trailing)
100 | }
101 | }
102 | }
103 | }
104 | #endif
105 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/LayoutGuides/LayoutGuidesObserverView.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS) || os(tvOS)
2 | import SwiftUI
3 | import SwiftBoost
4 |
5 | internal struct LayoutGuidesObserverView: UIViewRepresentable {
6 |
7 | var layoutMarginsDidChanged: ((EdgeInsets) -> Void)? = nil
8 | var readableMarginsDidChanged: ((EdgeInsets) -> Void)? = nil
9 |
10 | init(
11 | layoutMarginsDidChanged: ((EdgeInsets) -> Void)? = nil,
12 | readableMarginsDidChanged: ((EdgeInsets) -> Void)? = nil
13 | ) {
14 | self.layoutMarginsDidChanged = layoutMarginsDidChanged
15 | self.readableMarginsDidChanged = readableMarginsDidChanged
16 | }
17 |
18 | func makeUIView(context: Context) -> LayoutGuidesView {
19 | let uiView = LayoutGuidesView()
20 | uiView.layoutMarginsDidChanged = layoutMarginsDidChanged
21 | uiView.readableMarginsDidChanged = readableMarginsDidChanged
22 | return uiView
23 | }
24 |
25 | func updateUIView(_ uiView: LayoutGuidesView, context: Context) {
26 | uiView.layoutMarginsDidChanged = layoutMarginsDidChanged
27 | uiView.readableMarginsDidChanged = readableMarginsDidChanged
28 | }
29 |
30 | final class LayoutGuidesView: UIView {
31 |
32 | var layoutMarginsDidChanged: ((EdgeInsets) -> Void)? = nil
33 | var readableMarginsDidChanged: ((EdgeInsets) -> Void)? = nil
34 |
35 | var cachedLayoutMargins: EdgeInsets? = EdgeInsets()
36 | var cachedReadableMargins: EdgeInsets? = EdgeInsets()
37 |
38 | override func layoutMarginsDidChange() {
39 | super.layoutMarginsDidChange()
40 | update()
41 | }
42 |
43 | override func layoutSubviews() {
44 | super.layoutSubviews()
45 | update()
46 | }
47 |
48 | private func update() {
49 | guard let viewController = self.viewController else { return }
50 | let safeAreaInsets = viewController.view.safeAreaInsets.edgeInsets
51 |
52 | let layout = viewController.view.layoutMargins.edgeInsets
53 | let correctedLayout = layout - safeAreaInsets
54 |
55 | if cachedLayoutMargins != correctedLayout {
56 | cachedLayoutMargins = correctedLayout
57 | layoutMarginsDidChanged?(correctedLayout)
58 | }
59 |
60 | let readable = viewController.view.readableMargins.edgeInsets
61 | let correctedReadable = readable - safeAreaInsets
62 | if cachedReadableMargins != correctedReadable {
63 | cachedReadableMargins = correctedReadable
64 | readableMarginsDidChanged?(correctedReadable)
65 | }
66 | }
67 | }
68 | }
69 |
70 | extension UIEdgeInsets {
71 |
72 | var edgeInsets: EdgeInsets {
73 | .init(top: self.top, leading: self.left, bottom: self.bottom, trailing: self.right)
74 | }
75 | }
76 | #endif
77 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/ActivityView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | #if os(iOS) || os(tvOS)
4 | @available(iOS 13.0, tvOS 13.0, *)
5 | public struct ActivityView: UIViewControllerRepresentable {
6 |
7 | var activityItems: [Any]
8 | var applicationActivities: [UIActivity]? = nil
9 |
10 | public init(activityItems: [Any], applicationActivities: [UIActivity]? = nil) {
11 | self.activityItems = activityItems
12 | self.applicationActivities = applicationActivities
13 | }
14 |
15 | public func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController {
16 | let controller = UIActivityViewController(
17 | activityItems: activityItems,
18 | applicationActivities: applicationActivities
19 | )
20 | return controller
21 | }
22 |
23 | public func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) {}
24 | }
25 | #endif
26 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/AdaptiveStack.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | #if os(iOS)
4 | public struct AdaptiveStack: View {
5 |
6 | @State private var orientation = UIDeviceOrientation.unknown
7 | let content: () -> Content
8 |
9 | public init(@ViewBuilder content: @escaping () -> Content) {
10 | self.content = content
11 | }
12 |
13 | public var body: some View {
14 | #if os(visionOS)
15 | Text("Unsupported")
16 | #else
17 | Group {
18 | if orientation.isLandscape {
19 | HStack {
20 | content()
21 | }
22 | } else {
23 | VStack {
24 | content()
25 | }
26 | }
27 | }
28 | .detectOrientation($orientation)
29 | #endif
30 | }
31 | }
32 |
33 | struct DetectOrientation: ViewModifier {
34 |
35 | @Binding var orientation: UIDeviceOrientation
36 |
37 | func body(content: Content) -> some View {
38 | content
39 | .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
40 | orientation = UIDevice.current.orientation
41 | }
42 | }
43 | }
44 |
45 |
46 | extension View {
47 |
48 | func detectOrientation(_ orientation: Binding) -> some View {
49 | modifier(DetectOrientation(orientation: orientation))
50 | }
51 | }
52 | #endif
53 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/CustomBlurView.swift:
--------------------------------------------------------------------------------
1 | #if canImport(SwiftUI) && canImport(UIKit) && os(iOS)
2 | import UIKit
3 | import SwiftUI
4 |
5 | public struct CustomBlurView: View {
6 |
7 | let radius: CGFloat
8 |
9 | public init(radius: CGFloat) {
10 | self.radius = radius
11 | }
12 |
13 | public var body: some View {
14 | WrapperCustomBlurView(radius: self.radius)
15 | }
16 | }
17 |
18 | // MARK: - Private
19 |
20 | private struct WrapperCustomBlurView: UIViewRepresentable {
21 |
22 | let radius: CGFloat
23 |
24 | init(radius: CGFloat) {
25 | self.radius = radius
26 | }
27 |
28 | func makeUIView(context: Context) -> ALBlurView {
29 | ALBlurView()
30 | }
31 |
32 | func updateUIView(_ uiView: ALBlurView, context: Context) {
33 | uiView.blurRadius = radius
34 | }
35 | }
36 |
37 | private class ALBlurView: UIView {
38 |
39 | private enum Constants {
40 | static let blurRadiusKey = "blurRadius"
41 | static let colorTintKey = "colorTint"
42 | static let colorTintAlphaKey = "colorTintAlpha"
43 | }
44 |
45 | // MARK: - Public
46 |
47 | /// Blur radius. Defaults to `10`
48 | open var blurRadius: CGFloat = 10.0 {
49 | didSet {
50 | _setValue(blurRadius, forKey: Constants.blurRadiusKey)
51 | }
52 | }
53 |
54 | /// Tint color. Defaults to `nil`
55 | open var colorTint: UIColor? {
56 | didSet {
57 | _setValue(colorTint, forKey: Constants.colorTintKey)
58 | }
59 | }
60 |
61 | /// Tint color alpha. Defaults to `0.8`
62 | open var colorTintAlpha: CGFloat = 0.8 {
63 | didSet {
64 | _setValue(colorTintAlpha, forKey: Constants.colorTintAlphaKey)
65 | }
66 | }
67 |
68 | /// Visual effect view layer.
69 | public var blurLayer: CALayer {
70 | return visualEffectView.layer
71 | }
72 |
73 | // MARK: - Initialization
74 |
75 | public init(
76 | radius: CGFloat = 10.0,
77 | color: UIColor? = nil,
78 | colorAlpha: CGFloat = 0.8) {
79 | blurRadius = radius
80 | super.init(frame: .zero)
81 | backgroundColor = .clear
82 | setupViews()
83 | defer {
84 | blurRadius = radius
85 | colorTint = color
86 | colorTintAlpha = colorAlpha
87 | }
88 | }
89 |
90 | required public init?(coder: NSCoder) {
91 | super.init(coder: coder)
92 | backgroundColor = .clear
93 | setupViews()
94 | defer {
95 | blurRadius = 10.0
96 | colorTint = nil
97 | colorTintAlpha = 0.8
98 | }
99 | }
100 |
101 |
102 | // MARK: - Private
103 |
104 | /// Visual effect view.
105 | private lazy var visualEffectView: UIVisualEffectView = {
106 | if #available(iOS 14.0, *) {
107 | return UIVisualEffectView(effect: customBlurEffect_ios14)
108 | } else {
109 | return UIVisualEffectView(effect: customBlurEffect)
110 | }
111 | }()
112 |
113 | /// Blur effect for IOS >= 14
114 | private lazy var customBlurEffect_ios14: BlurEffect = {
115 | let effect = BlurEffect.effect(with: .extraLight)
116 | effect.blurRadius = blurRadius
117 | return effect
118 | }()
119 |
120 | /// Blur effect for IOS < 14
121 | private lazy var customBlurEffect: UIBlurEffect = {
122 | return (NSClassFromString("_UICustomBlurEffect") as! UIBlurEffect.Type).init()
123 | }()
124 |
125 | /// Sets the value for the key on the blurEffect.
126 | private func _setValue(_ value: Any?, forKey key: String) {
127 | if #available(iOS 14.0, *) {
128 | if key == Constants.blurRadiusKey {
129 | updateViews()
130 | }
131 | let subviewClass = NSClassFromString("_UIVisualEffectSubview") as? UIView.Type
132 | let visualEffectSubview: UIView? = visualEffectView.subviews.filter({ type(of: $0) == subviewClass }).first
133 | visualEffectSubview?.backgroundColor = colorTint
134 | visualEffectSubview?.alpha = colorTintAlpha
135 | } else {
136 | customBlurEffect.setValue(value, forKeyPath: key)
137 | visualEffectView.effect = customBlurEffect
138 | }
139 | }
140 |
141 | /// Setup views.
142 | private func setupViews() {
143 | addSubview(visualEffectView)
144 | visualEffectView.translatesAutoresizingMaskIntoConstraints = false
145 | NSLayoutConstraint.activate([
146 | visualEffectView.topAnchor.constraint(equalTo: topAnchor),
147 | visualEffectView.trailingAnchor.constraint(equalTo: trailingAnchor),
148 | visualEffectView.bottomAnchor.constraint(equalTo: bottomAnchor),
149 | visualEffectView.leadingAnchor.constraint(equalTo: leadingAnchor),
150 | ])
151 | }
152 |
153 | /// Update visualEffectView for ios14+, if we need to change blurRadius
154 | private func updateViews() {
155 | if #available(iOS 14.0, *) {
156 | visualEffectView.removeFromSuperview()
157 | let newEffect = BlurEffect.effect(with: .extraLight)
158 | newEffect.blurRadius = blurRadius
159 | customBlurEffect_ios14 = newEffect
160 | visualEffectView = UIVisualEffectView(effect: customBlurEffect_ios14)
161 | setupViews()
162 | }
163 | }
164 | }
165 |
166 | private class BlurEffect: UIBlurEffect {
167 |
168 | public var blurRadius: CGFloat = 10.0
169 |
170 | private enum Constants {
171 | static let blurRadiusSettingKey = "blurRadius"
172 | }
173 |
174 | class func effect(with style: UIBlurEffect.Style) -> BlurEffect {
175 | let result = super.init(style: style)
176 | object_setClass(result, self)
177 | return result as! BlurEffect
178 | }
179 |
180 | override func copy(with zone: NSZone? = nil) -> Any {
181 | let result = super.copy(with: zone)
182 | object_setClass(result, Self.self)
183 | return result
184 | }
185 |
186 | override var effectSettings: AnyObject {
187 | get {
188 | let settings = super.effectSettings
189 | settings.setValue(blurRadius, forKey: Constants.blurRadiusSettingKey)
190 | return settings
191 | }
192 | set {
193 | super.effectSettings = newValue
194 | }
195 | }
196 | }
197 |
198 | private var AssociatedObjectHandle: UInt8 = 0
199 |
200 | private extension UIVisualEffect {
201 |
202 | @objc var effectSettings: AnyObject {
203 | get {
204 | return objc_getAssociatedObject(self, &AssociatedObjectHandle) as AnyObject
205 | }
206 | set {
207 | objc_setAssociatedObject(self, &AssociatedObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
208 | }
209 | }
210 | }
211 | #endif
212 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/ExpandableSection.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct ExpandableSection: View {
4 |
5 | @State private var isExpanded = true
6 |
7 | private var content: () -> Content
8 | private var header: () -> Header
9 |
10 | private let isExpandable: Bool
11 |
12 | public init(isExpandable: Bool, @ViewBuilder content: @escaping () -> Content, @ViewBuilder header: @escaping () -> Header) {
13 | self.isExpandable = isExpandable
14 | self.content = content
15 | self.header = header
16 | }
17 |
18 | public var body: some View {
19 | if isExpandable {
20 | if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) {
21 | Section(isExpanded: $isExpanded) {
22 | content()
23 | } header: {
24 | header()
25 | }
26 | } else {
27 | Section {
28 | content()
29 | } header: {
30 | header()
31 | }
32 | }
33 | } else {
34 | Section {
35 | content()
36 | } header: {
37 | header()
38 | }
39 | }
40 | }
41 | }
42 |
43 | public extension ExpandableSection where Header == EmptyView {}
44 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/FadeBlurView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @available(iOS 15.0, iOSApplicationExtension 15.0, watchOS 8.0, macOS 12.0, *)
4 | public struct FadeBlurView: View {
5 |
6 | let style: any ShapeStyle
7 | let height: Double
8 | let startPoint: UnitPoint
9 |
10 | public init(style: any ShapeStyle, height: Double, startPoint: UnitPoint) {
11 | guard startPoint == .top || startPoint == .bottom else { fatalError() }
12 | self.style = style
13 | self.height = height
14 | self.startPoint = startPoint
15 | }
16 |
17 | public var body: some View {
18 | Rectangle()
19 | .fill(AnyShapeStyle(style))
20 | .frame(height: height)
21 | .mask {
22 | LinearGradient(
23 | colors: [.clear, .black, .black],
24 | startPoint: startPoint,
25 | endPoint: startPoint == .top ? .bottom : .top
26 | )
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/FixedSpacer.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct FixedSpacer: View {
4 |
5 | let height: CGFloat?
6 | let width: CGFloat?
7 |
8 | public init(height: CGFloat) {
9 | self.height = height
10 | self.width = nil
11 | }
12 |
13 | public init(width: CGFloat) {
14 | self.width = width
15 | self.height = nil
16 | }
17 |
18 | public var body: some View {
19 | Spacer()
20 | .if(height != nil, transform: { view in
21 | view.frame(height: height)
22 | })
23 | .if(width != nil, transform: { view in
24 | view.frame(width: width)
25 | })
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/LargeButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension ButtonStyle where Self == LargeButtonStyle {
4 |
5 | public static func large(_ colorise: ButtonColorise) -> Self {
6 | return LargeButtonStyle(colorise)
7 | }
8 | }
9 |
10 | public struct LargeButtonStyle: ButtonStyle {
11 |
12 | @Environment(\.isEnabled) private var isEnabled: Bool
13 |
14 | let colorise: ButtonColorise
15 |
16 | init(_ colorise: ButtonColorise) {
17 | self.colorise = colorise
18 | }
19 |
20 | public func makeBody(configuration: Configuration) -> some View {
21 | configuration.label
22 | .font(.headline)
23 | .foregroundColor(colorise.foregroundColor)
24 | .frame(maxWidth: .infinity)
25 | .padding(.vertical, 15)
26 | .padding(.horizontal, 10)
27 | .background {
28 | colorise.backgroundColor
29 | .clipShape(.rect(cornerRadius: 12))
30 | }
31 | .opacity(configuration.isPressed ? 0.7 : 1)
32 | .animation(.default, value: configuration.isPressed)
33 | .opacity(isEnabled ? 1 : 0.4)
34 | .animation(.default, value: isEnabled)
35 | }
36 | }
37 |
38 | public enum ButtonColorise {
39 |
40 | case colorful
41 | case tinted
42 | case grayed
43 |
44 | var foregroundColor: Color {
45 | switch self {
46 | case .colorful:
47 | return .white
48 | case .tinted:
49 | return .accentColor
50 | case .grayed:
51 | return .accentColor
52 | }
53 | }
54 | var backgroundColor: Color {
55 | switch self {
56 | case .colorful:
57 | return .accentColor
58 | case .tinted:
59 | return .accentColor.opacity(0.12)
60 | case .grayed:
61 | return .gray.opacity(0.15)
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/MimicrateCloseButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct MimicrateCloseButton: View {
4 |
5 | let action: () -> Void
6 |
7 | var body: some View {
8 | Button {
9 | action()
10 | } label: {
11 | Image(systemName: "xmark")
12 | .font(.footnote.bold())
13 | .foregroundColor(.gray)
14 | .padding(7)
15 | .background {
16 | Circle()
17 | }
18 | .tint(.gray.opacity(0.2))
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/ModalHeaderView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct ModalHeaderView: View {
4 |
5 | let topView: AnyView
6 |
7 | let systemIconName: String?
8 | let titleText: String
9 | let bodyText: String
10 |
11 | public init(@ViewBuilder topView: @escaping () -> TopView, title: String, body: String) {
12 | self.systemIconName = nil
13 | self.topView = AnyView(topView())
14 | self.titleText = title
15 | self.bodyText = body
16 | }
17 |
18 | public init(systemIconName: String, title: String, body: String) {
19 | self.init(topView: {
20 | Image(systemName: systemIconName)
21 | .resizable()
22 | .scaledToFit()
23 | .foregroundColor(.accentColor)
24 | .font(.body.weight(.regular))
25 | .frame(width: 52)
26 | }, title: title, body: body)
27 | }
28 |
29 | public var body: some View {
30 | VStack(spacing: .zero) {
31 |
32 | topView
33 |
34 | FixedSpacer(height: 32)
35 |
36 | VStack(spacing: 8) {
37 | Text(titleText)
38 | .font(.title.weight(.semibold))
39 | .foregroundStyle(.primary)
40 | Text(bodyText)
41 | .font(.subheadline)
42 | .foregroundStyle(.secondary)
43 | }
44 | .multilineTextAlignment(.center)
45 | }
46 | .padding(.horizontal, 24)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/ModalSheet/ModalSheet.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import SwiftUI
3 |
4 | extension View {
5 |
6 | public func modalSheet(
7 | isPresented: Binding,
8 | selection: Binding,
9 | dismissable: Bool,
10 | onDismiss: (() -> Void)? = nil,
11 | @ViewBuilder content: @escaping () -> Content
12 | ) -> some View where Content : View {
13 |
14 | let sheet = ModalSheetModifier(
15 | isPresented: isPresented,
16 | selection: selection,
17 | dismissable: dismissable,
18 | onDismiss: onDismiss,
19 | modalContent: content
20 | )
21 |
22 | return self.modifier(sheet)
23 | }
24 |
25 | public func modalSheet(
26 | isPresented: Binding,
27 | dismissable: Bool,
28 | onDismiss: (() -> Void)? = nil,
29 | @ViewBuilder content: @escaping () -> Content
30 | ) -> some View where Content : View {
31 |
32 | let sheet = ModalSheetModifier(
33 | isPresented: isPresented,
34 | selection: .constant(""),
35 | dismissable: dismissable,
36 | onDismiss: onDismiss,
37 | modalContent: content
38 | )
39 |
40 | return self.modifier(sheet)
41 | }
42 | }
43 | #endif
44 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/ModalSheet/ModalSheetContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /**
4 | Idea have to reusable basic style of modal sheet content with title+descrition & action button.
5 | */
6 | public struct ModalSheetContentView: View {
7 |
8 | let title: String
9 | let description: String?
10 | let content: () -> Content
11 | let actionContent: () -> ActionContent
12 | let actionEnabled: Bool
13 | let action: () -> Void
14 | let dismissable: Bool
15 |
16 | public init(
17 | title: String,
18 | description: String?,
19 | @ViewBuilder content: @escaping () -> Content,
20 | @ViewBuilder actionContent: @escaping () -> ActionContent,
21 | actionEnabled: Bool,
22 | action: @escaping () -> Void,
23 | dismissable: Bool
24 | ) {
25 | self.title = title
26 | self.description = description
27 | self.content = content
28 | self.actionContent = actionContent
29 | self.actionEnabled = actionEnabled
30 | self.action = action
31 |
32 | self.dismissable = dismissable
33 | }
34 |
35 | public var body: some View {
36 | VStack(alignment: .center, spacing: .zero) {
37 |
38 | VStack(alignment: .center) {
39 | Text(title)
40 | .font(.title.weight(.bold))
41 |
42 | if let description {
43 | Text(description)
44 | }
45 | }
46 |
47 | FixedSpacer(height: Spaces.default_double)
48 | content()
49 | FixedSpacer(height: Spaces.default_double)
50 |
51 | Button {
52 | action()
53 | } label: {
54 | actionContent()
55 | }
56 | .buttonStyle(.large(.tinted))
57 | .disabled(!actionEnabled)
58 | }
59 | .multilineTextAlignment(.center)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/ModalSheet/ModalSheetModifier.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS)
2 | import SwiftUI
3 | import SwiftBoost
4 |
5 | struct ModalSheetModifier: ViewModifier {
6 |
7 | // States
8 | @Binding private var isPresented: Bool
9 | @Binding private var selection: Selection
10 | private let dismissable: Bool
11 | private let onDismiss: (() -> Void)?
12 |
13 | // Content
14 | private let modalContent: () -> ModalContent
15 |
16 | // Private
17 | @State private var dragOffset: CGSize = .zero
18 |
19 | init(isPresented: Binding, selection: Binding, dismissable: Bool, onDismiss: (() -> Void)?, @ViewBuilder modalContent: @escaping () -> ModalContent) {
20 | self._isPresented = isPresented
21 | self._selection = selection
22 | self.dismissable = dismissable
23 | self.onDismiss = onDismiss
24 | self.modalContent = modalContent
25 | }
26 |
27 | func body(content: Content) -> some View {
28 | ZStack {
29 |
30 | content
31 | .zIndex(0)
32 |
33 | if isPresented {
34 |
35 | // Background
36 | Color.black.opacity(0.4)
37 | .ignoresSafeArea()
38 | .transition(.opacity)
39 | .animation(.easeOut, value: isPresented)
40 | .zIndex(1)
41 |
42 | // Content
43 | VStack {
44 | Spacer()
45 | VStack {
46 | modalContent()
47 | .transitionBlurReplaceCombability()
48 | .padding(.top, Spaces.default_half + Spaces.default_more)
49 | .padding(.bottom, Spaces.default_more)
50 | .padding(.horizontal, Spaces.default_double)
51 | .overlay {
52 | if dismissable {
53 | VStack {
54 | MimicrateCloseButton {
55 | isPresented = false
56 | }
57 | Spacer()
58 | }
59 | .frame(maxWidth: .infinity, alignment: .trailing)
60 | .padding(Spaces.default_more)
61 | }
62 | }
63 | }
64 | .background {
65 | Color(uiColor: .secondarySystemGroupedBackground)
66 | }
67 | .clipShape(.rect(cornerRadius: cornerRadius))
68 | .shadow(color: .black.opacity(0.12), radius: 6, x: .zero, y: 6)
69 | .shadow(color: .black.opacity(0.15), radius: 16, x: .zero, y: 12)
70 | .overlay {
71 | let color = Color(uiColor: .tertiarySystemGroupedBackground)
72 | RoundedRectangle(cornerRadius: cornerRadius)
73 | .strokeBorder(color, lineWidth: 1)
74 | }
75 | .padding(.horizontal, padding)
76 | .padding(.bottom, padding)
77 | .frame(maxWidth: 440)
78 | .offset(y: calculateDragOffset)
79 | .gesture(
80 | DragGesture()
81 | .onChanged { gesture in
82 | dragOffset = gesture.translation
83 | }
84 | .onEnded { _ in
85 | if dragOffset.height > 100 && dismissable {
86 | isPresented = false
87 | } else {
88 | withAnimation(.interpolatingSpring(duration: 0.26)) {
89 | dragOffset = .zero
90 | }
91 | }
92 | }
93 | )
94 | }
95 | .ignoresSafeArea()
96 | .transition(.move(edge: .bottom))
97 | .zIndex(2)
98 | }
99 | }
100 | .animation(.smooth(duration: presentDimissDuration), value: isPresented)
101 | .animation(.default, value: selection)
102 | .onChange(of: isPresented) { isPresented in
103 | if !isPresented {
104 | dragOffset = .zero
105 | delay(presentDimissDuration) {
106 | self.onDismiss?()
107 | }
108 | }
109 | }
110 | }
111 |
112 | // MARK: - Private
113 |
114 | private var calculateDragOffset: CGFloat {
115 | let dragDistance = dragOffset.height
116 | let calm = dragDistance < 0 || !dismissable
117 |
118 | if calm {
119 | let squaredDistance = sqrt(abs(dragDistance))
120 | if dragDistance < 0 {
121 | return max(-squaredDistance * 3, dragDistance)
122 | } else {
123 | return min(squaredDistance * 2, dragDistance)
124 | }
125 | } else {
126 | return dragDistance
127 | }
128 | }
129 |
130 | // MARK: - Constants
131 |
132 | private var padding: CGFloat = 10
133 | private var cornerRadius: CGFloat { UIScreen.main.displayCornerRadius - padding }
134 | private var presentDimissDuration: TimeInterval { 0.41 }
135 | }
136 | #endif
137 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/NativeSection.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SwiftUIIntrospect
3 |
4 | public struct NativeSection: View {
5 |
6 | let title: String
7 | @ViewBuilder let content: () -> Content
8 | let detail: (() -> Detail)?
9 |
10 | public init(
11 | _ title: String,
12 | @ViewBuilder content: @escaping () -> Content,
13 | detail: (() -> Detail)? = nil
14 | ) {
15 | self.title = title
16 | self.content = content
17 | self.detail = detail
18 | }
19 |
20 | public var body: some View {
21 | VStack(spacing: Spaces.default_less) {
22 | Section {
23 | content()
24 | } header: {
25 | HStack(spacing: Spaces.step) {
26 |
27 | if let detail = self.detail {
28 | #if os(iOS)
29 | NavigationLink {
30 | detail()
31 | .navigationTitle(title)
32 | .navigationBarTitleDisplayMode(.inline)
33 | } label: {
34 | VStack {
35 | HStack(alignment: .firstTextBaseline, spacing: Spaces.step) {
36 | Text(title)
37 | .foregroundColor(.primary)
38 | .font(.title2.weight(.bold))
39 | Image(systemName: "chevron.right")
40 | .foregroundColor(.secondary)
41 | .font(.footnote.weight(.heavy))
42 | }
43 | .baselineOffsetCompability(-1.5)
44 | //.padding(.leading, Spaces.default)
45 | }
46 | .frame(maxWidth: .infinity, alignment: .leading)
47 | }
48 | .buttonStyle(.plain)
49 | #elseif os(visionOS)
50 | HStack(alignment: .center, spacing: Spaces.default_half) {
51 |
52 | Text(title)
53 | .foregroundColor(.primary)
54 | .font(.title2)
55 | .fontWeightCompability(.bold)
56 |
57 | NavigationLink {
58 | detail()
59 | .navigationTitle(title)
60 | .navigationBarTitleDisplayMode(.inline)
61 | } label: {
62 | Image(.chevron.right)
63 | .foregroundColor(.primary)
64 | .font(.footnote)
65 | .fontWeightCompability(.bold)
66 | }
67 | .buttonStyle(.borderedProminent)
68 | .buttonBorderShape(.circle)
69 | .controlSize(.mini)
70 |
71 | Spacer()
72 | }
73 | #endif
74 | } else {
75 | VStack {
76 | Text(title)
77 | .foregroundColor(.primary)
78 | .font(.title2.weight(.bold))
79 | //.padding(.leading, Spaces.default)
80 | //.frame(maxWidth: .infinity, alignment: .leading)
81 | }
82 | .frame(maxWidth: .infinity, alignment: .leading)
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | private var space: CGFloat {
90 | #if os(iOS)
91 | return Spaces.default_less
92 | #elseif os(visionOS)
93 | return Spaces.step
94 | #else
95 | return 0
96 | #endif
97 | }
98 | }
99 |
100 | extension NativeSection where Detail == Never {
101 |
102 | public init(_ title: String, @ViewBuilder content: @escaping () -> Content) {
103 | self.init(title, content: content, detail: nil)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/NavigationBarCloseButton.swift:
--------------------------------------------------------------------------------
1 | #if canImport(SwiftUI) && canImport(UIKit) && os(iOS)
2 | import SwiftUI
3 | import UIKit
4 |
5 | public struct NavigationBarCloseButton: UIViewRepresentable {
6 |
7 | private let action: () -> Void
8 |
9 | public init(action: @escaping () -> Void) {
10 | self.action = action
11 | }
12 |
13 | public func makeUIView(context: Context) -> UIButton {
14 | let button = UIButton(type: .close)
15 |
16 | button.setContentCompressionResistancePriority(.required, for: .horizontal)
17 | button.setContentCompressionResistancePriority(.required, for: .vertical)
18 | button.setContentHuggingPriority(.required, for: .horizontal)
19 | button.setContentHuggingPriority(.required, for: .vertical)
20 |
21 | button.addTarget(context.coordinator, action: #selector(Coordinator.perform), for: .primaryActionTriggered)
22 | return button
23 | }
24 |
25 | public func updateUIView(_ uiView: UIButton, context: Context) {
26 | context.coordinator.action = action
27 | }
28 |
29 | public func makeCoordinator() -> Coordinator {
30 | Coordinator(action: action)
31 | }
32 |
33 | public class Coordinator {
34 |
35 | var action: () -> Void
36 |
37 | init(action: @escaping () -> Void) {
38 | self.action = action
39 | }
40 |
41 | @objc func perform() {
42 | action()
43 | }
44 | }
45 | }
46 | #endif
47 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/Mimicrate/ScrollWithBottomContent.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS) || os(tvOS)
2 | import SwiftUI
3 |
4 | public struct ScrollWithBottomContent: View {
5 |
6 | private var content: () -> Content
7 | private var bottom: () -> Bottom
8 |
9 | private let topFade: CGFloat?
10 | private let bottomFade: CGFloat?
11 |
12 | public init(
13 | topFade: CGFloat? = nil,
14 | bottomFade: CGFloat? = Spaces.default_double,
15 | @ViewBuilder content: @escaping () -> Content,
16 | @ViewBuilder bottom: @escaping () -> Bottom
17 | ) {
18 | self.topFade = topFade
19 | self.bottomFade = bottomFade
20 |
21 | self.content = content
22 | self.bottom = bottom
23 | }
24 |
25 | public var body: some View {
26 | ScrollView(.vertical) {
27 | content()
28 | .frame(maxWidth: .infinity)
29 | .padding(.top, topFade ?? .zero)
30 | .padding(.bottom, bottomFade ?? .zero)
31 | }
32 | .fitGuide(.layoutMargings, padding: .scroll)
33 | .fade(top: topFade, bottom: bottomFade)
34 | .safeAreaInset(edge: .bottom) {
35 | bottom()
36 | .padding(.bottom, Spaces.default_more)
37 | .fitGuide(.layoutMargings)
38 | }
39 | }
40 | }
41 | #endif
42 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/SelectableStack.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct SelectableStack: View where Data : RandomAccessCollection, Data.Element : Identifiable, Data.Element : Equatable, Content: View {
4 |
5 | private let alignment: Alignment
6 | private let space: CGFloat
7 | private let data: Data?
8 | private let content: (Data.Element, Bool) -> Content
9 |
10 | @Binding private var selectedElement: Data.Element?
11 |
12 | public init(
13 | alignment: Alignment,
14 | space: CGFloat = Spaces.default_half,
15 | data: Data?,
16 | selectedElement: Binding,
17 | @ViewBuilder content: @escaping (Data.Element, Bool) -> Content
18 | ) {
19 | self.alignment = alignment
20 | self.space = space
21 | self.data = data
22 | self._selectedElement = selectedElement
23 | self.content = content
24 | }
25 |
26 | public var body: some View {
27 | switch alignment {
28 | case .vertical:
29 | VStack(spacing: space) {
30 | if let data {
31 | ForEach(data) { element in
32 | content(element, selectedElement == element)
33 | }
34 | }
35 | }
36 | #if os(iOS)
37 | .onChange(of: selectedElement) { _ in
38 | UIFeedbackGenerator.impactOccurred(.light)
39 | }
40 | #endif
41 | case .horizontal:
42 | HStack(spacing: space) {
43 | if let data {
44 | ForEach(data) { element in
45 | content(element, selectedElement == element)
46 | }
47 | }
48 | }
49 | #if os(iOS)
50 | .onChange(of: selectedElement) { _ in
51 | UIFeedbackGenerator.impactOccurred(.light)
52 | }
53 | #endif
54 | }
55 | }
56 |
57 | public enum Alignment {
58 |
59 | case vertical
60 | case horizontal
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/SwiftUIExtension/Views/SelectableVStack.swift:
--------------------------------------------------------------------------------
1 | /*import SwiftUI
2 |
3 | public struct SelectableVStack: View where Data : RandomAccessCollection, Data.Element : Identifiable, Data.Element : Equatable, Content: View {
4 |
5 | @Binding private var selectedElement: Data.Element
6 |
7 | private let data: Data
8 | private let spacing: CGFloat
9 | private let content: (Data.Element, Bool) -> Content
10 |
11 | public init(
12 | data: Data,
13 | selectedElement: Binding,
14 | spacing: CGFloat,
15 | @ViewBuilder content: @escaping (Data.Element, Bool) -> Content
16 | ) {
17 | self.data = data
18 | self.spacing = spacing
19 | self._selectedElement = selectedElement
20 | self.content = content
21 | }
22 |
23 | public var body: some View {
24 | VStack(spacing: spacing) {
25 | ForEach(data) { element in
26 | content(element, selectedElement == element)
27 | }
28 | }
29 | }
30 | }*/
31 |
32 |
--------------------------------------------------------------------------------