├── .gitignore ├── .spi.yml ├── .swiftlint.yml ├── Demo ├── Demo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── Demo │ ├── Alerts │ ├── CustomAlerts.swift │ ├── InputAlerts.swift │ ├── MultiButtonAlerts.swift │ ├── OtherAlerts.swift │ ├── ScrollContentAlerts.swift │ └── SimpleAlerts.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── jane.imageset │ │ ├── Contents.json │ │ └── jane.svg │ ├── ContentView.swift │ ├── Demo.entitlements │ ├── DemoApp.swift │ ├── DetailLabel.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── Package.swift ├── Package@swift-6.0.swift ├── README.md └── Sources └── CustomAlert ├── API+Bool.swift ├── API+Identifiable.swift ├── AlertButtonStyle.swift ├── Assets.xcassets ├── Contents.json ├── DimmingBackround.colorset │ └── Contents.json └── Disabled.colorset │ └── Contents.json ├── Configuration ├── CustomAlertBackground.swift ├── CustomAlertConfiguration.Alert.swift ├── CustomAlertConfiguration.Button.swift ├── CustomAlertConfiguration.swift └── CustomAlertShadow.swift ├── CustomAlertHandler+Item.swift ├── CustomAlertHandler.swift ├── Documentation.docc ├── API.md ├── Documentation.md └── Resources │ ├── Complex.png │ ├── Custom.png │ ├── CustomConfiguration.png │ ├── Fancy.png │ ├── InlineAlert.png │ └── SwiftUI.png ├── Environment ├── Environment+AlertButtonHeight.swift ├── Environment+AlertDismissAction.swift └── Environment+CustomAlertConfiguration.swift ├── Extensions ├── ColorExtensions.swift ├── EdgeInsetsExtensions.swift ├── MainActorExtensions.swift └── ProcessInfoExtensions.swift ├── Helper ├── CaptureSize.swift ├── OnSimultaneousTapGesture.swift ├── ScrollViewDisabled.swift └── ShadowApplier.swift └── Views ├── BackgroundView.swift ├── BlurView.swift ├── CustomAlert.swift ├── CustomAlertRow.swift ├── CustomAlertSection.swift └── MultiButton.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [CustomAlert] 5 | custom_documentation_parameters: [--include-extended-types] 6 | platform: ios -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # By default, SwiftLint uses a set of sensible default rules you can adjust: 2 | disabled_rules: # rule identifiers turned on by default to exclude from running 3 | # - attribute_name_spacing 4 | # - blanket_disable_command 5 | # - block_based_kvo 6 | # - class_delegate_protocol 7 | # - closing_brace 8 | # - closure_parameter_position 9 | # - colon 10 | # - comma 11 | # - comment_spacing 12 | # - compiler_protocol_init 13 | # - computed_accessors_order 14 | # - control_statement 15 | # - cyclomatic_complexity 16 | # - deployment_target 17 | # - discouraged_direct_init 18 | # - duplicate_conditions 19 | # - duplicate_enum_cases 20 | # - duplicate_imports 21 | # - duplicated_key_in_dictionary_literal 22 | # - dynamic_inline 23 | # - empty_enum_arguments 24 | # - empty_parameters 25 | # - empty_parentheses_with_trailing_closure 26 | # - file_length 27 | # - for_where 28 | # - force_cast 29 | # - force_try 30 | # - function_body_length 31 | # - function_parameter_count 32 | # - generic_type_name 33 | # - identifier_name 34 | # - implicit_getter 35 | # - inclusive_language 36 | # - invalid_swiftlint_command 37 | # - is_disjoint 38 | # - large_tuple 39 | # - leading_whitespace 40 | # - legacy_cggeometry_functions 41 | # - legacy_constant 42 | # - legacy_constructor 43 | # - legacy_hashing 44 | # - legacy_nsgeometry_functions 45 | # - legacy_random 46 | - line_length 47 | # - mark 48 | # - multiple_closures_with_trailing_closure 49 | # - nesting 50 | # - no_fallthrough_only 51 | # - no_space_in_method_call 52 | # - non_optional_string_data_conversion 53 | # - notification_center_detachment 54 | # - ns_number_init_as_function_reference 55 | # - nsobject_prefer_isequal 56 | # - opening_brace 57 | # - operator_whitespace 58 | # - optional_data_string_conversion 59 | # - orphaned_doc_comment 60 | # - prefer_type_checking 61 | # - private_over_fileprivate 62 | # - private_unit_test 63 | # - protocol_property_accessors_order 64 | # - reduce_boolean 65 | # - redundant_discardable_let 66 | # - redundant_objc_attribute 67 | # - redundant_optional_initialization 68 | - redundant_sendable 69 | # - redundant_set_access_control 70 | # - redundant_string_enum_value 71 | # - redundant_void_return 72 | # - return_arrow_whitespace 73 | # - self_in_property_initialization 74 | # - shorthand_operator 75 | # - statement_position 76 | # - static_over_final_class 77 | # - superfluous_disable_command 78 | # - switch_case_alignment 79 | # - syntactic_sugar 80 | # - todo 81 | # - trailing_comma 82 | # - trailing_newline 83 | # - trailing_semicolon 84 | - trailing_whitespace 85 | # - type_body_length 86 | # - type_name 87 | # - unavailable_condition 88 | # - unneeded_break_in_switch 89 | # - unneeded_override 90 | # - unneeded_synthesized_initializer 91 | # - unused_closure_parameter 92 | # - unused_control_flow_label 93 | # - unused_enumerated 94 | # - unused_optional_binding 95 | # - unused_setter_value 96 | # - valid_ibinspectable 97 | # - vertical_parameter_alignment 98 | # - vertical_whitespace 99 | # - void_function_in_ternary 100 | # - void_return 101 | # - xctfail_message 102 | opt_in_rules: # some rules are turned off by default, so you need to opt-in 103 | - accessibility_label_for_image 104 | - accessibility_trait_for_button 105 | # - anonymous_argument_in_multiline_closure 106 | - array_init 107 | # - async_without_await 108 | # - attributes 109 | - balanced_xctest_lifecycle 110 | # - capture_variable 111 | # - closure_body_length 112 | - closure_end_indentation 113 | - closure_spacing 114 | - collection_alignment 115 | - comma_inheritance 116 | # - conditional_returns_on_newline 117 | - contains_over_filter_count 118 | - contains_over_filter_is_empty 119 | - contains_over_first_not_nil 120 | - contains_over_range_nil_comparison 121 | # - contrasted_opening_brace 122 | - convenience_type 123 | - direct_return 124 | - discarded_notification_center_observer 125 | - discouraged_assert 126 | - discouraged_none_name 127 | - discouraged_object_literal 128 | - discouraged_optional_boolean 129 | # - discouraged_optional_collection 130 | - empty_collection_literal 131 | - empty_count 132 | - empty_string 133 | - empty_xctest_method 134 | - enum_case_associated_values_count 135 | - expiring_todo 136 | # - explicit_acl 137 | # - explicit_enum_raw_value 138 | - explicit_init 139 | # - explicit_self 140 | # - explicit_top_level_acl 141 | # - explicit_type_interface 142 | # - extension_access_modifier 143 | - fallthrough 144 | - fatal_error_message 145 | - file_header 146 | # - file_name 147 | - file_name_no_space 148 | # - file_types_order 149 | # - final_test_case 150 | - first_where 151 | - flatmap_over_map_reduce 152 | - force_unwrapping 153 | # - function_default_parameter_at_end 154 | - ibinspectable_in_extension 155 | - identical_operands 156 | # - implicit_return 157 | - implicitly_unwrapped_optional 158 | - indentation_width 159 | - joined_default_parameter 160 | - last_where 161 | - legacy_multiple 162 | - legacy_objc_type 163 | - let_var_whitespace 164 | - literal_expression_end_indentation 165 | - local_doc_comment 166 | - lower_acl_than_parent 167 | - missing_docs 168 | - modifier_order 169 | - multiline_arguments 170 | - multiline_arguments_brackets 171 | - multiline_function_chains 172 | - multiline_literal_brackets 173 | - multiline_parameters 174 | - multiline_parameters_brackets 175 | - nimble_operator 176 | # - no_empty_block 177 | # - no_extension_access_modifier 178 | - no_grouping_extension 179 | # - no_magic_numbers 180 | # - non_overridable_class_declaration 181 | - nslocalizedstring_key 182 | # - nslocalizedstring_require_bundle 183 | - number_separator 184 | - object_literal 185 | # - one_declaration_per_file 186 | - operator_usage_whitespace 187 | - optional_enum_case_matching 188 | - overridden_super_call 189 | - override_in_extension 190 | - pattern_matching_keywords 191 | - period_spacing 192 | # - prefer_key_path 193 | - prefer_nimble 194 | # - prefer_self_in_static_references 195 | # - prefer_self_type_over_type_of_self 196 | - prefer_zero_over_explicit_init 197 | - prefixed_toplevel_constant 198 | - private_action 199 | - private_outlet 200 | - private_subject 201 | - private_swiftui_state 202 | - prohibited_interface_builder 203 | - prohibited_super_call 204 | - quick_discouraged_call 205 | - quick_discouraged_focused_test 206 | - quick_discouraged_pending_test 207 | - raw_value_for_camel_cased_codable_enum 208 | - reduce_into 209 | - redundant_nil_coalescing 210 | - redundant_self_in_closure 211 | # - redundant_type_annotation 212 | # - required_deinit 213 | - required_enum_case 214 | - return_value_from_void_function 215 | - self_binding 216 | # - shorthand_argument 217 | - shorthand_optional_binding 218 | - single_test_class 219 | # - sorted_enum_cases 220 | - sorted_first_last 221 | # - sorted_imports 222 | - static_operator 223 | - strict_fileprivate 224 | - strong_iboutlet 225 | # - superfluous_else 226 | - switch_case_on_newline 227 | - test_case_accessibility 228 | - toggle_bool 229 | # - trailing_closure 230 | # - type_contents_order 231 | # - typesafe_array_init 232 | - unavailable_function 233 | - unhandled_throwing_task 234 | - unneeded_parentheses_in_closure_argument 235 | - unowned_variable_capture 236 | - untyped_error_in_catch 237 | # - unused_declaration 238 | # - unused_import 239 | # - unused_parameter 240 | - vertical_parameter_alignment_on_call 241 | # - vertical_whitespace_between_cases 242 | - vertical_whitespace_closing_braces 243 | - vertical_whitespace_opening_braces 244 | - weak_delegate 245 | - xct_specific_matcher 246 | - yoda_condition 247 | 248 | included: 249 | - Sources 250 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6302131F2960A4850065DBDD /* InputAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6302131E2960A4850065DBDD /* InputAlerts.swift */; }; 11 | 630213212960A4EE0065DBDD /* MultiButtonAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630213202960A4EE0065DBDD /* MultiButtonAlerts.swift */; }; 12 | 630213232960A55B0065DBDD /* OtherAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630213222960A55B0065DBDD /* OtherAlerts.swift */; }; 13 | 630CF3DF2C39B52500B6FF68 /* DetailLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630CF3DE2C39B52500B6FF68 /* DetailLabel.swift */; }; 14 | 630CF3E22C39B6E400B6FF68 /* CustomAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630CF3E12C39B6E400B6FF68 /* CustomAlerts.swift */; }; 15 | 630CF3E42C39B6ED00B6FF68 /* SimpleAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 630CF3E32C39B6ED00B6FF68 /* SimpleAlerts.swift */; }; 16 | 6364916428AFEAFB00FA518B /* DemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6364916328AFEAFB00FA518B /* DemoApp.swift */; }; 17 | 6364916628AFEAFB00FA518B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6364916528AFEAFB00FA518B /* ContentView.swift */; }; 18 | 6364916828AFEAFC00FA518B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6364916728AFEAFC00FA518B /* Assets.xcassets */; }; 19 | 6364916B28AFEAFC00FA518B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6364916A28AFEAFC00FA518B /* Preview Assets.xcassets */; }; 20 | 6364917528AFEB6D00FA518B /* CustomAlert in Frameworks */ = {isa = PBXBuildFile; productRef = 6364917428AFEB6D00FA518B /* CustomAlert */; }; 21 | 63787E4D2D43D0740004009E /* ScrollContentAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63787E4C2D43D0740004009E /* ScrollContentAlerts.swift */; }; 22 | /* End PBXBuildFile section */ 23 | 24 | /* Begin PBXFileReference section */ 25 | 6302131E2960A4850065DBDD /* InputAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputAlerts.swift; sourceTree = ""; }; 26 | 630213202960A4EE0065DBDD /* MultiButtonAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiButtonAlerts.swift; sourceTree = ""; }; 27 | 630213222960A55B0065DBDD /* OtherAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherAlerts.swift; sourceTree = ""; }; 28 | 630CF3DE2C39B52500B6FF68 /* DetailLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailLabel.swift; sourceTree = ""; }; 29 | 630CF3E12C39B6E400B6FF68 /* CustomAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomAlerts.swift; sourceTree = ""; }; 30 | 630CF3E32C39B6ED00B6FF68 /* SimpleAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleAlerts.swift; sourceTree = ""; }; 31 | 6332AEB02CC4F19A00C4A1BC /* Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Demo.entitlements; sourceTree = ""; }; 32 | 6364916028AFEAFB00FA518B /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 6364916328AFEAFB00FA518B /* DemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoApp.swift; sourceTree = ""; }; 34 | 6364916528AFEAFB00FA518B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 35 | 6364916728AFEAFC00FA518B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 36 | 6364916A28AFEAFC00FA518B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 37 | 6364917228AFEB0700FA518B /* CustomAlert */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CustomAlert; path = ..; sourceTree = ""; }; 38 | 63787E4C2D43D0740004009E /* ScrollContentAlerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollContentAlerts.swift; sourceTree = ""; }; 39 | /* End PBXFileReference section */ 40 | 41 | /* Begin PBXFrameworksBuildPhase section */ 42 | 6364915D28AFEAFB00FA518B /* Frameworks */ = { 43 | isa = PBXFrameworksBuildPhase; 44 | buildActionMask = 2147483647; 45 | files = ( 46 | 6364917528AFEB6D00FA518B /* CustomAlert in Frameworks */, 47 | ); 48 | runOnlyForDeploymentPostprocessing = 0; 49 | }; 50 | /* End PBXFrameworksBuildPhase section */ 51 | 52 | /* Begin PBXGroup section */ 53 | 630CF3E02C39B68F00B6FF68 /* Alerts */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 630CF3E32C39B6ED00B6FF68 /* SimpleAlerts.swift */, 57 | 63787E4C2D43D0740004009E /* ScrollContentAlerts.swift */, 58 | 6302131E2960A4850065DBDD /* InputAlerts.swift */, 59 | 630213202960A4EE0065DBDD /* MultiButtonAlerts.swift */, 60 | 630213222960A55B0065DBDD /* OtherAlerts.swift */, 61 | 630CF3E12C39B6E400B6FF68 /* CustomAlerts.swift */, 62 | ); 63 | path = Alerts; 64 | sourceTree = ""; 65 | }; 66 | 6364915728AFEAFB00FA518B = { 67 | isa = PBXGroup; 68 | children = ( 69 | 6364916228AFEAFB00FA518B /* Demo */, 70 | 6364917128AFEB0700FA518B /* Packages */, 71 | 6364916128AFEAFB00FA518B /* Products */, 72 | 6364917328AFEB6D00FA518B /* Frameworks */, 73 | ); 74 | sourceTree = ""; 75 | }; 76 | 6364916128AFEAFB00FA518B /* Products */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | 6364916028AFEAFB00FA518B /* Demo.app */, 80 | ); 81 | name = Products; 82 | sourceTree = ""; 83 | }; 84 | 6364916228AFEAFB00FA518B /* Demo */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | 6332AEB02CC4F19A00C4A1BC /* Demo.entitlements */, 88 | 6364916328AFEAFB00FA518B /* DemoApp.swift */, 89 | 6364916528AFEAFB00FA518B /* ContentView.swift */, 90 | 630CF3E02C39B68F00B6FF68 /* Alerts */, 91 | 630CF3DE2C39B52500B6FF68 /* DetailLabel.swift */, 92 | 6364916728AFEAFC00FA518B /* Assets.xcassets */, 93 | 6364916928AFEAFC00FA518B /* Preview Content */, 94 | ); 95 | path = Demo; 96 | sourceTree = ""; 97 | }; 98 | 6364916928AFEAFC00FA518B /* Preview Content */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | 6364916A28AFEAFC00FA518B /* Preview Assets.xcassets */, 102 | ); 103 | path = "Preview Content"; 104 | sourceTree = ""; 105 | }; 106 | 6364917128AFEB0700FA518B /* Packages */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 6364917228AFEB0700FA518B /* CustomAlert */, 110 | ); 111 | name = Packages; 112 | sourceTree = ""; 113 | }; 114 | 6364917328AFEB6D00FA518B /* Frameworks */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | ); 118 | name = Frameworks; 119 | sourceTree = ""; 120 | }; 121 | /* End PBXGroup section */ 122 | 123 | /* Begin PBXNativeTarget section */ 124 | 6364915F28AFEAFB00FA518B /* Demo */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = 6364916E28AFEAFC00FA518B /* Build configuration list for PBXNativeTarget "Demo" */; 127 | buildPhases = ( 128 | 6364915C28AFEAFB00FA518B /* Sources */, 129 | 6364915D28AFEAFB00FA518B /* Frameworks */, 130 | 6364915E28AFEAFB00FA518B /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = Demo; 137 | packageProductDependencies = ( 138 | 6364917428AFEB6D00FA518B /* CustomAlert */, 139 | ); 140 | productName = Demo; 141 | productReference = 6364916028AFEAFB00FA518B /* Demo.app */; 142 | productType = "com.apple.product-type.application"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | 6364915828AFEAFB00FA518B /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | BuildIndependentTargetsInParallel = 1; 151 | LastSwiftUpdateCheck = 1340; 152 | LastUpgradeCheck = 1530; 153 | TargetAttributes = { 154 | 6364915F28AFEAFB00FA518B = { 155 | CreatedOnToolsVersion = 13.4.1; 156 | }; 157 | }; 158 | }; 159 | buildConfigurationList = 6364915B28AFEAFB00FA518B /* Build configuration list for PBXProject "Demo" */; 160 | compatibilityVersion = "Xcode 13.0"; 161 | developmentRegion = en; 162 | hasScannedForEncodings = 0; 163 | knownRegions = ( 164 | en, 165 | Base, 166 | ); 167 | mainGroup = 6364915728AFEAFB00FA518B; 168 | productRefGroup = 6364916128AFEAFB00FA518B /* Products */; 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | 6364915F28AFEAFB00FA518B /* Demo */, 173 | ); 174 | }; 175 | /* End PBXProject section */ 176 | 177 | /* Begin PBXResourcesBuildPhase section */ 178 | 6364915E28AFEAFB00FA518B /* Resources */ = { 179 | isa = PBXResourcesBuildPhase; 180 | buildActionMask = 2147483647; 181 | files = ( 182 | 6364916B28AFEAFC00FA518B /* Preview Assets.xcassets in Resources */, 183 | 6364916828AFEAFC00FA518B /* Assets.xcassets in Resources */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXResourcesBuildPhase section */ 188 | 189 | /* Begin PBXSourcesBuildPhase section */ 190 | 6364915C28AFEAFB00FA518B /* Sources */ = { 191 | isa = PBXSourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | 6364916628AFEAFB00FA518B /* ContentView.swift in Sources */, 195 | 6364916428AFEAFB00FA518B /* DemoApp.swift in Sources */, 196 | 630CF3E42C39B6ED00B6FF68 /* SimpleAlerts.swift in Sources */, 197 | 63787E4D2D43D0740004009E /* ScrollContentAlerts.swift in Sources */, 198 | 630CF3E22C39B6E400B6FF68 /* CustomAlerts.swift in Sources */, 199 | 630213232960A55B0065DBDD /* OtherAlerts.swift in Sources */, 200 | 6302131F2960A4850065DBDD /* InputAlerts.swift in Sources */, 201 | 630213212960A4EE0065DBDD /* MultiButtonAlerts.swift in Sources */, 202 | 630CF3DF2C39B52500B6FF68 /* DetailLabel.swift in Sources */, 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXSourcesBuildPhase section */ 207 | 208 | /* Begin XCBuildConfiguration section */ 209 | 6364916C28AFEAFC00FA518B /* Debug */ = { 210 | isa = XCBuildConfiguration; 211 | buildSettings = { 212 | ALWAYS_SEARCH_USER_PATHS = NO; 213 | CLANG_ANALYZER_NONNULL = YES; 214 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 215 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 216 | CLANG_ENABLE_MODULES = YES; 217 | CLANG_ENABLE_OBJC_ARC = YES; 218 | CLANG_ENABLE_OBJC_WEAK = YES; 219 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 220 | CLANG_WARN_BOOL_CONVERSION = YES; 221 | CLANG_WARN_COMMA = YES; 222 | CLANG_WARN_CONSTANT_CONVERSION = YES; 223 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 224 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 225 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 226 | CLANG_WARN_EMPTY_BODY = YES; 227 | CLANG_WARN_ENUM_CONVERSION = YES; 228 | CLANG_WARN_INFINITE_RECURSION = YES; 229 | CLANG_WARN_INT_CONVERSION = YES; 230 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 231 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 232 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 233 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 234 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 235 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 236 | CLANG_WARN_STRICT_PROTOTYPES = YES; 237 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 238 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 239 | CLANG_WARN_UNREACHABLE_CODE = YES; 240 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 241 | COPY_PHASE_STRIP = NO; 242 | DEBUG_INFORMATION_FORMAT = dwarf; 243 | ENABLE_STRICT_OBJC_MSGSEND = YES; 244 | ENABLE_TESTABILITY = YES; 245 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 246 | GCC_C_LANGUAGE_STANDARD = gnu11; 247 | GCC_DYNAMIC_NO_PIC = NO; 248 | GCC_NO_COMMON_BLOCKS = YES; 249 | GCC_OPTIMIZATION_LEVEL = 0; 250 | GCC_PREPROCESSOR_DEFINITIONS = ( 251 | "DEBUG=1", 252 | "$(inherited)", 253 | ); 254 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 255 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 256 | GCC_WARN_UNDECLARED_SELECTOR = YES; 257 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 258 | GCC_WARN_UNUSED_FUNCTION = YES; 259 | GCC_WARN_UNUSED_VARIABLE = YES; 260 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 261 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 262 | MTL_FAST_MATH = YES; 263 | ONLY_ACTIVE_ARCH = YES; 264 | SDKROOT = iphoneos; 265 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 266 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 267 | }; 268 | name = Debug; 269 | }; 270 | 6364916D28AFEAFC00FA518B /* Release */ = { 271 | isa = XCBuildConfiguration; 272 | buildSettings = { 273 | ALWAYS_SEARCH_USER_PATHS = NO; 274 | CLANG_ANALYZER_NONNULL = YES; 275 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 276 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 277 | CLANG_ENABLE_MODULES = YES; 278 | CLANG_ENABLE_OBJC_ARC = YES; 279 | CLANG_ENABLE_OBJC_WEAK = YES; 280 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 281 | CLANG_WARN_BOOL_CONVERSION = YES; 282 | CLANG_WARN_COMMA = YES; 283 | CLANG_WARN_CONSTANT_CONVERSION = YES; 284 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 285 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 286 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 287 | CLANG_WARN_EMPTY_BODY = YES; 288 | CLANG_WARN_ENUM_CONVERSION = YES; 289 | CLANG_WARN_INFINITE_RECURSION = YES; 290 | CLANG_WARN_INT_CONVERSION = YES; 291 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 292 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 293 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 294 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 295 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 296 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 297 | CLANG_WARN_STRICT_PROTOTYPES = YES; 298 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 299 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 300 | CLANG_WARN_UNREACHABLE_CODE = YES; 301 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 302 | COPY_PHASE_STRIP = NO; 303 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 304 | ENABLE_NS_ASSERTIONS = NO; 305 | ENABLE_STRICT_OBJC_MSGSEND = YES; 306 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 307 | GCC_C_LANGUAGE_STANDARD = gnu11; 308 | GCC_NO_COMMON_BLOCKS = YES; 309 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 310 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 311 | GCC_WARN_UNDECLARED_SELECTOR = YES; 312 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 313 | GCC_WARN_UNUSED_FUNCTION = YES; 314 | GCC_WARN_UNUSED_VARIABLE = YES; 315 | IPHONEOS_DEPLOYMENT_TARGET = 15.5; 316 | MTL_ENABLE_DEBUG_INFO = NO; 317 | MTL_FAST_MATH = YES; 318 | SDKROOT = iphoneos; 319 | SWIFT_COMPILATION_MODE = wholemodule; 320 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 321 | VALIDATE_PRODUCT = YES; 322 | }; 323 | name = Release; 324 | }; 325 | 6364916F28AFEAFC00FA518B /* Debug */ = { 326 | isa = XCBuildConfiguration; 327 | buildSettings = { 328 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 329 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 330 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; 331 | CODE_SIGN_IDENTITY = "Apple Development"; 332 | CODE_SIGN_STYLE = Automatic; 333 | CURRENT_PROJECT_VERSION = 1; 334 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 335 | DEVELOPMENT_TEAM = ""; 336 | ENABLE_PREVIEWS = YES; 337 | GENERATE_INFOPLIST_FILE = YES; 338 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 339 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 340 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 341 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 342 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 343 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 344 | LD_RUNPATH_SEARCH_PATHS = ( 345 | "$(inherited)", 346 | "@executable_path/Frameworks", 347 | ); 348 | MARKETING_VERSION = 1.0; 349 | PRODUCT_BUNDLE_IDENTIFIER = at.davidwalter.CustomAlert.Demo; 350 | PRODUCT_NAME = "$(TARGET_NAME)"; 351 | PROVISIONING_PROFILE_SPECIFIER = ""; 352 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 353 | SUPPORTS_MACCATALYST = YES; 354 | SWIFT_EMIT_LOC_STRINGS = YES; 355 | SWIFT_VERSION = 5.0; 356 | TARGETED_DEVICE_FAMILY = "1,2"; 357 | }; 358 | name = Debug; 359 | }; 360 | 6364917028AFEAFC00FA518B /* Release */ = { 361 | isa = XCBuildConfiguration; 362 | buildSettings = { 363 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 364 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 365 | CODE_SIGN_ENTITLEMENTS = Demo/Demo.entitlements; 366 | CODE_SIGN_IDENTITY = "Apple Development"; 367 | CODE_SIGN_STYLE = Automatic; 368 | CURRENT_PROJECT_VERSION = 1; 369 | DEVELOPMENT_ASSET_PATHS = "\"Demo/Preview Content\""; 370 | DEVELOPMENT_TEAM = ""; 371 | ENABLE_PREVIEWS = YES; 372 | GENERATE_INFOPLIST_FILE = YES; 373 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 374 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 375 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 376 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 377 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 378 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 379 | LD_RUNPATH_SEARCH_PATHS = ( 380 | "$(inherited)", 381 | "@executable_path/Frameworks", 382 | ); 383 | MARKETING_VERSION = 1.0; 384 | PRODUCT_BUNDLE_IDENTIFIER = at.davidwalter.CustomAlert.Demo; 385 | PRODUCT_NAME = "$(TARGET_NAME)"; 386 | PROVISIONING_PROFILE_SPECIFIER = ""; 387 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 388 | SUPPORTS_MACCATALYST = YES; 389 | SWIFT_EMIT_LOC_STRINGS = YES; 390 | SWIFT_VERSION = 5.0; 391 | TARGETED_DEVICE_FAMILY = "1,2"; 392 | }; 393 | name = Release; 394 | }; 395 | /* End XCBuildConfiguration section */ 396 | 397 | /* Begin XCConfigurationList section */ 398 | 6364915B28AFEAFB00FA518B /* Build configuration list for PBXProject "Demo" */ = { 399 | isa = XCConfigurationList; 400 | buildConfigurations = ( 401 | 6364916C28AFEAFC00FA518B /* Debug */, 402 | 6364916D28AFEAFC00FA518B /* Release */, 403 | ); 404 | defaultConfigurationIsVisible = 0; 405 | defaultConfigurationName = Release; 406 | }; 407 | 6364916E28AFEAFC00FA518B /* Build configuration list for PBXNativeTarget "Demo" */ = { 408 | isa = XCConfigurationList; 409 | buildConfigurations = ( 410 | 6364916F28AFEAFC00FA518B /* Debug */, 411 | 6364917028AFEAFC00FA518B /* Release */, 412 | ); 413 | defaultConfigurationIsVisible = 0; 414 | defaultConfigurationName = Release; 415 | }; 416 | /* End XCConfigurationList section */ 417 | 418 | /* Begin XCSwiftPackageProductDependency section */ 419 | 6364917428AFEB6D00FA518B /* CustomAlert */ = { 420 | isa = XCSwiftPackageProductDependency; 421 | productName = CustomAlert; 422 | }; 423 | /* End XCSwiftPackageProductDependency section */ 424 | }; 425 | rootObject = 6364915828AFEAFB00FA518B /* Project object */; 426 | } 427 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo/Alerts/CustomAlerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlerts.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 17.10.23. 6 | // 7 | 8 | import SwiftUI 9 | import CustomAlert 10 | 11 | struct CustomAlerts: View { 12 | @State private var showAlert = false 13 | 14 | @State private var showChangingAlert = false 15 | @State private var next: Int = 0 16 | 17 | var body: some View { 18 | Section { 19 | Button { 20 | showAlert = true 21 | } label: { 22 | DetailLabel("Custom Config", detail: "CustomAlert with heavily modified styling") 23 | } 24 | .customAlert("Preview", isPresented: $showAlert) { 25 | CustomContent() 26 | } actions: { 27 | MultiButton { 28 | Button { 29 | print("CustomStyling.MyConfig - Cancel") 30 | } label: { 31 | Text("Cancel") 32 | } 33 | Button(role: .destructive) { 34 | print("CustomStyling.MyConfig - Delete") 35 | } label: { 36 | Text("Delete") 37 | } 38 | } 39 | } 40 | .configureCustomAlert(.myConfig) 41 | Button { 42 | next = 0 43 | showChangingAlert = true 44 | } label: { 45 | DetailLabel("Changing Alert", detail: "CustomAlert that changes") 46 | } 47 | .customAlert("Preview", isPresented: $showChangingAlert) { 48 | ZStack { 49 | Group { 50 | switch next { 51 | case 0: 52 | Text("Initial Content. Press next to continue.") 53 | case 1: 54 | Text("Content changed, to display the next step. Press next to continue.") 55 | default: 56 | Text("Final Content. Press done to dismiss the alert.") 57 | } 58 | } 59 | .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading))) 60 | } 61 | .animation(.default, value: next) 62 | } actions: { 63 | MultiButton { 64 | Button(role: .cancel) { 65 | print("CustomStyling.MyConfig - Cancel") 66 | } label: { 67 | Text("Cancel") 68 | } 69 | ZStack { 70 | switch next { 71 | case 0, 1: 72 | Button { 73 | next += 1 74 | } label: { 75 | Text("Next") 76 | } 77 | .transition(.opacity) 78 | .buttonStyle(.alert(triggerDismiss: false)) 79 | default: 80 | Button(role: .destructive) { 81 | print("CustomStyling.MyConfig - Done") 82 | } label: { 83 | Text("Done") 84 | } 85 | .transition(.opacity) 86 | } 87 | } 88 | .animation(.default, value: next) 89 | } 90 | } 91 | } header: { 92 | Text("Custom Styling") 93 | } 94 | } 95 | } 96 | 97 | struct CustomContent: View { 98 | @Environment(\.alertDismiss) private var alertDismiss 99 | @Environment(\.customAlertConfiguration) private var configuration 100 | 101 | var body: some View { 102 | VStack(alignment: .leading) { 103 | Text("Content") 104 | 105 | Button { 106 | alertDismiss() 107 | } label: { 108 | Text("Custom Dismiss Button") 109 | } 110 | .buttonStyle(.bordered) 111 | .tint(configuration.button.tintColor) 112 | } 113 | } 114 | } 115 | 116 | extension CustomAlertConfiguration { 117 | static let myConfig: CustomAlertConfiguration = .create { configuration in 118 | configuration.background = .blurEffect(.dark) 119 | configuration.padding = EdgeInsets() 120 | configuration.alert = .create { alert in 121 | alert.background = .color(.white) 122 | alert.cornerRadius = 4 123 | alert.padding = EdgeInsets(top: 20, leading: 20, bottom: 15, trailing: 20) 124 | alert.minWidth = 300 125 | alert.titleFont = .headline 126 | alert.contentFont = .subheadline 127 | alert.alignment = .leading 128 | alert.spacing = 10 129 | } 130 | configuration.button = .create { button in 131 | button.tintColor = .purple 132 | button.pressedTintColor = .white 133 | button.padding = EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10) 134 | button.font = .callout.weight(.semibold) 135 | button.hideDivider = true 136 | button.pressedBackground = .color(.purple) 137 | } 138 | } 139 | } 140 | 141 | #Preview { 142 | List { 143 | CustomAlerts() 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Demo/Demo/Alerts/InputAlerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputAlerts.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 31.12.22. 6 | // 7 | 8 | import SwiftUI 9 | import CustomAlert 10 | 11 | struct InputAlerts: View { 12 | @State private var showTextField = false 13 | @State private var text = "" 14 | @State private var showTextEditor = false 15 | @State private var editorText = "" 16 | 17 | var body: some View { 18 | Section { 19 | Button { 20 | showTextField = true 21 | } label: { 22 | DetailLabel("TextField", detail: "CustomAlert with a TextField") 23 | } 24 | .customAlert("TextField", isPresented: $showTextField) { 25 | TextField("Enter some String", text: $text) 26 | .font(.body) 27 | .padding(4) 28 | .background { 29 | RoundedRectangle(cornerRadius: 8) 30 | .fill(Color(uiColor: .systemBackground)) 31 | } 32 | } actions: { 33 | Button(role: .cancel) { 34 | print("Input.TextField - Cancel") 35 | } label: { 36 | Text("Cancel") 37 | } 38 | } 39 | 40 | Button { 41 | showTextEditor = true 42 | } label: { 43 | DetailLabel("TextEditor", detail: "CustomAlert with a TextEditor") 44 | } 45 | .customAlert("TextEditor", isPresented: $showTextEditor) { 46 | TextEditor(text: $text) 47 | .font(.body) 48 | .padding(4) 49 | .frame(height: 100) 50 | .background { 51 | RoundedRectangle(cornerRadius: 8) 52 | .fill(Color(uiColor: .systemBackground)) 53 | } 54 | } actions: { 55 | Button(role: .cancel) { 56 | print("Input.TextEditor - Cancel") 57 | } label: { 58 | Text("Cancel") 59 | } 60 | } 61 | } header: { 62 | Text("Input") 63 | } 64 | } 65 | } 66 | 67 | #Preview { 68 | List { 69 | InputAlerts() 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Demo/Demo/Alerts/MultiButtonAlerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiButtonAlerts.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 31.12.22. 6 | // 7 | 8 | import SwiftUI 9 | import CustomAlert 10 | 11 | struct MultiButtonAlerts: View { 12 | @State private var showSimple = false 13 | @State private var showComplex = false 14 | 15 | var body: some View { 16 | Section { 17 | Button { 18 | showSimple = true 19 | } label: { 20 | DetailLabel("Simple", detail: "CustomAlert using MultiButton with a simple layout") 21 | } 22 | .customAlert("Multibutton Alert", isPresented: $showSimple) { 23 | Text("Simple MultiButton") 24 | } actions: { 25 | MultiButton { 26 | Button { 27 | print("MultiButton.Simple - OK") 28 | } label: { 29 | Text("OK") 30 | } 31 | Button(role: .cancel) { 32 | print("MultiButton.Simple - Cancel") 33 | } label: { 34 | Text("Cancel") 35 | } 36 | } 37 | } 38 | 39 | Button { 40 | showComplex = true 41 | } label: { 42 | DetailLabel("Complex", detail: "CustomAlert using MultiButton with a slighly more complex layout") 43 | } 44 | .customAlert("MultiButton Alert", isPresented: $showComplex) { 45 | Text("Complex MultiButton") 46 | } actions: { 47 | MultiButton { 48 | Button { 49 | print("MultiButton.Complex - A") 50 | } label: { 51 | Text("A") 52 | } 53 | 54 | Button { 55 | print("MultiButton.Complex - B") 56 | } label: { 57 | Text("B") 58 | } 59 | .disabled(true) 60 | 61 | Button { 62 | print("MultiButton.Complex - C") 63 | } label: { 64 | Text("C") 65 | } 66 | } 67 | 68 | Button(role: .cancel) { 69 | print("MultiButton.Complex - Cancel") 70 | } label: { 71 | Text("Cancel") 72 | } 73 | } 74 | } header: { 75 | Text("Multibutton") 76 | } 77 | } 78 | } 79 | 80 | #Preview { 81 | List { 82 | MultiButtonAlerts() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Demo/Demo/Alerts/OtherAlerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OtherAlerts.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 31.12.22. 6 | // 7 | 8 | import SwiftUI 9 | import CustomAlert 10 | 11 | struct OtherAlerts: View { 12 | @State private var showStacked = false 13 | @State private var showFancy = false 14 | @State private var showNoButton = false 15 | 16 | @State private var message = "" 17 | 18 | var body: some View { 19 | Section { 20 | Button { 21 | showNoButton = true 22 | } label: { 23 | DetailLabel("No Button", detail: "CustomAlert without a Button") 24 | } 25 | .customAlert("No Button Alert", isPresented: $showNoButton) { 26 | ProgressView() 27 | .progressViewStyle(.circular) 28 | .onAppear { 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 3) { 30 | showNoButton = false 31 | } 32 | } 33 | } 34 | 35 | Button { 36 | showStacked = true 37 | } label: { 38 | DetailLabel("Stacked", detail: "CustomAlert with custom content") 39 | } 40 | .customAlert("Stacked", isPresented: $showStacked) { 41 | VStack { 42 | HStack { 43 | Text("Left") 44 | Spacer() 45 | Text("Right") 46 | } 47 | .padding(.horizontal) 48 | 49 | Image(systemName: "swift") 50 | .resizable() 51 | .frame(width: 50, height: 50) 52 | .foregroundColor(.orange) 53 | 54 | HStack { 55 | Text("Left") 56 | Spacer() 57 | Text("Center") 58 | Spacer() 59 | Text("Right") 60 | } 61 | 62 | HStack { 63 | Text("Left") 64 | Spacer() 65 | Text("Noteworthy") 66 | Spacer() 67 | Text("Right") 68 | } 69 | .font(.custom("Noteworthy", size: 20)) 70 | } 71 | } actions: { 72 | Button(role: .cancel) { 73 | print("Other.Stacked - Cancel") 74 | } label: { 75 | Text("Cancel") 76 | } 77 | } 78 | 79 | Button { 80 | showFancy = true 81 | } label: { 82 | DetailLabel("Fancy", detail: "A fancier looking alert") 83 | } 84 | .customAlert(isPresented: $showFancy) { 85 | VStack(spacing: 20) { 86 | Image("jane") 87 | .resizable() 88 | .frame(width: 60, height: 60) 89 | .background(.ultraThinMaterial.blendMode(.multiply)) 90 | .clipShape(Circle()) 91 | 92 | VStack(spacing: 4) { 93 | Text("Remind Jane") 94 | .font(.headline) 95 | 96 | Text("Send a reminder to Jane about \"Birthday Party\"") 97 | .font(.footnote) 98 | } 99 | 100 | TextField("Message", text: $message) 101 | .multilineTextAlignment(.leading) 102 | .frame(maxWidth: .infinity) 103 | .padding(EdgeInsets(top: 4, leading: 8, bottom: 4, trailing: 8)) 104 | .background(.ultraThinMaterial.blendMode(.multiply)) 105 | .clipShape(RoundedRectangle(cornerRadius: 8)) 106 | } 107 | } actions: { 108 | MultiButton { 109 | Button(role: .cancel) { 110 | message = "" 111 | print("Other.Fancy - Cancel") 112 | } label: { 113 | Text("Cancel") 114 | } 115 | 116 | Button { 117 | message = "" 118 | print("Other.Fancy - Send") 119 | } label: { 120 | Text("Send") 121 | } 122 | .disabled(message.isEmpty) 123 | } 124 | } 125 | } header: { 126 | Text("Other") 127 | } 128 | } 129 | } 130 | 131 | #Preview { 132 | List { 133 | OtherAlerts() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Demo/Demo/Alerts/ScrollContentAlerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollContentAlerts.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 24.01.25. 6 | // 7 | 8 | import SwiftUI 9 | import CustomAlert 10 | 11 | struct ScrollContentAlerts: View { 12 | @State private var showNative = false 13 | @State private var showCustom = false 14 | 15 | var body: some View { 16 | Section { 17 | Button { 18 | showNative = true 19 | } label: { 20 | DetailLabel("Native", detail: "SwiftUI native alert") 21 | } 22 | .alert("Native Alert", isPresented: $showNative) { 23 | Button(role: .cancel) { 24 | print("Simple.Native - Cancel") 25 | } label: { 26 | Text("Cancel") 27 | } 28 | } message: { 29 | Text( 30 | """ 31 | Lorem ipsum odor amet, consectetuer adipiscing elit. Alacinia curae euismod amet; felis amet vitae elementum. Nec aptent vestibulum fusce gravida justo penatibus. Ad suscipit dui nostra pharetra mus finibus porttitor eget ullamcorper. Ipsum leo cubilia interdum elementum felis. Vulputate ornare duis aliquet erat curabitur tempor. Efficitur proin cursus porta dictum, gravida diam donec cursus. Proin cubilia penatibus duis vulputate est semper luctus. Penatibus nascetur dui ad rhoncus neque. 32 | 33 | Curae molestie etiam parturient taciti ex curae nostra. Orci elementum integer fusce vitae parturient duis venenatis. Elit venenatis magnis dolor blandit elit tristique. Lacinia sapien fusce; sodales mi aptent dictum semper. Rutrum leo malesuada est ligula placerat pellentesque morbi magna. Eleifend lorem torquent placerat cubilia gravida cursus sapien? Fusce semper inceptos id semper orci viverra eget bibendum. 34 | 35 | Mollis duis nascetur ex inceptos fermentum leo. Dis sodales ex potenti sodales eu facilisis volutpat. Mus ornare eros senectus torquent ultrices nullam. Bibendum fringilla dignissim est odio pretium aliquam penatibus aenean. Congue justo et sociosqu sit fames taciti magnis. Netus sem imperdiet; lacus vivamus finibus fusce habitant? Elementum habitasse duis eu dapibus facilisis placerat sit pulvinar. Est vehicula suscipit pellentesque parturient nec sapien habitasse. Nostra adipiscing ut posuere, bibendum sed hendrerit tincidunt consectetur. 36 | """ 37 | ) 38 | } 39 | 40 | Button { 41 | showCustom = true 42 | } label: { 43 | DetailLabel("Custom", detail: "CustomAlert looking like native alert") 44 | } 45 | .customAlert("Custom Alert", isPresented: $showCustom) { 46 | Text( 47 | """ 48 | Lorem ipsum odor amet, consectetuer adipiscing elit. Alacinia curae euismod amet; felis amet vitae elementum. Nec aptent vestibulum fusce gravida justo penatibus. Ad suscipit dui nostra pharetra mus finibus porttitor eget ullamcorper. Ipsum leo cubilia interdum elementum felis. Vulputate ornare duis aliquet erat curabitur tempor. Efficitur proin cursus porta dictum, gravida diam donec cursus. Proin cubilia penatibus duis vulputate est semper luctus. Penatibus nascetur dui ad rhoncus neque. 49 | 50 | Curae molestie etiam parturient taciti ex curae nostra. Orci elementum integer fusce vitae parturient duis venenatis. Elit venenatis magnis dolor blandit elit tristique. Lacinia sapien fusce; sodales mi aptent dictum semper. Rutrum leo malesuada est ligula placerat pellentesque morbi magna. Eleifend lorem torquent placerat cubilia gravida cursus sapien? Fusce semper inceptos id semper orci viverra eget bibendum. 51 | 52 | Mollis duis nascetur ex inceptos fermentum leo. Dis sodales ex potenti sodales eu facilisis volutpat. Mus ornare eros senectus torquent ultrices nullam. Bibendum fringilla dignissim est odio pretium aliquam penatibus aenean. Congue justo et sociosqu sit fames taciti magnis. Netus sem imperdiet; lacus vivamus finibus fusce habitant? Elementum habitasse duis eu dapibus facilisis placerat sit pulvinar. Est vehicula suscipit pellentesque parturient nec sapien habitasse. Nostra adipiscing ut posuere, bibendum sed hendrerit tincidunt consectetur. 53 | """ 54 | ) 55 | } actions: { 56 | Button(role: .cancel) { 57 | print("Simple.Custom - Cancel") 58 | } label: { 59 | Text("Cancel") 60 | } 61 | } 62 | } header: { 63 | Text("Scroll Content") 64 | } 65 | } 66 | } 67 | 68 | #Preview { 69 | ScrollContentAlerts() 70 | } 71 | -------------------------------------------------------------------------------- /Demo/Demo/Alerts/SimpleAlerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleAlerts.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 31.12.22. 6 | // 7 | 8 | import SwiftUI 9 | import CustomAlert 10 | 11 | struct SimpleAlerts: View { 12 | @State private var showNative = false 13 | @State private var showCustom = false 14 | 15 | var body: some View { 16 | Section { 17 | Button { 18 | showNative = true 19 | } label: { 20 | DetailLabel("Native", detail: "SwiftUI native alert") 21 | } 22 | .alert("Native Alert", isPresented: $showNative) { 23 | Button(role: .cancel) { 24 | print("Simple.Native - Cancel") 25 | } label: { 26 | Text("Cancel") 27 | } 28 | } message: { 29 | Text("Some Message") 30 | } 31 | 32 | Button { 33 | showCustom = true 34 | } label: { 35 | DetailLabel("Custom", detail: "CustomAlert looking like native alert") 36 | } 37 | .customAlert("Custom Alert", isPresented: $showCustom) { 38 | Text("Some Message") 39 | } actions: { 40 | Button(role: .cancel) { 41 | print("Simple.Custom - Cancel") 42 | } label: { 43 | Text("Cancel") 44 | } 45 | } 46 | } header: { 47 | Text("Simple") 48 | } 49 | } 50 | } 51 | 52 | #Preview { 53 | List { 54 | SimpleAlerts() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "2x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "83.5x83.5" 82 | }, 83 | { 84 | "idiom" : "ios-marketing", 85 | "scale" : "1x", 86 | "size" : "1024x1024" 87 | } 88 | ], 89 | "info" : { 90 | "author" : "xcode", 91 | "version" : 1 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/jane.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "jane.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Demo/Demo/Assets.xcassets/jane.imageset/jane.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Demo/Demo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 19.08.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | NavigationView { 13 | List { 14 | SimpleAlerts() 15 | 16 | ScrollContentAlerts() 17 | 18 | InputAlerts() 19 | 20 | MultiButtonAlerts() 21 | 22 | OtherAlerts() 23 | 24 | CustomAlerts() 25 | } 26 | .navigationTitle("Custom Alert") 27 | } 28 | .navigationViewStyle(.stack) 29 | } 30 | } 31 | 32 | #Preview { 33 | ContentView() 34 | } 35 | -------------------------------------------------------------------------------- /Demo/Demo/Demo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Demo/Demo/DemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DemoApp.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 19.08.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct DemoApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Demo/Demo/DetailLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailLabel.swift 3 | // Demo 4 | // 5 | // Created by David Walter on 06.07.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DetailLabel: View { 11 | let title: String 12 | let detail: String? 13 | 14 | init(_ title: String, detail: String? = nil) { 15 | self.title = title 16 | self.detail = detail 17 | } 18 | 19 | var body: some View { 20 | VStack(alignment: .leading, spacing: 2) { 21 | Text(title) 22 | .font(.body) 23 | .foregroundStyle(.primary) 24 | if let detail { 25 | Text(detail) 26 | .font(.subheadline) 27 | .foregroundStyle(.secondary) 28 | } 29 | } 30 | } 31 | } 32 | 33 | #Preview { 34 | List { 35 | DetailLabel("Hello", detail: "World") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Demo/Demo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 David Walter (davidwalter.at) 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.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CustomAlert", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | .library( 13 | name: "CustomAlert", 14 | targets: ["CustomAlert"] 15 | ) 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/divadretlaw/WindowKit", from: "2.5.2") 19 | ], 20 | targets: [ 21 | .target(name: "CustomAlert", dependencies: ["WindowKit"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Package@swift-6.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "CustomAlert", 8 | platforms: [ 9 | .iOS(.v15) 10 | ], 11 | products: [ 12 | .library( 13 | name: "CustomAlert", 14 | targets: ["CustomAlert"] 15 | ) 16 | ], 17 | dependencies: [ 18 | .package(url: "https://github.com/divadretlaw/WindowKit", from: "2.5.2") 19 | ], 20 | targets: [ 21 | .target(name: "CustomAlert", dependencies: ["WindowKit"]) 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CustomAlert 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdivadretlaw%2FCustomAlert%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/divadretlaw/CustomAlert) 4 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdivadretlaw%2FCustomAlert%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/divadretlaw/CustomAlert) 5 | 6 | ## Why 7 | 8 | In iOS Alerts cannot contain Images or anything other than Text. This allows you to easily customize the message part with any custom view. 9 | 10 | While the alert is completely rebuilt in SwiftUI, it has been designed to look and behave exactly like a native alert. The alert uses it's own window to be displayed and utilizes accessibility scaling but with the advantage of a custom view. 11 | 12 | If the content is too large because the text is too long or the text doesn't fit because of accessibility scaling the content will scroll just like in a SwiftUI Alert. 13 | 14 | ## Usage 15 | 16 | | SwiftUI Alert | Custom Alert | 17 | |:-:|:-:| 18 | | ![Native Alert](Sources/CustomAlert/Documentation.docc/Resources/SwiftUI.png) | ![Custom Alert](Sources/CustomAlert/Documentation.docc/Resources/Custom.png) | 19 | 20 | You can easily add an Image or change the Font used in the alert, or anything else to your imagination. 21 | 22 | Something simple with an image and a text field 23 | 24 | 25 | 26 | Or more complex layouts 27 | 28 | 29 | 30 | The API is very similar to the SwiftUI Alerts 31 | 32 | ```swift 33 | .customAlert("Some Fancy Alert", isPresented: $showAlert) { 34 | Text("I'm a custom Message") 35 | .font(.custom("Noteworthy", size: 24)) 36 | Image(systemName: "swift") 37 | .resizable() 38 | .scaledToFit() 39 | .frame(maxHeight: 100) 40 | .foregroundColor(.blue) 41 | } actions: { 42 | Button { 43 | // some Action 44 | } label: { 45 | Label("Swift", systemImage: "swift") 46 | } 47 | 48 | Button(role: .cancel) { 49 | // some Action 50 | } label: { 51 | Text("Cancel") 52 | } 53 | } 54 | ``` 55 | 56 | You can create Side by Side Buttons using `MultiButton` 57 | 58 | ```swift 59 | .customAlert("Alert with Side by Side Buttons", isPresented: $showAlert) { 60 | Text("Choose left or right") 61 | } actions: { 62 | MultiButton { 63 | Button { 64 | // some Action 65 | } label: { 66 | Text("Left") 67 | } 68 | 69 | Button { 70 | // some Action 71 | } label: { 72 | Text("Right") 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | The alert is customizable via the `Environment` 79 | 80 | 81 | 82 | ```swift 83 | .configureCustomAlert { configuration in 84 | // Adapt the default configuration 85 | } 86 | ``` 87 | 88 | You can also display an Alert inline, within a `List` for example 89 | 90 | 91 | 92 | ```swift 93 | CustomAlertRow { 94 | // Content 95 | } actions: { 96 | // Actions 97 | } 98 | ``` 99 | 100 | ## Install 101 | 102 | ### SwiftPM 103 | 104 | ``` 105 | https://github.com/divadretlaw/CustomAlert.git 106 | ``` 107 | 108 | ## License 109 | 110 | See [LICENSE](LICENSE) 111 | -------------------------------------------------------------------------------- /Sources/CustomAlert/API+Bool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API+Bool.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 03.04.22. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import WindowKit 11 | 12 | // MARK: - Default API 13 | 14 | public extension View { 15 | /// Presents an alert when a given condition is true, using an optional text view for 16 | /// the title. 17 | /// 18 | /// All actions in an alert dismiss the alert after the action runs. 19 | /// 20 | /// - Parameters: 21 | /// - title: The optional title of the alert. 22 | /// - isPresented: A binding to a Boolean value that determines whether to 23 | /// present the alert. When the user presses or taps one of the alert's 24 | /// actions, the system sets this value to `false` and dismisses. 25 | /// - content: A `ViewBuilder` returing the alerts main view. 26 | /// - actions: A `ViewBuilder` returning the alert's actions. 27 | @MainActor 28 | func customAlert( 29 | _ title: @autoclosure @escaping () -> Text? = nil, 30 | isPresented: Binding, 31 | @ViewBuilder content: @escaping () -> Content, 32 | @ViewBuilder actions: @escaping () -> Actions 33 | ) -> some View where Content: View, Actions: View { 34 | modifier( 35 | CustomAlertHandler( 36 | isPresented: isPresented, 37 | windowScene: nil, 38 | alertTitle: title, 39 | alertContent: content, 40 | alertActions: actions 41 | ) 42 | ) 43 | } 44 | 45 | /// Presents an alert when a given condition is true, using 46 | /// a localized string key for a title. 47 | /// 48 | /// All actions in an alert dismiss the alert after the action runs. 49 | /// 50 | /// - Parameters: 51 | /// - title: The title of the alert. 52 | /// - isPresented: A binding to a Boolean value that determines whether to 53 | /// present the alert. When the user presses or taps one of the alert's 54 | /// actions, the system sets this value to `false` and dismisses. 55 | /// - content: A `ViewBuilder` returing the alerts main view. 56 | /// - actions: A `ViewBuilder` returning the alert's actions. 57 | @MainActor 58 | func customAlert( 59 | _ title: LocalizedStringKey, 60 | isPresented: Binding, 61 | @ViewBuilder content: @escaping () -> Content, 62 | @ViewBuilder actions: @escaping () -> Actions 63 | ) -> some View where Content: View, Actions: View { 64 | customAlert(Text(title), isPresented: isPresented, content: content, actions: actions) 65 | } 66 | 67 | /// Presents an alert when a given condition is true 68 | /// 69 | /// All actions in an alert dismiss the alert after the action runs. 70 | /// 71 | /// - Parameters: 72 | /// - title: The title of the alert. 73 | /// - isPresented: A binding to a Boolean value that determines whether to 74 | /// present the alert. When the user presses or taps one of the alert's 75 | /// actions, the system sets this value to `false` and dismisses. 76 | /// - content: A `ViewBuilder` returing the alerts main view. 77 | /// - actions: A `ViewBuilder` returning the alert's actions. 78 | @MainActor 79 | @_disfavoredOverload 80 | func customAlert( 81 | _ title: Title, 82 | isPresented: Binding, 83 | @ViewBuilder content: @escaping () -> Content, 84 | @ViewBuilder actions: @escaping () -> Actions 85 | ) -> some View where Title: StringProtocol, Content: View, Actions: View { 86 | customAlert(Text(title), isPresented: isPresented, content: content, actions: actions) 87 | } 88 | 89 | /// Presents an alert when a given condition is true, using an optional text view for 90 | /// the title. 91 | /// 92 | /// All actions in an alert dismiss the alert after the action runs. 93 | /// 94 | /// - Parameters: 95 | /// - isPresented: A binding to a Boolean value that determines whether to 96 | /// present the alert. When the user presses or taps one of the alert's 97 | /// actions, the system sets this value to `false` and dismisses. 98 | /// - title: Callback for the optional title of the alert. 99 | /// - content: A `ViewBuilder` returing the alerts main view. 100 | /// - actions: A `ViewBuilder` returning the alert's actions. 101 | @MainActor 102 | func customAlert( 103 | isPresented: Binding, 104 | title: @escaping () -> Text?, 105 | @ViewBuilder content: @escaping () -> Content, 106 | @ViewBuilder actions: @escaping () -> Actions 107 | ) -> some View where Content: View, Actions: View { 108 | modifier( 109 | CustomAlertHandler( 110 | isPresented: isPresented, 111 | windowScene: nil, 112 | alertTitle: title, 113 | alertContent: content, 114 | alertActions: actions 115 | ) 116 | ) 117 | } 118 | } 119 | 120 | // MARK: - WindowScene API 121 | 122 | public extension View { 123 | /// Presents an alert when a given condition is true, using an optional text view for 124 | /// the title. 125 | /// 126 | /// All actions in an alert dismiss the alert after the action runs. 127 | /// 128 | /// - Parameters: 129 | /// - title: The optional title of the alert. 130 | /// - isPresented: A binding to a Boolean value that determines whether to 131 | /// present the alert. When the user presses or taps one of the alert's 132 | /// actions, the system sets this value to `false` and dismisses. 133 | /// - windowScene: The window scene to present the alert on. 134 | /// - content: A `ViewBuilder` returing the alerts main view. 135 | /// - actions: A `ViewBuilder` returning the alert's actions. 136 | @MainActor 137 | func customAlert( 138 | _ title: @autoclosure @escaping () -> Text? = nil, 139 | isPresented: Binding, 140 | on windowScene: UIWindowScene, 141 | @ViewBuilder content: @escaping () -> Content, 142 | @ViewBuilder actions: @escaping () -> Actions 143 | ) -> some View where Content: View, Actions: View { 144 | modifier( 145 | CustomAlertHandler( 146 | isPresented: isPresented, 147 | windowScene: windowScene, 148 | alertTitle: title, 149 | alertContent: content, 150 | alertActions: actions 151 | ) 152 | ) 153 | } 154 | 155 | /// Presents an alert when a given condition is true, using 156 | /// a localized string key for a title. 157 | /// 158 | /// All actions in an alert dismiss the alert after the action runs. 159 | /// 160 | /// - Parameters: 161 | /// - title: The title of the alert. 162 | /// - isPresented: A binding to a Boolean value that determines whether to 163 | /// present the alert. When the user presses or taps one of the alert's 164 | /// actions, the system sets this value to `false` and dismisses. 165 | /// - windowScene: The window scene to present the alert on. 166 | /// - content: A `ViewBuilder` returing the alerts main view. 167 | /// - actions: A `ViewBuilder` returning the alert's actions. 168 | @MainActor 169 | func customAlert( 170 | _ title: LocalizedStringKey, 171 | isPresented: Binding, 172 | on windowScene: UIWindowScene, 173 | @ViewBuilder content: @escaping () -> Content, 174 | @ViewBuilder actions: @escaping () -> Actions 175 | ) -> some View where Content: View, Actions: View { 176 | customAlert(Text(title), isPresented: isPresented, on: windowScene, content: content, actions: actions) 177 | } 178 | 179 | /// Presents an alert when a given condition is true 180 | /// 181 | /// All actions in an alert dismiss the alert after the action runs. 182 | /// 183 | /// - Parameters: 184 | /// - title: The title of the alert. 185 | /// - isPresented: A binding to a Boolean value that determines whether to 186 | /// present the alert. When the user presses or taps one of the alert's 187 | /// actions, the system sets this value to `false` and dismisses. 188 | /// - windowScene: The window scene to present the alert on. 189 | /// - content: A `ViewBuilder` returing the alerts main view. 190 | /// - actions: A `ViewBuilder` returning the alert's actions. 191 | @MainActor 192 | @_disfavoredOverload 193 | func customAlert( 194 | _ title: Title, 195 | isPresented: Binding, 196 | on windowScene: UIWindowScene, 197 | @ViewBuilder content: @escaping () -> Content, 198 | @ViewBuilder actions: @escaping () -> Actions 199 | ) -> some View where Title: StringProtocol, Content: View, Actions: View { 200 | customAlert(Text(title), isPresented: isPresented, on: windowScene, content: content, actions: actions) 201 | } 202 | 203 | /// Presents an alert when a given condition is true, using an optional text view for 204 | /// the title. 205 | /// 206 | /// All actions in an alert dismiss the alert after the action runs. 207 | /// 208 | /// - Parameters: 209 | /// - isPresented: A binding to a Boolean value that determines whether to 210 | /// present the alert. When the user presses or taps one of the alert's 211 | /// actions, the system sets this value to `false` and dismisses. 212 | /// - windowScene: The window scene to present the alert on. 213 | /// - title: Callback for the optional title of the alert. 214 | /// - content: A `ViewBuilder` returing the alerts main view. 215 | /// - actions: A `ViewBuilder` returning the alert's actions. 216 | @MainActor 217 | func customAlert( 218 | isPresented: Binding, 219 | on windowScene: UIWindowScene, 220 | title: @escaping () -> Text?, 221 | @ViewBuilder content: @escaping () -> Content, 222 | @ViewBuilder actions: @escaping () -> Actions 223 | ) -> some View where Content: View, Actions: View { 224 | customAlert(title(), isPresented: isPresented, on: windowScene, content: content, actions: actions) 225 | } 226 | } 227 | 228 | // MARK: - Convenience API 229 | 230 | public extension View { 231 | /// Presents an alert when a given condition is true, using an optional text view for 232 | /// the title. 233 | /// 234 | /// All actions in an alert dismiss the alert after the action runs. 235 | /// 236 | /// - Parameters: 237 | /// - title: The optional title of the alert. 238 | /// - isPresented: A binding to a Boolean value that determines whether to 239 | /// present the alert. When the user presses or taps one of the alert's 240 | /// actions, the system sets this value to `false` and dismisses. 241 | /// - content: A `ViewBuilder` returing the alerts main view. 242 | @MainActor 243 | func customAlert( 244 | _ title: Text? = nil, 245 | isPresented: Binding, 246 | @ViewBuilder content: @escaping () -> Content 247 | ) -> some View where Content: View { 248 | customAlert(title, isPresented: isPresented, content: content, actions: { /* no actions */ }) 249 | } 250 | 251 | /// Presents an alert when a given condition is true, using 252 | /// a localized string key for a title. 253 | /// 254 | /// All actions in an alert dismiss the alert after the action runs. 255 | /// 256 | /// - Parameters: 257 | /// - title: The title of the alert. 258 | /// - isPresented: A binding to a Boolean value that determines whether to 259 | /// present the alert. When the user presses or taps one of the alert's 260 | /// actions, the system sets this value to `false` and dismisses. 261 | /// - content: A `ViewBuilder` returing the alerts main view. 262 | @MainActor 263 | func customAlert( 264 | _ title: LocalizedStringKey, 265 | isPresented: Binding, 266 | @ViewBuilder content: @escaping () -> Content 267 | ) -> some View where Content: View { 268 | customAlert(Text(title), isPresented: isPresented, content: content, actions: { /* no actions */ }) 269 | } 270 | 271 | /// Presents an alert when a given condition is true 272 | /// 273 | /// All actions in an alert dismiss the alert after the action runs. 274 | /// 275 | /// - Parameters: 276 | /// - title: The title of the alert. 277 | /// - isPresented: A binding to a Boolean value that determines whether to 278 | /// present the alert. When the user presses or taps one of the alert's 279 | /// actions, the system sets this value to `false` and dismisses. 280 | /// - content: A `ViewBuilder` returing the alerts main view. 281 | @MainActor 282 | @_disfavoredOverload 283 | func customAlert( 284 | _ title: Title, 285 | isPresented: Binding, 286 | @ViewBuilder content: @escaping () -> Content 287 | ) -> some View where Title: StringProtocol, Content: View { 288 | customAlert(Text(title), isPresented: isPresented, content: content, actions: { /* no actions */ }) 289 | } 290 | 291 | /// Presents an alert when a given condition is true, using an optional text view for 292 | /// the title. 293 | /// 294 | /// All actions in an alert dismiss the alert after the action runs. 295 | /// 296 | /// - Parameters: 297 | /// - isPresented: A binding to a Boolean value that determines whether to 298 | /// present the alert. When the user presses or taps one of the alert's 299 | /// actions, the system sets this value to `false` and dismisses. 300 | /// - title: Callback for the optional title of the alert. 301 | /// - content: A `ViewBuilder` returing the alerts main view. 302 | @MainActor 303 | func customAlert( 304 | isPresented: Binding, 305 | title: @escaping () -> Text?, 306 | @ViewBuilder content: @escaping () -> Content 307 | ) -> some View where Content: View { 308 | customAlert(title(), isPresented: isPresented, content: content, actions: { /* no actions */ }) 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /Sources/CustomAlert/API+Identifiable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // API+Identifiable.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 28.02.24. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import WindowKit 11 | 12 | // MARK: - Default API 13 | 14 | public extension View { 15 | /// Presents an alert when a given condition is true, using an optional text view for 16 | /// the title. 17 | /// 18 | /// All actions in an alert dismiss the alert after the action runs. 19 | /// 20 | /// - Parameters: 21 | /// - title: The optional title of the alert. 22 | /// - item: A binding to an optional source of truth for the alert. 23 | /// When `item` is non-`nil`, the system passes the item's content to 24 | /// the modifier's closure. You display this content in a alert that you 25 | /// create that the system displays to the user. If `item` changes, 26 | /// the system dismisses the alert and replaces it with a new one 27 | /// using the same process. 28 | /// - content: A `ViewBuilder` returing the alerts main view. 29 | /// - actions: A `ViewBuilder` returning the alert's actions. 30 | @MainActor 31 | func customAlert( 32 | _ title: @autoclosure @escaping () -> Text? = nil, 33 | item: Binding, 34 | @ViewBuilder content: @escaping (Item) -> Content, 35 | @ViewBuilder actions: @escaping (Item) -> Actions 36 | ) -> some View where Item: Identifiable, Content: View, Actions: View { 37 | modifier( 38 | CustomAlertItemHandler( 39 | item: item, 40 | windowScene: nil, 41 | alertTitle: title, 42 | alertContent: content, 43 | alertActions: actions 44 | ) 45 | ) 46 | } 47 | 48 | /// Presents an alert when a given condition is true, using 49 | /// a localized string key for a title. 50 | /// 51 | /// All actions in an alert dismiss the alert after the action runs. 52 | /// 53 | /// - Parameters: 54 | /// - title: The title of the alert. 55 | /// - item: A binding to an optional source of truth for the alert. 56 | /// When `item` is non-`nil`, the system passes the item's content to 57 | /// the modifier's closure. You display this content in a alert that you 58 | /// create that the system displays to the user. If `item` changes, 59 | /// the system dismisses the alert and replaces it with a new one 60 | /// using the same process. 61 | /// - content: A `ViewBuilder` returing the alerts main view. 62 | /// - actions: A `ViewBuilder` returning the alert's actions. 63 | @MainActor 64 | func customAlert( 65 | _ title: LocalizedStringKey, 66 | item: Binding, 67 | @ViewBuilder content: @escaping (Item) -> Content, 68 | @ViewBuilder actions: @escaping (Item) -> Actions 69 | ) -> some View where Item: Identifiable, Content: View, Actions: View { 70 | customAlert(Text(title), item: item, content: content, actions: actions) 71 | } 72 | 73 | /// Presents an alert when a given condition is true 74 | /// 75 | /// All actions in an alert dismiss the alert after the action runs. 76 | /// 77 | /// - Parameters: 78 | /// - title: The title of the alert. 79 | /// - item: A binding to an optional source of truth for the alert. 80 | /// When `item` is non-`nil`, the system passes the item's content to 81 | /// the modifier's closure. You display this content in a alert that you 82 | /// create that the system displays to the user. If `item` changes, 83 | /// the system dismisses the alert and replaces it with a new one 84 | /// using the same process. 85 | /// - content: A `ViewBuilder` returing the alerts main view. 86 | /// - actions: A `ViewBuilder` returning the alert's actions. 87 | @MainActor 88 | @_disfavoredOverload 89 | func customAlert( 90 | _ title: Title, 91 | item: Binding, 92 | @ViewBuilder content: @escaping (Item) -> Content, 93 | @ViewBuilder actions: @escaping (Item) -> Actions 94 | ) -> some View where Item: Identifiable, Title: StringProtocol, Content: View, Actions: View { 95 | customAlert(Text(title), item: item, content: content, actions: actions) 96 | } 97 | 98 | /// Presents an alert when a given condition is true, using an optional text view for 99 | /// the title. 100 | /// 101 | /// All actions in an alert dismiss the alert after the action runs. 102 | /// 103 | /// - Parameters: 104 | /// - item: A binding to an optional source of truth for the alert. 105 | /// When `item` is non-`nil`, the system passes the item's content to 106 | /// the modifier's closure. You display this content in a alert that you 107 | /// create that the system displays to the user. If `item` changes, 108 | /// the system dismisses the alert and replaces it with a new one 109 | /// using the same process. 110 | /// - title: Callback for the optional title of the alert. 111 | /// - content: A `ViewBuilder` returing the alerts main view. 112 | /// - actions: A `ViewBuilder` returning the alert's actions. 113 | @MainActor 114 | func customAlert( 115 | item: Binding, 116 | title: @escaping () -> Text?, 117 | @ViewBuilder content: @escaping (Item) -> Content, 118 | @ViewBuilder actions: @escaping (Item) -> Actions 119 | ) -> some View where Item: Identifiable, Content: View, Actions: View { 120 | customAlert(title(), item: item, content: content, actions: actions) 121 | } 122 | } 123 | 124 | // MARK: - WindowScene API 125 | 126 | public extension View { 127 | /// Presents an alert when a given condition is true, using an optional text view for 128 | /// the title. 129 | /// 130 | /// All actions in an alert dismiss the alert after the action runs. 131 | /// 132 | /// - Parameters: 133 | /// - title: The optional title of the alert. 134 | /// - item: A binding to an optional source of truth for the alert. 135 | /// When `item` is non-`nil`, the system passes the item's content to 136 | /// the modifier's closure. You display this content in a alert that you 137 | /// create that the system displays to the user. If `item` changes, 138 | /// the system dismisses the alert and replaces it with a new one 139 | /// using the same process. 140 | /// - windowScene: The window scene to present the alert on. 141 | /// - content: A `ViewBuilder` returing the alerts main view. 142 | /// - actions: A `ViewBuilder` returning the alert's actions. 143 | @MainActor 144 | func customAlert( 145 | _ title: @autoclosure @escaping () -> Text? = nil, 146 | item: Binding, 147 | on windowScene: UIWindowScene, 148 | @ViewBuilder content: @escaping (Item) -> Content, 149 | @ViewBuilder actions: @escaping (Item) -> Actions 150 | ) -> some View where Item: Identifiable, Content: View, Actions: View { 151 | modifier( 152 | CustomAlertItemHandler( 153 | item: item, 154 | windowScene: windowScene, 155 | alertTitle: title, 156 | alertContent: content, 157 | alertActions: actions 158 | ) 159 | ) 160 | } 161 | 162 | /// Presents an alert when a given condition is true, using 163 | /// a localized string key for a title. 164 | /// 165 | /// All actions in an alert dismiss the alert after the action runs. 166 | /// 167 | /// - Parameters: 168 | /// - title: The title of the alert. 169 | /// - item: A binding to an optional source of truth for the alert. 170 | /// When `item` is non-`nil`, the system passes the item's content to 171 | /// the modifier's closure. You display this content in a alert that you 172 | /// create that the system displays to the user. If `item` changes, 173 | /// the system dismisses the alert and replaces it with a new one 174 | /// using the same process. 175 | /// - windowScene: The window scene to present the alert on. 176 | /// - content: A `ViewBuilder` returing the alerts main view. 177 | /// - actions: A `ViewBuilder` returning the alert's actions. 178 | @MainActor 179 | func customAlert( 180 | _ title: LocalizedStringKey, 181 | item: Binding, 182 | on windowScene: UIWindowScene, 183 | @ViewBuilder content: @escaping (Item) -> Content, 184 | @ViewBuilder actions: @escaping (Item) -> Actions 185 | ) -> some View where Item: Identifiable, Content: View, Actions: View { 186 | customAlert(Text(title), item: item, on: windowScene, content: content, actions: actions) 187 | } 188 | 189 | /// Presents an alert when a given condition is true 190 | /// 191 | /// All actions in an alert dismiss the alert after the action runs. 192 | /// 193 | /// - Parameters: 194 | /// - title: The title of the alert. 195 | /// - item: A binding to an optional source of truth for the alert. 196 | /// When `item` is non-`nil`, the system passes the item's content to 197 | /// the modifier's closure. You display this content in a alert that you 198 | /// create that the system displays to the user. If `item` changes, 199 | /// the system dismisses the alert and replaces it with a new one 200 | /// using the same process. 201 | /// - windowScene: The window scene to present the alert on. 202 | /// - content: A `ViewBuilder` returing the alerts main view. 203 | /// - actions: A `ViewBuilder` returning the alert's actions. 204 | @MainActor 205 | @_disfavoredOverload 206 | func customAlert( 207 | _ title: Title, 208 | item: Binding, 209 | on windowScene: UIWindowScene, 210 | @ViewBuilder content: @escaping (Item) -> Content, 211 | @ViewBuilder actions: @escaping (Item) -> Actions 212 | ) -> some View where Item: Identifiable, Title: StringProtocol, Content: View, Actions: View { 213 | customAlert(Text(title), item: item, on: windowScene, content: content, actions: actions) 214 | } 215 | 216 | /// Presents an alert when a given condition is true, using an optional text view for 217 | /// the title. 218 | /// 219 | /// All actions in an alert dismiss the alert after the action runs. 220 | /// 221 | /// - Parameters: 222 | /// - item: A binding to an optional source of truth for the alert. 223 | /// When `item` is non-`nil`, the system passes the item's content to 224 | /// the modifier's closure. You display this content in a alert that you 225 | /// create that the system displays to the user. If `item` changes, 226 | /// the system dismisses the alert and replaces it with a new one 227 | /// using the same process. 228 | /// - windowScene: The window scene to present the alert on. 229 | /// - title: Callback for the optional title of the alert. 230 | /// - content: A `ViewBuilder` returing the alerts main view. 231 | /// - actions: A `ViewBuilder` returning the alert's actions. 232 | @MainActor 233 | func customAlert( 234 | item: Binding, 235 | on windowScene: UIWindowScene, 236 | title: @escaping () -> Text?, 237 | @ViewBuilder content: @escaping (Item) -> Content, 238 | @ViewBuilder actions: @escaping (Item) -> Actions 239 | ) -> some View where Item: Identifiable, Content: View, Actions: View { 240 | customAlert(title(), item: item, on: windowScene, content: content, actions: actions) 241 | } 242 | } 243 | 244 | // MARK: - Convenience API 245 | 246 | public extension View { 247 | /// Presents an alert when a given condition is true, using an optional text view for 248 | /// the title. 249 | /// 250 | /// All actions in an alert dismiss the alert after the action runs. 251 | /// 252 | /// - Parameters: 253 | /// - title: The optional title of the alert. 254 | /// - item: A binding to an optional source of truth for the alert. 255 | /// When `item` is non-`nil`, the system passes the item's content to 256 | /// the modifier's closure. You display this content in a alert that you 257 | /// create that the system displays to the user. If `item` changes, 258 | /// the system dismisses the alert and replaces it with a new one 259 | /// using the same process. 260 | /// - content: A `ViewBuilder` returing the alerts main view. 261 | @MainActor 262 | func customAlert( 263 | _ title: Text? = nil, 264 | item: Binding, 265 | @ViewBuilder content: @escaping (Item) -> Content 266 | ) -> some View where Item: Identifiable, Content: View { 267 | customAlert(title, item: item, content: content, actions: { _ in /* no actions */ }) 268 | } 269 | 270 | /// Presents an alert when a given condition is true, using 271 | /// a localized string key for a title. 272 | /// 273 | /// All actions in an alert dismiss the alert after the action runs. 274 | /// 275 | /// - Parameters: 276 | /// - title: The title of the alert. 277 | /// - item: A binding to an optional source of truth for the alert. 278 | /// When `item` is non-`nil`, the system passes the item's content to 279 | /// the modifier's closure. You display this content in a alert that you 280 | /// create that the system displays to the user. If `item` changes, 281 | /// the system dismisses the alert and replaces it with a new one 282 | /// using the same process. 283 | /// - content: A `ViewBuilder` returing the alerts main view. 284 | @MainActor 285 | func customAlert( 286 | _ title: LocalizedStringKey, 287 | item: Binding, 288 | @ViewBuilder content: @escaping (Item) -> Content 289 | ) -> some View where Item: Identifiable, Content: View { 290 | customAlert(Text(title), item: item, content: content, actions: { _ in /* no actions */ }) 291 | } 292 | 293 | /// Presents an alert when a given condition is true 294 | /// 295 | /// All actions in an alert dismiss the alert after the action runs. 296 | /// 297 | /// - Parameters: 298 | /// - title: The title of the alert. 299 | /// - item: A binding to an optional source of truth for the alert. 300 | /// When `item` is non-`nil`, the system passes the item's content to 301 | /// the modifier's closure. You display this content in a alert that you 302 | /// create that the system displays to the user. If `item` changes, 303 | /// the system dismisses the alert and replaces it with a new one 304 | /// using the same process. 305 | /// - content: A `ViewBuilder` returing the alerts main view. 306 | @MainActor 307 | @_disfavoredOverload 308 | func customAlert( 309 | _ title: Title, 310 | item: Binding, 311 | @ViewBuilder content: @escaping (Item) -> Content 312 | ) -> some View where Item: Identifiable, Title: StringProtocol, Content: View { 313 | customAlert(Text(title), item: item, content: content, actions: { _ in /* no actions */ }) 314 | } 315 | 316 | /// Presents an alert when a given condition is true, using an optional text view for 317 | /// the title. 318 | /// 319 | /// All actions in an alert dismiss the alert after the action runs. 320 | /// 321 | /// - Parameters: 322 | /// - item: A binding to an optional source of truth for the alert. 323 | /// When `item` is non-`nil`, the system passes the item's content to 324 | /// the modifier's closure. You display this content in a alert that you 325 | /// create that the system displays to the user. If `item` changes, 326 | /// the system dismisses the alert and replaces it with a new one 327 | /// using the same process. 328 | /// - title: Callback for the optional title of the alert. 329 | /// - content: A `ViewBuilder` returing the alerts main view. 330 | @MainActor 331 | func customAlert( 332 | item: Binding, 333 | title: @escaping () -> Text?, 334 | @ViewBuilder content: @escaping (Item) -> Content 335 | ) -> some View where Item: Identifiable, Content: View { 336 | customAlert(title(), item: item, content: content, actions: { _ in /* no actions */ }) 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /Sources/CustomAlert/AlertButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertButtonStyle.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 03.04.22. 6 | // 7 | 8 | import SwiftUI 9 | import WindowReader 10 | 11 | /// A button style that applies standard alert styling 12 | /// 13 | /// You can also use ``alert`` to construct this style. 14 | public struct AlertButtonStyle: ButtonStyle { 15 | @Environment(\.customAlertConfiguration.button) private var buttonConfiguration 16 | @Environment(\.alertDismiss) private var alertDismiss 17 | @Environment(\.alertButtonHeight) private var maxHeight 18 | @Environment(\.dynamicTypeSize) private var dynamicTypeSize 19 | 20 | @Environment(\.isEnabled) private var isEnabled 21 | @Environment(\.colorScheme) private var colorScheme 22 | @Environment(\.window) private var window 23 | 24 | var triggerDismiss: Bool 25 | 26 | public func makeBody(configuration: Configuration) -> some View { 27 | if triggerDismiss { 28 | makeLabel(configuration: configuration) 29 | .onSimultaneousTapGesture { 30 | alertDismiss() 31 | } 32 | } else { 33 | makeLabel(configuration: configuration) 34 | } 35 | } 36 | 37 | func makeLabel(configuration: Configuration) -> some View { 38 | HStack { 39 | Spacer() 40 | label(configuration: configuration) 41 | .lineLimit(0) 42 | .minimumScaleFactor(0.66) 43 | .truncationMode(.middle) 44 | Spacer() 45 | } 46 | .padding(padding) 47 | .frame(maxHeight: maxHeight) 48 | .background(background(configuration: configuration)) 49 | .fixedSize(horizontal: false, vertical: true) 50 | } 51 | 52 | var padding: EdgeInsets { 53 | if dynamicTypeSize.isAccessibilitySize { 54 | buttonConfiguration.accessibilityPadding 55 | } else { 56 | buttonConfiguration.padding 57 | } 58 | } 59 | 60 | @ViewBuilder func label(configuration: Configuration) -> some View { 61 | switch configuration.role { 62 | case .some(.destructive): 63 | configuration.label 64 | .font(resolvedFont(role: .destructive)) 65 | .foregroundColor(resolvedColor(role: .destructive, isPressed: configuration.isPressed)) 66 | case .some(.cancel): 67 | configuration.label 68 | .font(resolvedFont(role: .cancel)) 69 | .foregroundColor(resolvedColor(role: .cancel, isPressed: configuration.isPressed)) 70 | default: 71 | configuration.label 72 | .font(resolvedFont()) 73 | .foregroundColor(resolvedColor(isPressed: configuration.isPressed)) 74 | } 75 | } 76 | 77 | @ViewBuilder func background(configuration: Self.Configuration) -> some View { 78 | if configuration.isPressed { 79 | BackgroundView(background: buttonConfiguration.pressedBackground) 80 | } else { 81 | BackgroundView(background: buttonConfiguration.background) 82 | } 83 | } 84 | 85 | func resolvedColor(role: ButtonType? = nil, isPressed: Bool) -> Color { 86 | if isEnabled { 87 | if isPressed, let color = buttonConfiguration.pressedTintColor { 88 | return color 89 | } else if let role, let color = buttonConfiguration.roleColor[role] { 90 | return color 91 | } else if let color = buttonConfiguration.tintColor { 92 | return color 93 | } 94 | 95 | // Fallback 96 | guard let color = window?.tintColor else { 97 | return .accentColor 98 | } 99 | 100 | return Color(uiColor: color) 101 | } else { 102 | return Color("Disabled", bundle: .module) 103 | } 104 | } 105 | 106 | func resolvedFont(role: ButtonType? = nil) -> Font { 107 | if let role, let font = buttonConfiguration.roleFont[role] { 108 | return font 109 | } else { 110 | return buttonConfiguration.font 111 | } 112 | } 113 | } 114 | 115 | public extension ButtonStyle where Self == AlertButtonStyle { 116 | /// A button style that applies standard alert styling 117 | /// 118 | /// A tap on the button will trigger `EnvironmentValues.alertDismiss` 119 | static var alert: Self { 120 | AlertButtonStyle(triggerDismiss: true) 121 | } 122 | 123 | /// A button style that applies standard alert styling 124 | /// 125 | /// - Parameter triggerDismiss: Whether the button should trigger `EnvironmentValues.alertDismiss` or not. 126 | static func alert(triggerDismiss: Bool) -> Self { 127 | AlertButtonStyle(triggerDismiss: triggerDismiss) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Assets.xcassets/DimmingBackround.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "0.200", 8 | "blue" : "0x00", 9 | "green" : "0x00", 10 | "red" : "0x00" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.480", 26 | "blue" : "0x00", 27 | "green" : "0x00", 28 | "red" : "0x00" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Assets.xcassets/Disabled.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "0.800", 8 | "blue" : "0.480", 9 | "green" : "0.480", 10 | "red" : "0.480" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "0.800", 26 | "blue" : "0.570", 27 | "green" : "0.570", 28 | "red" : "0.570" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | }, 33 | { 34 | "appearances" : [ 35 | { 36 | "appearance" : "contrast", 37 | "value" : "high" 38 | } 39 | ], 40 | "color" : { 41 | "color-space" : "display-p3", 42 | "components" : { 43 | "alpha" : "0.800", 44 | "blue" : "0.310", 45 | "green" : "0.310", 46 | "red" : "0.310" 47 | } 48 | }, 49 | "idiom" : "universal" 50 | }, 51 | { 52 | "appearances" : [ 53 | { 54 | "appearance" : "luminosity", 55 | "value" : "dark" 56 | }, 57 | { 58 | "appearance" : "contrast", 59 | "value" : "high" 60 | } 61 | ], 62 | "color" : { 63 | "color-space" : "srgb", 64 | "components" : { 65 | "alpha" : "0.800", 66 | "blue" : "0.670", 67 | "green" : "0.670", 68 | "red" : "0.670" 69 | } 70 | }, 71 | "idiom" : "universal" 72 | } 73 | ], 74 | "info" : { 75 | "author" : "xcode", 76 | "version" : 1 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Configuration/CustomAlertBackground.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertBackground.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 17.10.23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | import UIKit 11 | 12 | /// Wrapped background of the alert 13 | public enum CustomAlertBackground: Sendable { 14 | /// A `UIBlurEffect` as background 15 | case blurEffect(UIBlurEffect.Style) 16 | /// A `Color` as background 17 | case color(Color) 18 | /// A `UIBlurEffect` as background with a `Color` as background 19 | case colorBlurEffect(Color, UIBlurEffect.Style) 20 | case anyView(AnyView) 21 | 22 | @MainActor public static func view(@ViewBuilder builder: () -> Content) -> CustomAlertBackground where Content: View { 23 | CustomAlertBackground.anyView(AnyView(builder: builder)) 24 | } 25 | 26 | @MainActor public static func view(_ view: Content) -> CustomAlertBackground where Content: View { 27 | CustomAlertBackground.anyView(AnyView(view)) 28 | } 29 | 30 | @MainActor public struct AnyView: View, Sendable { 31 | let wrappedView: SwiftUI.AnyView 32 | 33 | init(@ViewBuilder builder: () -> Content) where Content: View { 34 | self.wrappedView = SwiftUI.AnyView(builder()) 35 | } 36 | 37 | init(_ view: Content) where Content: View { 38 | self.wrappedView = SwiftUI.AnyView(view) 39 | } 40 | 41 | public var body: some View { 42 | wrappedView 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Configuration/CustomAlertConfiguration.Alert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertConfiguration.Alert.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 17.10.23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension CustomAlertConfiguration { 12 | /// The custom alert configuration 13 | public struct Alert: Sendable { 14 | /// The background of the alert view 15 | public var background: CustomAlertBackground 16 | /// The corner radius of the alert view 17 | public var cornerRadius: CGFloat 18 | /// The padding of the content of the alert view 19 | public var padding: EdgeInsets 20 | /// The padding of the content of the alert view when using accessibility scaling 21 | public var accessibilityPadding: EdgeInsets 22 | /// The minimum width of the alert view 23 | public var minWidth: CGFloat 24 | /// The minimum width of the alert view when using accessibility scaling 25 | public var accessibilityMinWidth: CGFloat 26 | /// The default font of the title of the alert view 27 | public var titleFont: Font 28 | /// The default font of the content of the alert view 29 | public var contentFont: Font 30 | /// The spacing of the content of the alert view 31 | public var spacing: CGFloat 32 | /// The alignment of the content of the alert view 33 | public var alignment: CustomAlertAlignment 34 | /// Optional shadow applied to the alert 35 | public var shadow: CustomAlertShadow? 36 | 37 | init() { 38 | self.background = .blurEffect(.systemMaterial) 39 | self.cornerRadius = 13.3333 40 | self.padding = EdgeInsets(top: 20, leading: 8, bottom: 20, trailing: 8) 41 | self.accessibilityPadding = EdgeInsets(top: 37.5, leading: 12, bottom: 37.5, trailing: 12) 42 | self.minWidth = 270 43 | self.accessibilityMinWidth = 329 44 | self.titleFont = .headline 45 | self.contentFont = .footnote 46 | self.spacing = 4 47 | self.alignment = .center 48 | self.shadow = nil 49 | } 50 | 51 | /// Create a custom configuration 52 | /// 53 | /// - Parameter configure: Callback to change the default configuration 54 | /// 55 | /// - Returns: The customized ``CustomAlertConfiguration/Alert`` configuration 56 | public static func create(configure: (inout Self) -> Void) -> Self { 57 | var configuration = Self() 58 | configure(&configuration) 59 | return configuration 60 | } 61 | 62 | /// The default configuration 63 | @MainActor public static var `default`: CustomAlertConfiguration { 64 | CustomAlertConfiguration() 65 | } 66 | 67 | var textAlignment: TextAlignment { 68 | switch alignment { 69 | case .leading: 70 | return .leading 71 | case .trailing: 72 | return .leading 73 | case .center: 74 | return .center 75 | } 76 | } 77 | 78 | var horizontalAlignment: HorizontalAlignment { 79 | switch alignment { 80 | case .leading: 81 | return .leading 82 | case .trailing: 83 | return .leading 84 | case .center: 85 | return .center 86 | } 87 | } 88 | 89 | var frameAlignment: Alignment { 90 | switch alignment { 91 | case .leading: 92 | return .leading 93 | case .trailing: 94 | return .leading 95 | case .center: 96 | return .center 97 | } 98 | } 99 | } 100 | } 101 | 102 | /// The alignment of the content of the custom alert 103 | public enum CustomAlertAlignment: Sendable { 104 | /// The content is aligned in the center 105 | case center 106 | /// The content is aligned on the leading edge 107 | case leading 108 | /// The content is aligned on the trailing edge 109 | case trailing 110 | } 111 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Configuration/CustomAlertConfiguration.Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertConfiguration.Button.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 17.10.23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension CustomAlertConfiguration { 12 | public struct Button: Sendable { 13 | /// Configuration values of a custom alert button 14 | /// The tint color of the alert button 15 | public var tintColor: Color? 16 | /// The pressed tint color of the alert button 17 | public var pressedTintColor: Color? 18 | internal var roleColor: [ButtonType: Color] 19 | /// The padding of the alert button 20 | public var padding: EdgeInsets 21 | /// The padding of the alert button when using accessibility scaling 22 | public var accessibilityPadding: EdgeInsets 23 | /// The font of the alert button 24 | public var font: Font 25 | internal var roleFont: [ButtonType: Font] 26 | /// Whether to hide the dividers between the buttons 27 | public var hideDivider: Bool 28 | /// The background of the alert button 29 | public var background: CustomAlertBackground 30 | /// The pressed background of the alert button 31 | public var pressedBackground: CustomAlertBackground 32 | 33 | init() { 34 | self.tintColor = nil 35 | self.pressedTintColor = nil 36 | self.roleColor = [.destructive: .red] 37 | self.padding = EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12) 38 | self.accessibilityPadding = EdgeInsets(top: 20, leading: 12, bottom: 20, trailing: 12) 39 | self.font = .body 40 | self.roleFont = [.cancel: .headline] 41 | self.hideDivider = false 42 | self.background = .color(.almostClear) 43 | self.pressedBackground = .color(Color(.customAlertBackgroundColor)) 44 | } 45 | 46 | public mutating func font(_ font: Font, for role: ButtonRole) { 47 | guard let type = ButtonType(from: role) else { return } 48 | self.roleFont[type] = font 49 | } 50 | 51 | public mutating func color(_ color: Color, for role: ButtonRole) { 52 | guard let type = ButtonType(from: role) else { return } 53 | self.roleColor[type] = color 54 | } 55 | 56 | /// Create a custom configuration 57 | /// 58 | /// - Parameter configure: Callback to change the default configuration 59 | /// 60 | /// - Returns: The customized ``CustomAlertConfiguration/Button`` configuration 61 | public static func create(configure: (inout Self) -> Void) -> Self { 62 | var configuration = Self() 63 | configure(&configuration) 64 | return configuration 65 | } 66 | 67 | /// The default configuration 68 | public static var `default`: Self { 69 | Self() 70 | } 71 | } 72 | } 73 | 74 | /// Internal button type because `ButtonRole` is not `Hashable` 75 | enum ButtonType: Hashable { 76 | case destructive 77 | case cancel 78 | 79 | init?(from role: ButtonRole) { 80 | switch role { 81 | case .destructive: 82 | self = .destructive 83 | case .cancel: 84 | self = .cancel 85 | default: 86 | return nil 87 | } 88 | } 89 | } 90 | 91 | private extension UIColor { 92 | static var customAlertColor: UIColor { 93 | let traitCollection = UITraitCollection(activeAppearance: .active) 94 | return .tintColor.resolvedColor(with: traitCollection) 95 | } 96 | 97 | static var customAlertBackgroundColor: UIColor { 98 | UIColor { traitCollection in 99 | switch traitCollection.userInterfaceStyle { 100 | case .dark: 101 | UIColor.white.withAlphaComponent(0.135) 102 | default: 103 | UIColor.black.withAlphaComponent(0.085) 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Configuration/CustomAlertConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertConfiguration.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 17.10.23. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /// Configuration values for custom alerts 12 | @MainActor public struct CustomAlertConfiguration: Sendable { 13 | /// The configuration of the alert view 14 | public var alert: Alert 15 | /// The configuration of the alert buttons 16 | public var button: Button 17 | /// The window background behind the alert 18 | public var background: CustomAlertBackground 19 | /// The padding around the alert 20 | public var padding: EdgeInsets 21 | /// The transition the alert appears with 22 | public var transition: AnyTransition 23 | /// Animate the alert appearance 24 | public var animateTransition: Bool 25 | /// The vertical alginment of the alert 26 | public var alignment: VerticalAlignment 27 | /// Allow dismissing the alert when tapping on the background 28 | public var dismissOnBackgroundTap: Bool 29 | 30 | public init() { 31 | self.alert = .init() 32 | self.button = .init() 33 | self.background = .color(Color("DimmingBackround", bundle: .module)) 34 | self.padding = EdgeInsets(top: 0, leading: 30, bottom: 16, trailing: 30) 35 | self.transition = .opacity.combined(with: .scale(scale: 1.1)) 36 | self.animateTransition = true 37 | self.alignment = .center 38 | self.dismissOnBackgroundTap = false 39 | } 40 | 41 | /// Create a custom configuration 42 | /// 43 | /// - Parameter configure: Callback to change the default configuration 44 | /// 45 | /// - Returns: The customized ``CustomAlertConfiguration`` configuration 46 | public static func create(configure: (inout Self) -> Void) -> Self { 47 | var configuration = Self() 48 | configure(&configuration) 49 | return configuration 50 | } 51 | 52 | /// The default configuration 53 | public static nonisolated var `default`: CustomAlertConfiguration { 54 | MainActor.runSync { 55 | CustomAlertConfiguration() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Configuration/CustomAlertShadow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertShadow.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 16.11.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Shadow configuration of the alert 11 | public struct CustomAlertShadow: Sendable { 12 | let color: Color 13 | let radius: CGFloat 14 | let x: CGFloat 15 | let y: CGFloat 16 | 17 | /// Create a custom alert shadow 18 | /// 19 | /// - Parameters: 20 | /// - color: The shadow's color. 21 | /// - radius: A measure of how much to blur the shadow. Larger values 22 | /// result in more blur. 23 | /// - x: An amount to offset the shadow horizontally from the view. 24 | /// - y: An amount to offset the shadow vertically from the view. 25 | public init( 26 | color: Color = Color(.sRGBLinear, white: 0, opacity: 0.33), 27 | radius: CGFloat, 28 | x: CGFloat = 0, 29 | y: CGFloat = 0 30 | ) { 31 | self.color = color 32 | self.radius = radius 33 | self.x = x 34 | self.y = y 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/CustomAlert/CustomAlertHandler+Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertHandler+Item.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 28.06.23. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import WindowKit 11 | 12 | @MainActor struct CustomAlertItemHandler: ViewModifier where AlertItem: Identifiable, AlertContent: View, AlertActions: View { 13 | @Environment(\.customAlertConfiguration) private var configuration 14 | 15 | @Binding var item: AlertItem? 16 | var windowScene: UIWindowScene? 17 | var alertTitle: () -> Text? 18 | @ViewBuilder var alertContent: (AlertItem) -> AlertContent 19 | @ViewBuilder var alertActions: (AlertItem) -> AlertActions 20 | 21 | init( 22 | item: Binding, 23 | windowScene: UIWindowScene? = nil, 24 | alertTitle: @escaping () -> Text?, 25 | @ViewBuilder alertContent: @escaping (AlertItem) -> AlertContent, 26 | @ViewBuilder alertActions: @escaping (AlertItem) -> AlertActions 27 | ) { 28 | self._item = item 29 | self.windowScene = windowScene 30 | self.alertTitle = alertTitle 31 | self.alertContent = alertContent 32 | self.alertActions = alertActions 33 | } 34 | 35 | func body(content: Content) -> some View { 36 | if let windowScene { 37 | content 38 | .disabled(item != nil) 39 | .windowCover(item: $item, on: windowScene) { item in 40 | alertView(for: item) 41 | } configure: { configuration in 42 | configuration.tintColor = .customAlertColor 43 | configuration.modalPresentationStyle = .overFullScreen 44 | configuration.modalTransitionStyle = .crossDissolve 45 | } 46 | .background(alertIdentity) 47 | } else { 48 | content 49 | .disabled(item != nil) 50 | .windowCover(item: $item) { item in 51 | alertView(for: item) 52 | } configure: { configuration in 53 | configuration.tintColor = .customAlertColor 54 | configuration.modalPresentationStyle = .overFullScreen 55 | configuration.modalTransitionStyle = .crossDissolve 56 | } 57 | .background(alertIdentity) 58 | } 59 | } 60 | 61 | func alertView(for item: AlertItem) -> some View { 62 | CustomAlert(isPresented: isPresented) { 63 | alertTitle() 64 | } content: { 65 | alertContent(item) 66 | } actions: { 67 | alertActions(item) 68 | } 69 | .transformEnvironment(\.self) { environment in 70 | environment.isEnabled = true 71 | } 72 | } 73 | 74 | /// The view identity of the alert 75 | /// 76 | /// The `alertIdentity` represents the individual parts of the alert but combined into a single view. 77 | /// 78 | /// When attached to the content of the represeting view, any changes here will propagate to the content of the window which hosts the alert. 79 | @ViewBuilder var alertIdentity: some View { 80 | if let item { 81 | ZStack { 82 | alertTitle() 83 | alertContent(item) 84 | alertActions(item) 85 | } 86 | .accessibilityHidden(true) 87 | .frame(width: 0, height: 0) 88 | .hidden() 89 | } 90 | } 91 | 92 | private var isPresented: Binding { 93 | Binding { 94 | item != nil 95 | } set: { newValue in 96 | guard !newValue else { return } 97 | item = nil 98 | } 99 | } 100 | } 101 | 102 | private extension UIColor { 103 | static var customAlertColor: UIColor { 104 | let traitCollection = UITraitCollection(activeAppearance: .active) 105 | return .tintColor.resolvedColor(with: traitCollection) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/CustomAlert/CustomAlertHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertHandler.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 26.05.24. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import WindowKit 11 | 12 | @MainActor struct CustomAlertHandler: ViewModifier where AlertContent: View, AlertActions: View { 13 | @Environment(\.customAlertConfiguration) private var configuration 14 | 15 | @Binding var isPresented: Bool 16 | var windowScene: UIWindowScene? 17 | var alertTitle: () -> Text? 18 | @ViewBuilder var alertContent: () -> AlertContent 19 | @ViewBuilder var alertActions: () -> AlertActions 20 | 21 | init( 22 | isPresented: Binding, 23 | windowScene: UIWindowScene? = nil, 24 | alertTitle: @escaping () -> Text?, 25 | @ViewBuilder alertContent: @escaping () -> AlertContent, 26 | @ViewBuilder alertActions: @escaping () -> AlertActions 27 | ) { 28 | self._isPresented = isPresented 29 | self.windowScene = windowScene 30 | self.alertTitle = alertTitle 31 | self.alertContent = alertContent 32 | self.alertActions = alertActions 33 | } 34 | 35 | func body(content: Content) -> some View { 36 | if let windowScene { 37 | content 38 | .disabled(isPresented) 39 | .windowCover(isPresented: $isPresented, on: windowScene) { 40 | alertView 41 | } configure: { configuration in 42 | configuration.tintColor = .customAlertColor 43 | configuration.modalPresentationStyle = .overFullScreen 44 | configuration.modalTransitionStyle = .crossDissolve 45 | } 46 | .background(alertIdentity) 47 | } else { 48 | content 49 | .disabled(isPresented) 50 | .windowCover(isPresented: $isPresented) { 51 | alertView 52 | } configure: { configuration in 53 | configuration.tintColor = .customAlertColor 54 | configuration.modalPresentationStyle = .overFullScreen 55 | configuration.modalTransitionStyle = .crossDissolve 56 | } 57 | .background(alertIdentity) 58 | } 59 | } 60 | 61 | var alertView: some View { 62 | CustomAlert(isPresented: $isPresented) { 63 | alertTitle() 64 | } content: { 65 | alertContent() 66 | } actions: { 67 | alertActions() 68 | } 69 | .transformEnvironment(\.self) { environment in 70 | environment.isEnabled = true 71 | } 72 | } 73 | 74 | /// The view identity of the alert 75 | /// 76 | /// The `alertIdentity` represents the individual parts of the alert but combined into a single view. 77 | /// 78 | /// When attached to the content of the represeting view, any changes here will propagate to the content of the window which hosts the alert. 79 | @ViewBuilder var alertIdentity: some View { 80 | ZStack { 81 | alertTitle() 82 | alertContent() 83 | alertActions() 84 | } 85 | .accessibilityHidden(true) 86 | .frame(width: 0, height: 0) 87 | .hidden() 88 | } 89 | } 90 | 91 | private extension UIColor { 92 | static var customAlertColor: UIColor { 93 | let traitCollection = UITraitCollection(activeAppearance: .active) 94 | return .tintColor.resolvedColor(with: traitCollection) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | The API is very similar to the SwiftUI Alerts 4 | 5 | ## Overview 6 | 7 | | SwiftUI Alert | Custom Alert | 8 | |:-:|:-:| 9 | | ![Native Alert](SwiftUI) | ![Custom Alert](Custom) | 10 | 11 | You can easily add an Image or change the Font used in the alert, or anything else to your imagination. 12 | 13 | Something simple with an image and a text field 14 | 15 | ![Custom Alert](Fancy) 16 | 17 | Or more complex layouts 18 | 19 | ![Custom Alert](Complex) 20 | 21 | The API is very similar to the SwiftUI Alerts 22 | 23 | ```swift 24 | .customAlert("Some Fancy Alert", isPresented: $showAlert) { 25 | Text("I'm a custom Message") 26 | .font(.custom("Noteworthy", size: 24)) 27 | Image(systemName: "swift") 28 | .resizable() 29 | .scaledToFit() 30 | .frame(maxHeight: 100) 31 | .foregroundColor(.blue) 32 | } actions: { 33 | Button { 34 | // some Action 35 | } label: { 36 | Label("Swift", systemImage: "swift") 37 | } 38 | 39 | Button(role: .cancel) { 40 | // some Action 41 | } label: { 42 | Text("Cancel") 43 | } 44 | } 45 | ``` 46 | 47 | You can create Side by Side Buttons using `MultiButton` 48 | 49 | ```swift 50 | .customAlert("Alert with Side by Side Buttons", isPresented: $showAlert) { 51 | Text("Choose left or right") 52 | } actions: { 53 | MultiButton { 54 | Button { 55 | // some Action 56 | } label: { 57 | Text("Left") 58 | } 59 | 60 | Button { 61 | // some Action 62 | } label: { 63 | Text("Right") 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | The alert is customizable via the `Environment` 70 | 71 | ![Inline Alert](CustomConfiguration) 72 | 73 | ```swift 74 | .configureCustomAlert { configuration in 75 | // Adapt the default configuration 76 | } 77 | ``` 78 | 79 | You can also display an Alert inline, within a `List` for example 80 | 81 | ![Inline Alert](InlineAlert) 82 | 83 | ```swift 84 | CustomAlertRow { 85 | // Content 86 | } actions: { 87 | // Actions 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/Documentation.md: -------------------------------------------------------------------------------- 1 | # ``CustomAlert`` 2 | 3 | In iOS Alerts cannot contain Images or anything other than Text. This allows you to easily customize the message part with any custom view. 4 | 5 | While the alert is completely rebuilt in SwiftUI, it has been designed to look and behave exactly like a native alert. The alert uses it's own window to be displayed and utilizes accessibility scaling but with the advantage of a custom view. 6 | 7 | If the content is too large because the text is too long or the text doesn't fit because of accessibility scaling the content will scroll just like in a SwiftUI Alert. 8 | 9 | ## Topics 10 | 11 | ### Articles 12 | 13 | - 14 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/Resources/Complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divadretlaw/CustomAlert/a76ce9ffaca03ff2ee21bc51cf4f8da95eeadf18/Sources/CustomAlert/Documentation.docc/Resources/Complex.png -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/Resources/Custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divadretlaw/CustomAlert/a76ce9ffaca03ff2ee21bc51cf4f8da95eeadf18/Sources/CustomAlert/Documentation.docc/Resources/Custom.png -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/Resources/CustomConfiguration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divadretlaw/CustomAlert/a76ce9ffaca03ff2ee21bc51cf4f8da95eeadf18/Sources/CustomAlert/Documentation.docc/Resources/CustomConfiguration.png -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/Resources/Fancy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divadretlaw/CustomAlert/a76ce9ffaca03ff2ee21bc51cf4f8da95eeadf18/Sources/CustomAlert/Documentation.docc/Resources/Fancy.png -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/Resources/InlineAlert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divadretlaw/CustomAlert/a76ce9ffaca03ff2ee21bc51cf4f8da95eeadf18/Sources/CustomAlert/Documentation.docc/Resources/InlineAlert.png -------------------------------------------------------------------------------- /Sources/CustomAlert/Documentation.docc/Resources/SwiftUI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divadretlaw/CustomAlert/a76ce9ffaca03ff2ee21bc51cf4f8da95eeadf18/Sources/CustomAlert/Documentation.docc/Resources/SwiftUI.png -------------------------------------------------------------------------------- /Sources/CustomAlert/Environment/Environment+AlertButtonHeight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment+AlertButtonHeight.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 26.03.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private struct AlertButtonHeightKey: EnvironmentKey { 11 | static var defaultValue: CGFloat? { 12 | nil 13 | } 14 | } 15 | 16 | extension EnvironmentValues { 17 | var alertButtonHeight: CGFloat? { 18 | get { self[AlertButtonHeightKey.self] } 19 | set { self[AlertButtonHeightKey.self] = newValue } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Environment/Environment+AlertDismissAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment+AlertDismissAction.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 25.03.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// An action that dismisses a custom alert presentation. 11 | /// 12 | /// Use the `alertDismiss` environment value to get the instance 13 | /// of this structure for a given `Environment`. Then call the instance 14 | /// to perform the dismissal. You call the instance directly because 15 | /// it defines a ``AlertDismissAction/callAsFunction()`` 16 | /// method that Swift calls when you call the instance. 17 | public struct AlertDismissAction { 18 | let action: () -> Void 19 | 20 | /// Dismisses the alert if it is currently presented. 21 | /// 22 | /// Don't call this method directly. SwiftUI calls it for you when you 23 | /// call the ``AlertDismissAction`` structure that you get from the 24 | /// `Environment`: 25 | /// 26 | /// ```swift 27 | /// private struct SheetContents: View { 28 | /// @Environment(\.alertDismiss) private var alertDismiss 29 | /// 30 | /// var body: some View { 31 | /// Button("Done") { 32 | /// alertDismiss() // Implicitly calls dismiss.callAsFunction() 33 | /// } 34 | /// } 35 | /// } 36 | /// ``` 37 | /// 38 | /// For information about how Swift uses the `callAsFunction()` method to 39 | /// simplify call site syntax, see 40 | /// [Methods with Special Names](https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#ID622) 41 | /// in *The Swift Programming Language*. 42 | public func callAsFunction() { 43 | action() 44 | } 45 | } 46 | 47 | private struct AlertDismissActionKey: EnvironmentKey { 48 | static var defaultValue: AlertDismissAction { 49 | AlertDismissAction { 50 | // nothing 51 | } 52 | } 53 | } 54 | 55 | public extension EnvironmentValues { 56 | /// An action that dismisses the current alert presentation presentation. 57 | var alertDismiss: AlertDismissAction { 58 | get { self[AlertDismissActionKey.self] } 59 | set { self[AlertDismissActionKey.self] = newValue } 60 | } 61 | } 62 | 63 | public extension View { 64 | /// Perform an action when the alert dismisses 65 | func onAlertDismiss(action: @escaping () -> Void) -> some View { 66 | environment(\.alertDismiss, AlertDismissAction(action: action)) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Environment/Environment+CustomAlertConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment+CustomAlertConfiguration.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 17.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private struct CustomAlertConfigurationKey: EnvironmentKey { 11 | static var defaultValue: CustomAlertConfiguration { 12 | CustomAlertConfiguration.default 13 | } 14 | } 15 | 16 | public extension EnvironmentValues { 17 | /// The configuration values for custom alerts 18 | var customAlertConfiguration: CustomAlertConfiguration { 19 | get { self[CustomAlertConfigurationKey.self] } 20 | set { self[CustomAlertConfigurationKey.self] = newValue } 21 | } 22 | } 23 | 24 | public extension View { 25 | /// Create a custom alert configuration 26 | /// 27 | /// - Parameter configure: Callback to change the current configuration 28 | /// 29 | /// - Returns: The view with a customized ``CustomAlertConfiguration`` 30 | func configureCustomAlert(configure: @escaping (inout CustomAlertConfiguration) -> Void) -> some View { 31 | modifier(CustomAlertConfigurator(configure: configure)) 32 | } 33 | 34 | /// Create a custom alert configuration 35 | /// 36 | /// - Parameter configuration: The custom alert configuration 37 | /// 38 | /// - Returns: The view with a customized ``CustomAlertConfiguration`` 39 | func configureCustomAlert(_ configuration: CustomAlertConfiguration) -> some View { 40 | environment(\.customAlertConfiguration, configuration) 41 | } 42 | } 43 | 44 | private struct CustomAlertConfigurator: ViewModifier { 45 | @Environment(\.customAlertConfiguration) private var configuration 46 | var configure: (inout CustomAlertConfiguration) -> Void 47 | 48 | func body(content: Content) -> some View { 49 | content 50 | .environment(\.customAlertConfiguration, update(configure: configure)) 51 | } 52 | 53 | private func update(configure: (inout CustomAlertConfiguration) -> Void) -> CustomAlertConfiguration { 54 | var configuration = self.configuration 55 | configure(&configuration) 56 | return configuration 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Extensions/ColorExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorExtensions.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 31.03.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension Color { 11 | static var almostClear: Color { 12 | Color.black.opacity(0.0001) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Extensions/EdgeInsetsExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeInsetsExtensions.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 31.03.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension EdgeInsets { 11 | static var zero: EdgeInsets { 12 | EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Extensions/MainActorExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainActorExtensions.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 20.07.24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension MainActor { 11 | #if swift(<5.10) 12 | @_unavailableFromAsync 13 | private static func runUnsafely(_ body: @MainActor () throws -> T) rethrows -> T { 14 | if #available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *) { 15 | return try MainActor.assumeIsolated(body) 16 | } else { 17 | dispatchPrecondition(condition: .onQueue(.main)) 18 | return try withoutActuallyEscaping(body) { function in 19 | try unsafeBitCast(function, to: (() throws -> T).self)() 20 | } 21 | } 22 | } 23 | #endif 24 | 25 | /// Execute the given body closure on the main actor without enforcing MainActor isolation. 26 | /// 27 | /// The method will be dispatched in sync to the main-thread if its on a non-main thread. 28 | @_unavailableFromAsync 29 | static func runSync(_ body: @MainActor () throws -> T) rethrows -> T where T: Sendable { 30 | if Thread.isMainThread { 31 | #if swift(>=5.10) 32 | try MainActor.assumeIsolated(body) 33 | #else 34 | try MainActor.runUnsafely(body) 35 | #endif 36 | } else { 37 | try DispatchQueue.main.sync { 38 | #if swift(>=5.10) 39 | try MainActor.assumeIsolated(body) 40 | #else 41 | try MainActor.runUnsafely(body) 42 | #endif 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Extensions/ProcessInfoExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessInfoExtensions.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 20.10.24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ProcessInfo { 11 | var isiOSAppOnVision: Bool { 12 | NSClassFromString("UIWindowSceneGeometryPreferencesVision") != nil 13 | } 14 | 15 | var isiOSAppOnOtherPlatform: Bool { 16 | isiOSAppOnMac || isMacCatalystApp || isiOSAppOnVision 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Helper/CaptureSize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaptureSize.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 31.03.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private struct IntrinsicContentSizePreferenceKey: PreferenceKey { 11 | static let defaultValue: CGSize = .zero 12 | 13 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) { 14 | value = nextValue() 15 | } 16 | } 17 | 18 | private struct IntrinsicSafeAreaPreferenceKey: PreferenceKey { 19 | static let defaultValue: EdgeInsets = .zero 20 | 21 | static func reduce(value: inout EdgeInsets, nextValue: () -> EdgeInsets) { 22 | value = nextValue() 23 | } 24 | } 25 | 26 | extension View { 27 | func captureSize(_ size: Binding) -> some View { 28 | captureSize { anchor, proxy in 29 | anchor 30 | .preference(key: IntrinsicContentSizePreferenceKey.self, value: proxy.size) 31 | .onPreferenceChange(IntrinsicContentSizePreferenceKey.self) { 32 | size.wrappedValue = $0 33 | } 34 | } 35 | } 36 | 37 | func captureTotalSize(_ size: Binding) -> some View { 38 | captureSize { anchor, proxy in 39 | anchor 40 | .preference(key: IntrinsicContentSizePreferenceKey.self, value: proxy.size) 41 | .onPreferenceChange(IntrinsicContentSizePreferenceKey.self) { 42 | size.wrappedValue = $0 + proxy.safeAreaInsets 43 | } 44 | .preference(key: IntrinsicSafeAreaPreferenceKey.self, value: proxy.safeAreaInsets) 45 | .onPreferenceChange(IntrinsicSafeAreaPreferenceKey.self) { 46 | size.wrappedValue = proxy.size + $0 47 | } 48 | } 49 | } 50 | 51 | func captureSafeAreaInsets(_ safeArea: Binding) -> some View { 52 | captureSize { anchor, proxy in 53 | anchor 54 | .preference(key: IntrinsicSafeAreaPreferenceKey.self, value: proxy.safeAreaInsets) 55 | .onPreferenceChange(IntrinsicSafeAreaPreferenceKey.self) { 56 | safeArea.wrappedValue = $0 57 | } 58 | } 59 | } 60 | } 61 | 62 | private extension View { 63 | func captureSize( 64 | @ViewBuilder builder: @escaping (_ anchor: AnyView, _ proxy: SendableGeometryProxy) -> Result 65 | ) -> some View where Result: View { 66 | modifier(CaptureSize(builder: builder)) 67 | } 68 | } 69 | 70 | private struct CaptureSize: ViewModifier where Result: View { 71 | let builder: (_ anchor: AnyView, _ proxy: SendableGeometryProxy) -> Result 72 | 73 | init( 74 | @ViewBuilder builder: @escaping (_ anchor: AnyView, _ proxy: SendableGeometryProxy) -> Result 75 | ) { 76 | self.builder = builder 77 | } 78 | 79 | func body(content: Content) -> some View { 80 | content 81 | .background { 82 | GeometryReader { proxy in 83 | builder(AnyView(Color.clear), SendableGeometryProxy(proxy: proxy)) 84 | } 85 | } 86 | } 87 | } 88 | 89 | private struct SendableGeometryProxy: Sendable { 90 | let size: CGSize 91 | let safeAreaInsets: EdgeInsets 92 | 93 | init(proxy: GeometryProxy) { 94 | self.size = proxy.size 95 | self.safeAreaInsets = proxy.safeAreaInsets 96 | } 97 | } 98 | 99 | 100 | private extension CGSize { 101 | static func + (lhs: CGSize, rhs: EdgeInsets) -> CGSize { 102 | let width = lhs.width + rhs.leading + rhs.trailing 103 | let height = lhs.height + rhs.top + rhs.bottom 104 | return CGSize(width: width, height: height) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Helper/OnSimultaneousTapGesture.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnSimultaneousTapGesture.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 05.04.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | func onSimultaneousTapGesture( 12 | count: Int = 1, 13 | perform action: @escaping () -> Void 14 | ) -> some View { 15 | modifier(SimultaneousTapGestureViewModifier(count: count, perform: action)) 16 | } 17 | } 18 | 19 | private struct SimultaneousTapGestureViewModifier: ViewModifier { 20 | let count: Int 21 | let action: () -> Void 22 | 23 | init( 24 | count: Int, 25 | perform action: @escaping () -> Void 26 | ) { 27 | self.count = count 28 | self.action = action 29 | } 30 | 31 | func body(content: Content) -> some View { 32 | #if compiler(<6.0) 33 | content 34 | .overlay( 35 | SimultaneousTapGesture( 36 | numberOfTapsRequired: count, 37 | action: action 38 | ) 39 | ) 40 | #else 41 | if #available(iOS 18.0, *) { 42 | if ProcessInfo.processInfo.isiOSAppOnOtherPlatform { 43 | content 44 | .overlay( 45 | SimultaneousTapGesture( 46 | numberOfTapsRequired: count, 47 | action: action 48 | ) 49 | ) 50 | } else { 51 | content 52 | .simultaneousGesture(simultaneousTapGesture) 53 | } 54 | } else { 55 | content 56 | .overlay( 57 | SimultaneousTapGesture( 58 | numberOfTapsRequired: count, 59 | action: action 60 | ) 61 | ) 62 | } 63 | #endif 64 | } 65 | 66 | var simultaneousTapGesture: some Gesture { 67 | TapGesture().onEnded { 68 | action() 69 | } 70 | } 71 | } 72 | 73 | private struct SimultaneousTapGesture: UIViewRepresentable { 74 | let numberOfTapsRequired: Int 75 | let action: () -> Void 76 | 77 | func makeUIView(context: Context) -> UIView { 78 | let view = UIView() 79 | view.isUserInteractionEnabled = true 80 | 81 | let tapGestureRecognizer = UITapGestureRecognizer( 82 | target: context.coordinator, 83 | action: #selector(Coordinator.tap) 84 | ) 85 | tapGestureRecognizer.numberOfTapsRequired = numberOfTapsRequired 86 | tapGestureRecognizer.delegate = context.coordinator 87 | view.addGestureRecognizer(tapGestureRecognizer) 88 | 89 | return view 90 | } 91 | 92 | func updateUIView(_ uiView: UIView, context: Context) { 93 | } 94 | 95 | func makeCoordinator() -> Coordinator { 96 | Coordinator(action: action) 97 | } 98 | 99 | final class Coordinator: NSObject, UIGestureRecognizerDelegate { 100 | let action: () -> Void 101 | 102 | init(action: @escaping () -> Void) { 103 | self.action = action 104 | } 105 | 106 | @objc func tap() { 107 | action() 108 | } 109 | 110 | // MARK: UIGestureRecognizerDelegate 111 | 112 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 113 | return true 114 | } 115 | 116 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 117 | return true 118 | } 119 | 120 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { 121 | return false 122 | } 123 | 124 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { 125 | return false 126 | } 127 | 128 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { 129 | return true 130 | } 131 | 132 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool { 133 | return true 134 | } 135 | 136 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive event: UIEvent) -> Bool { 137 | return true 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Helper/ScrollViewDisabled.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollViewDisabled.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 31.03.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | func scrollViewDisabled(_ disabled: Bool) -> some View { 12 | modifier(DisabledViewModifier(isDisabled: disabled)) 13 | } 14 | } 15 | 16 | private struct DisabledViewModifier: ViewModifier { 17 | let isDisabled: Bool 18 | 19 | func body(content: Content) -> some View { 20 | if #available(iOS 16.0, *) { 21 | content.scrollDisabled(isDisabled) 22 | } else { 23 | content.disabled(isDisabled) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Helper/ShadowApplier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShadowApplier.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 16.11.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | func shadow(_ shadow: CustomAlertShadow?) -> some View { 12 | modifier(ShadowApplier(shadow: shadow)) 13 | } 14 | } 15 | 16 | private struct ShadowApplier: ViewModifier { 17 | let shadow: CustomAlertShadow? 18 | 19 | func body(content: Content) -> some View { 20 | if let shadow { 21 | content.shadow(color: shadow.color, radius: shadow.radius, x: shadow.x, y: shadow.y) 22 | } else { 23 | content 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Views/BackgroundView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundView.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 17.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BackgroundView: View { 11 | var background: CustomAlertBackground 12 | 13 | var body: some View { 14 | switch background { 15 | case let .blurEffect(style): 16 | BlurView(style: style) 17 | case let .color(color): 18 | color 19 | case let .colorBlurEffect(color, style): 20 | ZStack { 21 | color 22 | BlurView(style: style) 23 | } 24 | case let .anyView(view): 25 | view 26 | } 27 | } 28 | } 29 | 30 | struct BackgroundView_Previews: PreviewProvider { 31 | static var previews: some View { 32 | BackgroundView(background: .blurEffect(.regular)) 33 | BackgroundView(background: .color(.blue)) 34 | BackgroundView(background: .colorBlurEffect(.blue, .regular)) 35 | BackgroundView(background: .view { 36 | ZStack { 37 | Color.green 38 | Text("Test") 39 | } 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Views/BlurView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlurView.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 31.03.24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | // Using UIViewRepresentable BlurView for backwards compatibility 11 | struct BlurView: UIViewRepresentable { 12 | let style: UIBlurEffect.Style 13 | 14 | func makeUIView(context: UIViewRepresentableContext) -> UIView { 15 | let view = UIView(frame: .zero) 16 | view.backgroundColor = .clear 17 | let blurEffect = UIBlurEffect(style: style) 18 | let blurView = UIVisualEffectView(effect: blurEffect) 19 | blurView.translatesAutoresizingMaskIntoConstraints = false 20 | view.insertSubview(blurView, at: 0) 21 | NSLayoutConstraint.activate([ 22 | blurView.heightAnchor.constraint(equalTo: view.heightAnchor), 23 | blurView.widthAnchor.constraint(equalTo: view.widthAnchor) 24 | ]) 25 | return view 26 | } 27 | 28 | func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext) { 29 | // empty 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Views/CustomAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlert.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 03.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Custom Alert 11 | @MainActor public struct CustomAlert: View where Content: View, Actions: View { 12 | @Environment(\.customAlertConfiguration) private var configuration 13 | @Environment(\.dynamicTypeSize) private var dynamicTypeSize 14 | 15 | @Binding var isPresented: Bool 16 | var title: Text? 17 | var content: Content 18 | var actions: Actions 19 | 20 | // Size holders to enable scrolling of the content if needed 21 | @State private var viewSize: CGSize = .zero 22 | @State private var safeAreaInsets: EdgeInsets = .zero 23 | @State private var contentSize: CGSize = .zero 24 | @State private var actionsSize: CGSize = .zero 25 | @State private var alertId: Int = 0 26 | 27 | @State private var fitInScreen = false 28 | 29 | // Used to animate the appearance 30 | @State private var isShowing = false 31 | 32 | public init( 33 | isPresented: Binding, 34 | title: @escaping () -> Text?, 35 | @ViewBuilder content: () -> Content, 36 | @ViewBuilder actions: () -> Actions 37 | ) { 38 | self._isPresented = isPresented 39 | self.title = title() 40 | self.content = content() 41 | self.actions = actions() 42 | } 43 | 44 | public init( 45 | title: @autoclosure @escaping () -> Text?, 46 | @ViewBuilder content: () -> Content, 47 | @ViewBuilder actions: () -> Actions 48 | ) { 49 | self._isPresented = .constant(true) 50 | self.title = title() 51 | self.content = content() 52 | self.actions = actions() 53 | } 54 | 55 | public var body: some View { 56 | GeometryReader { proxy in 57 | ZStack { 58 | BackgroundView(background: configuration.background) 59 | .edgesIgnoringSafeArea(.all) 60 | .accessibilityAddTraits(configuration.dismissOnBackgroundTap ? [.isButton] : []) 61 | .onTapGesture { 62 | if configuration.dismissOnBackgroundTap { 63 | isPresented = false 64 | } 65 | } 66 | 67 | VStack(spacing: 0) { 68 | if configuration.alignment.hasTopSpacer { 69 | Spacer() 70 | } 71 | 72 | if isShowing { 73 | alert 74 | .animation(nil, value: height) 75 | .id(alertId) 76 | } 77 | 78 | if configuration.alignment.hasBottomSpacer { 79 | Spacer() 80 | } 81 | } 82 | } 83 | .frame(maxWidth: proxy.totalWidth, maxHeight: proxy.totalHeight) 84 | .captureTotalSize($viewSize) 85 | } 86 | .captureSafeAreaInsets($safeAreaInsets) 87 | .onAppear { 88 | if configuration.animateTransition { 89 | withAnimation { 90 | isShowing = true 91 | } 92 | } else { 93 | isShowing = true 94 | } 95 | } 96 | } 97 | 98 | var height: CGFloat { 99 | // View height - padding top and bottom - actions height 100 | let maxHeight = viewSize.height 101 | - configuration.padding.top 102 | - configuration.padding.bottom 103 | - safeAreaInsets.top 104 | - safeAreaInsets.bottom 105 | - actionsSize.height 106 | let min = min(maxHeight, contentSize.height) 107 | return max(min, 0) 108 | } 109 | 110 | var minWidth: CGFloat { 111 | // View width - padding leading and trailing 112 | let maxWidth = viewSize.width 113 | - configuration.padding.leading 114 | - configuration.padding.trailing 115 | // Make sure it fits in the content 116 | let min = min(maxWidth, contentSize.width) 117 | return max(min, 0) 118 | } 119 | 120 | var maxWidth: CGFloat { 121 | // View width - padding leading and trailing 122 | let maxWidth = viewSize.width 123 | - configuration.padding.leading 124 | - configuration.padding.trailing 125 | - safeAreaInsets.leading 126 | - safeAreaInsets.trailing 127 | // Make sure it fits in the content 128 | let min = min(maxWidth, contentSize.width) 129 | 130 | if dynamicTypeSize.isAccessibilitySize { 131 | // Smallest AlertView should be 329 132 | return max(min, configuration.alert.accessibilityMinWidth) 133 | } else { 134 | // Smallest AlertView should be 270 135 | return max(min, configuration.alert.minWidth) 136 | } 137 | } 138 | 139 | var alertPadding: EdgeInsets { 140 | if dynamicTypeSize.isAccessibilitySize { 141 | configuration.alert.accessibilityPadding 142 | } else { 143 | configuration.alert.padding 144 | } 145 | } 146 | 147 | var alert: some View { 148 | VStack(spacing: 0) { 149 | GeometryReader { proxy in 150 | ScrollView(.vertical) { 151 | VStack(alignment: configuration.alert.horizontalAlignment, spacing: configuration.alert.spacing) { 152 | title? 153 | .font(configuration.alert.titleFont) 154 | .multilineTextAlignment(configuration.alert.textAlignment) 155 | 156 | content 157 | .font(configuration.alert.contentFont) 158 | .multilineTextAlignment(configuration.alert.textAlignment) 159 | .frame(maxWidth: .infinity, alignment: configuration.alert.frameAlignment) 160 | } 161 | .foregroundColor(.primary) 162 | .padding(alertPadding) 163 | .frame(maxWidth: .infinity) 164 | .captureSize($contentSize) 165 | // Force `Environment.isEnabled` to `true` because outer ScrollView is most likely disabled 166 | .environment(\.isEnabled, true) 167 | } 168 | .frame(height: height) 169 | .onChange(of: contentSize) { contentSize in 170 | fitInScreen = contentSize.height <= proxy.size.height 171 | } 172 | .scrollViewDisabled(fitInScreen) 173 | } 174 | .frame(height: height) 175 | 176 | Group { 177 | #if swift(>=6.0) 178 | if #available(iOS 18.0, *) { 179 | VStack(spacing: 0) { 180 | ForEach(subviews: actions) { child in 181 | if !configuration.button.hideDivider { 182 | Divider() 183 | } 184 | child 185 | } 186 | } 187 | } else { 188 | _VariadicView.Tree(ActionLayout()) { 189 | actions 190 | } 191 | } 192 | #else 193 | _VariadicView.Tree(ActionLayout()) { 194 | actions 195 | } 196 | #endif 197 | } 198 | .buttonStyle(.alert) 199 | .captureSize($actionsSize) 200 | } 201 | .onAlertDismiss { 202 | isPresented = false 203 | } 204 | .frame(minWidth: minWidth, maxWidth: maxWidth) 205 | .background(BackgroundView(background: configuration.alert.background)) 206 | .cornerRadius(configuration.alert.cornerRadius) 207 | .shadow(configuration.alert.shadow) 208 | .padding(configuration.padding) 209 | .transition(configuration.transition) 210 | .animation(.default, value: isPresented) 211 | .onChange(of: dynamicTypeSize) { _ in 212 | redrawAlert() 213 | } 214 | } 215 | 216 | func calculateAlertId() { 217 | var hasher = Hasher() 218 | hasher.combine(dynamicTypeSize) 219 | alertId = hasher.finalize() 220 | } 221 | 222 | func redrawAlert() { 223 | // Reset calculated sizes 224 | contentSize = .zero 225 | actionsSize = .zero 226 | // Force redraw 227 | calculateAlertId() 228 | } 229 | } 230 | 231 | @available(iOS, introduced: 14.0, deprecated: 18.0, message: "Use `ForEach(subviewOf:content:)` instead") 232 | @MainActor struct ActionLayout: _VariadicView_ViewRoot { 233 | @Environment(\.customAlertConfiguration) private var configuration 234 | 235 | #if swift(>=6.0) 236 | func body(children: _VariadicView.Children) -> some View { 237 | VStack(spacing: 0) { 238 | ForEach(children) { child in 239 | if !configuration.button.hideDivider { 240 | Divider() 241 | } 242 | child 243 | } 244 | } 245 | } 246 | #else 247 | nonisolated func body(children: _VariadicView.Children) -> some View { 248 | VStack(spacing: 0) { 249 | ForEach(children) { child in 250 | if !hideDivider { 251 | Divider() 252 | } 253 | child 254 | } 255 | } 256 | } 257 | 258 | nonisolated var hideDivider: Bool { 259 | MainActor.runSync { 260 | configuration.button.hideDivider 261 | } 262 | } 263 | #endif 264 | } 265 | 266 | private extension VerticalAlignment { 267 | var hasTopSpacer: Bool { 268 | switch self { 269 | case .top, .firstTextBaseline: 270 | return false 271 | default: 272 | return true 273 | } 274 | } 275 | 276 | var hasBottomSpacer: Bool { 277 | switch self { 278 | case .bottom, .lastTextBaseline: 279 | return false 280 | default: 281 | return true 282 | } 283 | } 284 | } 285 | 286 | private extension GeometryProxy { 287 | var totalWidth: CGFloat { 288 | size.width + safeAreaInsets.leading + safeAreaInsets.trailing 289 | } 290 | 291 | var totalHeight: CGFloat { 292 | size.height + safeAreaInsets.top + safeAreaInsets.bottom 293 | } 294 | } 295 | 296 | struct CustomAlert_Previews: PreviewProvider { 297 | static var previews: some View { 298 | CustomAlert(isPresented: .constant(true)) { 299 | Text("Preview") 300 | } content: { 301 | Text("Content") 302 | } actions: { 303 | Button { 304 | } label: { 305 | Text("OK") 306 | } 307 | } 308 | .previewDisplayName("Default") 309 | 310 | CustomAlert(isPresented: .constant(true)) { 311 | Text("Preview") 312 | } content: { 313 | Text("Content") 314 | } actions: { 315 | MultiButton { 316 | Button { 317 | } label: { 318 | Text("Cancel") 319 | } 320 | Button { 321 | } label: { 322 | Text("OK") 323 | } 324 | } 325 | } 326 | .environment(\.customAlertConfiguration, .create { configuration in 327 | configuration.background = .blurEffect(.dark) 328 | configuration.padding = EdgeInsets() 329 | configuration.alert = .create { alert in 330 | alert.background = .color(.white) 331 | alert.cornerRadius = 4 332 | alert.padding = EdgeInsets(top: 20, leading: 20, bottom: 15, trailing: 20) 333 | alert.minWidth = 300 334 | alert.titleFont = .headline 335 | alert.contentFont = .subheadline 336 | alert.alignment = .leading 337 | alert.spacing = 10 338 | } 339 | configuration.button = .create { button in 340 | button.tintColor = .purple 341 | button.padding = EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10) 342 | button.font = .callout.weight(.semibold) 343 | button.hideDivider = true 344 | } 345 | }) 346 | .previewDisplayName("Custom") 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Views/CustomAlertRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertRow.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 30.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Display a custom alert inlined into a `List` 11 | public struct CustomAlertRow: View where Content: View, Actions: View { 12 | @Binding var isPresented: Bool 13 | 14 | let content: Content 15 | let actions: Actions 16 | 17 | public var body: some View { 18 | if isPresented { 19 | VStack(spacing: 0) { 20 | content 21 | 22 | #if swift(>=6.0) 23 | if #available(iOS 18.0, *) { 24 | ForEach(subviews: actions) { child in 25 | Divider() 26 | child 27 | } 28 | } else { 29 | _VariadicView.Tree(ContentLayout()) { 30 | actions 31 | } 32 | } 33 | #else 34 | _VariadicView.Tree(ContentLayout()) { 35 | actions 36 | } 37 | #endif 38 | } 39 | .buttonStyle(.alert(triggerDismiss: false)) 40 | .listRowInsets(.zero) 41 | } 42 | } 43 | 44 | public init( 45 | isPresented: Binding, 46 | @ViewBuilder content: @escaping () -> Content, 47 | @ViewBuilder actions: @escaping () -> Actions 48 | ) { 49 | self._isPresented = isPresented 50 | self.content = content() 51 | self.actions = actions() 52 | } 53 | 54 | public init( 55 | @ViewBuilder content: @escaping () -> Content, 56 | @ViewBuilder actions: @escaping () -> Actions 57 | ) { 58 | self._isPresented = .constant(true) 59 | self.content = content() 60 | self.actions = actions() 61 | } 62 | 63 | @available(iOS, introduced: 14.0, deprecated: 18.0, message: "Use `ForEach(subviewOf:content:)` instead") 64 | struct ContentLayout: _VariadicView_ViewRoot { 65 | func body(children: _VariadicView.Children) -> some View { 66 | VStack(spacing: 0) { 67 | ForEach(children) { child in 68 | Divider() 69 | child 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | struct CustomAlertRow_Preview: PreviewProvider { 77 | static var previews: some View { 78 | Preview() 79 | } 80 | 81 | struct Preview: View { 82 | @State private var isPresented = false 83 | 84 | var body: some View { 85 | List { 86 | Section { 87 | Button { 88 | isPresented = true 89 | } label: { 90 | Text("Show Custom Alert Row") 91 | } 92 | 93 | CustomAlertRow(isPresented: $isPresented) { 94 | Text("Hello World") 95 | .padding() 96 | } actions: { 97 | MultiButton { 98 | Button(role: .cancel) { 99 | isPresented = false 100 | print("Cancel") 101 | } label: { 102 | Text("Cancel") 103 | } 104 | Button { 105 | isPresented = false 106 | print("OK") 107 | } label: { 108 | Text("OK") 109 | } 110 | } 111 | } 112 | } 113 | } 114 | .animation(.default, value: isPresented) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Views/CustomAlertSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomAlertSection.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 30.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// Display a custom alert inlined into a `List` 11 | public struct CustomAlertSection: View where Content: View, Actions: View, Header: View, Footer: View { 12 | @Binding var isPresented: Bool 13 | 14 | let content: Content 15 | let actions: Actions 16 | let header: Header 17 | let footer: Footer 18 | 19 | public var body: some View { 20 | if isPresented { 21 | Section { 22 | CustomAlertRow { 23 | content 24 | } actions: { 25 | actions 26 | } 27 | } header: { 28 | header 29 | } footer: { 30 | footer 31 | } 32 | } 33 | } 34 | 35 | public init( 36 | isPresented: Binding, 37 | @ViewBuilder content: @escaping () -> Content, 38 | @ViewBuilder actions: @escaping () -> Actions, 39 | @ViewBuilder header: @escaping () -> Header, 40 | @ViewBuilder footer: @escaping () -> Footer 41 | ) { 42 | self._isPresented = isPresented 43 | self.content = content() 44 | self.actions = actions() 45 | self.header = header() 46 | self.footer = footer() 47 | } 48 | 49 | public init( 50 | @ViewBuilder content: @escaping () -> Content, 51 | @ViewBuilder actions: @escaping () -> Actions, 52 | @ViewBuilder header: @escaping () -> Header, 53 | @ViewBuilder footer: @escaping () -> Footer 54 | ) { 55 | self._isPresented = .constant(true) 56 | self.content = content() 57 | self.actions = actions() 58 | self.header = header() 59 | self.footer = footer() 60 | } 61 | 62 | @available(iOS, introduced: 14.0, deprecated: 18.0, message: "Use `ForEach(subviewOf:content:)` instead") 63 | struct ContentLayout: _VariadicView_ViewRoot { 64 | func body(children: _VariadicView.Children) -> some View { 65 | VStack(spacing: 0) { 66 | ForEach(children) { child in 67 | Divider() 68 | child 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | extension CustomAlertSection where Header == EmptyView, Footer == EmptyView { 76 | public init( 77 | isPresented: Binding, 78 | @ViewBuilder content: @escaping () -> Content, 79 | @ViewBuilder actions: @escaping () -> Actions 80 | ) { 81 | self._isPresented = isPresented 82 | self.content = content() 83 | self.actions = actions() 84 | self.header = EmptyView() 85 | self.footer = EmptyView() 86 | } 87 | 88 | public init( 89 | @ViewBuilder content: @escaping () -> Content, 90 | @ViewBuilder actions: @escaping () -> Actions 91 | ) { 92 | self._isPresented = .constant(true) 93 | self.content = content() 94 | self.actions = actions() 95 | self.header = EmptyView() 96 | self.footer = EmptyView() 97 | } 98 | } 99 | 100 | extension CustomAlertSection where Footer == EmptyView { 101 | public init( 102 | isPresented: Binding, 103 | @ViewBuilder content: @escaping () -> Content, 104 | @ViewBuilder actions: @escaping () -> Actions, 105 | @ViewBuilder header: @escaping () -> Header 106 | ) { 107 | self._isPresented = isPresented 108 | self.content = content() 109 | self.actions = actions() 110 | self.header = header() 111 | self.footer = EmptyView() 112 | } 113 | 114 | public init( 115 | @ViewBuilder content: @escaping () -> Content, 116 | @ViewBuilder actions: @escaping () -> Actions, 117 | @ViewBuilder header: @escaping () -> Header 118 | ) { 119 | self._isPresented = .constant(true) 120 | self.content = content() 121 | self.actions = actions() 122 | self.header = header() 123 | self.footer = EmptyView() 124 | } 125 | } 126 | 127 | extension CustomAlertSection where Header == EmptyView { 128 | public init( 129 | isPresented: Binding, 130 | @ViewBuilder content: @escaping () -> Content, 131 | @ViewBuilder actions: @escaping () -> Actions, 132 | @ViewBuilder footer: @escaping () -> Footer 133 | ) { 134 | self._isPresented = isPresented 135 | self.content = content() 136 | self.actions = actions() 137 | self.header = EmptyView() 138 | self.footer = footer() 139 | } 140 | 141 | public init( 142 | @ViewBuilder content: @escaping () -> Content, 143 | @ViewBuilder actions: @escaping () -> Actions, 144 | @ViewBuilder footer: @escaping () -> Footer 145 | ) { 146 | self._isPresented = .constant(true) 147 | self.content = content() 148 | self.actions = actions() 149 | self.header = EmptyView() 150 | self.footer = footer() 151 | } 152 | } 153 | 154 | struct CustomAlertSection_Preview: PreviewProvider { 155 | static var previews: some View { 156 | Preview() 157 | } 158 | 159 | struct Preview: View { 160 | @State private var isPresented = false 161 | 162 | var body: some View { 163 | List { 164 | CustomAlertSection(isPresented: $isPresented) { 165 | Text("Hello World") 166 | .padding() 167 | } actions: { 168 | MultiButton { 169 | Button(role: .cancel) { 170 | isPresented = false 171 | print("Cancel") 172 | } label: { 173 | Text("Cancel") 174 | } 175 | Button { 176 | isPresented = false 177 | print("OK") 178 | } label: { 179 | Text("OK") 180 | } 181 | } 182 | } 183 | .transition(.move(edge: .leading)) 184 | 185 | Section { 186 | Button { 187 | isPresented = true 188 | } label: { 189 | Text("Show Custom Alert Section") 190 | } 191 | } 192 | } 193 | .animation(.default, value: isPresented) 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Sources/CustomAlert/Views/MultiButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiButton.swift 3 | // CustomAlert 4 | // 5 | // Created by David Walter on 29.04.22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | /// A control that wraps multiple actions horizontally. 11 | /// 12 | /// Used to create side by side buttons on a `.customAlert` 13 | public struct MultiButton: View where Content: View { 14 | @Environment(\.customAlertConfiguration) private var configuration 15 | 16 | let content: Content 17 | 18 | /// Creates multiple buttons within the MultiButton Layout. 19 | /// 20 | /// - Parameters: 21 | /// - content: The `ViewBuilder` with multiple `Button`s 22 | public init(@ViewBuilder content: () -> Content) { 23 | self.content = content() 24 | } 25 | 26 | public var body: some View { 27 | #if swift(>=6.0) 28 | if #available(iOS 18.0, *) { 29 | HStack(spacing: 0) { 30 | Group(subviews: content) { subviews in 31 | subviews.first 32 | ForEach(subviews.dropFirst()) { child in 33 | if !configuration.button.hideDivider { 34 | Divider() 35 | } 36 | child 37 | } 38 | } 39 | } 40 | .fixedSize(horizontal: false, vertical: true) 41 | .buttonStyle(.alert) 42 | .environment(\.alertButtonHeight, .infinity) 43 | } else { 44 | _VariadicView.Tree(ContentLayout()) { 45 | content 46 | } 47 | } 48 | #else 49 | _VariadicView.Tree(ContentLayout()) { 50 | content 51 | } 52 | #endif 53 | } 54 | 55 | @available(iOS, introduced: 14.0, deprecated: 18.0, message: "Use `ForEach(subviewOf:content:)` instead") 56 | @MainActor struct ContentLayout: _VariadicView_ViewRoot { 57 | @Environment(\.customAlertConfiguration) private var configuration 58 | 59 | #if swift(>=6.0) 60 | func body(children: _VariadicView.Children) -> some View { 61 | HStack(spacing: 0) { 62 | children.first 63 | ForEach(children.dropFirst()) { child in 64 | if !configuration.button.hideDivider { 65 | Divider() 66 | } 67 | child 68 | } 69 | } 70 | .fixedSize(horizontal: false, vertical: true) 71 | .buttonStyle(.alert) 72 | .environment(\.alertButtonHeight, .infinity) 73 | } 74 | #else 75 | nonisolated func body(children: _VariadicView.Children) -> some View { 76 | HStack(spacing: 0) { 77 | children.first 78 | ForEach(children.dropFirst()) { child in 79 | if !hideDivider { 80 | Divider() 81 | } 82 | child 83 | } 84 | } 85 | .fixedSize(horizontal: false, vertical: true) 86 | .buttonStyle(buttonStyle) 87 | .environment(\.alertButtonHeight, .infinity) 88 | } 89 | 90 | nonisolated var buttonStyle: some ButtonStyle { 91 | MainActor.runSync { 92 | AlertButtonStyle(triggerDismiss: true) 93 | } 94 | } 95 | 96 | nonisolated var hideDivider: Bool { 97 | MainActor.runSync { 98 | configuration.button.hideDivider 99 | } 100 | } 101 | #endif 102 | } 103 | } 104 | 105 | #Preview { 106 | List { 107 | CustomAlertRow { 108 | Text("Hello World") 109 | .padding() 110 | } actions: { 111 | MultiButton { 112 | Button(role: .cancel) { 113 | print("Cancel") 114 | } label: { 115 | Text("Cancel") 116 | } 117 | Button { 118 | print("OK") 119 | } label: { 120 | Text("OK") 121 | } 122 | } 123 | } 124 | } 125 | } 126 | --------------------------------------------------------------------------------