├── .gitignore ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── FloatingButton.xcscheme ├── FloatingButtonExample ├── FloatingButtonExample.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── FloatingButtonExample │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── FloatingButtonExample.entitlements │ ├── FloatingButtonExampleApp.swift │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── Package.swift ├── README.md └── Sources └── FloatingButton ├── FloatingButton.swift └── Utils.swift /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift,macos,carthage,cocoapods 3 | # Edit at https://www.gitignore.io/?templates=swift,macos,carthage,cocoapods 4 | 5 | ### Carthage ### 6 | # Carthage 7 | # 8 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 9 | # Carthage/Checkouts 10 | 11 | Carthage/Build 12 | 13 | ### CocoaPods ### 14 | ## CocoaPods GitIgnore Template 15 | 16 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 17 | # - Also handy if you have a large number of dependant pods 18 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 19 | Pods/ 20 | 21 | ### macOS ### 22 | # General 23 | .DS_Store 24 | .AppleDouble 25 | .LSOverride 26 | 27 | # Icon must end with two \r 28 | Icon 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Swift ### 50 | # Xcode 51 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 52 | 53 | ## Build generated 54 | build/ 55 | DerivedData/ 56 | 57 | ## Various settings 58 | *.pbxuser 59 | !default.pbxuser 60 | *.mode1v3 61 | !default.mode1v3 62 | *.mode2v3 63 | !default.mode2v3 64 | *.perspectivev3 65 | !default.perspectivev3 66 | xcuserdata/ 67 | 68 | ## Other 69 | *.moved-aside 70 | *.xccheckout 71 | *.xcscmblueprint 72 | 73 | ## Obj-C/Swift specific 74 | *.hmap 75 | *.ipa 76 | *.dSYM.zip 77 | *.dSYM 78 | 79 | ## Playgrounds 80 | timeline.xctimeline 81 | playground.xcworkspace 82 | 83 | # Swift Package Manager 84 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 85 | # Packages/ 86 | # Package.pins 87 | # Package.resolved 88 | .build/ 89 | # Add this line if you want to avoid checking in Xcode SPM integration. 90 | # .swiftpm/xcode 91 | 92 | # CocoaPods 93 | # We recommend against adding the Pods directory to your .gitignore. However 94 | # you should judge for yourself, the pros and cons are mentioned at: 95 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 96 | # Pods/ 97 | # Add this line if you want to avoid checking in source code from the Xcode workspace 98 | # *.xcworkspace 99 | 100 | # Carthage 101 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 102 | # Carthage/Checkouts 103 | 104 | 105 | # Accio dependency management 106 | Dependencies/ 107 | .accio/ 108 | 109 | # fastlane 110 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 111 | # screenshots whenever they are needed. 112 | # For more information about the recommended setup visit: 113 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 114 | 115 | fastlane/report.xml 116 | fastlane/Preview.html 117 | fastlane/screenshots/**/*.png 118 | fastlane/test_output 119 | 120 | # Code Injection 121 | # After new code Injection tools there's a generated folder /iOSInjectionProject 122 | # https://github.com/johnno1962/injectionforxcode 123 | 124 | iOSInjectionProject/ 125 | 126 | # End of https://www.gitignore.io/api/swift,macos,carthage,cocoapods 127 | 128 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/FloatingButton.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5B0E109C2A08B74600E2E4F9 /* FloatingButtonExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0E109B2A08B74600E2E4F9 /* FloatingButtonExampleApp.swift */; }; 11 | 5B0E109E2A08B74600E2E4F9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0E109D2A08B74600E2E4F9 /* ContentView.swift */; }; 12 | 5B0E10A02A08B74700E2E4F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5B0E109F2A08B74700E2E4F9 /* Assets.xcassets */; }; 13 | 5B0E10A42A08B74700E2E4F9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5B0E10A32A08B74700E2E4F9 /* Preview Assets.xcassets */; }; 14 | 5B0E10AD2A08B78F00E2E4F9 /* FloatingButton in Frameworks */ = {isa = PBXBuildFile; productRef = 5B0E10AC2A08B78F00E2E4F9 /* FloatingButton */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 5B0E10982A08B74600E2E4F9 /* FloatingButtonExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FloatingButtonExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 5B0E109B2A08B74600E2E4F9 /* FloatingButtonExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingButtonExampleApp.swift; sourceTree = ""; }; 20 | 5B0E109D2A08B74600E2E4F9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 21 | 5B0E109F2A08B74700E2E4F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 22 | 5B0E10A12A08B74700E2E4F9 /* FloatingButtonExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FloatingButtonExample.entitlements; sourceTree = ""; }; 23 | 5B0E10A32A08B74700E2E4F9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 24 | 5B0E10AA2A08B75300E2E4F9 /* FloatingButton */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FloatingButton; path = ..; sourceTree = ""; }; 25 | /* End PBXFileReference section */ 26 | 27 | /* Begin PBXFrameworksBuildPhase section */ 28 | 5B0E10952A08B74600E2E4F9 /* Frameworks */ = { 29 | isa = PBXFrameworksBuildPhase; 30 | buildActionMask = 2147483647; 31 | files = ( 32 | 5B0E10AD2A08B78F00E2E4F9 /* FloatingButton in Frameworks */, 33 | ); 34 | runOnlyForDeploymentPostprocessing = 0; 35 | }; 36 | /* End PBXFrameworksBuildPhase section */ 37 | 38 | /* Begin PBXGroup section */ 39 | 5B0E108F2A08B74600E2E4F9 = { 40 | isa = PBXGroup; 41 | children = ( 42 | 5B0E10AA2A08B75300E2E4F9 /* FloatingButton */, 43 | 5B0E109A2A08B74600E2E4F9 /* FloatingButtonExample */, 44 | 5B0E10992A08B74600E2E4F9 /* Products */, 45 | 5B0E10AB2A08B78F00E2E4F9 /* Frameworks */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | 5B0E10992A08B74600E2E4F9 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 5B0E10982A08B74600E2E4F9 /* FloatingButtonExample.app */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | 5B0E109A2A08B74600E2E4F9 /* FloatingButtonExample */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | 5B0E109B2A08B74600E2E4F9 /* FloatingButtonExampleApp.swift */, 61 | 5B0E109D2A08B74600E2E4F9 /* ContentView.swift */, 62 | 5B0E109F2A08B74700E2E4F9 /* Assets.xcassets */, 63 | 5B0E10A12A08B74700E2E4F9 /* FloatingButtonExample.entitlements */, 64 | 5B0E10A22A08B74700E2E4F9 /* Preview Content */, 65 | ); 66 | path = FloatingButtonExample; 67 | sourceTree = ""; 68 | }; 69 | 5B0E10A22A08B74700E2E4F9 /* Preview Content */ = { 70 | isa = PBXGroup; 71 | children = ( 72 | 5B0E10A32A08B74700E2E4F9 /* Preview Assets.xcassets */, 73 | ); 74 | path = "Preview Content"; 75 | sourceTree = ""; 76 | }; 77 | 5B0E10AB2A08B78F00E2E4F9 /* Frameworks */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | ); 81 | name = Frameworks; 82 | sourceTree = ""; 83 | }; 84 | /* End PBXGroup section */ 85 | 86 | /* Begin PBXNativeTarget section */ 87 | 5B0E10972A08B74600E2E4F9 /* FloatingButtonExample */ = { 88 | isa = PBXNativeTarget; 89 | buildConfigurationList = 5B0E10A72A08B74700E2E4F9 /* Build configuration list for PBXNativeTarget "FloatingButtonExample" */; 90 | buildPhases = ( 91 | 5B0E10942A08B74600E2E4F9 /* Sources */, 92 | 5B0E10952A08B74600E2E4F9 /* Frameworks */, 93 | 5B0E10962A08B74600E2E4F9 /* Resources */, 94 | ); 95 | buildRules = ( 96 | ); 97 | dependencies = ( 98 | ); 99 | name = FloatingButtonExample; 100 | packageProductDependencies = ( 101 | 5B0E10AC2A08B78F00E2E4F9 /* FloatingButton */, 102 | ); 103 | productName = FloatingButtonExample; 104 | productReference = 5B0E10982A08B74600E2E4F9 /* FloatingButtonExample.app */; 105 | productType = "com.apple.product-type.application"; 106 | }; 107 | /* End PBXNativeTarget section */ 108 | 109 | /* Begin PBXProject section */ 110 | 5B0E10902A08B74600E2E4F9 /* Project object */ = { 111 | isa = PBXProject; 112 | attributes = { 113 | BuildIndependentTargetsInParallel = 1; 114 | LastSwiftUpdateCheck = 1430; 115 | LastUpgradeCheck = 1620; 116 | TargetAttributes = { 117 | 5B0E10972A08B74600E2E4F9 = { 118 | CreatedOnToolsVersion = 14.3; 119 | }; 120 | }; 121 | }; 122 | buildConfigurationList = 5B0E10932A08B74600E2E4F9 /* Build configuration list for PBXProject "FloatingButtonExample" */; 123 | compatibilityVersion = "Xcode 14.0"; 124 | developmentRegion = en; 125 | hasScannedForEncodings = 0; 126 | knownRegions = ( 127 | en, 128 | Base, 129 | ); 130 | mainGroup = 5B0E108F2A08B74600E2E4F9; 131 | productRefGroup = 5B0E10992A08B74600E2E4F9 /* Products */; 132 | projectDirPath = ""; 133 | projectRoot = ""; 134 | targets = ( 135 | 5B0E10972A08B74600E2E4F9 /* FloatingButtonExample */, 136 | ); 137 | }; 138 | /* End PBXProject section */ 139 | 140 | /* Begin PBXResourcesBuildPhase section */ 141 | 5B0E10962A08B74600E2E4F9 /* Resources */ = { 142 | isa = PBXResourcesBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 5B0E10A42A08B74700E2E4F9 /* Preview Assets.xcassets in Resources */, 146 | 5B0E10A02A08B74700E2E4F9 /* Assets.xcassets in Resources */, 147 | ); 148 | runOnlyForDeploymentPostprocessing = 0; 149 | }; 150 | /* End PBXResourcesBuildPhase section */ 151 | 152 | /* Begin PBXSourcesBuildPhase section */ 153 | 5B0E10942A08B74600E2E4F9 /* Sources */ = { 154 | isa = PBXSourcesBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | 5B0E109E2A08B74600E2E4F9 /* ContentView.swift in Sources */, 158 | 5B0E109C2A08B74600E2E4F9 /* FloatingButtonExampleApp.swift in Sources */, 159 | ); 160 | runOnlyForDeploymentPostprocessing = 0; 161 | }; 162 | /* End PBXSourcesBuildPhase section */ 163 | 164 | /* Begin XCBuildConfiguration section */ 165 | 5B0E10A52A08B74700E2E4F9 /* Debug */ = { 166 | isa = XCBuildConfiguration; 167 | buildSettings = { 168 | ALWAYS_SEARCH_USER_PATHS = NO; 169 | CLANG_ANALYZER_NONNULL = YES; 170 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 171 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 172 | CLANG_ENABLE_MODULES = YES; 173 | CLANG_ENABLE_OBJC_ARC = YES; 174 | CLANG_ENABLE_OBJC_WEAK = YES; 175 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 176 | CLANG_WARN_BOOL_CONVERSION = YES; 177 | CLANG_WARN_COMMA = YES; 178 | CLANG_WARN_CONSTANT_CONVERSION = YES; 179 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 180 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 181 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 182 | CLANG_WARN_EMPTY_BODY = YES; 183 | CLANG_WARN_ENUM_CONVERSION = YES; 184 | CLANG_WARN_INFINITE_RECURSION = YES; 185 | CLANG_WARN_INT_CONVERSION = YES; 186 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 187 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 188 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 189 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 190 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 191 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 192 | CLANG_WARN_STRICT_PROTOTYPES = YES; 193 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 194 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 195 | CLANG_WARN_UNREACHABLE_CODE = YES; 196 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 197 | COPY_PHASE_STRIP = NO; 198 | DEAD_CODE_STRIPPING = YES; 199 | DEBUG_INFORMATION_FORMAT = dwarf; 200 | ENABLE_STRICT_OBJC_MSGSEND = YES; 201 | ENABLE_TESTABILITY = YES; 202 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 203 | GCC_C_LANGUAGE_STANDARD = gnu11; 204 | GCC_DYNAMIC_NO_PIC = NO; 205 | GCC_NO_COMMON_BLOCKS = YES; 206 | GCC_OPTIMIZATION_LEVEL = 0; 207 | GCC_PREPROCESSOR_DEFINITIONS = ( 208 | "DEBUG=1", 209 | "$(inherited)", 210 | ); 211 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 212 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 213 | GCC_WARN_UNDECLARED_SELECTOR = YES; 214 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 215 | GCC_WARN_UNUSED_FUNCTION = YES; 216 | GCC_WARN_UNUSED_VARIABLE = YES; 217 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 218 | MTL_FAST_MATH = YES; 219 | ONLY_ACTIVE_ARCH = YES; 220 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 221 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 222 | SWIFT_VERSION = 6.0; 223 | }; 224 | name = Debug; 225 | }; 226 | 5B0E10A62A08B74700E2E4F9 /* Release */ = { 227 | isa = XCBuildConfiguration; 228 | buildSettings = { 229 | ALWAYS_SEARCH_USER_PATHS = NO; 230 | CLANG_ANALYZER_NONNULL = YES; 231 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 232 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 233 | CLANG_ENABLE_MODULES = YES; 234 | CLANG_ENABLE_OBJC_ARC = YES; 235 | CLANG_ENABLE_OBJC_WEAK = YES; 236 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 237 | CLANG_WARN_BOOL_CONVERSION = YES; 238 | CLANG_WARN_COMMA = YES; 239 | CLANG_WARN_CONSTANT_CONVERSION = YES; 240 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 241 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 242 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 243 | CLANG_WARN_EMPTY_BODY = YES; 244 | CLANG_WARN_ENUM_CONVERSION = YES; 245 | CLANG_WARN_INFINITE_RECURSION = YES; 246 | CLANG_WARN_INT_CONVERSION = YES; 247 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 248 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 249 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 250 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 251 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 252 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 253 | CLANG_WARN_STRICT_PROTOTYPES = YES; 254 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 255 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 256 | CLANG_WARN_UNREACHABLE_CODE = YES; 257 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 258 | COPY_PHASE_STRIP = NO; 259 | DEAD_CODE_STRIPPING = YES; 260 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 261 | ENABLE_NS_ASSERTIONS = NO; 262 | ENABLE_STRICT_OBJC_MSGSEND = YES; 263 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 264 | GCC_C_LANGUAGE_STANDARD = gnu11; 265 | GCC_NO_COMMON_BLOCKS = YES; 266 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 267 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 268 | GCC_WARN_UNDECLARED_SELECTOR = YES; 269 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 270 | GCC_WARN_UNUSED_FUNCTION = YES; 271 | GCC_WARN_UNUSED_VARIABLE = YES; 272 | MTL_ENABLE_DEBUG_INFO = NO; 273 | MTL_FAST_MATH = YES; 274 | SWIFT_COMPILATION_MODE = wholemodule; 275 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 276 | SWIFT_VERSION = 6.0; 277 | }; 278 | name = Release; 279 | }; 280 | 5B0E10A82A08B74700E2E4F9 /* Debug */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 284 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 285 | CODE_SIGN_ENTITLEMENTS = FloatingButtonExample/FloatingButtonExample.entitlements; 286 | CODE_SIGN_STYLE = Automatic; 287 | CURRENT_PROJECT_VERSION = 1; 288 | DEAD_CODE_STRIPPING = YES; 289 | DEVELOPMENT_ASSET_PATHS = "\"FloatingButtonExample/Preview Content\""; 290 | DEVELOPMENT_TEAM = FZXCM5CJ7P; 291 | ENABLE_PREVIEWS = YES; 292 | GENERATE_INFOPLIST_FILE = YES; 293 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 294 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 295 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 296 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 297 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 298 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 299 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 300 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 301 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 302 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 303 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 304 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 305 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 306 | MACOSX_DEPLOYMENT_TARGET = 13.1; 307 | MARKETING_VERSION = 1.0; 308 | PRODUCT_BUNDLE_IDENTIFIER = com.exyte.FloatingButtonExample; 309 | PRODUCT_NAME = "$(TARGET_NAME)"; 310 | SDKROOT = auto; 311 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 312 | SUPPORTS_MACCATALYST = NO; 313 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 314 | SWIFT_EMIT_LOC_STRINGS = YES; 315 | SWIFT_VERSION = 6.0; 316 | TARGETED_DEVICE_FAMILY = "1,2"; 317 | }; 318 | name = Debug; 319 | }; 320 | 5B0E10A92A08B74700E2E4F9 /* Release */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 324 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 325 | CODE_SIGN_ENTITLEMENTS = FloatingButtonExample/FloatingButtonExample.entitlements; 326 | CODE_SIGN_STYLE = Automatic; 327 | CURRENT_PROJECT_VERSION = 1; 328 | DEAD_CODE_STRIPPING = YES; 329 | DEVELOPMENT_ASSET_PATHS = "\"FloatingButtonExample/Preview Content\""; 330 | DEVELOPMENT_TEAM = FZXCM5CJ7P; 331 | ENABLE_PREVIEWS = YES; 332 | GENERATE_INFOPLIST_FILE = YES; 333 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 334 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 335 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 336 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 337 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 338 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 339 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 340 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 341 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 342 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 343 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 344 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 345 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 346 | MACOSX_DEPLOYMENT_TARGET = 13.1; 347 | MARKETING_VERSION = 1.0; 348 | PRODUCT_BUNDLE_IDENTIFIER = com.exyte.FloatingButtonExample; 349 | PRODUCT_NAME = "$(TARGET_NAME)"; 350 | SDKROOT = auto; 351 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; 352 | SUPPORTS_MACCATALYST = NO; 353 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 354 | SWIFT_EMIT_LOC_STRINGS = YES; 355 | SWIFT_VERSION = 6.0; 356 | TARGETED_DEVICE_FAMILY = "1,2"; 357 | }; 358 | name = Release; 359 | }; 360 | /* End XCBuildConfiguration section */ 361 | 362 | /* Begin XCConfigurationList section */ 363 | 5B0E10932A08B74600E2E4F9 /* Build configuration list for PBXProject "FloatingButtonExample" */ = { 364 | isa = XCConfigurationList; 365 | buildConfigurations = ( 366 | 5B0E10A52A08B74700E2E4F9 /* Debug */, 367 | 5B0E10A62A08B74700E2E4F9 /* Release */, 368 | ); 369 | defaultConfigurationIsVisible = 0; 370 | defaultConfigurationName = Release; 371 | }; 372 | 5B0E10A72A08B74700E2E4F9 /* Build configuration list for PBXNativeTarget "FloatingButtonExample" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 5B0E10A82A08B74700E2E4F9 /* Debug */, 376 | 5B0E10A92A08B74700E2E4F9 /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | /* End XCConfigurationList section */ 382 | 383 | /* Begin XCSwiftPackageProductDependency section */ 384 | 5B0E10AC2A08B78F00E2E4F9 /* FloatingButton */ = { 385 | isa = XCSwiftPackageProductDependency; 386 | productName = FloatingButton; 387 | }; 388 | /* End XCSwiftPackageProductDependency section */ 389 | }; 390 | rootObject = 5B0E10902A08B74600E2E4F9 /* Project object */; 391 | } 392 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample/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 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "1x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "2x", 16 | "size" : "16x16" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "1x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "2x", 26 | "size" : "32x32" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "2x", 36 | "size" : "128x128" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "1x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "2x", 46 | "size" : "256x256" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "1x", 51 | "size" : "512x512" 52 | }, 53 | { 54 | "idiom" : "mac", 55 | "scale" : "2x", 56 | "size" : "512x512" 57 | } 58 | ], 59 | "info" : { 60 | "author" : "xcode", 61 | "version" : 1 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // FloatingButtonExample 4 | // 5 | // Created by Alisa Mylnikova on 08.05.2023. 6 | // 7 | 8 | import SwiftUI 9 | import FloatingButton 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | NavigationView { 14 | List { 15 | NavigationLink(destination: ScreenIconsAndText()) { 16 | Text("IconsAndText") 17 | } 18 | NavigationLink(destination: ScreenStraight()) { 19 | Text("Straight") 20 | } 21 | NavigationLink(destination: ScreenCircle()) { 22 | Text("Circle") 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | struct ScreenIconsAndText: View { 30 | 31 | @State var isOpen = false 32 | 33 | var body: some View { 34 | let mainButton1 = MainButton(imageName: "star.fill", colorHex: "f7b731", width: 60) 35 | let mainButton2 = MainButton(imageName: "heart.fill", colorHex: "eb3b5a", width: 60) 36 | let textButtons = MockData.iconAndTextTitles.enumerated().map { index, value in 37 | IconAndTextButton(imageName: MockData.iconAndTextImageNames[index], buttonText: value) 38 | .onTapGesture { isOpen.toggle() } 39 | } 40 | 41 | let menu1 = FloatingButton(mainButtonView: mainButton1, buttons: textButtons, isOpen: $isOpen) 42 | .straight() 43 | .direction(.top) 44 | .alignment(.left) 45 | .spacing(10) 46 | .initialOffset(x: -1000) 47 | .animation(.spring()) 48 | 49 | let menu2 = FloatingButton(mainButtonView: mainButton2, buttons: textButtons) 50 | .straight() 51 | .direction(.top) 52 | .alignment(.right) 53 | .spacing(10) 54 | .initialOpacity(0) 55 | 56 | return VStack { 57 | HStack { 58 | menu1 59 | Spacer() 60 | menu2 61 | } 62 | } 63 | .padding(20) 64 | } 65 | } 66 | 67 | struct ScreenStraight: View { 68 | 69 | @Environment(\.presentationMode) var presentationMode: Binding 70 | 71 | var body: some View { 72 | let mainButton1 = MainButton(imageName: "thermometer", colorHex: "f7b731") 73 | let mainButton2 = MainButton(imageName: "cloud.fill", colorHex: "eb3b5a") 74 | let buttonsImage = MockData.iconImageNames.enumerated().map { index, value in 75 | IconButton(imageName: value, color: MockData.colors[index]) 76 | } 77 | 78 | let menu1 = FloatingButton(mainButtonView: mainButton1, buttons: buttonsImage) 79 | .straight() 80 | .direction(.right) 81 | .delays(delayDelta: 0.1) 82 | 83 | let menu2 = FloatingButton(mainButtonView: mainButton2, buttons: buttonsImage) 84 | .straight() 85 | .direction(.top) 86 | .delays(delayDelta: 0.1) 87 | 88 | return VStack { 89 | Spacer() 90 | HStack { 91 | menu1 92 | Spacer() 93 | menu2 94 | } 95 | .padding(20) 96 | } 97 | } 98 | } 99 | 100 | struct ScreenCircle: View { 101 | 102 | @Environment(\.presentationMode) var presentationMode: Binding 103 | 104 | var body: some View { 105 | let mainButton1 = MainButton(imageName: "message.fill", colorHex: "f7b731") 106 | let mainButton2 = MainButton(imageName: "umbrella.fill", colorHex: "eb3b5a") 107 | let mainButton3 = MainButton(imageName: "message.fill", colorHex: "f7b731") 108 | let buttonsImage = MockData.iconImageNames.enumerated().map { index, value in 109 | IconButton(imageName: value, color: MockData.colors[index]) 110 | } 111 | 112 | let menu1 = FloatingButton(mainButtonView: mainButton2, buttons: buttonsImage.dropLast()) 113 | .circle() 114 | .startAngle(3/2 * .pi) 115 | .endAngle(2 * .pi) 116 | .radius(70) 117 | let menu2 = FloatingButton(mainButtonView: mainButton1, buttons: buttonsImage) 118 | .circle() 119 | .delays(delayDelta: 0.1) 120 | let menu3 = FloatingButton(mainButtonView: mainButton3, buttons: buttonsImage.dropLast()) 121 | .circle() 122 | .layoutDirection(.counterClockwise) 123 | .startAngle(3/2 * .pi) 124 | .endAngle(2 * .pi) 125 | .radius(70) 126 | 127 | return VStack { 128 | Spacer() 129 | HStack { 130 | menu1 131 | Spacer() 132 | menu2 133 | Spacer() 134 | menu3 135 | } 136 | .padding(20) 137 | } 138 | } 139 | } 140 | 141 | struct MainButton: View { 142 | 143 | var imageName: String 144 | var colorHex: String 145 | var width: CGFloat = 50 146 | 147 | var body: some View { 148 | ZStack { 149 | Color(hex: colorHex) 150 | .frame(width: width, height: width) 151 | .cornerRadius(width / 2) 152 | .shadow(color: Color(hex: colorHex).opacity(0.3), radius: 15, x: 0, y: 15) 153 | Image(systemName: imageName) 154 | .foregroundColor(.white) 155 | } 156 | } 157 | } 158 | 159 | struct IconButton: View { 160 | 161 | var imageName: String 162 | var color: Color 163 | let imageWidth: CGFloat = 20 164 | let buttonWidth: CGFloat = 45 165 | 166 | var body: some View { 167 | ZStack { 168 | color 169 | Image(systemName: imageName) 170 | .frame(width: imageWidth, height: imageWidth) 171 | .foregroundColor(.white) 172 | } 173 | .frame(width: buttonWidth, height: buttonWidth) 174 | .cornerRadius(buttonWidth / 2) 175 | } 176 | } 177 | 178 | struct IconAndTextButton: View { 179 | 180 | var imageName: String 181 | var buttonText: String 182 | let imageWidth: CGFloat = 22 183 | 184 | var body: some View { 185 | ZStack { 186 | Color.white 187 | HStack { 188 | Image(systemName: imageName) 189 | .resizable() 190 | .aspectRatio(1, contentMode: .fill) 191 | .foregroundColor(Color(hex: "778ca3")) 192 | .frame(width: imageWidth, height: imageWidth) 193 | .clipped() 194 | Spacer() 195 | Text(buttonText) 196 | .font(.system(size: 16, weight: .semibold, design: .default)) 197 | .foregroundColor(Color(hex: "4b6584")) 198 | Spacer() 199 | } 200 | .padding(.horizontal, 15) 201 | } 202 | .frame(width: 160, height: 45) 203 | .cornerRadius(8) 204 | .shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 1) 205 | .overlay( 206 | RoundedRectangle(cornerRadius: 8) 207 | .stroke(Color(hex: "F4F4F4"), lineWidth: 1) 208 | ) 209 | } 210 | } 211 | 212 | struct MockData { 213 | 214 | static let colors = [ 215 | "e84393", 216 | "0984e3", 217 | "6c5ce7", 218 | "00b894" 219 | ].map { Color(hex: $0) } 220 | 221 | static let iconImageNames = [ 222 | "sun.max.fill", 223 | "cloud.fill", 224 | "cloud.rain.fill", 225 | "cloud.snow.fill" 226 | ] 227 | 228 | static let iconAndTextImageNames = [ 229 | "plus.circle.fill", 230 | "minus.circle.fill", 231 | "pencil.circle.fill" 232 | ] 233 | 234 | static let iconAndTextTitles = [ 235 | "Add New", 236 | "Remove", 237 | "Rename" 238 | ] 239 | } 240 | 241 | extension Color { 242 | 243 | init(hex: String) { 244 | let scanner = Scanner(string: hex) 245 | var rgbValue: UInt64 = 0 246 | scanner.scanHexInt64(&rgbValue) 247 | 248 | let r = (rgbValue & 0xff0000) >> 16 249 | let g = (rgbValue & 0xff00) >> 8 250 | let b = rgbValue & 0xff 251 | 252 | self.init(red: Double(r) / 0xff, green: Double(g) / 0xff, blue: Double(b) / 0xff) 253 | } 254 | } 255 | 256 | #if DEBUG 257 | struct ContentView_Previews: PreviewProvider { 258 | static var previews: some View { 259 | ContentView() 260 | } 261 | } 262 | #endif 263 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample/FloatingButtonExample.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample/FloatingButtonExampleApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatingButtonExampleApp.swift 3 | // FloatingButtonExample 4 | // 5 | // Created by Alisa Mylnikova on 08.05.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct FloatingButtonExampleApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /FloatingButtonExample/FloatingButtonExample/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 exyte 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "FloatingButton", 7 | platforms: [ 8 | .iOS(.v14), 9 | .macOS(.v11), 10 | .tvOS(.v14), 11 | .watchOS(.v7) 12 | ], 13 | products: [ 14 | .library( 15 | name: "FloatingButton", 16 | targets: ["FloatingButton"] 17 | ) 18 | ], 19 | targets: [ 20 | .target( 21 | name: "FloatingButton", 22 | dependencies: [], 23 | swiftSettings: [ 24 | .enableExperimentalFeature("StrictConcurrency") 25 | ] 26 | ) 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |       4 | 5 | 6 | 7 | 8 |

