├── .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 | --------------------------------------------------------------------------------