16 Commits

Author SHA1 Message Date
igor.k d400fc7d14 small code improvements 2020-05-23 01:27:56 +03:00
igor.k baa29a2838 update access to blob menu parameters 2020-05-22 19:20:15 +03:00
igor.k ece274dfb2 fix problem with reinit pager view content on every swipe 2020-05-22 15:41:13 +03:00
igor.k 43b9c9b3db dark mode 2020-05-22 15:13:41 +03:00
igor.k c54b4a353a additional UI elements 2020-05-22 04:06:57 +03:00
igor.k c16b72e8a8 small improvements 2020-05-22 00:47:26 +03:00
igor.k 22f4b0e895 exchange & stocks screens UI 2020-05-21 17:49:53 +03:00
igor.k 2caa7ed0dc fix problem with animation completion 2020-05-21 03:47:15 +03:00
igor.k 2883fa121f exchange screen ui 2020-05-21 02:44:36 +03:00
igor.k 5bdd0d75a2 additional example UI screens (wip) 2020-05-20 17:30:49 +03:00
igor.k 5f95198b36 example UI (wip), made blob menu semitransparent when user is dragging scrollable content 2020-05-20 17:13:51 +03:00
igor.k f62fcc70e0 example code structure improvements 2020-05-20 01:47:07 +03:00
igor.k c8bbea2846 Merge branch 'master' into example-ui
# Conflicts:
#	Example/Example/Screens/ContentView.swift
2020-05-19 15:53:07 +03:00
igor.k 0eda4769b8 blob menu config 2020-05-19 03:55:47 +03:00
igor.k 19a94e592a small layout improvement 2020-05-06 00:57:59 +03:00
igor.k 043bf58bcb add some UI for test purposes 2020-05-05 18:21:30 +03:00
35 changed files with 1419 additions and 202 deletions
+26 -10
View File
@@ -9,8 +9,7 @@
/* Begin PBXBuildFile section */
3908002824474A3800E7727C /* BlobMenu.h in Headers */ = {isa = PBXBuildFile; fileRef = 3908002624474A3800E7727C /* BlobMenu.h */; settings = {ATTRIBUTES = (Public, ); }; };
393AAE9E246053B60059752A /* Transitions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE82246053B50059752A /* Transitions.swift */; };
393AAE9F246053B60059752A /* BlobMenuEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE84246053B50059752A /* BlobMenuEnvironment.swift */; };
393AAEA0246053B60059752A /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE85246053B50059752A /* MenuItem.swift */; };
393AAEA0246053B60059752A /* BlobMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE85246053B50059752A /* BlobMenuItem.swift */; };
393AAEA1246053B60059752A /* StickyEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE87246053B50059752A /* StickyEffectView.swift */; };
393AAEA2246053B60059752A /* HamburgerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE88246053B50059752A /* HamburgerView.swift */; };
393AAEA3246053B60059752A /* MenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE89246053B50059752A /* MenuItemView.swift */; };
@@ -32,6 +31,9 @@
393AAEB3246053B60059752A /* AnimationCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE9B246053B50059752A /* AnimationCompletion.swift */; };
393AAEB4246053B60059752A /* BezierUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE9C246053B50059752A /* BezierUtilities.swift */; };
393AAEB5246053B60059752A /* CommonUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393AAE9D246053B50059752A /* CommonUtilities.swift */; };
39484546247823ED0046236D /* BlobMenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39484545247823ED0046236D /* BlobMenuModel.swift */; };
398270BD2474B93100BB7A2B /* CGRect+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398270BC2474B93100BB7A2B /* CGRect+Extensions.swift */; };
39A635A02478843A007946A6 /* BlobMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A6359F2478843A007946A6 /* BlobMenuConfiguration.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -39,8 +41,7 @@
3908002624474A3800E7727C /* BlobMenu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BlobMenu.h; sourceTree = "<group>"; };
3908002724474A3800E7727C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
393AAE82246053B50059752A /* Transitions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transitions.swift; sourceTree = "<group>"; };
393AAE84246053B50059752A /* BlobMenuEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlobMenuEnvironment.swift; sourceTree = "<group>"; };
393AAE85246053B50059752A /* MenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuItem.swift; sourceTree = "<group>"; };
393AAE85246053B50059752A /* BlobMenuItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlobMenuItem.swift; sourceTree = "<group>"; };
393AAE87246053B50059752A /* StickyEffectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StickyEffectView.swift; sourceTree = "<group>"; };
393AAE88246053B50059752A /* HamburgerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HamburgerView.swift; sourceTree = "<group>"; };
393AAE89246053B50059752A /* MenuItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuItemView.swift; sourceTree = "<group>"; };
@@ -62,6 +63,9 @@
393AAE9B246053B50059752A /* AnimationCompletion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationCompletion.swift; sourceTree = "<group>"; };
393AAE9C246053B50059752A /* BezierUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BezierUtilities.swift; sourceTree = "<group>"; };
393AAE9D246053B50059752A /* CommonUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommonUtilities.swift; sourceTree = "<group>"; };
39484545247823ED0046236D /* BlobMenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlobMenuModel.swift; sourceTree = "<group>"; };
398270BC2474B93100BB7A2B /* CGRect+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Extensions.swift"; sourceTree = "<group>"; };
39A6359F2478843A007946A6 /* BlobMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlobMenuConfiguration.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -95,9 +99,10 @@
isa = PBXGroup;
children = (
393AAE86246053B50059752A /* Views */,
39A6359E2478841F007946A6 /* Configuration */,
393AAE83246053B50059752A /* Models */,
393AAE81246053B50059752A /* Effects */,
393AAE8E246053B50059752A /* Extensions */,
393AAE83246053B50059752A /* Models */,
393AAE95246053B50059752A /* Utilities */,
3908002624474A3800E7727C /* BlobMenu.h */,
3908002724474A3800E7727C /* Info.plist */,
@@ -116,8 +121,8 @@
393AAE83246053B50059752A /* Models */ = {
isa = PBXGroup;
children = (
393AAE84246053B50059752A /* BlobMenuEnvironment.swift */,
393AAE85246053B50059752A /* MenuItem.swift */,
393AAE85246053B50059752A /* BlobMenuItem.swift */,
39484545247823ED0046236D /* BlobMenuModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -131,7 +136,6 @@
393AAE8C246053B50059752A /* StickyPathGenerator.swift */,
393AAE88246053B50059752A /* HamburgerView.swift */,
393AAE89246053B50059752A /* MenuItemView.swift */,
393AAE8D246053B50059752A /* Theme.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -140,6 +144,7 @@
isa = PBXGroup;
children = (
393AAE8F246053B50059752A /* CGSize+Extensions.swift */,
398270BC2474B93100BB7A2B /* CGRect+Extensions.swift */,
393AAE90246053B50059752A /* UIGestureRecognizer+Extensions.swift */,
393AAE91246053B50059752A /* UIWindow+Extensions.swift */,
393AAE92246053B50059752A /* CGPoint+Extensions.swift */,
@@ -164,6 +169,15 @@
path = Utilities;
sourceTree = "<group>";
};
39A6359E2478841F007946A6 /* Configuration */ = {
isa = PBXGroup;
children = (
393AAE8D246053B50059752A /* Theme.swift */,
39A6359F2478843A007946A6 /* BlobMenuConfiguration.swift */,
);
path = Configuration;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -253,7 +267,10 @@
393AAE9E246053B60059752A /* Transitions.swift in Sources */,
393AAEAE246053B60059752A /* ScaleKeyframesAnimation.swift in Sources */,
393AAEB1246053B60059752A /* KayframesAnimation.swift in Sources */,
393AAEA0246053B60059752A /* MenuItem.swift in Sources */,
39484546247823ED0046236D /* BlobMenuModel.swift in Sources */,
398270BD2474B93100BB7A2B /* CGRect+Extensions.swift in Sources */,
393AAEA0246053B60059752A /* BlobMenuItem.swift in Sources */,
39A635A02478843A007946A6 /* BlobMenuConfiguration.swift in Sources */,
393AAEAA246053B60059752A /* UIWindow+Extensions.swift in Sources */,
393AAEB2246053B60059752A /* Then.swift in Sources */,
393AAEA7246053B60059752A /* Theme.swift in Sources */,
@@ -262,7 +279,6 @@
393AAEA1246053B60059752A /* StickyEffectView.swift in Sources */,
393AAEA4246053B60059752A /* BlobMenuView.swift in Sources */,
393AAEA3246053B60059752A /* MenuItemView.swift in Sources */,
393AAE9F246053B60059752A /* BlobMenuEnvironment.swift in Sources */,
393AAEAD246053B60059752A /* Comparable+Extensions.swift in Sources */,
393AAEAC246053B60059752A /* Collection+Extensions.swift in Sources */,
393AAEB0246053B60059752A /* SizeKeyframesAnimation.swift in Sources */,
+81 -9
View File
@@ -9,13 +9,27 @@
/* Begin PBXBuildFile section */
3908003B2447D01D00E7727C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3908003A2447D01D00E7727C /* AppDelegate.swift */; };
3908003D2447D01D00E7727C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3908003C2447D01D00E7727C /* SceneDelegate.swift */; };
3908003F2447D01D00E7727C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3908003E2447D01D00E7727C /* ContentView.swift */; };
3908003F2447D01D00E7727C /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3908003E2447D01D00E7727C /* RootView.swift */; };
390800412447D01F00E7727C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 390800402447D01F00E7727C /* Assets.xcassets */; };
390800442447D01F00E7727C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 390800432447D01F00E7727C /* Preview Assets.xcassets */; };
390800472447D01F00E7727C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 390800452447D01F00E7727C /* LaunchScreen.storyboard */; };
3908005A2447D25700E7727C /* BlobMenu.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 390800562447D23300E7727C /* BlobMenu.framework */; };
3908005B2447D25700E7727C /* BlobMenu.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 390800562447D23300E7727C /* BlobMenu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
390800602447D49200E7727C /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 3908005F2447D49200E7727C /* README.md */; };
393BF6972474131C004D193D /* PaginatedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393BF6962474131C004D193D /* PaginatedScrollView.swift */; };
393BF69A24742B4F004D193D /* ExtendedScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 393BF69924742B4F004D193D /* ExtendedScrollView.swift */; };
3948454224774B8A0046236D /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948454124774B8A0046236D /* Theme.swift */; };
3948454424774EF40046236D /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3948454324774EF40046236D /* Screen.swift */; };
394DFE2A2477385700D89A1B /* RandomIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398270D224772F4100BB7A2B /* RandomIcon.swift */; };
398270BF2475756E00BB7A2B /* ExchangeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398270BE2475756E00BB7A2B /* ExchangeView.swift */; };
398270C1247575A600BB7A2B /* CommerceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398270C0247575A600BB7A2B /* CommerceView.swift */; };
398270C3247575B500BB7A2B /* StocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398270C2247575B500BB7A2B /* StocksView.swift */; };
398270CB2475F02B00BB7A2B /* PageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398270CA2475F02B00BB7A2B /* PageControl.swift */; };
398270D12476A60600BB7A2B /* TouchGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398270D02476A60600BB7A2B /* TouchGesture.swift */; };
39A6D8AB246172AE0090F507 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A6D8AA246172AE0090F507 /* WalletView.swift */; };
39A6D8AD2461856B0090F507 /* GridStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A6D8AC2461856B0090F507 /* GridStack.swift */; };
39A6D8AF24619B650090F507 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A6D8AE24619B650090F507 /* Utilities.swift */; };
39A6D8B22461AC2F0090F507 /* Lorem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A6D8B12461AC2F0090F507 /* Lorem.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -50,16 +64,30 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
390800372447D01D00E7727C /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
390800372447D01D00E7727C /* Blob Menu.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Blob Menu.app"; sourceTree = BUILT_PRODUCTS_DIR; };
3908003A2447D01D00E7727C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
3908003C2447D01D00E7727C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
3908003E2447D01D00E7727C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
3908003E2447D01D00E7727C /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
390800402447D01F00E7727C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
390800432447D01F00E7727C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
390800462447D01F00E7727C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
390800482447D01F00E7727C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
390800512447D23300E7727C /* BlobMenu.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = BlobMenu.xcodeproj; path = ../BlobMenu.xcodeproj; sourceTree = "<group>"; };
3908005F2447D49200E7727C /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../../../README.md; sourceTree = "<group>"; };
393BF6962474131C004D193D /* PaginatedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginatedScrollView.swift; sourceTree = "<group>"; };
393BF69924742B4F004D193D /* ExtendedScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtendedScrollView.swift; sourceTree = "<group>"; };
3948454124774B8A0046236D /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
3948454324774EF40046236D /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = "<group>"; };
398270BE2475756E00BB7A2B /* ExchangeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExchangeView.swift; sourceTree = "<group>"; };
398270C0247575A600BB7A2B /* CommerceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommerceView.swift; sourceTree = "<group>"; };
398270C2247575B500BB7A2B /* StocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StocksView.swift; sourceTree = "<group>"; };
398270CA2475F02B00BB7A2B /* PageControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageControl.swift; sourceTree = "<group>"; };
398270D02476A60600BB7A2B /* TouchGesture.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchGesture.swift; sourceTree = "<group>"; };
398270D224772F4100BB7A2B /* RandomIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomIcon.swift; sourceTree = "<group>"; };
39A6D8AA246172AE0090F507 /* WalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletView.swift; sourceTree = "<group>"; };
39A6D8AC2461856B0090F507 /* GridStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridStack.swift; sourceTree = "<group>"; };
39A6D8AE24619B650090F507 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
39A6D8B12461AC2F0090F507 /* Lorem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lorem.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -87,7 +115,7 @@
390800382447D01D00E7727C /* Products */ = {
isa = PBXGroup;
children = (
390800372447D01D00E7727C /* Example.app */,
390800372447D01D00E7727C /* Blob Menu.app */,
);
name = Products;
sourceTree = "<group>";
@@ -97,6 +125,8 @@
children = (
3908004E2447D04800E7727C /* App */,
390800502447D0D500E7727C /* Screens */,
393BF69824741D33004D193D /* UI */,
39A6D8B02461AC180090F507 /* Utilities */,
3908004F2447D05A00E7727C /* Resources */,
390800422447D01F00E7727C /* Preview Content */,
);
@@ -116,6 +146,7 @@
children = (
3908003A2447D01D00E7727C /* AppDelegate.swift */,
3908003C2447D01D00E7727C /* SceneDelegate.swift */,
3948454124774B8A0046236D /* Theme.swift */,
);
path = App;
sourceTree = "<group>";
@@ -134,7 +165,11 @@
390800502447D0D500E7727C /* Screens */ = {
isa = PBXGroup;
children = (
3908003E2447D01D00E7727C /* ContentView.swift */,
3908003E2447D01D00E7727C /* RootView.swift */,
39A6D8AA246172AE0090F507 /* WalletView.swift */,
398270BE2475756E00BB7A2B /* ExchangeView.swift */,
398270C0247575A600BB7A2B /* CommerceView.swift */,
398270C2247575B500BB7A2B /* StocksView.swift */,
);
path = Screens;
sourceTree = "<group>";
@@ -154,6 +189,29 @@
name = Frameworks;
sourceTree = "<group>";
};
393BF69824741D33004D193D /* UI */ = {
isa = PBXGroup;
children = (
39A6D8AC2461856B0090F507 /* GridStack.swift */,
398270CA2475F02B00BB7A2B /* PageControl.swift */,
393BF6962474131C004D193D /* PaginatedScrollView.swift */,
393BF69924742B4F004D193D /* ExtendedScrollView.swift */,
398270D224772F4100BB7A2B /* RandomIcon.swift */,
);
path = UI;
sourceTree = "<group>";
};
39A6D8B02461AC180090F507 /* Utilities */ = {
isa = PBXGroup;
children = (
398270D02476A60600BB7A2B /* TouchGesture.swift */,
39A6D8AE24619B650090F507 /* Utilities.swift */,
39A6D8B12461AC2F0090F507 /* Lorem.swift */,
3948454324774EF40046236D /* Screen.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -173,7 +231,7 @@
);
name = Example;
productName = Example;
productReference = 390800372447D01D00E7727C /* Example.app */;
productReference = 390800372447D01D00E7727C /* Blob Menu.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
@@ -244,9 +302,23 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
39A6D8AF24619B650090F507 /* Utilities.swift in Sources */,
398270C1247575A600BB7A2B /* CommerceView.swift in Sources */,
39A6D8AD2461856B0090F507 /* GridStack.swift in Sources */,
398270D12476A60600BB7A2B /* TouchGesture.swift in Sources */,
398270CB2475F02B00BB7A2B /* PageControl.swift in Sources */,
3908003B2447D01D00E7727C /* AppDelegate.swift in Sources */,
398270BF2475756E00BB7A2B /* ExchangeView.swift in Sources */,
393BF69A24742B4F004D193D /* ExtendedScrollView.swift in Sources */,
393BF6972474131C004D193D /* PaginatedScrollView.swift in Sources */,
3948454224774B8A0046236D /* Theme.swift in Sources */,
3908003D2447D01D00E7727C /* SceneDelegate.swift in Sources */,
3908003F2447D01D00E7727C /* ContentView.swift in Sources */,
39A6D8B22461AC2F0090F507 /* Lorem.swift in Sources */,
398270C3247575B500BB7A2B /* StocksView.swift in Sources */,
3908003F2447D01D00E7727C /* RootView.swift in Sources */,
3948454424774EF40046236D /* Screen.swift in Sources */,
39A6D8AB246172AE0090F507 /* WalletView.swift in Sources */,
394DFE2A2477385700D89A1B /* RandomIcon.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -400,7 +472,7 @@
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.ramotion.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
PRODUCT_NAME = "Blob Menu";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
@@ -420,7 +492,7 @@
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.ramotion.Example;
PRODUCT_NAME = "$(TARGET_NAME)";
PRODUCT_NAME = "Blob Menu";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
+1 -1
View File
@@ -10,7 +10,7 @@ import UIKit
import BlobMenu
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
final class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+2 -3
View File
@@ -9,16 +9,15 @@
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
window.rootViewController = UIHostingController(rootView: RootView())
self.window = window
window.makeKeyAndVisible()
}
+30
View File
@@ -0,0 +1,30 @@
//
// Theme.swift
// Example
//
// Created by Igor K. on 22.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
extension Color {
static let lightGray = Color(#colorLiteral(red: 0.899865165, green: 0.899865165, blue: 0.899865165, alpha: 1))
static var background: Color {
return Color(UIColor { $0.userInterfaceStyle == .dark ? #colorLiteral(red: 0.1960526407, green: 0.1960932612, blue: 0.1960500479, alpha: 1) : #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) })
}
static var contrast: Color {
return Color(UIColor { $0.userInterfaceStyle == .dark ? #colorLiteral(red: 0.9705940673, green: 0.9705940673, blue: 0.9705940673, alpha: 1) : #colorLiteral(red: 0.1960526407, green: 0.1960932612, blue: 0.1960500479, alpha: 1) })
}
static var shadow: Color { contrast.opacity(0.2) }
static var stroke = Color.gray
static var textColor: Color { contrast }
static var bodyTextColor: Color { contrast }
static var informationColor: Color { background.opacity(0.6) }
static var contrastInformationColor: Color { contrast.opacity(0.6) }
}
@@ -0,0 +1,55 @@
//
// CommerceView.swift
// Example
//
// Created by Igor K. on 20.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import SwiftUI
struct CommerceView: View {
var body: some View {
Screen(color: .background) {
VStack {
self.avatar
self.title
self.info
}
}
}
private var avatar: some View {
ZStack {
Color.background
.clipShape(Circle())
.overlay(Circle().stroke(Color.stroke, lineWidth: 0.5))
.frame(size: CGSize(uniform: 60))
.shadow(color: Color.shadow, radius: 7, y: 3)
RandomIcon()
}
}
private var title: some View {
Text(Lorem.words(3).capitalized)
.font(.subheadline)
.foregroundColor(.textColor)
.lineLimit(1)
.padding(.top, 40)
.padding(.horizontal)
}
private var info: some View {
Text(Lorem.sentences(3))
.font(.body)
.foregroundColor(.contrastInformationColor)
.fixedSize(horizontal: false, vertical: true)
.padding(.all)
}
}
struct CommerceView_Previews: PreviewProvider {
static var previews: some View {
CommerceView()
}
}
-85
View File
@@ -1,85 +0,0 @@
//
// ContentView.swift
// Example
//
// Created by Igor K. on 16.04.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import SwiftUI
import BlobMenu
struct ContentView: View {
enum Screen: Int {
case wallet
case exchange
case commerce
case stocks
}
@State var screen: Screen = .wallet
@Environment(\.blobMenuEnvironment) var menuEnvironment: BlobMenuEnvironment
var body: some View {
ZStack {
screenView.edgesIgnoringSafeArea(Edge.Set.all)
menuView
}
}
private var screenView: some View {
switch screen {
case .wallet: return Rectangle().fill(Color.random)
case .exchange: return Rectangle().fill(Color.random)
case .commerce: return Rectangle().fill(Color.random)
case .stocks: return Rectangle().fill(Color.random)
}
}
private var menuView: some View {
VStack {
Spacer()
BlobMenuView.createMenu(items: MenuItem.all, selectedIndex: self.screen.rawValue).padding(.bottom, 30)
}.onReceive(menuEnvironment.$selectedIndex) { index in
guard let screen = Screen(rawValue: index) else { return }
self.screen = screen
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
extension MenuItem {
static let all: [MenuItem] = [
MenuItem(selectedIcon: Image.walletSelected, unselectedIcon: Image.walletUnselected, offset: CGPoint(x: 1, y: -2)),
MenuItem(selectedIcon: Image.exchangeSelected, unselectedIcon: Image.exchangeUnselected),
MenuItem(selectedIcon: Image.bitcoinSelected, unselectedIcon: Image.bitcoinUnselected),
MenuItem(selectedIcon: Image.gridSelected, unselectedIcon: Image.gridUnselected)
]
}
extension Image {
static let walletSelected = Image("Icon_Wallet_black")
static let walletUnselected = Image("Icon_Wallet_gray")
static let bitcoinSelected = Image("Icon_Bitcoin_black")
static let bitcoinUnselected = Image("Icon_Bitcoin_gray")
static let exchangeSelected = Image("Icon_Exchange_black")
static let exchangeUnselected = Image("Icon_Exchange_gray")
static let gridSelected = Image("Icon_Grid_black")
static let gridUnselected = Image("Icon_Grid_gray")
}
extension Color {
static var random: Color {
return Color(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1))
}
}
@@ -0,0 +1,71 @@
//
// ExchangeView.swift
// Example
//
// Created by Igor K. on 20.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import SwiftUI
struct ExchangeView: View {
@State private var currentIndex: Int = 0
var body: some View {
let pagerView = SwiftUIPagerView(pages: self.pages) { index in
withAnimation { self.currentIndex = index }
}
return Screen(color: .background) {
VStack {
self.title
self.description
pagerView.frame(height: 350)
PageControl(pagesCount: 4, index: self.$currentIndex)
Spacer()
}
}
}
private var title: some View {
Text(Lorem.words(3).capitalized)
.font(.subheadline)
.foregroundColor(.textColor)
.lineLimit(1)
.padding(.top, 40)
.padding(.horizontal)
}
private var description: some View {
Text(Lorem.sentences(3))
.font(.body)
.foregroundColor(.contrastInformationColor)
.fixedSize(horizontal: false, vertical: true)
.padding(.all)
}
private var pages: [Page] {
return (0..<4).map { index in Page() }
}
}
struct Page: View, Identifiable {
let id = UUID()
var body: some View {
Color.background
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.stroke, lineWidth: 0.5))
.frame(height: 300)
.shadow(color: Color.shadow, radius: 7, y: 3)
.overlay(RandomIcon().padding(), alignment: .topLeading)
.padding(EdgeInsets(top: 0, leading: 15, bottom: 0, trailing: 15))
}
}
struct ExchangeView_Previews: PreviewProvider {
static var previews: some View {
ExchangeView()
}
}
+77
View File
@@ -0,0 +1,77 @@
//
// ContentView.swift
// Example
//
// Created by Igor K. on 16.04.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import SwiftUI
import BlobMenu
struct RootView: View {
enum Screen: Int, CaseIterable {
case wallet
case exchange
case commerce
case stocks
}
@State private var screen: Screen = .wallet
@State private var isDragging: Bool = false
@ObservedObject private var blobMenuModel = BlobMenuModel(items: BlobMenuItem.all)
var body: some View {
ZStack {
screenView.edgesIgnoringSafeArea(Edge.Set.all.subtracting(.top))
menuView.opacity(isDragging ? 0.1 : 1)
}
.background(Color.background)
}
private var screenView: some View {
let screen = Screen(rawValue: blobMenuModel.selectedIndex) ?? .wallet
switch screen {
case .wallet: return WalletView(isDragging: $isDragging.animatable).asAnyView
case .exchange: return ExchangeView().asAnyView
case .commerce: return CommerceView().asAnyView
case .stocks: return StocksView(isDragging: $isDragging.animatable).asAnyView
}
}
private var menuView: some View {
VStack {
Spacer()
BlobMenuView(model: blobMenuModel).padding(.bottom, 30)
}
}
}
struct RootView_Previews: PreviewProvider {
static var previews: some View {
RootView()
}
}
extension BlobMenuItem {
static let all: [BlobMenuItem] = [
BlobMenuItem(selectedIcon: Image.walletSelected, unselectedIcon: Image.walletUnselected, offset: CGPoint(x: 1, y: -2)),
BlobMenuItem(selectedIcon: Image.exchangeSelected, unselectedIcon: Image.exchangeUnselected),
BlobMenuItem(selectedIcon: Image.bitcoinSelected, unselectedIcon: Image.bitcoinUnselected),
BlobMenuItem(selectedIcon: Image.gridSelected, unselectedIcon: Image.gridUnselected)
]
}
extension Image {
static let walletSelected = Image("Icon_Wallet_black")
static let walletUnselected = Image("Icon_Wallet_gray")
static let bitcoinSelected = Image("Icon_Bitcoin_black")
static let bitcoinUnselected = Image("Icon_Bitcoin_gray")
static let exchangeSelected = Image("Icon_Exchange_black")
static let exchangeUnselected = Image("Icon_Exchange_gray")
static let gridSelected = Image("Icon_Grid_black")
static let gridUnselected = Image("Icon_Grid_gray")
}
+85
View File
@@ -0,0 +1,85 @@
//
// StocksView.swift
// Example
//
// Created by Igor K. on 20.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import SwiftUI
struct StocksView: View {
@Binding var isDragging: Bool
var body: some View {
Screen(color: .background) {
ExtendedScrollView(isDragging: self.$isDragging, contentInset: Theme.contentInset) {
VStack(spacing: Theme.padding) {
self.verticalCollection
self.horizontalCollection
self.verticalCollection
}
.background(Color.background)
}
}
}
private var verticalCollection: some View {
GridStack(columns: 4, rows: 2, spacing: Theme.padding) { row, col in
self.cell(size: Theme.cellSize)
.overlay(RandomIcon().padding(), alignment: .topLeading)
}.background(Color.background)
}
private var horizontalCollection: some View {
ExtendedScrollView(axis: .horizontal, isDragging: $isDragging, contentInset: Theme.horizontalContentInset) {
HStack(spacing: Theme.padding) {
ForEach(0..<10) { index in
self.cell(size: Theme.horizontalCellSize)
.padding(.vertical, 20)
.overlay(RandomIcon())
}
}.background(Color.background)
}
.frame(height: Theme.horizontalCellSize.height + 40)
}
private func cell(size: CGSize) -> some View {
Color.background
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.stroke, lineWidth: 0.5))
.frame(size: size)
.shadow(color: Color.shadow, radius: 7, y: 3)
}
}
struct StocksView_Previews: PreviewProvider {
static var previews: some View {
StocksView(isDragging: .constant(false))
}
}
//MARK: - Theme
extension StocksView {
enum Theme {
static let padding: CGFloat = 20
static private let w = (UIScreen.main.bounds.width - padding * 3) / 2
static var horizontalCellSize: CGSize {
let h = floor(w * 0.5)
return CGSize(width: w, height: h)
}
static var cellSize: CGSize {
let w = (UIScreen.main.bounds.width - padding * 3) / 2
let h = floor(w * 1.25)
return CGSize(width: w, height: h)
}
static let contentInset = UIEdgeInsets(top: 20, left: 0, bottom: UIWindow.safeInsets.bottom + 20, right: 0)
static let horizontalContentInset = UIEdgeInsets(horizontal: 20, vertical: 0)
}
}
+63
View File
@@ -0,0 +1,63 @@
//
// WalletView.swift
// Example
//
// Created by Igor K. on 05.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
struct WalletView: View {
@Binding var isDragging: Bool
var body: some View {
Screen(color: .background) {
ExtendedScrollView(isDragging: self.$isDragging, contentInset: Theme.contentInset) {
ForEach(0..<10) { index in
ItemCell()
}.background(Color.background)
}
}
}
}
private struct ItemCell: View {
var body: some View {
VStack(alignment: .leading) {
Rectangle()
.fill(Color.lightGray)
.aspectRatio(5/3, contentMode: .fill)
.layoutPriority(1)
.overlay(RandomIcon().padding(), alignment: .topLeading)
Text(Lorem.words(3).capitalized)
.font(.subheadline)
.foregroundColor(.textColor)
.lineLimit(1)
.padding(Edge.Set.all.subtracting(.bottom))
Text(Lorem.paragraph)
.font(.body)
.foregroundColor(.contrastInformationColor)
.fixedSize(horizontal: false, vertical: true)
.padding(.all)
}
.background(Color.background)
.clipShape(RoundedRectangle(cornerRadius: 8))
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(Color.stroke, lineWidth: 0.5))
.shadow(color: Color.shadow, radius: 7, y: 3)
.padding(EdgeInsets(top: 0, leading: 15, bottom: 5, trailing: 15))
}
}
//MARK: - Theme
extension WalletView {
enum Theme {
static let contentInset = UIEdgeInsets(top: 20, left: 0, bottom: UIWindow.safeInsets.bottom + 20, right: 0)
}
}
@@ -0,0 +1,90 @@
//
// ExtendedScrollView.swift
// Example
//
// Created by Igor K. on 19.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
struct ExtendedScrollView: UIViewRepresentable {
private let isDragging: Binding<Bool>
private let scrollView = UIScrollView()
func makeCoordinator() -> Coordinator {
return Coordinator(control: self, isDragging: isDragging)
}
func makeUIView(context: Context) -> UIScrollView {
scrollView.delegate = context.coordinator
return scrollView
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
}
init<Content: View>(axis: Axis = .vertical,
isDragging: Binding<Bool> = .constant(false),
showsIndicators: Bool = false,
contentInset: UIEdgeInsets = .zero,
@ViewBuilder content: () -> Content) {
self.isDragging = isDragging
let hosting = UIHostingController(rootView: content())
hosting.view.translatesAutoresizingMaskIntoConstraints = false
hosting.edgesForExtendedLayout = .all
hosting.extendedLayoutIncludesOpaqueBars = true
scrollView.addSubview(hosting.view)
scrollView.showsVerticalScrollIndicator = showsIndicators
scrollView.showsHorizontalScrollIndicator = showsIndicators
scrollView.contentInsetAdjustmentBehavior = .never
scrollView.contentInset = contentInset
scrollView.contentOffset = .zero
let constraints: [NSLayoutConstraint]
switch axis {
case .horizontal:
constraints = [
hosting.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
hosting.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
hosting.view.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
]
case .vertical:
constraints = [
hosting.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
hosting.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
hosting.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
hosting.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
hosting.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
]
}
scrollView.addConstraints(constraints)
}
final class Coordinator: NSObject, UIScrollViewDelegate {
private let isDragging: Binding<Bool>
private let control: ExtendedScrollView
init(control: ExtendedScrollView, isDragging: Binding<Bool>) {
self.control = control
self.isDragging = isDragging
}
//MARK: - UIScrollView delegate methods
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDragging.wrappedValue = true
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
isDragging.wrappedValue = false
}
}
}
+36
View File
@@ -0,0 +1,36 @@
//
// GridStack.swift
// Example
//
// Created by Igor K. on 05.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
public struct GridStack<Content: View>: View {
let columns: Int
let rows: Int
let spacing: CGFloat
let content: (Int, Int) -> Content
public init(columns: Int, rows: Int, spacing: CGFloat = 0, @ViewBuilder content: @escaping (Int, Int) -> Content) {
self.columns = columns
self.rows = rows
self.spacing = spacing
self.content = content
}
public var body: some View {
VStack(spacing: self.spacing) {
ForEach(0..<self.rows, id: \.self) { row in
HStack(spacing: self.spacing) {
ForEach(0..<self.columns, id: \.self) { column in
self.content(column, row)
}
}
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import Foundation
import SwiftUI
struct PageControl: View {
@Binding private var index: Int
private let pagesCount: Int
private let diameter: CGFloat
private let spacing: CGFloat
private var width: CGFloat {
return CGFloat(pagesCount) * diameter + CGFloat(pagesCount - 1) * spacing
}
private var size: CGSize {
return CGSize(width: width, height: diameter)
}
init(diameter: CGFloat = 6,
spacing: CGFloat = 10,
pagesCount: Int,
index: Binding<Int>) {
self.diameter = diameter
self.spacing = spacing
self.pagesCount = pagesCount
self._index = index
}
var body: some View {
ZStack {
HStack(spacing: spacing) {
ForEach(0..<pagesCount) { i in
Circle().fill(Color.contrast.opacity(self.index == i ? 1 : 0.3))
}
}
.frame(size: size)
Circle()
.offset(CGPoint(x: getCenteredXPosition(for: self.index), y: 0))
.fill(Color.contrast)
.frame(size: size)
}
}
private func getCenteredXPosition(for index: Int) -> CGFloat {
let position = CGFloat(index) * (diameter + spacing)
let halfAlldotsWidthWithSpaces = (CGFloat(pagesCount - 1) * (diameter + spacing) + diameter) / 2.0
return position - halfAlldotsWidthWithSpaces + diameter / 2
}
}
@@ -0,0 +1,45 @@
import Foundation
import SwiftUI
struct SwiftUIPagerView<Content: View & Identifiable>: View {
@State private var index: Int = 0
@State private var offset: CGFloat = 0
private let pages: [Content]
private let indexChanged: (Int) -> Void
init(pages: [Content], indexChanged: @escaping (Int) -> Void) {
self.pages = pages
self.indexChanged = indexChanged
}
var body: some View {
GeometryReader { geometry in
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .center, spacing: 0) {
ForEach(self.pages) { page in
page
.frame(width: geometry.size.width, height: nil)
}
}
}
.content.offset(x: self.offset)
.frame(width: geometry.size.width, height: nil, alignment: .leading)
.gesture(DragGesture()
.onChanged({ value in
self.offset = value.translation.width - geometry.size.width * CGFloat(self.index)
})
.onEnded({ value in
if abs(value.predictedEndTranslation.width) >= geometry.size.width / 2 {
var nextIndex: Int = (value.predictedEndTranslation.width < 0) ? 1 : -1
nextIndex += self.index
self.index = nextIndex.limited(min: 0, max: self.pages.endIndex - 1)
self.indexChanged(nextIndex)
}
withAnimation { self.offset = -geometry.size.width * CGFloat(self.index) }
})
)
}
}
}
+76
View File
@@ -0,0 +1,76 @@
//
// RandomIcon.swift
// Example
//
// Created by Igor K. on 22.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
struct RandomIcon: View {
var body: some View {
let index = Int.random(in: 0..<5)
let result: AnyView
switch index {
case 0: result = circle.frame(size: CGSize(uniform: 30)).asAnyView
case 1: result = roundedRectangle.frame(size: CGSize(uniform: 30)).asAnyView
case 2: result = capsule.frame(size: CGSize(width: 30, height: 20)).asAnyView
case 3: result = elipse.frame(size: CGSize(width: 20, height: 30)).asAnyView
default: result = polygon.frame(size: CGSize(uniform: 30)).asAnyView
}
return result.rotateAroundOnTap
}
private var circle: some View {
Circle().fill(Color.random)
}
private var roundedRectangle: some View {
RoundedRectangle(cornerRadius: 6).fill(Color.random)
}
private var rectangle: some View {
Rectangle().fill(Color.random)
}
private var capsule: some View {
Capsule(style: RoundedCornerStyle.circular).fill(Color.random)
}
private var elipse: some View {
Ellipse().fill(Color.random)
}
private var polygon: some View {
let sidesCount = Int.random(in: 3...8)
return PolygonShape(sides: sidesCount).fill(Color.random)
}
}
private struct PolygonShape: Shape {
var sides: Int
func path(in rect: CGRect) -> Path {
let h = Double(min(rect.size.width, rect.size.height)) / 2.0
let c = CGPoint(x: rect.size.width / 2.0, y: rect.size.height / 2.0)
var path = Path()
for i in 0..<sides {
let angle = Double(i) / Double(sides) * 2 * Double.pi
let pt = CGPoint(x: c.x + CGFloat(cos(angle) * h), y: c.y + CGFloat(sin(angle) * h))
if i == 0 {
path.move(to: pt)
} else {
path.addLine(to: pt)
}
}
path.closeSubpath()
return path
}
}
+291
View File
@@ -0,0 +1,291 @@
//
// Lorem.swift
// Example
//
// Author Lukas Kubanek
// https://github.com/lukaskubanek/LoremSwiftum
//
import Foundation
import Foundation
/// A lightweight lorem ipsum generator.
public final class Lorem {
// ======================================================= //
// MARK: - Text
// ======================================================= //
/// Generates a single word.
public static var word: String {
return allWords.randomElement()!
}
/// Generates multiple words whose count is defined by the given value.
///
/// - Parameter count: The number of words to generate.
/// - Returns: The generated words joined by a space character.
public static func words(_ count: Int) -> String {
return _compose(
word,
count: count,
joinBy: .space
)
}
/// Generates multiple words whose count is randomly selected from within the given range.
///
/// - Parameter range: The range of number of words to generate.
/// - Returns: The generated words joined by a space character.
public static func words(_ range: Range<Int>) -> String {
return _compose(word, count: Int.random(in: range), joinBy: .space)
}
/// Generates multiple words whose count is randomly selected from within the given closed range.
///
/// - Parameter range: The range of number of words to generate.
/// - Returns: The generated words joined by a space character.
public static func words(_ range: ClosedRange<Int>) -> String {
return _compose(word, count: Int.random(in: range), joinBy: .space)
}
/// Generates a single sentence.
public static var sentence: String {
let numberOfWords = Int.random(
in: minWordsCountInSentence...maxWordsCountInSentence
)
return _compose(
word,
count: numberOfWords,
joinBy: .space,
endWith: .dot,
decorate: { $0.firstLetterCapitalized }
)
}
/// Generates multiple sentences whose count is defined by the given value.
///
/// - Parameter count: The number of sentences to generate.
/// - Returns: The generated sentences joined by a space character.
public static func sentences(_ count: Int) -> String {
return _compose(
sentence,
count: count,
joinBy: .space
)
}
/// Generates multiple sentences whose count is selected from within the given range.
///
/// - Parameter count: The number of sentences to generate.
/// - Returns: The generated sentences joined by a space character.
public static func sentences(_ range: Range<Int>) -> String {
return _compose(sentence, count: Int.random(in: range), joinBy: .space)
}
/// Generates multiple sentences whose count is selected from within the given closed range.
///
/// - Parameter count: The number of sentences to generate.
/// - Returns: The generated sentences joined by a space character.
public static func sentences(_ range: ClosedRange<Int>) -> String {
return _compose(sentence, count: Int.random(in: range), joinBy: .space)
}
/// Generates a single paragraph.
public static var paragraph: String {
let numberOfSentences = Int.random(
in: minSentencesCountInParagraph...maxSentencesCountInParagraph
)
return _compose(
sentence,
count: numberOfSentences,
joinBy: .space
)
}
/// Generates multiple paragraphs whose count is defined by the given value.
///
/// - Parameter count: The number of paragraphs to generate.
/// - Returns: The generated paragraphs joined by a new line character.
public static func paragraphs(_ count: Int) -> String {
return _compose(
paragraph,
count: count,
joinBy: .newLine
)
}
/// Generates multiple paragraphs whose count is selected from within the given range.
///
/// - Parameter count: The number of paragraphs to generate.
/// - Returns: The generated paragraphs joined by a new line character.
public static func paragraphs(_ range: Range<Int>) -> String {
return _compose(
paragraph,
count: Int.random(in: range),
joinBy: .newLine
)
}
/// Generates multiple paragraphs whose count is selected from within the given closed range.
///
/// - Parameter count: The number of paragraphs to generate.
/// - Returns: The generated paragraphs joined by a new line character.
public static func paragraphs(_ range: ClosedRange<Int>) -> String {
return _compose(
paragraph,
count: Int.random(in: range),
joinBy: .newLine
)
}
/// Generates a capitalized title.
public static var title: String {
let numberOfWords = Int.random(
in: minWordsCountInTitle...maxWordsCountInTitle
)
return _compose(
word,
count: numberOfWords,
joinBy: .space,
decorate: { $0.capitalized }
)
}
// ======================================================= //
// MARK: - Names
// ======================================================= //
/// Generates a first name.
public static var firstName: String {
return firstNames.randomElement()!
}
/// Generates a last name.
public static var lastName: String {
return lastNames.randomElement()!
}
/// Generates a full name.
public static var fullName: String {
return "\(firstName) \(lastName)"
}
// ======================================================= //
// MARK: - Email Addresses & URLs
// ======================================================= //
/// Generates an email address.
public static var emailAddress: String {
let emailDelimiter = emailDelimiters.randomElement()!
let emailDomain = emailDomains.randomElement()!
return "\(firstName)\(emailDelimiter)\(lastName)@\(emailDomain)".lowercased()
}
/// Generates a URL.
public static var url: String {
let urlScheme = urlSchemes.randomElement()!
let urlDomain = urlDomains.randomElement()!
return "\(urlScheme)://\(urlDomain)"
}
// ======================================================= //
// MARK: - Tweets
// ======================================================= //
/// Generates a random tweet which is shorter than 140 characters.
public static var shortTweet: String {
return _composeTweet(shortTweetMaxLength)
}
/// Generates a random tweet which is shorter than 280 characters.
public static var tweet: String {
return _composeTweet(tweetMaxLength)
}
}
extension Lorem {
fileprivate enum Separator: String {
case none = ""
case space = " "
case dot = "."
case newLine = "\n"
}
fileprivate static func _compose(
_ provider: @autoclosure () -> String,
count: Int,
joinBy middleSeparator: Separator,
endWith endSeparator: Separator = .none,
decorate decorator: ((String) -> String)? = nil
) -> String {
var string = ""
for index in 0..<count {
string += provider()
if (index < count - 1) {
string += middleSeparator.rawValue
} else {
string += endSeparator.rawValue
}
}
if let decorator = decorator {
string = decorator(string)
}
return string
}
fileprivate static func _composeTweet(_ maxLength: Int) -> String {
for numberOfSentences in [4, 3, 2, 1] {
let tweet = sentences(numberOfSentences)
if tweet.count < maxLength {
return tweet
}
}
return ""
}
fileprivate static let minWordsCountInSentence = 4
fileprivate static let maxWordsCountInSentence = 16
fileprivate static let minSentencesCountInParagraph = 3
fileprivate static let maxSentencesCountInParagraph = 9
fileprivate static let minWordsCountInTitle = 2
fileprivate static let maxWordsCountInTitle = 7
fileprivate static let shortTweetMaxLength = 140
fileprivate static let tweetMaxLength = 280
fileprivate static let allWords = ["alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat"]
fileprivate static let firstNames = ["Judith", "Angelo", "Margarita", "Kerry", "Elaine", "Lorenzo", "Justice", "Doris", "Raul", "Liliana", "Kerry", "Elise", "Ciaran", "Johnny", "Moses", "Davion", "Penny", "Mohammed", "Harvey", "Sheryl", "Hudson", "Brendan", "Brooklynn", "Denis", "Sadie", "Trisha", "Jacquelyn", "Virgil", "Cindy", "Alexa", "Marianne", "Giselle", "Casey", "Alondra", "Angela", "Katherine", "Skyler", "Kyleigh", "Carly", "Abel", "Adrianna", "Luis", "Dominick", "Eoin", "Noel", "Ciara", "Roberto", "Skylar", "Brock", "Earl", "Dwayne", "Jackie", "Hamish", "Sienna", "Nolan", "Daren", "Jean", "Shirley", "Connor", "Geraldine", "Niall", "Kristi", "Monty", "Yvonne", "Tammie", "Zachariah", "Fatima", "Ruby", "Nadia", "Anahi", "Calum", "Peggy", "Alfredo", "Marybeth", "Bonnie", "Gordon", "Cara", "John", "Staci", "Samuel", "Carmen", "Rylee", "Yehudi", "Colm", "Beth", "Dulce", "Darius", "inley", "Javon", "Jason", "Perla", "Wayne", "Laila", "Kaleigh", "Maggie", "Don", "Quinn", "Collin", "Aniya", "Zoe", "Isabel", "Clint", "Leland", "Esmeralda", "Emma", "Madeline", "Byron", "Courtney", "Vanessa", "Terry", "Antoinette", "George", "Constance", "Preston", "Rolando", "Caleb", "Kenneth", "Lynette", "Carley", "Francesca", "Johnnie", "Jordyn", "Arturo", "Camila", "Skye", "Guy", "Ana", "Kaylin", "Nia", "Colton", "Bart", "Brendon", "Alvin", "Daryl", "Dirk", "Mya", "Pete", "Joann", "Uriel", "Alonzo", "Agnes", "Chris", "Alyson", "Paola", "Dora", "Elias", "Allen", "Jackie", "Eric", "Bonita", "Kelvin", "Emiliano", "Ashton", "Kyra", "Kailey", "Sonja", "Alberto", "Ty", "Summer", "Brayden", "Lori", "Kelly", "Tomas", "Joey", "Billie", "Katie", "Stephanie", "Danielle", "Alexis", "Jamal", "Kieran", "Lucinda", "Eliza", "Allyson", "Melinda", "Alma", "Piper", "Deana", "Harriet", "Bryce", "Eli", "Jadyn", "Rogelio", "Orlaith", "Janet", "Randal", "Toby", "Carla", "Lorie", "Caitlyn", "Annika", "Isabelle", "inn", "Ewan", "Maisie", "Michelle", "Grady", "Ida", "Reid", "Emely", "Tricia", "Beau", "Reese", "Vance", "Dalton", "Lexi", "Rafael", "Makenzie", "Mitzi", "Clinton", "Xena", "Angelina", "Kendrick", "Leslie", "Teddy", "Jerald", "Noelle", "Neil", "Marsha", "Gayle", "Omar", "Abigail", "Alexandra", "Phil", "Andre", "Billy", "Brenden", "Bianca", "Jared", "Gretchen", "Patrick", "Antonio", "Josephine", "Kyla", "Manuel", "Freya", "Kellie", "Tonia", "Jamie", "Sydney", "Andres", "Ruben", "Harrison", "Hector", "Clyde", "Wendell", "Kaden", "Ian", "Tracy", "Cathleen", "Shawn"]
fileprivate static let lastNames = ["Chung", "Chen", "Melton", "Hill", "Puckett", "Song", "Hamilton", "Bender", "Wagner", "McLaughlin", "McNamara", "Raynor", "Moon", "Woodard", "Desai", "Wallace", "Lawrence", "Griffin", "Dougherty", "Powers", "May", "Steele", "Teague", "Vick", "Gallagher", "Solomon", "Walsh", "Monroe", "Connolly", "Hawkins", "Middleton", "Goldstein", "Watts", "Johnston", "Weeks", "Wilkerson", "Barton", "Walton", "Hall", "Ross", "Chung", "Bender", "Woods", "Mangum", "Joseph", "Rosenthal", "Bowden", "Barton", "Underwood", "Jones", "Baker", "Merritt", "Cross", "Cooper", "Holmes", "Sharpe", "Morgan", "Hoyle", "Allen", "Rich", "Rich", "Grant", "Proctor", "Diaz", "Graham", "Watkins", "Hinton", "Marsh", "Hewitt", "Branch", "Walton", "O'Brien", "Case", "Watts", "Christensen", "Parks", "Hardin", "Lucas", "Eason", "Davidson", "Whitehead", "Rose", "Sparks", "Moore", "Pearson", "Rodgers", "Graves", "Scarborough", "Sutton", "Sinclair", "Bowman", "Olsen", "Love", "McLean", "Christian", "Lamb", "James", "Chandler", "Stout", "Cowan", "Golden", "Bowling", "Beasley", "Clapp", "Abrams", "Tilley", "Morse", "Boykin", "Sumner", "Cassidy", "Davidson", "Heath", "Blanchard", "McAllister", "McKenzie", "Byrne", "Schroeder", "Griffin", "Gross", "Perkins", "Robertson", "Palmer", "Brady", "Rowe", "Zhang", "Hodge", "Li", "Bowling", "Justice", "Glass", "Willis", "Hester", "Floyd", "Graves", "Fischer", "Norman", "Chan", "Hunt", "Byrd", "Lane", "Kaplan", "Heller", "May", "Jennings", "Hanna", "Locklear", "Holloway", "Jones", "Glover", "Vick", "O'Donnell", "Goldman", "McKenna", "Starr", "Stone", "McClure", "Watson", "Monroe", "Abbott", "Singer", "Hall", "Farrell", "Lucas", "Norman", "Atkins", "Monroe", "Robertson", "Sykes", "Reid", "Chandler", "Finch", "Hobbs", "Adkins", "Kinney", "Whitaker", "Alexander", "Conner", "Waters", "Becker", "Rollins", "Love", "Adkins", "Black", "Fox", "Hatcher", "Wu", "Lloyd", "Joyce", "Welch", "Matthews", "Chappell", "MacDonald", "Kane", "Butler", "Pickett", "Bowman", "Barton", "Kennedy", "Branch", "Thornton", "McNeill", "Weinstein", "Middleton", "Moss", "Lucas", "Rich", "Carlton", "Brady", "Schultz", "Nichols", "Harvey", "Stevenson", "Houston", "Dunn", "West", "O'Brien", "Barr", "Snyder", "Cain", "Heath", "Boswell", "Olsen", "Pittman", "Weiner", "Petersen", "Davis", "Coleman", "Terrell", "Norman", "Burch", "Weiner", "Parrott", "Henry", "Gray", "Chang", "McLean", "Eason", "Weeks", "Siegel", "Puckett", "Heath", "Hoyle", "Garrett", "Neal", "Baker", "Goldman", "Shaffer", "Choi", "Carver"]
fileprivate static let emailDomains = ["gmail.com", "yahoo.com", "hotmail.com", "email.com", "live.com", "me.com", "mac.com", "aol.com", "fastmail.com", "mail.com"]
fileprivate static let emailDelimiters = ["", ".", "-", "_"]
fileprivate static let urlSchemes = ["http", "https"]
fileprivate static let urlDomains = ["twitter.com", "google.com", "youtube.com", "wordpress.org", "adobe.com", "blogspot.com", "godaddy.com", "wikipedia.org", "wordpress.com", "yahoo.com", "linkedin.com", "amazon.com", "flickr.com", "w3.org", "apple.com", "myspace.com", "tumblr.com", "digg.com", "microsoft.com", "vimeo.com", "pinterest.com", "stumbleupon.com", "youtu.be", "miibeian.gov.cn", "baidu.com", "feedburner.com", "bit.ly"]
}
extension String {
fileprivate var firstLetterCapitalized: String {
guard !isEmpty else { return self }
return prefix(1).capitalized + dropFirst()
}
}
+27
View File
@@ -0,0 +1,27 @@
//
// Screen.swift
// Example
//
// Created by Igor K. on 22.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
struct Screen<Content>: View where Content: View {
let content: () -> Content
let backgoundColor: Color
init(color: Color = .white, @ViewBuilder content: @escaping () -> Content) {
self.content = content
self.backgoundColor = color
}
var body: some View {
ZStack {
backgoundColor.edgesIgnoringSafeArea(.all)
content()
}
}
}
@@ -0,0 +1,45 @@
import Foundation
import SwiftUI
struct TouchGestureViewModifier: ViewModifier {
let touchBegan: () -> Void
let touchEnd: (_ success: Bool) -> Void
@State private var hasBegun = false
@State private var hasEnded = false
private func isTooFar(_ translation: CGSize) -> Bool {
let distance = sqrt(pow(translation.width, 2) + pow(translation.height, 2))
return distance >= 20.0
}
func body(content: Content) -> some View {
content.gesture(DragGesture(minimumDistance: 0)
.onChanged { event in
guard !self.hasEnded else { return }
if self.hasBegun == false {
self.hasBegun = true
self.touchBegan()
} else if self.isTooFar(event.translation) {
self.hasEnded = true
self.touchEnd(false)
}
}
.onEnded { event in
if !self.hasEnded {
let success = !self.isTooFar(event.translation)
self.touchEnd(success)
}
self.hasBegun = false
self.hasEnded = false
})
}
}
extension View {
func onTouchGesture(touchBegan: @escaping () -> Void,
touchEnd: @escaping (_ success: Bool) -> Void) -> some View {
modifier(TouchGestureViewModifier(touchBegan: touchBegan, touchEnd: touchEnd))
}
}
+100
View File
@@ -0,0 +1,100 @@
//
// Utilities.swift
// Example
//
// Created by Igor K. on 05.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
extension Color {
static var random: Color {
return Color(
red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1)
)
}
}
extension UIWindow {
static var safeInsets: UIEdgeInsets {
return current?.safeAreaInsets ?? UIEdgeInsets(top: 44, left: 0, bottom: 34, right: 0)
}
static var isFullScreen: Bool {
return current?.frame == UIScreen.main.bounds
}
}
extension View {
public func greedyFrame(alignment: Alignment) -> some View {
return self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: alignment)
}
}
extension Binding where Value: BindingAnimatable {
var animatable: Binding<Value> {
return Binding<Value>(get: { return self.wrappedValue },
set: { b in withAnimation { self.wrappedValue = b } })
}
}
protocol BindingAnimatable { }
extension Bool: BindingAnimatable { }
extension Int: BindingAnimatable { }
extension View {
var rotateAroundOnTap: some View {
self.modifier(RotationAroundModifier())
}
}
struct RotationAroundModifier: ViewModifier {
@State private var animationAngle = 0.0
func body(content: Content) -> some View {
return content
.onTapGesture { withAnimation { self.animationAngle += 360 } }
.rotation3DEffect(.degrees(animationAngle), axis: (x: 0, y: 1, z: 0))
.animation(.interpolatingSpring(stiffness: 150, damping: 17))
}
}
extension View {
var scaleOnTap: some View {
self.modifier(TapScaleModifier())
}
}
struct TapScaleModifier: ViewModifier {
@State private var scaleValue: CGFloat = 1
func body(content: Content) -> some View {
content
.scaleEffect(self.scaleValue)
.onTouchGesture(
touchBegan: { withAnimation { self.scaleValue = 1.05 } },
touchEnd: { _ in withAnimation { self.scaleValue = 1.0 } }
)
}
}
extension UIEdgeInsets {
public init(uniform value: CGFloat) {
self.init(top: value, left: value, bottom: value, right: value)
}
public init(horizontal h: CGFloat, vertical v: CGFloat) {
self.init(top: v, left: h, bottom: v, right: h)
}
}
@@ -0,0 +1,32 @@
//
// BlobMenuConfiguration.swift
// BlobMenu
//
// Created by Igor K. on 23.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
public struct BlobMenuConfiguration {
public let hamburgerColor: Color
public let backgroundColor: Color
public let selectionColor: Color
public init(hamburgerColor: Color,
backgroundColor: Color,
selectionColor: Color) {
self.hamburgerColor = hamburgerColor
self.backgroundColor = backgroundColor
self.selectionColor = selectionColor
}
public static var `default`: BlobMenuConfiguration {
BlobMenuConfiguration(hamburgerColor: Color.hamburgerColor,
backgroundColor: Color.backgroundColor,
selectionColor: Color.selectionColor)
}
}
@@ -19,5 +19,5 @@ extension Color {
return Color(UIColor { $0.userInterfaceStyle == .dark ? #colorLiteral(red: 0.9705940673, green: 0.9705940673, blue: 0.9705940673, alpha: 1) : #colorLiteral(red: 0.1960526407, green: 0.1960932612, blue: 0.1960500479, alpha: 1) })
}
static let selection = Color(#colorLiteral(red: 0.9983773828, green: 0.7375702262, blue: 0.1739521325, alpha: 1))
static let selectionColor = Color(#colorLiteral(red: 0.9983773828, green: 0.7375702262, blue: 0.1739521325, alpha: 1))
}
+1 -1
View File
@@ -10,7 +10,7 @@ import Foundation
import SwiftUI
extension AnyTransition {
static var blobMenuItem: AnyTransition {
static var blobBlobMenuItem: AnyTransition {
return AnyTransition.scale.combined(with: AnyTransition.rotation)
.animation(.easeOut(duration: 0.35))
}
@@ -0,0 +1,25 @@
//
// CGRect+Extensions.swift
// BlobMenu
//
// Created by Igor K. on 20.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
extension CGRect {
public init(size: CGSize) {
self.init(origin: .zero, size: size)
}
public var center: CGPoint {
return CGPoint(x: midX, y: midY)
}
public init(center: CGPoint, size: CGSize) {
self.init(x: center.x - size.width / 2, y: center.y - size.height / 2, width: size.width, height: size.height)
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
import Foundation
import UIKit
extension UIWindow {
public extension UIWindow {
static var current: UIWindow? {
//if scene is not connected will use first normal level key window
-29
View File
@@ -1,29 +0,0 @@
//
// BlobMenuEnvironment.swift
// BlobMenu
//
// Created by Igor K. on 23.04.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
public extension EnvironmentValues {
var blobMenuEnvironment: BlobMenuEnvironment {
get { return self[BlobMenuEnvironmentKey.self] }
set { self[BlobMenuEnvironmentKey.self] = newValue }
}
}
struct BlobMenuEnvironmentKey: EnvironmentKey {
static let defaultValue: BlobMenuEnvironment = BlobMenuEnvironment()
}
public final class BlobMenuEnvironment: ObservableObject {
@Published public internal(set) var isOpened: Bool = false
@Published public internal(set) var isMenuItemsVisible: Bool = false
@Published public internal(set) var selectedIndex: Int = 0
fileprivate init() {}
}
@@ -1,5 +1,5 @@
//
// MenuItem.swift
// BlobMenuItem.swift
// BlobMenu
//
// Created by Igor K. on 29.04.2020.
@@ -9,7 +9,7 @@
import Foundation
import SwiftUI
public struct MenuItem: Identifiable, Hashable {
public struct BlobMenuItem: Identifiable, Hashable {
public let id = UUID()
public let selectedIcon: Image
+31
View File
@@ -0,0 +1,31 @@
//
// BlobMenuMode.swift
// BlobMenu
//
// Created by Igor K. on 22.05.2020.
// Copyright © 2020 Ramotion. All rights reserved.
//
import Foundation
import SwiftUI
public final class BlobMenuModel: ObservableObject {
@Published public var items: [BlobMenuItem]
@Published public var isOpened: Bool
@Published public internal(set) var selectedIndex: Int
@Published public internal(set) var isBlobMenuItemsVisible: Bool = false
public init(items: [BlobMenuItem],
selectedIndex: Int = 0,
isOpened: Bool = false) {
self.items = items
self.isOpened = isOpened
self.selectedIndex = selectedIndex.limited(0, items.count - 1)
}
public func selectIndex(_ index: Int) {
let limitedIndex = selectedIndex.limited(0, items.count - 1)
selectedIndex = limitedIndex
}
}
+4
View File
@@ -18,6 +18,10 @@ extension View {
public func offset(_ offset: CGPoint) -> some View {
return self.offset(x: offset.x, y: offset.y)
}
public var asAnyView: AnyView {
return AnyView(self)
}
}
+4 -2
View File
@@ -28,10 +28,12 @@ struct BackgroundPreferenceKey: PreferenceKey {
struct BackgroundView: View {
let color: Color
var body: some View {
return Rectangle()
.fill(Color.backgroundColor)
.shadow(color: Color.backgroundColor.opacity(0.45), radius: 8, x: 0, y: 4)
.fill(color)
.shadow(color: color.opacity(0.45), radius: 8, x: 0, y: 4)
.anchorPreference(key: BackgroundPreferenceKey.self, value: .bounds, transform: { [BackgroundPreferenceData(bounds: $0)] })
}
}
+38 -42
View File
@@ -10,26 +10,18 @@ import SwiftUI
public struct BlobMenuView: View {
@State public var selectedIndex: Int
private let configuration: BlobMenuConfiguration
@ObservedObject private var viewModel: BlobMenuModel
@EnvironmentObject private var environment: BlobMenuEnvironment
private let items: [MenuItem]
public static func createMenu(items: [MenuItem], selectedIndex: Int = 0) -> some View {
return BlobMenuView(items: items, selectedIndex: selectedIndex).environmentObject(BlobMenuEnvironmentKey.defaultValue)
}
private init(items: [MenuItem], selectedIndex: Int = 0) {
self.items = items
public init(model: BlobMenuModel,
configuration: BlobMenuConfiguration = .default) {
let limitedIndex = selectedIndex.limited(0, items.count - 1)
_selectedIndex = State<Int>.init(initialValue: limitedIndex)
self.viewModel = model
self.configuration = configuration
BlobMenuEnvironmentKey.defaultValue.selectedIndex = limitedIndex
UIWindow.current?.addGesture(type: .tap) { _ in
BlobMenuEnvironmentKey.defaultValue.isMenuItemsVisible = false
BlobMenuEnvironmentKey.defaultValue.isOpened = false
UIWindow.current?.addGesture(type: .tap) {[weak model] _ in
model?.isBlobMenuItemsVisible = false
model?.isOpened = false
}
}
@@ -38,7 +30,7 @@ public struct BlobMenuView: View {
HStack {
Spacer()
self.background.overlay(self.itemsView)
Spacer().size(width: self.environment.isOpened ? nil : Theme.padding)
Spacer().size(width: self.viewModel.isOpened ? nil : Theme.padding)
}
.backgroundPreferenceValue(BackgroundPreferenceKey.self) { p in
GeometryReader { geometry in
@@ -48,7 +40,7 @@ public struct BlobMenuView: View {
}
HStack {
Spacer()
HamburgerView(isOpened: environment.isOpened)
HamburgerView(isOpened: viewModel.isOpened, color: configuration.hamburgerColor)
Spacer().size(width: Theme.padding)
}
}
@@ -57,7 +49,7 @@ public struct BlobMenuView: View {
private func createStickyView(geometry: GeometryProxy, preferences: [BackgroundPreferenceData]) -> some View {
guard !self.environment.isMenuItemsVisible else {
guard !self.viewModel.isBlobMenuItemsVisible else {
return AnyView(Color.clear)
}
@@ -71,29 +63,31 @@ public struct BlobMenuView: View {
let effectView = StickyEffectShape(baseRect: base, figureRect: b, figureCornerRadius: r, avulsionDistance: Theme.stickyEffectAvulsionDistance)
return AnyView(effectView.fill(Color.backgroundColor)
.frame(size: CGSize(width: w, height: f.height)))
return effectView
.fill(configuration.backgroundColor)
.frame(size: CGSize(width: w, height: f.height))
.asAnyView
}
private var background: some View {
BackgroundView()
BackgroundView(color: configuration.backgroundColor)
.cornerRadius(Theme.closedSize.height / 2)
.keyframes(size: Theme.backgroundSizeKeyframes(isOpened: environment.isOpened, items: items), progress: environment.isOpened ? 1 : 0)
.onAnimationCompleted(condition: environment.isOpened) {
self.environment.isMenuItemsVisible = true
.keyframes(size: Theme.backgroundSizeKeyframes(isOpened: viewModel.isOpened, items: viewModel.items), progress: viewModel.isOpened ? 1 : 0)
.animation(Animation.interpolatingSpring(mass: 1, stiffness: 170, damping: 15, initialVelocity: 1).delay(viewModel.isOpened ? 0.12 : 0))
.onAnimationCompleted(condition: viewModel.isOpened) {
self.viewModel.isBlobMenuItemsVisible = true
}
.animation(Animation.interpolatingSpring(mass: 1, stiffness: 170, damping: 15, initialVelocity: 1).delay(environment.isOpened ? 0.12 : 0))
.onTapGesture {
withAnimation {
self.environment.isOpened = true
self.viewModel.isOpened = true
}
}
.allowsHitTesting(!self.environment.isOpened)
.allowsHitTesting(!self.viewModel.isOpened)
}
private var itemsView: some View {
Group {
if Theme.isScrollable(items: items) {
if Theme.isScrollable(items: viewModel.items) {
ScrollView(.horizontal, showsIndicators: false) { itemsContent }
} else {
itemsContent
@@ -101,16 +95,18 @@ public struct BlobMenuView: View {
}
.clipShape(Capsule(style: .circular))
.animation(Animation.easeInOut(duration: 0.35).delay(0.15))
.allowsHitTesting(self.environment.isMenuItemsVisible)
.allowsHitTesting(self.viewModel.isBlobMenuItemsVisible)
}
private var itemsContent: some View {
HStack(spacing: 20) {
ForEach(items.enumeratedArray(), id: \.element) { index, item in
MenuItemView(item: item, isSelected: self.selectedIndex == index, isOpened: self.$environment.isMenuItemsVisible).onTapGesture {
self.selectedIndex = index
BlobMenuEnvironmentKey.defaultValue.selectedIndex = index
ForEach(viewModel.items.enumeratedArray(), id: \.element) { index, item in
BlobMenuItemView(item: item,
isSelected: self.viewModel.selectedIndex == index,
isOpened: self.viewModel.isBlobMenuItemsVisible,
selectionColor: self.configuration.selectionColor)
.onTapGesture {
self.viewModel.selectedIndex = index
}
}
}
@@ -123,18 +119,18 @@ extension BlobMenuView {
enum Theme {
static let height: CGFloat = 60
static let padding: CGFloat = 10
static let menuItemsSpace: CGFloat = 20
static let BlobMenuItemsSpace: CGFloat = 20
static let stickyEffectAvulsionDistance: CGFloat = 120
static let closedSize = CGSize(width: 60, height: height)
static let collapsedSize = CGSize(width: 90, height: 70)
static let maxOpenSize = CGSize(width: UIScreen.main.bounds.width - 60, height: height)
private static func itemsSize(items: [MenuItem]) -> CGSize {
let w = CGFloat(items.count) * MenuItemView.Theme.size.width + CGFloat(items.count - 1) * menuItemsSpace
private static func itemsSize(items: [BlobMenuItem]) -> CGSize {
let w = CGFloat(items.count) * BlobMenuItemView.Theme.size.width + CGFloat(items.count - 1) * BlobMenuItemsSpace
return CGSize(width: w, height: height)
}
static func openedSize(items: [MenuItem]) -> CGSize {
static func openedSize(items: [BlobMenuItem]) -> CGSize {
if isScrollable(items: items) {
return maxOpenSize
} else {
@@ -142,11 +138,11 @@ extension BlobMenuView {
}
}
static func isScrollable(items: [MenuItem]) -> Bool {
static func isScrollable(items: [BlobMenuItem]) -> Bool {
return itemsSize(items: items).width > maxOpenSize.width
}
static func backgroundSizeKeyframes(isOpened: Bool, items: [MenuItem]) -> [Keyframe<CGSize>] {
static func backgroundSizeKeyframes(isOpened: Bool, items: [BlobMenuItem]) -> [Keyframe<CGSize>] {
if isOpened {
//will use during opening animation
return [
+4 -3
View File
@@ -9,8 +9,9 @@
import SwiftUI
public struct HamburgerView: View {
public let isOpened: Bool
public let color: Color
private var rotationAngle: Angle {
return Angle(degrees: isOpened ? -90 : 0)
@@ -31,7 +32,7 @@ public struct HamburgerView: View {
private var line: some View {
Rectangle()
.frame(width: Theme.lineWidth, height: Theme.lineThickness)
.foregroundColor(Color.hamburgerColor)
.foregroundColor(color)
.cornerRadius(Theme.lineCornerRadius)
}
}
@@ -40,7 +41,7 @@ public struct HamburgerView: View {
//MARK: - Preview
struct HamburgerView_Previews: PreviewProvider {
static var previews: some View {
return HamburgerView(isOpened: false)
return HamburgerView(isOpened: false, color: .hamburgerColor)
}
}
+10 -9
View File
@@ -1,5 +1,5 @@
//
// MenuItemView.swift
// BlobMenuItemView.swift
// BlobMenu
//
// Created by Igor K. on 29.04.2020.
@@ -9,11 +9,12 @@
import Foundation
import SwiftUI
public struct MenuItemView: View {
public struct BlobMenuItemView: View {
public let item: MenuItem
public let isSelected: Bool
@Binding public var isOpened: Bool
let item: BlobMenuItem
let isSelected: Bool
let isOpened: Bool
let selectionColor: Color
public var body: some View {
ZStack {
@@ -30,12 +31,12 @@ public struct MenuItemView: View {
let image = isSelected ? item.selectedIcon : item.unselectedIcon
return image
.offset(item.offset)
.transition(AnyTransition.blobMenuItem)
.transition(AnyTransition.blobBlobMenuItem)
}
private var selectionView: some View {
Circle()
.foregroundColor(Color.selection)
.foregroundColor(selectionColor)
.frame(size: Theme.contentSize)
.opacity(isSelected ? 1 : 0)
.animation(nil)
@@ -46,7 +47,7 @@ public struct MenuItemView: View {
private var ringView: some View {
let show = isOpened && isSelected
return Circle()
.stroke(Color.selection)
.stroke(selectionColor)
.frame(size: Theme.contentSize)
.opacity(show ? 0 : 1)
.animation(show ? Animation.easeInOut.delay(0.2) : nil)
@@ -57,7 +58,7 @@ public struct MenuItemView: View {
//MARK: - Theme
extension MenuItemView {
extension BlobMenuItemView {
enum Theme {
static let size = CGSize(uniform: 60)
static let contentInsets: CGFloat = 15
+3 -3
View File
@@ -31,9 +31,9 @@ struct StickyEffectShape: Shape {
func path(in rect: CGRect) -> Path {
let path = pathGenerator.generatePath(baseRect: baseRect,
figureRect: figureRect,
figureCornerRadius: figureCornerRadius,
avulsionDistance: avulsionDistance)
figureRect: figureRect,
figureCornerRadius: figureCornerRadius,
avulsionDistance: avulsionDistance)
return Path(path)
}
+10 -1
View File
@@ -346,7 +346,16 @@ public final class StickyPathGenerator {
let curvePath = UIBezierPath()
curvePath.move(to: point0)
curvePath.addCurve(to: point1, controlPoint1: cpUp1, controlPoint2: cpUp2)
curvePath.addLine(to: point2)
if crossed {
curvePath.addLine(to: point2)
} else {
let center = input.figureRect.center
let dc = CGPoint(x: center.x - 0.5, y: center.y) // to fix small gap between menu and arc
let start = atan((point1.y - center.y) / (point1.x - center.x))
let end = atan((point2.y - center.y) / (point2.x - center.x))
curvePath.addArc(withCenter: dc, radius: input.figureCornerRadius, startAngle: start, endAngle: end, clockwise: true)
curvePath.addLine(to: point2)
}
curvePath.addCurve(to: point3, controlPoint1: cpDown2, controlPoint2: cpDown1)
return curvePath