FloatingButton

9 | 10 |

Easily customizable floating button menu created with SwiftUI

11 | 12 | ![](https://img.shields.io/github/v/tag/exyte/FloatingButton?label=Version) 13 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FFloatingButton%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/exyte/FloatingButton) 14 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fexyte%2FFloatingButton%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/exyte/FloatingButton) 15 | [![SPM](https://img.shields.io/badge/SPM-Compatible-brightgreen.svg)](https://swiftpackageindex.com/exyte/FloatingButton) 16 | [![Cocoapods](https://img.shields.io/badge/Cocoapods-Deprecated%20after%201.3.0-yellow.svg)](https://cocoapods.org/pods/FloatingButton) 17 | [![License: MIT](https://img.shields.io/badge/License-MIT-black.svg)](https://opensource.org/licenses/MIT) 18 | 19 | # Usage 20 | 21 | 1. Create main button view and a number of submenu buttons — both should be cast to `AnyView` type. 22 | 2. Pass both to `FloatingButton` constructor: 23 | 24 | ```swift 25 | FloatingButton(mainButtonView: mainButton, buttons: buttons) 26 | ``` 27 | 3. You may also pass a binding which will determine if the menu is currently open. You may use this to close the menu on any submenu button tap for example. 28 | ```swift 29 | FloatingButton(mainButtonView: mainButton, buttons: buttons, isOpen: $isOpen) 30 | ``` 31 | 4. Chain `.straight()` or `.circle()` to specify desired menu type. 32 | 5. Chain whatever you like afterwards. For example: 33 | ```swift 34 | FloatingButton(mainButtonView: mainButton, buttons: textButtons) 35 | .straight() 36 | .direction(.top) 37 | .alignment(.left) 38 | .spacing(10) 39 | .initialOffset(x: -1000) 40 | .animation(.spring()) 41 | 42 | FloatingButton(mainButtonView: mainButton2, buttons: buttonsImage.dropLast()) 43 | .circle() 44 | .startAngle(3/2 * .pi) 45 | .endAngle(2 * .pi) 46 | .radius(70) 47 | .layoutDirection(.counterClockwise) 48 | ``` 49 | 50 | ### Universal options 51 | `spacing` - space between submenu buttons 52 | `initialScaling` - size multiplyer for submenu buttons when the menu is closed 53 | `initialOffset` - offset for submenu buttons when the menu is closed 54 | `initialOpacity` - opacity for submenu buttons when the menu is closed 55 | `animation` - custom SwiftUI animation like `Animation.easeInOut()` or `Animation.spring()` 56 | `delays` - delay for each submenu button's animation start 57 | - you can pass array of delays - one for each element 58 | - or you can pass `delayDelta` - then this same delay will be used for each element 59 | `mainZStackAlignment` - main button and submenu buttons are contained in one ZStack (not an overlay so the menu has a correct size), you can change this ZStack's alignment with this parameter 60 | `inverseZIndex` - inverse zIndex of mainButton and the children. Use, for example, if you have a negative spacing and want to change the order 61 | `wholeMenuSize` - pass CGSize binding to get updates of menu's size. Menu's size includes main button frame and all of elements' frames 62 | `menuButtonsSize` - pass CGSize binding to get updates of combined menu elements' size 63 | 64 | ### Straight menu only options 65 | 66 | `direction` - position of submenu buttons relative to main menu button 67 | `alignment` - alignment of submenu buttons relative to main menu button 68 | 69 | ### Circle only options 70 | 71 | `startAngle` 72 | `endAngle` 73 | `radius` - distance between center of main button and centers of submenu buttons 74 | `layoutDirection` - changes the button layout direction from the startAngle to the endAngle 75 | 76 | ## Examples 77 | 78 | To try the FloatingButton examples: 79 | - Clone the repo `https://github.com/exyte/FloatingButton.git` 80 | - Open `FloatingButtonExample.xcodeproj` in the Xcode 81 | - Try it! 82 | 83 | ## Installation 84 | 85 | ### [Swift Package Manager](https://swift.org/package-manager/) 86 | 87 | ```swift 88 | dependencies: [ 89 | .package(url: "https://github.com/exyte/FloatingButton.git") 90 | ] 91 | ``` 92 | 93 | ## Requirements 94 | 95 | * iOS 14.0+ / macOS 11.0+ / watchOS 7.0+ 96 | * Xcode 12+ 97 | 98 | ## Our other open source SwiftUI libraries 99 | [PopupView](https://github.com/exyte/PopupView) - Toasts and popups library 100 | [AnchoredPopup](https://github.com/exyte/AnchoredPopup) - Anchored Popup grows "out" of a trigger view (similar to Hero animation) 101 | [Grid](https://github.com/exyte/Grid) - The most powerful Grid container 102 | [ScalingHeaderScrollView](https://github.com/exyte/ScalingHeaderScrollView) - A scroll view with a sticky header which shrinks as you scroll 103 | [AnimatedTabBar](https://github.com/exyte/AnimatedTabBar) - A tabbar with a number of preset animations 104 | [MediaPicker](https://github.com/exyte/mediapicker) - Customizable media picker 105 | [Chat](https://github.com/exyte/chat) - Chat UI framework with fully customizable message cells, input view, and a built-in media picker 106 | [OpenAI](https://github.com/exyte/OpenAI) Wrapper lib for [OpenAI REST API](https://platform.openai.com/docs/api-reference/introduction) 107 | [AnimatedGradient](https://github.com/exyte/AnimatedGradient) - Animated linear gradient 108 | [ConcentricOnboarding](https://github.com/exyte/ConcentricOnboarding) - Animated onboarding flow 109 | [ActivityIndicatorView](https://github.com/exyte/ActivityIndicatorView) - A number of animated loading indicators 110 | [ProgressIndicatorView](https://github.com/exyte/ProgressIndicatorView) - A number of animated progress indicators 111 | [FlagAndCountryCode](https://github.com/exyte/FlagAndCountryCode) - Phone codes and flags for every country 112 | [SVGView](https://github.com/exyte/SVGView) - SVG parser 113 | [LiquidSwipe](https://github.com/exyte/LiquidSwipe) - Liquid navigation animation 114 | 115 | -------------------------------------------------------------------------------- /Sources/FloatingButton/FloatingButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FloatingButton.swift 3 | // FloatingButton 4 | // 5 | // Created by Alisa Mylnikova on 27/11/2019. 6 | // Copyright © 2019 Exyte. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | public enum Direction { 12 | case left, right, top, bottom 13 | } 14 | 15 | public enum LayoutDirection { 16 | case clockwise, counterClockwise 17 | } 18 | 19 | public enum Alignment { 20 | case left, right, top, bottom, center 21 | } 22 | 23 | public struct FloatingButton: View where MainView: View, ButtonView: View { 24 | 25 | fileprivate enum MenuType { 26 | case straight 27 | case circle 28 | } 29 | 30 | fileprivate var mainButtonView: MainView 31 | fileprivate var buttons: [SubmenuButton] 32 | 33 | fileprivate var menuType: MenuType = .straight 34 | fileprivate var spacing: CGFloat = 10 35 | fileprivate var initialScaling: CGFloat = 1 36 | fileprivate var initialOffset: CGPoint = CGPoint() 37 | fileprivate var initialOpacity: Double = 1 38 | fileprivate var animation: Animation = .easeInOut(duration: 0.4) 39 | fileprivate var delays: [Double] = [] 40 | fileprivate var mainZStackAlignment: SwiftUI.Alignment = .center 41 | fileprivate var inverseZIndex: Bool = false 42 | 43 | fileprivate var wholeMenuSize: Binding = .constant(.zero) 44 | fileprivate var menuButtonsSize: Binding = .constant(.zero) 45 | 46 | // straight 47 | fileprivate var direction: Direction = .left 48 | fileprivate var alignment: Alignment = .center 49 | 50 | // circle 51 | fileprivate var layoutDirection: LayoutDirection = .clockwise 52 | fileprivate var startAngle: Double = .pi 53 | fileprivate var endAngle: Double = 2 * .pi 54 | fileprivate var radius: Double? 55 | 56 | @State private var privateIsOpen: Bool = false 57 | var isOpenBinding: Binding? 58 | var isOpen: Bool { 59 | get { isOpenBinding?.wrappedValue ?? privateIsOpen } 60 | } 61 | 62 | @State private var coords: [CGPoint] = [] 63 | @State private var alignmentOffsets: [CGSize] = [] 64 | @State private var initialPositions: [CGPoint] = [] // if there is initial offset 65 | @State private var sizes: [CGSize] = [] 66 | @State private var mainButtonSize = CGSize() 67 | 68 | private init(mainButtonView: MainView, buttons: [SubmenuButton], isOpenBinding: Binding?) { 69 | self.mainButtonView = mainButtonView 70 | self.buttons = buttons 71 | self.isOpenBinding = isOpenBinding 72 | } 73 | 74 | public init(mainButtonView: MainView, buttons: [ButtonView]) { 75 | self.mainButtonView = mainButtonView 76 | self.buttons = buttons.map { SubmenuButton(buttonView: $0) } 77 | } 78 | 79 | public init(mainButtonView: MainView, buttons: [ButtonView], isOpen: Binding) { 80 | self.mainButtonView = mainButtonView 81 | self.buttons = buttons.map { SubmenuButton(buttonView: $0) } 82 | self.isOpenBinding = isOpen 83 | } 84 | 85 | public var body: some View { 86 | ZStack(alignment: mainZStackAlignment) { 87 | ForEach((0.. CGSize { 118 | isOpen 119 | ? CGSize(width: coords[safe: i].x, height: coords[safe: i].y) 120 | : CGSize(width: (initialPositions.isEmpty ? 0 : initialPositions[safe: i].x), 121 | height: (initialPositions.isEmpty ? 0 : initialPositions[safe: i].y)) 122 | } 123 | 124 | fileprivate func buttonAnimation(at i: Int) -> Animation { 125 | animation.delay(delays.isEmpty ? Double(0) : 126 | (isOpen ? delays[delays.count - i - 1] : delays[i])) 127 | } 128 | 129 | fileprivate func calculateCoords() { 130 | switch menuType { 131 | case .straight: 132 | calculateCoordsStraight() 133 | case .circle: 134 | calculateCoordsCircle() 135 | } 136 | } 137 | 138 | fileprivate func calculateCoordsStraight() { 139 | guard sizes.count > 0, mainButtonSize != .zero else { 140 | return 141 | } 142 | 143 | let sizes = sizes.map { roundToTwoDigits($0) } 144 | let allSizes = [roundToTwoDigits(mainButtonSize)] + sizes 145 | 146 | var coord = CGPoint.zero 147 | coords = (0.. CGPoint in 148 | let width = allSizes[i].width / 2 + allSizes[i+1].width / 2 149 | let height = allSizes[i].height / 2 + allSizes[i+1].height / 2 150 | 151 | switch direction { 152 | case .left: 153 | coord = CGPoint(x: coord.x - width - spacing, y: coord.y) 154 | case .right: 155 | coord = CGPoint(x: coord.x + width + spacing, y: coord.y) 156 | case .top: 157 | coord = CGPoint(x: coord.x, y: coord.y - height - spacing) 158 | case .bottom: 159 | coord = CGPoint(x: coord.x, y: coord.y + height + spacing) 160 | } 161 | return coord 162 | } 163 | 164 | if initialOffset.x != 0 || initialOffset.y != 0 { 165 | initialPositions = (0.. CGPoint in 166 | CGPoint(x: coords[i].x + initialOffset.x, 167 | y: coords[i].y + initialOffset.y) 168 | } 169 | } else { 170 | initialPositions = Array(repeating: .zero, count: sizes.count) 171 | } 172 | 173 | alignmentOffsets = (0.. CGSize in 174 | switch alignment { 175 | case .left: 176 | return CGSize(width: sizes[i].width / 2 - mainButtonSize.width / 2, height: 0) 177 | case .right: 178 | return CGSize(width: -sizes[i].width / 2 + mainButtonSize.width / 2, height: 0) 179 | case .top: 180 | return CGSize(width: 0, height: sizes[i].height / 2 - mainButtonSize.height / 2) 181 | case .bottom: 182 | return CGSize(width: 0, height: -sizes[i].height / 2 + mainButtonSize.height / 2) 183 | case .center: 184 | return CGSize() 185 | } 186 | } 187 | 188 | var buttonsSize = CGSize.zero 189 | for size in sizes { 190 | if [.top, .bottom].contains(alignment) { 191 | buttonsSize = CGSize( 192 | width: max(size.width, buttonsSize.width), 193 | height: buttonsSize.height + size.height + spacing 194 | ) 195 | } else { 196 | buttonsSize = CGSize( 197 | width: buttonsSize.width + size.width + spacing, 198 | height: max(size.height, buttonsSize.height) 199 | ) 200 | } 201 | } 202 | 203 | var wholeSize = CGSize.zero 204 | if [.top, .bottom].contains(alignment) { 205 | wholeSize = CGSize( 206 | width: max(buttonsSize.width, mainButtonSize.width), 207 | height: buttonsSize.height + mainButtonSize.height 208 | ) 209 | } else { 210 | wholeSize = CGSize( 211 | width: buttonsSize.width + mainButtonSize.width, 212 | height: max(buttonsSize.height, mainButtonSize.height) 213 | ) 214 | } 215 | 216 | menuButtonsSize.wrappedValue = buttonsSize 217 | wholeMenuSize.wrappedValue = wholeSize 218 | } 219 | 220 | fileprivate func roundToTwoDigits(_ size: CGSize) -> CGSize { 221 | CGSize(width: ceil(size.width*100)/100, height: ceil(size.height*100)/100) 222 | } 223 | 224 | fileprivate func calculateCoordsCircle() { 225 | guard sizes.count > 0, mainButtonSize != .zero else { 226 | return 227 | } 228 | 229 | let count = buttons.count 230 | var radius: Double = 60 231 | if let r = self.radius { 232 | radius = r 233 | } else if let buttonWidth = sizes.first?.width { 234 | radius = Double((mainButtonSize.width + buttonWidth) / 2 + spacing) 235 | } 236 | 237 | coords = (0..: View { 262 | private var floatingButton: FloatingButton 263 | 264 | fileprivate init(floatingButton: FloatingButton) { 265 | self.floatingButton = floatingButton 266 | } 267 | 268 | fileprivate init() { 269 | fatalError("don't call this method") 270 | } 271 | 272 | public var body: some View { 273 | floatingButton 274 | } 275 | } 276 | 277 | public extension FloatingButton { 278 | 279 | func straight() -> FloatingButtonGeneric { 280 | var copy = self 281 | copy.menuType = .straight 282 | return FloatingButtonGeneric(floatingButton: copy) 283 | } 284 | 285 | func circle() -> FloatingButtonGeneric { 286 | var copy = self 287 | copy.menuType = .circle 288 | return FloatingButtonGeneric(floatingButton: copy) 289 | } 290 | } 291 | 292 | public extension FloatingButtonGeneric where T : DefaultFloatingButton { 293 | 294 | func spacing(_ spacing: CGFloat) -> FloatingButtonGeneric { 295 | var copy = self 296 | copy.floatingButton.spacing = spacing 297 | return copy 298 | } 299 | 300 | func initialScaling(_ initialScaling: CGFloat) -> FloatingButtonGeneric { 301 | var copy = self 302 | copy.floatingButton.initialScaling = initialScaling 303 | return copy 304 | } 305 | 306 | func initialOffset(_ initialOffset: CGPoint) -> FloatingButtonGeneric { 307 | var copy = self 308 | copy.floatingButton.initialOffset = initialOffset 309 | return copy 310 | } 311 | 312 | func initialOffset(x: CGFloat = 0, y: CGFloat = 0) -> FloatingButtonGeneric { 313 | var copy = self 314 | copy.floatingButton.initialOffset = CGPoint(x: x, y: y) 315 | return copy 316 | } 317 | 318 | func initialOpacity(_ initialOpacity: Double) -> FloatingButtonGeneric { 319 | var copy = self 320 | copy.floatingButton.initialOpacity = initialOpacity 321 | return copy 322 | } 323 | 324 | func animation(_ animation: Animation) -> FloatingButtonGeneric { 325 | var copy = self 326 | copy.floatingButton.animation = animation 327 | return copy 328 | } 329 | 330 | func delays(delayDelta: Double) -> FloatingButtonGeneric { 331 | var copy = self 332 | copy.floatingButton.delays = (0.. FloatingButtonGeneric { 339 | var copy = self 340 | copy.floatingButton.delays = delays 341 | return copy 342 | } 343 | 344 | func mainZStackAlignment(_ alignment: SwiftUI.Alignment) -> FloatingButtonGeneric { 345 | var copy = self 346 | copy.floatingButton.mainZStackAlignment = alignment 347 | return copy 348 | } 349 | 350 | func inverseZIndex(_ inverse: Bool) -> FloatingButtonGeneric { 351 | var copy = self 352 | copy.floatingButton.inverseZIndex = inverse 353 | return copy 354 | } 355 | 356 | func wholeMenuSize(_ wholeMenuSize: Binding) -> FloatingButtonGeneric { 357 | var copy = self 358 | copy.floatingButton.wholeMenuSize = wholeMenuSize 359 | return copy 360 | } 361 | 362 | func menuButtonsSize(_ menuButtonsSize: Binding) -> FloatingButtonGeneric { 363 | var copy = self 364 | copy.floatingButton.menuButtonsSize = menuButtonsSize 365 | return copy 366 | } 367 | } 368 | 369 | public extension FloatingButtonGeneric where T: StraightFloatingButton { 370 | 371 | func direction(_ direction: Direction) -> FloatingButtonGeneric { 372 | var copy = self 373 | copy.floatingButton.direction = direction 374 | return copy 375 | } 376 | 377 | func alignment(_ alignment: Alignment) -> FloatingButtonGeneric { 378 | var copy = self 379 | copy.floatingButton.alignment = alignment 380 | return copy 381 | } 382 | } 383 | 384 | public extension FloatingButtonGeneric where T: CircleFloatingButton { 385 | 386 | func startAngle(_ startAngle: Double) -> FloatingButtonGeneric { 387 | var copy = self 388 | copy.floatingButton.startAngle = startAngle 389 | return copy 390 | } 391 | 392 | func endAngle(_ endAngle: Double) -> FloatingButtonGeneric { 393 | var copy = self 394 | copy.floatingButton.endAngle = endAngle 395 | return copy 396 | } 397 | 398 | func radius(_ radius: Double) -> FloatingButtonGeneric { 399 | var copy = self 400 | copy.floatingButton.radius = radius 401 | return copy 402 | } 403 | 404 | func layoutDirection(_ layoutDirection: LayoutDirection) -> FloatingButtonGeneric { 405 | var copy = self 406 | copy.floatingButton.layoutDirection = layoutDirection 407 | return copy 408 | } 409 | 410 | } 411 | 412 | struct SubmenuButton: View { 413 | 414 | var buttonView: ButtonView 415 | var action: () -> Void = { } 416 | 417 | var body: some View { 418 | Button(action: { action() }) { 419 | buttonView 420 | } 421 | .buttonStyle(PlainButtonStyle()) 422 | } 423 | } 424 | 425 | private struct MainButtonViewInternal: View { 426 | 427 | @Binding public var isOpen: Bool 428 | fileprivate var mainButtonView: MainView 429 | 430 | public var body: some View { 431 | Button(action: { isOpen.toggle() }) { 432 | mainButtonView 433 | } 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /Sources/FloatingButton/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIView.swift 3 | // 4 | // 5 | // Created by Alisa Mylnikova on 31.03.2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | 12 | func sizeGetter(_ size: Binding) -> some View { 13 | modifier(SizeGetter(size: size)) 14 | } 15 | } 16 | 17 | extension Collection where Element == CGPoint { 18 | 19 | subscript (safe index: Index) -> CGPoint { 20 | return indices.contains(index) ? self[index] : .zero 21 | } 22 | } 23 | 24 | struct SizeGetter: ViewModifier { 25 | @Binding var size: CGSize 26 | 27 | func body(content: Content) -> some View { 28 | content 29 | .background( 30 | GeometryReader { proxy -> Color in 31 | if proxy.size != self.size { 32 | DispatchQueue.main.async { 33 | self.size = proxy.size 34 | } 35 | } 36 | return Color.clear 37 | } 38 | ) 39 | } 40 | } 41 | 42 | struct SubmenuButtonPreferenceKey: PreferenceKey { 43 | typealias Value = [CGSize] 44 | 45 | static let defaultValue: Value = [] 46 | 47 | static func reduce(value: inout Value, nextValue: () -> Value) { 48 | value.append(contentsOf: nextValue()) 49 | } 50 | } 51 | 52 | struct SubmenuButtonPreferenceViewSetter: View { 53 | 54 | var body: some View { 55 | GeometryReader { geometry in 56 | Rectangle() 57 | .fill(Color.clear) 58 | .preference(key: SubmenuButtonPreferenceKey.self, 59 | value: [geometry.frame(in: .global).size]) 60 | } 61 | } 62 | } 63 | --------------------------------------------------------------------------------