├── preview.gif
├── SuspensionEntrance
├── Assets.xcassets
│ ├── Contents.json
│ ├── favicon.imageset
│ │ ├── favicon.png
│ │ └── Contents.json
│ ├── blur_background.imageset
│ │ ├── 矩形@2x.png
│ │ ├── 矩形@3x.png
│ │ └── Contents.json
│ ├── web_entrance_close.imageset
│ │ ├── 关闭@2x.png
│ │ ├── 关闭@3x.png
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── AppDelegate.h
├── NormalViewController.h
├── BaseNavigationController.h
├── main.m
├── EntranceViewController.h
├── PresentNavigationController.h
├── BaseNavigationController.m
├── OC
│ ├── SEFloatingArea.h
│ ├── SEFloatingBall.h
│ ├── SETransitionAnimator.h
│ ├── SEFloatingList.h
│ ├── SuspensionEntrance.h
│ ├── SEFloatingArea.m
│ ├── SEFloatingList.m
│ ├── SETransitionAnimator.m
│ ├── SEFloatingBall.m
│ └── SuspensionEntrance.m
├── AppDelegate.m
├── Info.plist
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── EntranceViewController.m
├── PresentNavigationController.m
└── NormalViewController.m
├── SuspensionEntrance.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── XMFraker.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── XMFraker.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── xcshareddata
│ └── xcschemes
│ │ └── SuspensionEntrance.xcscheme
└── project.pbxproj
├── SuspensionEntrance.podspec
├── LICENSE
└── README.md
/preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/HEAD/preview.gif
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/favicon.imageset/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/HEAD/SuspensionEntrance/Assets.xcassets/favicon.imageset/favicon.png
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/blur_background.imageset/矩形@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/HEAD/SuspensionEntrance/Assets.xcassets/blur_background.imageset/矩形@2x.png
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/blur_background.imageset/矩形@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/HEAD/SuspensionEntrance/Assets.xcassets/blur_background.imageset/矩形@3x.png
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/关闭@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/HEAD/SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/关闭@2x.png
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/关闭@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/HEAD/SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/关闭@3x.png
--------------------------------------------------------------------------------
/SuspensionEntrance.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SuspensionEntrance.xcodeproj/project.xcworkspace/xcuserdata/XMFraker.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/HEAD/SuspensionEntrance.xcodeproj/project.xcworkspace/xcuserdata/XMFraker.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/SuspensionEntrance.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SuspensionEntrance/AppDelegate.h:
--------------------------------------------------------------------------------
1 | // AppDelegate.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class AppDelegate
7 |
8 | #import
9 |
10 | @interface AppDelegate : UIResponder
11 |
12 | @property (strong, nonatomic) UIWindow *window;
13 |
14 |
15 | @end
16 |
17 |
--------------------------------------------------------------------------------
/SuspensionEntrance/NormalViewController.h:
--------------------------------------------------------------------------------
1 | // NormalViewController.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class NormalViewController
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface NormalViewController : UIViewController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/favicon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "favicon.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/SuspensionEntrance/BaseNavigationController.h:
--------------------------------------------------------------------------------
1 | // BaseNavigationController.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class BaseNavigationController
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @interface BaseNavigationController : UINavigationController
13 |
14 | @end
15 |
16 | NS_ASSUME_NONNULL_END
17 |
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/blur_background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "矩形@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "矩形@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "关闭@2x.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "filename" : "关闭@3x.png",
15 | "scale" : "3x"
16 | }
17 | ],
18 | "info" : {
19 | "version" : 1,
20 | "author" : "xcode"
21 | }
22 | }
--------------------------------------------------------------------------------
/SuspensionEntrance/main.m:
--------------------------------------------------------------------------------
1 | // main.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class main
7 | // @version <#class version#>
8 | // @abstract <#class description#>
9 |
10 | #import
11 | #import "AppDelegate.h"
12 |
13 | int main(int argc, char * argv[]) {
14 | @autoreleasepool {
15 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
16 | }
17 | }
18 | //
--------------------------------------------------------------------------------
/SuspensionEntrance.xcodeproj/xcuserdata/XMFraker.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | SuspensionEntrance.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | 9747418722FBB7F600281B55
16 |
17 | primary
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SuspensionEntrance/EntranceViewController.h:
--------------------------------------------------------------------------------
1 | // EntranceViewController.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class EntranceViewController
7 |
8 | #import
9 |
10 | #import "SuspensionEntrance.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 |
14 | @interface EntranceViewController : UIViewController
15 | @property (copy , nonatomic) NSString *entranceTitle;
16 | @property (copy , nonatomic, nullable) NSURL *entranceIconUrl;
17 | @property (copy , nonatomic, nullable) NSDictionary *entranceUserInfo;
18 | @end
19 |
20 | NS_ASSUME_NONNULL_END
21 |
--------------------------------------------------------------------------------
/SuspensionEntrance/PresentNavigationController.h:
--------------------------------------------------------------------------------
1 | //
2 | // PresentNavigationController.h
3 | // SuspensionEntrance
4 | //
5 | // Created by XMFraker on 2020/5/18.
6 | // Copyright © 2020 Fraker.XM. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "SuspensionEntrance.h"
11 |
12 | NS_ASSUME_NONNULL_BEGIN
13 | // !!! : take care of the memory usage.
14 | @interface PresentNavigationController : UINavigationController
15 | @property (copy , nonatomic) NSString *entranceTitle;
16 | @property (copy , nonatomic, nullable) NSURL *entranceIconUrl;
17 | @property (copy , nonatomic, nullable) NSDictionary *entranceUserInfo;
18 | @end
19 |
20 | NS_ASSUME_NONNULL_END
21 |
--------------------------------------------------------------------------------
/SuspensionEntrance/BaseNavigationController.m:
--------------------------------------------------------------------------------
1 | // BaseNavigationController.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class BaseNavigationController
7 |
8 | #import "BaseNavigationController.h"
9 |
10 | #import "SuspensionEntrance.h"
11 |
12 | @interface BaseNavigationController ()
13 |
14 | @end
15 |
16 | @implementation BaseNavigationController
17 |
18 | - (void)viewDidLoad {
19 | [super viewDidLoad];
20 | self.delegate = [SuspensionEntrance shared];
21 | self.interactivePopGestureRecognizer.enabled = NO;
22 | }
23 |
24 | @end
25 |
--------------------------------------------------------------------------------
/SuspensionEntrance.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint WMReactNative.podspec' to ensure this is a
3 | # valid spec before submitting.
4 | #
5 | # Any lines starting with a # are optional, but their use is encouraged
6 | # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html
7 | #
8 |
9 | Pod::Spec.new do |s|
10 | s.name = 'SuspensionEntrance'
11 | s.version = '0.3.3'
12 | s.summary = '仿微信实现悬浮窗入口功能'
13 | s.homepage = 'https://github.com/ws00801526/SuspensionEntrance'
14 | s.license = { :type => 'MIT', :file => 'LICENSE' }
15 | s.author = { 'Fraker.XM' => '3057600441@qq.com' }
16 | s.source = { :git => 'https://github.com/ws00801526/SuspensionEntrance.git', :tag => s.version.to_s }
17 | s.swift_versions = '5.0'
18 | s.ios.deployment_target = '9.0'
19 | s.source_files = 'SuspensionEntrance/OC/**/*'
20 | end
21 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SEFloatingArea.h:
--------------------------------------------------------------------------------
1 | // SEFloatingArea.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/9
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SEFloatingArea
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | FOUNDATION_EXPORT const CGFloat kSEFloatingAreaWidth;
13 | FOUNDATION_EXPORT BOOL kSEFloatAreaContainsPoint(CGPoint point);
14 |
15 | typedef NS_ENUM(NSUInteger, SEFloatingAreaState) {
16 | SEFloatingAreaStateDefault,
17 | SEFloatingAreaStateHighlight,
18 | SEFloatingAreaStateDisabled,
19 | };
20 |
21 | @interface SEFloatingArea : UIVisualEffectView
22 |
23 | @property (assign, nonatomic, getter=isEnabled) BOOL enabled;
24 | @property (assign, nonatomic, getter=isHighlighted) BOOL highlighted;
25 |
26 | - (NSString *)titleForState:(SEFloatingAreaState)state;
27 | - (void)setTitle:(NSString *)title forState:(SEFloatingAreaState)state;
28 |
29 | @end
30 |
31 | NS_ASSUME_NONNULL_END
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 XMFraker
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SEFloatingBall.h:
--------------------------------------------------------------------------------
1 | // SEFloatingBall.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SEFloatingBall
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @class SEFloatingBall;
13 | @protocol SEFloatingBallDelegate
14 |
15 | @optional
16 | - (void)floatingBallDidClicked:(SEFloatingBall *)floatingBall;
17 |
18 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidBegan:(UILongPressGestureRecognizer *)gesture;
19 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidChanged:(UILongPressGestureRecognizer *)gesture;
20 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidEnded:(UILongPressGestureRecognizer *)gesture;
21 |
22 | @end
23 |
24 | @protocol SEItem;
25 | @interface SEFloatingBall : UIView
26 |
27 | @property (assign, nonatomic, readonly) CGRect floatingRect;
28 | @property (weak, nonatomic, nullable) id delegate;
29 |
30 | - (void)reloadIconViews:(NSArray> *)items;
31 | @end
32 |
33 | NS_ASSUME_NONNULL_END
34 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SETransitionAnimator.h:
--------------------------------------------------------------------------------
1 | // SETransitionAnimator.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/9
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SETransitionAnimator
7 |
8 | #import
9 |
10 | typedef NS_ENUM(NSUInteger, SETransitionAnimatorStyle) {
11 | SETransitionAnimatorStyleUnknown = 0,
12 | SETransitionAnimatorStyleRoundPush,
13 | SETransitionAnimatorStyleRoundPop,
14 | SETransitionAnimatorStyleContinuousPop
15 | };
16 |
17 | NS_ASSUME_NONNULL_BEGIN
18 |
19 | @interface SETransitionAnimator : NSObject
20 |
21 | @property (assign, nonatomic, readonly) NSTimeInterval animationDuration;
22 | @property (assign, nonatomic, readonly) SETransitionAnimatorStyle style;
23 |
24 | + (instancetype)roundPushAnimatorWithRect:(CGRect)rect;
25 | + (instancetype)roundPopAnimatorWithRect:(CGRect)rect;
26 | + (instancetype)continuousPopAnimatorWithRect:(CGRect)rect;
27 |
28 | - (void)finishContinousAnimation;
29 | - (void)cancelContinousAnimation;
30 | - (void)updateContinousAnimationPercent:(CGFloat)precent;
31 | - (void)finishContinousAnimationWithFastAnimating:(BOOL)fast;
32 |
33 | @end
34 |
35 | NS_ASSUME_NONNULL_END
36 |
--------------------------------------------------------------------------------
/SuspensionEntrance/AppDelegate.m:
--------------------------------------------------------------------------------
1 | // AppDelegate.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class AppDelegate
7 |
8 | #import "AppDelegate.h"
9 | #import "SuspensionEntrance.h"
10 | #import "SEFloatingBall.h"
11 | #import "EntranceViewController.h"
12 |
13 | @interface AppDelegate ()
14 |
15 | @end
16 |
17 | @implementation AppDelegate
18 |
19 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
20 | // Override point for customization after application launch.
21 |
22 | // dispatch_after(5.f, dispatch_get_main_queue(), ^{
23 | //
24 | // UIVisualEffect *effect = [UIVibrancyEffect effectForBlurEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]];
25 | // SEFloatingBall *ballView = [[SEFloatingBall alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleLight]];
26 | // [self.window insertSubview:ballView atIndex:100000];
27 | // });
28 |
29 | [SuspensionEntrance shared].available = YES;
30 | [SuspensionEntrance shared].closePlaceholder = [UIImage imageNamed:@"web_entrance_close"];
31 | return YES;
32 | }
33 |
34 | @end
35 |
--------------------------------------------------------------------------------
/SuspensionEntrance/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UILaunchStoryboardName
24 | LaunchScreen
25 | UIMainStoryboardFile
26 | Main
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 | UIInterfaceOrientationLandscapeLeft
35 | UIInterfaceOrientationLandscapeRight
36 |
37 | UISupportedInterfaceOrientations~ipad
38 |
39 | UIInterfaceOrientationPortrait
40 | UIInterfaceOrientationPortraitUpsideDown
41 | UIInterfaceOrientationLandscapeLeft
42 | UIInterfaceOrientationLandscapeRight
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/SuspensionEntrance/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SEFloatingList.h:
--------------------------------------------------------------------------------
1 | // SEFloatingList.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/13
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SEFloatingList
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 | @protocol SEItem;
13 | @class SEFloatingList;
14 | @protocol SEFloatingListDelegate
15 |
16 | @required
17 | - (NSUInteger)numberOfItemsInFloatingList:(SEFloatingList *)list;
18 | - (id)floatingList:(SEFloatingList *)list itemAtIndex:(NSUInteger)index;
19 | - (void)floatingListWillShow:(SEFloatingList *)list;
20 | - (void)floatingListWillHide:(SEFloatingList *)list;
21 |
22 | @optional
23 | - (BOOL)floatingList:(SEFloatingList *)list isItemVisible:(id)item;
24 | - (void)floatingList:(SEFloatingList *)list didSelectItem:(id)item;
25 | - (BOOL)floatingList:(SEFloatingList *)list willDeleteItem:(id)item;
26 |
27 | @end
28 |
29 |
30 | @interface SEFloatingListItem : UIView
31 |
32 | @property (assign, nonatomic, getter=isSelected) BOOL selected;
33 | @property (assign, nonatomic, getter=isHighlighted) BOOL highlighted;
34 | @property (weak, nonatomic, readonly) id item;
35 |
36 | @end
37 |
38 | @interface SEFloatingList : UIView
39 |
40 | @property (assign, nonatomic, getter=isEditable) BOOL editable;
41 | @property (weak , nonatomic, nullable) id delegate;
42 |
43 | @property (assign, nonatomic, readonly) CGRect floatingRect;
44 | @property (copy , nonatomic, readonly) NSArray *listItems;
45 |
46 | - (void)reloadData;
47 | - (void)dismissWithAnimated:(BOOL)animated;
48 | - (void)showAtRect:(CGRect)rect animated:(BOOL)animated;
49 |
50 | @end
51 |
52 | NS_ASSUME_NONNULL_END
53 |
--------------------------------------------------------------------------------
/SuspensionEntrance/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/SuspensionEntrance/EntranceViewController.m:
--------------------------------------------------------------------------------
1 | // EntranceViewController.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class EntranceViewController
7 |
8 | #import "EntranceViewController.h"
9 | #import "NormalViewController.h"
10 |
11 | #import
12 |
13 | @interface EntranceViewController ()
14 | @property (strong, nonatomic) WKWebView *webView;
15 | @end
16 |
17 | @implementation EntranceViewController
18 | @dynamic view;
19 |
20 | + (instancetype)entranceWithItem:(id)item {
21 | EntranceViewController *controller = [[EntranceViewController alloc] initWithNibName:nil bundle:nil];
22 | controller.entranceTitle = item.entranceTitle;
23 | controller.entranceIconUrl = item.entranceIconUrl;
24 | controller.entranceUserInfo = item.entranceUserInfo;
25 | return controller;
26 | }
27 |
28 | - (void)viewDidLoad {
29 | [super viewDidLoad];
30 |
31 | self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
32 | [self.view addSubview:self.webView];
33 | NSURL *url = [NSURL URLWithString:[self.entranceUserInfo objectForKey:@"url"]];
34 | if (url) [self.webView loadRequest:[NSURLRequest requestWithURL:url]];
35 |
36 | UIBarButtonItem *next = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemPlay target:self action:@selector(pushNormalController)];
37 | self.navigationItem.rightBarButtonItem = next;
38 |
39 | if (self.presentingViewController) {
40 | UIBarButtonItem *back = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemClose target:self action:@selector(viewBack)];
41 | self.navigationItem.leftBarButtonItem = back;
42 | }
43 | }
44 |
45 | - (void)viewBack {
46 | if (self.presentingViewController) [self.presentingViewController dismissViewControllerAnimated:YES completion:NULL];
47 | else [self.navigationController popViewControllerAnimated:YES];
48 | }
49 |
50 | - (void)pushNormalController {
51 |
52 | UIStoryboard *storyboard = self.storyboard;
53 | if (!storyboard) storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
54 | NormalViewController *controller = (NormalViewController *)[storyboard instantiateViewControllerWithIdentifier:@"NormalViewController"];
55 | [self.navigationController pushViewController:controller animated:YES];
56 | }
57 |
58 | - (void)dealloc {
59 |
60 | #if DEBUG
61 | NSLog(@"%@ is %@ing", self, NSStringFromSelector(_cmd));
62 | NSLog(@"self.gestures :%@", self.view.gestureRecognizers);
63 | #endif
64 | }
65 |
66 |
67 | @end
68 |
--------------------------------------------------------------------------------
/SuspensionEntrance.xcodeproj/xcshareddata/xcschemes/SuspensionEntrance.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
44 |
46 |
52 |
53 |
54 |
55 |
61 |
63 |
69 |
70 |
71 |
72 |
74 |
75 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/SuspensionEntrance/PresentNavigationController.m:
--------------------------------------------------------------------------------
1 | //
2 | // PresentNavigationController.m
3 | // SuspensionEntrance
4 | //
5 | // Created by XMFraker on 2020/5/18.
6 | // Copyright © 2020 Fraker.XM. All rights reserved.
7 | //
8 |
9 | #import "PresentNavigationController.h"
10 | #import "NormalViewController.h"
11 | #import "EntranceViewController.h"
12 |
13 | @interface PresentNavigationController ()
14 |
15 | @end
16 |
17 | @implementation PresentNavigationController
18 | @dynamic entranceTitle;
19 | @dynamic entranceIconUrl;
20 | @dynamic entranceUserInfo;
21 |
22 | + (instancetype)entranceWithItem:(id)item {
23 |
24 | EntranceViewController *normalController = [[UIStoryboard storyboardWithName:@"Main" bundle:nil] instantiateViewControllerWithIdentifier:@"EntranceViewController"];
25 | PresentNavigationController *controller = [[PresentNavigationController alloc] initWithRootViewController:normalController];
26 | controller.entranceTitle = item.entranceTitle;
27 | controller.entranceIconUrl = item.entranceIconUrl;
28 | controller.entranceUserInfo = item.entranceUserInfo;
29 | return controller;
30 | }
31 |
32 | - (instancetype)initWithRootViewController:(UIViewController *)rootViewController {
33 | self = [super initWithRootViewController:rootViewController];
34 | if (self) {
35 | // !!!: use full screen on iOS13+. otherwise the default present style will cause the gesture recognized failed.
36 | self.modalPresentationStyle = UIModalPresentationFullScreen;
37 | self.transitioningDelegate = [SuspensionEntrance shared];
38 | }
39 | return self;
40 | }
41 |
42 | - (void)viewDidLoad {
43 | [super viewDidLoad];
44 | }
45 |
46 | #pragma mark - Setter
47 |
48 | - (void)setEntranceTitle:(NSString *)entranceTitle {
49 | // maybe using visible-title
50 | // UIViewController *controller = self.visibleViewController ? : self.viewControllers.firstObject;
51 | UIViewController *controller = self.viewControllers.firstObject;
52 | if ([controller conformsToProtocol:@protocol(SEItem)]) ((id)controller).entranceTitle = entranceTitle;
53 | }
54 |
55 | - (void)setEntranceIconUrl:(NSURL *)entranceIconUrl {
56 | UIViewController *controller = self.viewControllers.firstObject;
57 | if ([controller conformsToProtocol:@protocol(SEItem)]) ((id)controller).entranceIconUrl = entranceIconUrl;
58 | }
59 |
60 | - (void)setEntranceUserInfo:(NSDictionary *)entranceUserInfo {
61 | UIViewController *controller = self.viewControllers.firstObject;
62 | if ([controller conformsToProtocol:@protocol(SEItem)]) ((id)controller).entranceUserInfo = entranceUserInfo;
63 | }
64 |
65 | #pragma mark - Getter
66 |
67 | - (NSURL *)entranceIconUrl {
68 | UIViewController *controller = self.viewControllers.firstObject;
69 | if ([controller conformsToProtocol:@protocol(SEItem)]) return ((id)controller).entranceIconUrl;
70 | return nil;
71 | }
72 |
73 | - (NSString *)entranceTitle {
74 | UIViewController *controller = self.viewControllers.firstObject;
75 | if ([controller conformsToProtocol:@protocol(SEItem)]) return ((id)controller).entranceTitle;
76 | return nil;
77 | }
78 |
79 | - (NSDictionary *)entranceUserInfo {
80 | UIViewController *controller = self.viewControllers.firstObject;
81 | if ([controller conformsToProtocol:@protocol(SEItem)]) return ((id)controller).entranceUserInfo;
82 | return nil;
83 | }
84 |
85 | @end
86 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SuspensionEntrance.h:
--------------------------------------------------------------------------------
1 | // SuspensionEntrance.h
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SuspensionEntrance
7 |
8 | #import
9 |
10 | NS_ASSUME_NONNULL_BEGIN
11 |
12 |
13 | @protocol SEItem
14 |
15 | @required
16 | /// the title of entrance
17 | @property (copy , nonatomic) NSString *entranceTitle;
18 |
19 | @optional
20 | /// the icon url of entrance
21 | @property (copy , nonatomic, nullable) NSURL *entranceIconUrl;
22 | /// the userInfo of entrance
23 | @property (copy , nonatomic, nullable) NSDictionary *entranceUserInfo;
24 |
25 | /**
26 | To archive & unarchive items
27 |
28 | @warning item won't be archived if the real class dont implement this method
29 | @param item the item to be archived
30 | @return the real Class instance
31 | */
32 | + (instancetype)entranceWithItem:(id)item;
33 |
34 | @end
35 |
36 | typedef void(^SEItemIconHandler)(UIImageView *iconView, id item);
37 |
38 | @interface SuspensionEntrance : NSObject
39 |
40 | /// max items can be stored, should not over 5. Default is 5.
41 | @property (assign, nonatomic) NSUInteger maxCount;
42 | /// The path to be archived of items. Default is ~/Documents/entrance.items.
43 | @property (copy , nonatomic) NSString *archivedPath;
44 | /// then handler to get correct icon of item.
45 | @property (copy , nonatomic) SEItemIconHandler iconHandler;
46 | /// The image of close icon
47 | @property (strong, nonatomic, nullable) UIImage *closePlaceholder;
48 | /// Should vibrate when the floating area is highlighted. Default is YES.
49 | @property (assign, nonatomic, getter=isVibratable) BOOL vibratable;
50 | /// Is entrance available. Default is YES.
51 | @property (assign, nonatomic, getter=isAvailable) BOOL available;
52 | /// The window where UI to be placed
53 | @property (weak, nonatomic, nullable) UIWindow *window;
54 | /// The entrance items
55 | @property (strong, nonatomic, readonly) NSArray *> *items;
56 | /// The entrance class should not be appeared.
57 | @property (strong, nonatomic, readonly) NSMutableSet *disabledClasses;
58 | /// The entrance class should be ignored.
59 | @property (strong, nonatomic, readonly) NSMutableSet *ignoredClasses;
60 |
61 | + (instancetype)shared;
62 |
63 | /**
64 | Check item is the entrance item
65 |
66 | @param item the item to be checked
67 | @return YES or NO
68 | */
69 | - (BOOL)isEntranceItem:(__kindof UIViewController *)item;
70 |
71 |
72 | /**
73 | Set item to be an entrance item
74 |
75 | @discussion Will auto pop if set succeed & navigationController.viewControllers.lastObject == item
76 | @param item the item
77 | */
78 | - (void)addEntranceItem:(__kindof UIViewController *)item;
79 |
80 |
81 | /**
82 | Cancel the entrance item
83 |
84 | @param item the item
85 | */
86 | - (void)cancelEntranceItem:(__kindof UIViewController *)item;
87 |
88 | /**
89 | Remove all entrance items
90 | */
91 | - (void)clearEntranceItems;
92 |
93 | @end
94 |
95 |
96 | @interface SuspensionEntrance (NavigationControllerDelegate)
97 | @end
98 |
99 | @interface SuspensionEntrance (TransitioningDelegate)
100 |
101 | @end
102 |
103 | NS_ASSUME_NONNULL_END
104 |
--------------------------------------------------------------------------------
/SuspensionEntrance/NormalViewController.m:
--------------------------------------------------------------------------------
1 | // NormalViewController.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class NormalViewController
7 |
8 | #import "NormalViewController.h"
9 | #import "EntranceViewController.h"
10 | #import "BaseNavigationController.h"
11 | #import "PresentNavigationController.h"
12 |
13 | @interface SEImageView : UIImageView
14 | @end
15 | @implementation SEImageView
16 | + (Class)layerClass { return [CAShapeLayer class]; }
17 | @end
18 |
19 | @interface NormalViewController ()
20 | @property (copy , nonatomic) NSArray *items;
21 | @end
22 |
23 | @implementation NormalViewController
24 |
25 | - (void)viewDidLoad {
26 | [super viewDidLoad];
27 | self.view.backgroundColor = [UIColor redColor];
28 | }
29 |
30 | - (void)dealloc {
31 |
32 | #if DEBUG
33 | NSLog(@"%@ is %@ing", self, NSStringFromSelector(_cmd));
34 | NSLog(@"self.gestures :%@", self.view.gestureRecognizers);
35 | #endif
36 | }
37 |
38 |
39 | #pragma mark - UITableViewDelegate & UITableViewSource
40 |
41 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
42 | return 2;
43 | }
44 |
45 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
46 |
47 | return self.items.count;
48 | }
49 |
50 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
51 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
52 | if (!cell) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
53 |
54 | UILabel *titleLabel = [cell.contentView viewWithTag:99];
55 | UIImageView *imageView = [cell.contentView viewWithTag:100];
56 |
57 | NSDictionary *item = [self.items objectAtIndex:indexPath.row];
58 | dispatch_async(dispatch_get_global_queue(0, 0), ^{
59 | NSURL *iconUrl = [item objectForKey:@"iconUrl"];
60 | NSData *data = [NSData dataWithContentsOfURL:iconUrl];
61 | UIImage *image = [UIImage imageWithData:data];
62 | dispatch_async(dispatch_get_main_queue(), ^{
63 | imageView.image = image;
64 | });
65 | });
66 |
67 | titleLabel.text = [item objectForKey:@"title"];
68 | return cell;
69 | }
70 |
71 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
72 | return 55.f;
73 | }
74 |
75 | - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
76 | return 30.f;
77 | }
78 |
79 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
80 | return section == 0 ? @"Push" : @"Present";
81 | }
82 |
83 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
84 | [tableView deselectRowAtIndexPath:indexPath animated:YES];
85 |
86 | // CGFloat const radius = 20.f;
87 | //
88 | // UIBezierPath *startPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.view.frame, 100.f, 100.f) cornerRadius:radius];
89 | // UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(200.f, 200.f, 50.f, 50.f) cornerRadius:radius];
90 | // CAShapeLayer *maskLayer = [CAShapeLayer layer];
91 | // maskLayer.fillColor = [UIColor blackColor].CGColor;
92 | // maskLayer.path = endPath.CGPath;
93 | // maskLayer.frame = CGRectInset(self.view.frame, 100.f, 100.f);
94 | // self.view.layer.mask = maskLayer;
95 | // CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
96 | // maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
97 | // maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath);
98 | // maskLayerAnimation.duration = 2.f;
99 | // maskLayerAnimation.delegate = (id)self;
100 | // [maskLayer addAnimation:maskLayerAnimation forKey:@"xw_path"];
101 | //
102 | // return;
103 | //
104 | NSDictionary *item = [self.items objectAtIndex:indexPath.row];
105 | EntranceViewController *controller = (EntranceViewController *)[self.storyboard instantiateViewControllerWithIdentifier:@"EntranceViewController"];
106 | controller.entranceTitle = [item objectForKey:@"title"];
107 | controller.entranceIconUrl = [item objectForKey:@"iconUrl"];
108 | controller.entranceUserInfo = [item objectForKey:@"userInfo"];
109 | if (indexPath.section == 0) {
110 | [self.navigationController pushViewController:controller animated:YES];
111 | } else {
112 | PresentNavigationController *nav = [[PresentNavigationController alloc] initWithRootViewController:controller];
113 | [self.navigationController presentViewController:nav animated:YES completion:NULL];
114 | }
115 | }
116 |
117 | - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
118 | self.view.layer.mask = nil;
119 | }
120 |
121 | #pragma mark - Getter
122 |
123 | - (NSArray *)items {
124 |
125 | return @[
126 | @{
127 | @"title" : @"Google",
128 | @"iconUrl" : [NSURL URLWithString:@"https://google.com/favicon.ico"],
129 | @"userInfo" : @{
130 | @"url" : @"https://www.google.com"
131 | }
132 | },
133 | @{
134 | @"title" : @"百度一下,你就知道",
135 | @"iconUrl" : [NSURL URLWithString:@"https://www.baidu.com/favicon.ico"],
136 | @"userInfo" : @{
137 | @"url" : @"https://www.baidu.com"
138 | }
139 | },
140 | @{
141 | @"title" : @"哔哩哔哩 (゜-゜)つロ 干杯~-bilibili",
142 | @"iconUrl" : [NSURL URLWithString:@"https://www.bilibili.com/favicon.ico"],
143 | @"userInfo" : @{
144 | @"url" : @"https://www.bilibili.com"
145 | }
146 | }
147 | ];
148 | }
149 |
150 |
151 | @end
152 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SEFloatingArea.m:
--------------------------------------------------------------------------------
1 | // SEFloatingArea.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/9
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SEFloatingArea
7 |
8 | #import "SEFloatingArea.h"
9 | #import "SuspensionEntrance.h"
10 |
11 | #import
12 | #import
13 |
14 | const CGFloat kSEFloatingAreaWidth = 180.f;
15 |
16 | BOOL kSEFloatAreaContainsPoint(CGPoint point) {
17 | CGRect bounds = UIScreen.mainScreen.bounds;
18 | CGPoint center = CGPointMake(CGRectGetWidth(bounds), CGRectGetHeight(bounds));
19 | double dx = fabs(point.x - center.x);
20 | double dy = fabs(point.y - center.y);
21 | double distance = hypot(dx, dy);
22 | return distance < kSEFloatingAreaWidth;
23 | }
24 |
25 | @interface SEFloatingArea ()
26 | @property (assign, nonatomic) CGFloat multiple;
27 | @property (copy , nonatomic) NSString *title;
28 | @property (assign, nonatomic) CGFloat outerRadius;
29 | @property (assign, nonatomic) CGFloat innerRadius;
30 |
31 | @property (strong, nonatomic) UILabel *titleLabel;
32 | @property (strong, nonatomic) CAShapeLayer *outerLayer;
33 | @property (strong, nonatomic) CAShapeLayer *innerLayer;
34 |
35 | @property (strong, nonatomic) NSMutableDictionary *stateTitles;
36 |
37 | @end
38 |
39 | @implementation SEFloatingArea
40 | @synthesize enabled = _enabled;
41 | @synthesize highlighted = _highlighted;
42 | #pragma mark - Life
43 |
44 | - (instancetype)initWithFrame:(CGRect)frame {
45 |
46 | CGRect rect = CGRectMake(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height, kSEFloatingAreaWidth, kSEFloatingAreaWidth);
47 | self = [super initWithFrame:rect];
48 | if (self) {
49 |
50 | _multiple = 0.875f;
51 | _outerRadius = 28.f;
52 | _innerRadius = 18.f;
53 |
54 | _enabled = YES;
55 | _highlighted = NO;
56 |
57 | _stateTitles = [@{
58 | @(SEFloatingAreaStateDefault) : @"浮窗",
59 | @(SEFloatingAreaStateDisabled) : @"浮窗已满"
60 | } mutableCopy];
61 |
62 | [self setupUI];
63 | [self setupMaskLayer];
64 | }
65 | return self;
66 | }
67 |
68 | #pragma mark - Override
69 |
70 | - (void)willMoveToSuperview:(UIView *)newSuperview {
71 | [super willMoveToSuperview:newSuperview];
72 | if (newSuperview) {
73 | // reset highlighted & frame
74 | self.highlighted = NO;
75 | self.frame = CGRectMake(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height, kSEFloatingAreaWidth, kSEFloatingAreaWidth);
76 | }
77 | }
78 |
79 | #pragma mark - Public
80 |
81 | - (void)setTitle:(NSString *)title forState:(SEFloatingAreaState)state {
82 | if (title.length <= 0) return;
83 | [self.stateTitles setObject:title forKey:@(state)];
84 | }
85 |
86 | - (NSString *)titleForState:(SEFloatingAreaState)state {
87 | NSString *title = [self.stateTitles objectForKey:@(state)];
88 | if (title.length <= 0) title = [self.stateTitles objectForKey:@(SEFloatingAreaStateDefault)];
89 | return title;
90 | }
91 |
92 | #pragma mark - Private
93 |
94 | - (void)setupUI {
95 |
96 | self.effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
97 | self.backgroundColor = [UIColor blackColor];
98 |
99 | UIOffset offset = UIOffsetMake(self.bounds.size.width * 0.1f + self.outerRadius,
100 | self.bounds.size.height * 0.1f + self.outerRadius * 2.f);
101 | CGPoint center = CGPointMake(self.bounds.size.width - offset.horizontal, self.bounds.size.height - offset.vertical);
102 |
103 | self.outerLayer = [CAShapeLayer layer];
104 | self.outerLayer.borderColor = [UIColor whiteColor].CGColor;
105 | self.outerLayer.borderWidth = 3.f;
106 | self.outerLayer.cornerRadius = self.outerRadius;
107 | self.outerLayer.masksToBounds = YES;
108 | self.outerLayer.fillColor = [UIColor clearColor].CGColor;
109 | self.outerLayer.frame = CGRectMake(0.f, 0.f, self.outerRadius * 2.f, self.outerRadius * 2.f);
110 | self.outerLayer.position = center;
111 | [self.contentView.layer addSublayer:self.outerLayer];
112 |
113 | CGFloat const multiple = self.isHighlighted ? 1.f : self.multiple;
114 | self.outerLayer.affineTransform = CGAffineTransformMakeScale(multiple, multiple);
115 |
116 | self.innerLayer = [CAShapeLayer layer];
117 | self.innerLayer.borderColor = [UIColor whiteColor].CGColor;
118 | self.innerLayer.borderWidth = 3.f;
119 | self.innerLayer.frame = CGRectMake(0.f, 0.f, self.innerRadius * 2.f, self.innerRadius * 2.f);
120 | self.innerLayer.position = center;
121 | self.innerLayer.cornerRadius = self.innerRadius;
122 | self.innerLayer.masksToBounds = YES;
123 | self.innerLayer.fillColor = [UIColor clearColor].CGColor;
124 | [self.contentView.layer addSublayer:self.innerLayer];
125 |
126 | UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.outerRadius * 2.f, 15.f)];
127 | textLabel.text = self.title;
128 | textLabel.textColor = [UIColor whiteColor];
129 | textLabel.textAlignment = NSTextAlignmentCenter;
130 | textLabel.numberOfLines = 1;
131 | textLabel.font = [UIFont systemFontOfSize:12.f];
132 | textLabel.center = CGPointMake(center.x, center.y + self.outerRadius + 15.f);
133 | [self.contentView addSubview:self.titleLabel = textLabel];
134 | }
135 |
136 | - (void)setupMaskLayer {
137 | CGFloat const multiple = self.isHighlighted ? 1.f : self.multiple;
138 |
139 | CGFloat const x = (1 - multiple) * self.bounds.size.width;
140 | CGFloat const y = (1 - multiple) * self.bounds.size.height;
141 | CGFloat const width = self.bounds.size.width * multiple;
142 | CGFloat const height = self.bounds.size.height * multiple;
143 |
144 | UIBezierPath *maskPath = [UIBezierPath bezierPath];
145 | [maskPath moveToPoint:CGPointMake(self.bounds.size.width, y)];
146 | [maskPath addLineToPoint:CGPointMake(self.bounds.size.width, self.bounds.size.height)];
147 | [maskPath addLineToPoint:CGPointMake(x, self.bounds.size.height)];
148 | CGPoint controlPoint1 = CGPointMake(x, y + height * 0.75);
149 | CGPoint controlPoint2 = CGPointMake(x + width * 0.25, y);
150 | [maskPath addCurveToPoint:CGPointMake(self.bounds.size.width, y) controlPoint1:controlPoint1 controlPoint2:controlPoint2];
151 | CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
152 | maskLayer.frame = self.bounds;
153 | maskLayer.path = maskPath.CGPath;
154 | self.layer.mask = maskLayer;
155 | }
156 |
157 | - (void)vibrateIfNeeded {
158 |
159 | if (![SuspensionEntrance shared].isVibratable) return;
160 |
161 | if (@available(iOS 10.0, *)) {
162 | UIImpactFeedbackGenerator *impactLight = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
163 | [impactLight impactOccurred];
164 | } else if (@available(iOS 9, *)) {
165 | AudioServicesPlaySystemSoundWithCompletion(kSystemSoundID_Vibrate, NULL);
166 | } else {
167 | AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
168 | }
169 | }
170 |
171 | #pragma mark - Setter
172 |
173 | - (void)setHighlighted:(BOOL)highlighted {
174 |
175 | if (_highlighted == highlighted) return;
176 | _highlighted = highlighted;
177 |
178 | if (highlighted) [self vibrateIfNeeded];
179 |
180 | // update outer layer transform
181 | CGFloat scale = 1.f * (highlighted ? 1.f : self.multiple);
182 | self.outerLayer.affineTransform = CGAffineTransformMakeScale(scale, scale);
183 |
184 | // update mask
185 | [self setupMaskLayer];
186 | }
187 |
188 | - (void)setEnabled:(BOOL)enabled {
189 |
190 | _enabled = enabled;
191 | UIColor * const color = enabled ? [UIColor whiteColor] : [UIColor lightGrayColor];
192 | self.titleLabel.text = [self titleForState:enabled ? SEFloatingAreaStateDefault : SEFloatingAreaStateDisabled];
193 | self.titleLabel.textColor = color;
194 | self.innerLayer.borderColor = self.outerLayer.borderColor = color.CGColor;
195 | self.effect = enabled ? [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark] : nil;
196 | self.backgroundColor = enabled ? [UIColor clearColor] : [UIColor colorWithWhite:0.875f alpha:1.f];
197 | }
198 |
199 | #pragma mark - Getter
200 |
201 | - (BOOL)isEnabled { return _enabled; }
202 | - (BOOL)isHighlighted { return _highlighted; }
203 |
204 | @end
205 |
--------------------------------------------------------------------------------
/SuspensionEntrance/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SuspensionEntrance
2 |
3 | 仿微信新版的悬浮窗入口功能
4 |
5 | 
6 |
7 |
8 |
9 | #### 1. 使用方式
10 |
11 | ```ruby
12 | pod SuspensionExtrance ~> 0.1.0 // 使用podfile方式引入
13 | ```
14 |
15 |
16 |
17 | ```objective-c
18 |
19 | @implementation BaseNavigationController
20 | - (void)viewDidLoad {
21 | [super viewDidLoad];
22 | // 在自定义的navigationController中 设置代理, 如果已经使用了代理,
23 | self.delegate = [SuspensionEntrance shared];
24 | // 关闭系统返回手势
25 | self.interactivePopGestureRecognizer.enabled = NO;
26 | }
27 | @end
28 |
29 | // 对于可以作为入口界面的Controller,实现SEItem协议
30 | @interface EntranceViewController : UIViewController
31 | @property (copy , nonatomic) NSString *entranceTitle;
32 | @property (copy , nonatomic, nullable) NSURL *entranceIconUrl;
33 | @property (copy , nonatomic, nullable) NSDictionary *entranceUserInfo;
34 | @end
35 |
36 | // 并实现下列构造方法, !!! 如果不实现则无法进行序列化存储
37 | + (instancetype)entranceWithItem:(id)item {
38 | EntranceViewController *controller = [[EntranceViewController alloc] initWithNibName:nil bundle:nil];
39 | controller.entranceTitle = item.entranceTitle;
40 | controller.entranceIconUrl = item.entranceIconUrl;
41 | controller.entranceUserInfo = item.entranceUserInfo;
42 | return controller;
43 | }
44 |
45 | ```
46 |
47 |
48 |
49 | ###### 一般情况下, 我们自己项目内都会使用自定义返回手势, 并且已经设置了代理, 那可以采用下列的方式进行对接
50 |
51 | ```objective-c
52 | // 在对应的代理方法里面调用
53 | - (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
54 | [[SuspensionEntrance shared] navigationController:navigationController willShowViewController:viewController animated:animated];
55 | }
56 |
57 | - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
58 | [[SuspensionEntrance shared] navigationController:navigationController didShowViewController:viewController animated:animated];
59 | }
60 |
61 | - (id)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id)animationController {
62 | return [[SuspensionEntrance shared] navigationController:navigationController interactionControllerForAnimationController:animationController];
63 | }
64 |
65 | - (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {
66 | return [[SuspensionEntrance shared] navigationController:navigationController animationControllerForOperation:operation fromViewController:fromVC toViewController:toVC];
67 | }
68 |
69 | // 然后同上面一步, 一样实现SEItem协议, 需要注意的事, 需要手动关闭自定义返回手势, 以避免手势冲突
70 | // 以集成了 forkingdog/FDFullscreenPopGesture(https://github.com/forkingdog/FDFullscreenPopGesture) 为例, 添加下列方法
71 | - (void)fd_interactivePopDisabled { return YES; }
72 | ```
73 |
74 |
75 |
76 | #### 2. 重点
77 |
78 | ##### 2.1 实现自定义的`UINavigationController`的`push & pop`动画效果
79 |
80 | 为了实现自定义的`push & pop`动画, 我们需要借助于苹果在iOS7开始提供的API: `UIViewControllerAnimatedTransitioning`可以实现具体效果
81 |
82 | ###### 2.1.1 自定义动画效果: `UIViewControllerAnimatedTransitioning`
83 |
84 | ```objective-c
85 | // 实现协议方法, 用于创建自定义的push & pop手势
86 | - (NSTimeInterval)transitionDuration:(id)transitionContext {
87 | // the duration for animation
88 | }
89 |
90 | // 在这里我们将此次使用到的动画效果大致分为三种
91 | // 1. 从圆球----push----->到具体的viewController
92 | // 2. 从viewController --pop--> 圆球效果
93 | // 3. 交互式滑动, 并根据滑动距离更新界面UI,最后 ---pop---> 圆球效果
94 | - (void)animateTransition:(id)transitionContext {
95 | // 自定义自己的动画效果, 利用CoreAnimations or [UIView animateWithDuration:0.25 animations:NULL] 都可以
96 | }
97 | ```
98 |
99 |
100 |
101 | ###### 2.1.2 实现交互式动画: `UIViewControllerInteractiveTransitioning`
102 |
103 | 接下来我们就需要自定义返回的交互式手势了, 好在苹果为我们也准备好了`API`接口, 我们只需要借助他即可实现
104 |
105 | ```objective-c
106 | // 1. 在对应的view上添加滑动手势, 这边我们直接借助于UIScreenEdgePanGestureRecognizer
107 | {
108 | UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTransition:)];
109 | pan.edges = UIRectEdgeLeft;
110 | pan.delegate = self;
111 | [viewController.view addGestureRecognizer:pan];
112 | }
113 | // 2. 实现手势方法
114 | - (void)handleTransition:(UIScreenEdgePanGestureRecognizer *)pan {
115 | // ...
116 | switch (pan.state) {
117 | case UIGestureRecognizerStateBegan:
118 | // 2.1 触发交互式返回, 创建UIPercentDrivenInteractiveTransition对象
119 | // 2.2 调用返回手势
120 | // 2.3 处理一些其他的初始化动作...
121 | self.interactive = [[UIPercentDrivenInteractiveTransition alloc] init];
122 | [tempItem.navigationController popViewControllerAnimated:YES];
123 | break;
124 | case UIGestureRecognizerStateChanged:
125 | // 2.4 更新交互式动画进度, 注意因为我们的使用的是自定义动画, 并没有一个完整的动画过程,
126 | // 所以我们需要自己更新动画过程, 如果直接使用的系统自带返回, 那么我们只需要更新interactive即可
127 | [self.animator updateContinousPopAnimationPercent:tPoint.x / SCREEN_WIDTH];
128 | [self.interactive updateInteractiveTransition:tPoint.x / SCREEN_WIDTH];
129 | // 2.5 处理其他一些判断条件(例如是否拖动到浮窗检测区域)...
130 | break;
131 | case UIGestureRecognizerStateEnded: // fall through
132 | case UIGestureRecognizerStateCancelled:
133 | // 2.6 判断动画完成情况, 是否具体完成 or 取消
134 | // 2.7 处理一些完成后动作(例如是否添加浮窗等)...
135 | break;
136 | }
137 | }
138 | ```
139 |
140 | 至此我们大致完成了一个简单的交互式自定义返回效果, 具体代码可以查看 `SuspensionEntrance`和`SETransitionAnimator`.
141 |
142 | ##### 2.2 浮球实现
143 |
144 | 接下来我们就需要对应的浮球效果,从微信分析可以看出,浮球主要包含了下面几个具体控件
145 |
146 | ###### 2.2.1 浮球 -- `SEFloatingBall`
147 |
148 | 主入口, 包含了点击、拖拽、长按等手势, 并提供了items的icon展示功能
149 |
150 | * 点击 -- 此处检点的利用`touchBegan`方法,来处理
151 | * 拖拽、长按
152 |
153 | ```objective-c
154 | - (void)setupGestures {
155 |
156 | // 添加长按手势
157 | UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
158 | longPress.minimumPressDuration = 0.5;
159 | longPress.allowableMovement = 5.f;
160 | // 关闭delays touches began功能, 因为我们在touchesBegan实现了点击方法, 并且动态高亮了点击背景, 所以我们需要实时呈现, 如果手势检测成功, 则会进入touchesCancelled
161 | longPress.delaysTouchesBegan = NO;
162 | [self addGestureRecognizer:longPress];
163 |
164 | // 添加拖拽手势
165 | UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
166 | // 原因同上
167 | pan.delaysTouchesBegan = NO;
168 | [self addGestureRecognizer:pan];
169 | // 注意此处优先检测长按手势, 检测失败后才开始检测拖拽
170 | [pan requireGestureRecognizerToFail:longPress];
171 | }
172 | ```
173 |
174 | * items 的icon展示 -- 此处用了比较暴力的直接计算....
175 |
176 | ###### 2.2.2 浮球检测窗 -- `SEFloatingArea`
177 |
178 | 主要用于检测浮球是否拖动到该区域, 用于判断是否需要将当前窗口作为浮窗入口. 这里并没有特别复杂的需要处理, 详细可以查看代码.
179 |
180 | ###### 2.2.3 浮球入口列表 -- `SEFloatingList`
181 |
182 | 主要用于展示已经标记为浮窗入口的列表项. 这里采用了代理模式,比较复杂的有下列几项
183 |
184 | * 需要注意的是,不是所有的item项目都会被展示 -- 已经打开的item会被隐藏入口,防止2次push进入
185 |
186 | * 计算展示位置,以及item的排列方式
187 |
188 |
189 |
190 | ```objective-c
191 | - (void)showAtRect:(CGRect)rect animated:(BOOL)animated {
192 |
193 | UIEdgeInsets safeAreaInsets = UIEdgeInsetsZero;
194 | if (@available(iOS 11.0, *)) safeAreaInsets = UIApplication.sharedApplication.keyWindow.safeAreaInsets;
195 |
196 | CGFloat const SCREEN_WIDTH = UIScreen.mainScreen.bounds.size.width;
197 | CGFloat const SCREEN_HEIGHT = UIScreen.mainScreen.bounds.size.height - safeAreaInsets.top - safeAreaInsets.bottom;
198 |
199 | // 获取可以被展示的item项
200 | NSArray *visibleListItems = [self.visibleItems copy];
201 |
202 | // 计算排列方式
203 | // inLeft: 是否在左侧 list主要显示位置
204 | // inBottom: 是否在底部 item在rect底部 or 顶部
205 | // isEnough: 是否有足够空间排列, 如果没有足够控件, 则采用自下而上(底部) or 自上而下的方式(顶部), 保证控件布局
206 | CGFloat const padding = 15.f;
207 | CGFloat const itemHeight = (padding + kSEFloatingListItemHeight);
208 | CGFloat height = visibleListItems.count * itemHeight;
209 | BOOL inLeft = rect.origin.x <= (SCREEN_WIDTH / 2.f);
210 | BOOL inBottom = (rect.origin.y + height < SCREEN_HEIGHT);
211 | BOOL isEnough = inBottom ? ( CGRectGetMaxY(rect) + height + safeAreaInsets.bottom < SCREEN_HEIGHT ) : (rect.origin.y > (height + safeAreaInsets.top));
212 |
213 | // 计算起始点位置
214 | CGFloat x = inLeft ? 0.f : (SCREEN_WIDTH / 3.f);
215 | CGFloat y = inBottom ? (rect.origin.y + rect.size.height + padding) : (rect.origin.y - itemHeight);
216 | if (!isEnough) { y = inBottom ? SCREEN_HEIGHT + safeAreaInsets.top - kSEFloatingListItemHeight - 5.f : safeAreaInsets.top; }
217 |
218 | // 如果控件不足, 我们布局采用逆序布局, 方便计算y轴起始点
219 | if (!isEnough) visibleListItems = [[[visibleListItems reverseObjectEnumerator] allObjects] mutableCopy];
220 |
221 | // 最后进行对应的布局, 并添加动画
222 | NSUInteger idx = 0;
223 | for (SEFloatingListItem *itemView in self.listItems) {
224 |
225 | itemView.alpha = .0f;
226 | itemView.selected = NO;
227 | itemView.highlighted = NO;
228 | itemView.frame = (CGRect) { CGPointMake(inLeft ? -itemView.frame.size.width : SCREEN_WIDTH, y), itemView.frame.size };
229 | itemView.corners = inLeft ? (UIRectCornerTopRight | UIRectCornerBottomRight) : (UIRectCornerTopLeft | UIRectCornerBottomLeft);
230 |
231 | if (![visibleListItems containsObject:itemView]) continue;
232 |
233 | [UIView animateWithDuration:0.15 delay:idx * 0.01 options:UIViewAnimationOptionCurveEaseInOut animations:^{
234 | itemView.alpha = 1.0f;
235 | itemView.frame = (CGRect){ CGPointMake(x, y), itemView.frame.size };
236 | } completion:NULL];
237 |
238 | idx += 1;
239 | if (((inBottom && isEnough) || (!inBottom && !isEnough))) { y += itemHeight; }
240 | else { y-= itemHeight; }
241 | }
242 |
243 | self.alpha = 0.3f;
244 | [UIView animateWithDuration:0.25 animations:^ { self.alpha = 1.f; }];
245 |
246 | if (self.delegate && [self.delegate respondsToSelector:@selector(floatingListWillShow:)])
247 | [self.delegate floatingListWillShow:self];
248 | }
249 | ```
250 |
251 |
252 |
253 | ##### 2.3 其他
254 |
255 | * items的序列化存储 -- 利用了`NSKeyedArchiver\NSKeyedUnarchiver`将items的JSON数据写入本地文件
256 |
257 | * 利用协议`SEItem`方式, 可以自定义任意的入口 -- 但是不建议针对内存消耗巨大的界面添加快捷入口, 内部并没有添加`UIApplicationDidReceiveMemoryWarningNotification`处理 -- (后期可能会考虑添加通知处理方法, 内存不足时回收快捷入口)
258 | * 利用序列化方法,生成的快捷入口, 创建后并不会消耗大量内存, 因为`viewController`并没有调用`viewDidLoad`方法
259 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SEFloatingList.m:
--------------------------------------------------------------------------------
1 | // SEFloatingList.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/13
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SEFloatingList
7 |
8 | #import "SEFloatingList.h"
9 | #import "SuspensionEntrance.h"
10 |
11 | static CGFloat const kSEFloatingListItemHeight = 56.0f;
12 |
13 | @interface SEFloatingListItem ()
14 |
15 | @property (weak, nonatomic) id item;
16 | @property (weak, nonatomic) UIImageView *iconView;
17 | @property (weak, nonatomic) UILabel *titleLabel;
18 | @property (weak, nonatomic) UIButton *deleteButton;
19 | @property (assign, nonatomic, getter=isEditable) BOOL editable;
20 | @property (assign, nonatomic) UIRectCorner corners;
21 |
22 | @property (strong, nonatomic) CAShapeLayer *backgroundLayer;
23 |
24 | @end
25 |
26 | @interface SEFloatingList ()
27 | @property (weak, nonatomic) SEFloatingListItem *tempItem;
28 | @property (strong, nonatomic) NSMutableArray *visibleItems;
29 | @end
30 |
31 | @implementation SEFloatingListItem
32 | @synthesize selected = _selected;
33 | @synthesize highlighted = _highlighted;
34 |
35 | - (instancetype)initWithItem:(id)item {
36 |
37 | CGFloat const padding = 10.f;
38 | CGFloat const SCREEN_WIDTH = UIScreen.mainScreen.bounds.size.width;
39 | CGRect const frame = CGRectMake(0, 0, SCREEN_WIDTH * 2.f / 3.f, kSEFloatingListItemHeight);
40 | if (self = [super initWithFrame:frame]) {
41 |
42 | _item = item;
43 |
44 | UIImageView *iconView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 35.f, 35.f)];
45 | iconView.center = CGPointMake(padding + 35.f / 2.f, kSEFloatingListItemHeight / 2.f);
46 | iconView.contentMode = UIViewContentModeScaleAspectFill;
47 | CAShapeLayer *maskLayer = [CAShapeLayer layer];
48 | maskLayer.path = [UIBezierPath bezierPathWithRoundedRect:iconView.bounds cornerRadius:17.5f].CGPath;
49 | iconView.layer.mask = maskLayer;
50 | [self addSubview:_iconView = iconView];
51 |
52 | UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(55.f, 10.5, frame.size.width - 55.f - padding - 50.f, 35.0)];
53 | titleLabel.numberOfLines = 2;
54 | titleLabel.textColor = [UIColor colorWithRed:0.21 green:0.21 blue:0.21 alpha:1.f];
55 | if (@available(iOS 8.0, *)) titleLabel.font = [UIFont systemFontOfSize:14.0 weight:UIFontWeightMedium];
56 | else titleLabel.font = [UIFont systemFontOfSize:14.0];
57 | titleLabel.text = item.entranceTitle ? : @" ";
58 | [self addSubview:_titleLabel = titleLabel];
59 |
60 | UIButton *deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
61 | deleteButton.frame = CGRectMake(frame.size.width - 60.f, 0.0f, 60.f, kSEFloatingListItemHeight);
62 | // [deleteButton setTitle:@"x" forState:UIControlStateNormal];
63 | // [deleteButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
64 | [deleteButton setImage:[SuspensionEntrance shared].closePlaceholder ? : [UIImage imageNamed:@"web_entrance_close"] forState:UIControlStateNormal];
65 | [self addSubview:_deleteButton = deleteButton];
66 |
67 | self.layer.shadowColor = [UIColor colorWithRed:0.75f green:0.75f blue:0.75f alpha:1.0].CGColor;
68 | self.layer.shadowOpacity = 1.f;
69 | self.layer.shadowOffset = CGSizeZero;
70 | self.layer.shadowRadius = 7.5f;
71 |
72 | _backgroundLayer = [CAShapeLayer layer];
73 | _backgroundLayer.fillColor = [UIColor whiteColor].CGColor;
74 | _backgroundLayer.frame = self.bounds;
75 | [self.layer insertSublayer:self.backgroundLayer atIndex:0];
76 |
77 | [SuspensionEntrance shared].iconHandler(iconView, item);
78 | }
79 | return self;
80 | }
81 |
82 | - (void)setCorners:(UIRectCorner)corners {
83 |
84 | _corners = corners;
85 | if (corners == UIRectEdgeNone) {
86 | self.layer.mask = nil;
87 | self.layer.shadowPath = nil;
88 | self.backgroundLayer.path = nil;
89 | } else {
90 | CGSize size = CGSizeApplyAffineTransform(self.bounds.size, CGAffineTransformMakeScale(0.5f, 0.5f));
91 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:corners cornerRadii:size];
92 | self.layer.shadowPath = path.CGPath;
93 | self.backgroundLayer.path = path.CGPath;
94 | }
95 | }
96 |
97 | - (void)setEditable:(BOOL)editable {
98 | self.deleteButton.hidden = !editable;
99 | }
100 |
101 | - (void)setSelected:(BOOL)selected {
102 | if (_selected == selected) return;
103 | _selected = selected;
104 | CGFloat scale = selected ? 1.1f : 1.f;
105 | self.transform = CGAffineTransformMakeScale(scale, scale);
106 | }
107 |
108 | - (void)setHighlighted:(BOOL)highlighted {
109 |
110 | if (_highlighted == highlighted) return;
111 | _highlighted = highlighted;
112 | UIColor *color = highlighted ? [UIColor colorWithWhite:0.75 alpha:1.f] : [UIColor whiteColor];
113 | self.backgroundLayer.fillColor = color.CGColor;
114 | }
115 |
116 | #pragma mark - Getter
117 |
118 | - (BOOL)isSelected { return _selected; }
119 | - (BOOL)isHighlighted { return _highlighted; }
120 | - (BOOL)isEditable { return !self.deleteButton.isHidden; }
121 |
122 | @end
123 |
124 | @implementation SEFloatingList
125 |
126 | #pragma mark - Life
127 |
128 | - (instancetype)initWithFrame:(CGRect)frame {
129 | self = [super initWithFrame:(CGRect){ CGPointZero, UIScreen.mainScreen.bounds.size }];
130 | if (self) {
131 |
132 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
133 | UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
134 | effectView.frame = self.bounds;
135 | [self addSubview:effectView];
136 | }
137 | return self;
138 | }
139 |
140 | #pragma mark - Override
141 |
142 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
143 |
144 | [super touchesBegan:touches withEvent:event];
145 | CGPoint point = [[touches anyObject] locationInView:self];
146 | for (SEFloatingListItem *subView in self.listItems) {
147 | if (CGRectContainsPoint(subView.frame, point)) {
148 | self.tempItem = subView;
149 | self.tempItem.highlighted = YES;
150 | break;
151 | }
152 | }
153 | }
154 |
155 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
156 |
157 | [super touchesEnded:touches withEvent:event];
158 | BOOL animated = YES;
159 | if (self.tempItem.highlighted) {
160 | if (self.delegate && [self.delegate respondsToSelector:@selector(floatingList:didSelectItem:)]) {
161 | animated = NO;
162 | [self.delegate floatingList:self didSelectItem:self.tempItem.item];
163 | }
164 | }
165 | [self dismissWithAnimated:animated];
166 | }
167 |
168 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
169 |
170 | [super touchesMoved:touches withEvent:event];
171 | CGPoint point = [[touches anyObject] locationInView:self];
172 | if (self.tempItem) self.tempItem.highlighted = CGRectContainsPoint(self.tempItem.frame, point);
173 | }
174 |
175 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
176 | [super touchesCancelled:touches withEvent:event];
177 | self.tempItem = nil;
178 | }
179 |
180 | #pragma mark - Public
181 |
182 | - (void)reloadData {
183 |
184 | if (self.delegate == nil) return;
185 |
186 | [self.listItems makeObjectsPerformSelector:@selector(removeFromSuperview)];
187 |
188 | NSUInteger count = [self.delegate numberOfItemsInFloatingList:self];
189 | for (int i = 0; i < count; i ++) {
190 | id item = [self.delegate floatingList:self itemAtIndex:i];
191 | SEFloatingListItem *listItem = [[SEFloatingListItem alloc] initWithItem:item];
192 | [listItem.deleteButton addTarget:self action:@selector(handleDeleteAction:) forControlEvents:UIControlEventTouchUpInside];
193 | [self addSubview:listItem];
194 | }
195 | }
196 |
197 | - (void)showAtRect:(CGRect)rect animated:(BOOL)animated {
198 |
199 | UIEdgeInsets safeAreaInsets = UIEdgeInsetsZero;
200 | if (@available(iOS 11.0, *)) safeAreaInsets = UIApplication.sharedApplication.keyWindow.safeAreaInsets;
201 |
202 | CGFloat const SCREEN_WIDTH = UIScreen.mainScreen.bounds.size.width;
203 | CGFloat const SCREEN_HEIGHT = UIScreen.mainScreen.bounds.size.height - safeAreaInsets.top - safeAreaInsets.bottom;
204 |
205 | NSArray *visibleListItems = [self.visibleItems copy];
206 |
207 | CGFloat const padding = 15.f;
208 | CGFloat const itemHeight = (padding + kSEFloatingListItemHeight);
209 | CGFloat height = visibleListItems.count * itemHeight;
210 | BOOL inLeft = rect.origin.x <= (SCREEN_WIDTH / 2.f);
211 | BOOL inBottom = (rect.origin.y + height < SCREEN_HEIGHT);
212 | BOOL isEnough = inBottom ? ( CGRectGetMaxY(rect) + height + safeAreaInsets.bottom < SCREEN_HEIGHT ) : (rect.origin.y > (height + safeAreaInsets.top));
213 |
214 | CGFloat x = inLeft ? 0.f : (SCREEN_WIDTH / 3.f);
215 | CGFloat y = inBottom ? (rect.origin.y + rect.size.height + padding) : (rect.origin.y - itemHeight);
216 | if (!isEnough) { y = inBottom ? SCREEN_HEIGHT + safeAreaInsets.top - kSEFloatingListItemHeight - 5.f : safeAreaInsets.top; }
217 |
218 | if (!isEnough) visibleListItems = [[[visibleListItems reverseObjectEnumerator] allObjects] mutableCopy];
219 |
220 | NSUInteger idx = 0;
221 | for (SEFloatingListItem *itemView in self.listItems) {
222 |
223 | itemView.alpha = .0f;
224 | itemView.selected = NO;
225 | itemView.highlighted = NO;
226 | itemView.frame = (CGRect) { CGPointMake(inLeft ? -itemView.frame.size.width : SCREEN_WIDTH, y), itemView.frame.size };
227 | itemView.corners = inLeft ? (UIRectCornerTopRight | UIRectCornerBottomRight) : (UIRectCornerTopLeft | UIRectCornerBottomLeft);
228 |
229 | if (![visibleListItems containsObject:itemView]) continue;
230 |
231 | [UIView animateWithDuration:0.15 delay:idx * 0.01 options:UIViewAnimationOptionCurveEaseInOut animations:^{
232 | itemView.alpha = 1.0f;
233 | itemView.frame = (CGRect){ CGPointMake(x, y), itemView.frame.size };
234 | } completion:NULL];
235 |
236 | idx += 1;
237 | if (((inBottom && isEnough) || (!inBottom && !isEnough))) { y += itemHeight; }
238 | else { y-= itemHeight; }
239 | }
240 |
241 | self.alpha = 0.3f;
242 | [UIView animateWithDuration:0.25 animations:^ { self.alpha = 1.f; }];
243 |
244 | if (self.delegate && [self.delegate respondsToSelector:@selector(floatingListWillShow:)])
245 | [self.delegate floatingListWillShow:self];
246 | }
247 |
248 | - (void)dismissWithAnimated:(BOOL)animated {
249 |
250 | CGFloat const SCREEN_WIDTH = UIScreen.mainScreen.bounds.size.width;
251 |
252 | NSArray *visibleItems = self.visibleItems;
253 | for (SEFloatingListItem *itemView in visibleItems) {
254 | NSUInteger const idx = [visibleItems indexOfObject:itemView];
255 | BOOL inLeft = (itemView.frame.origin.x <= 0.f);
256 | CGFloat const x = ((inLeft ? itemView.frame.size.width : SCREEN_WIDTH) + 30.f) * (inLeft ? -1.f : 1.f);
257 | [UIView animateWithDuration:animated ? .15f : CGFLOAT_MIN delay:idx * 0.05 options:UIViewAnimationOptionCurveEaseInOut animations:^{
258 | itemView.frame = (CGRect){ CGPointMake(x, itemView.frame.origin.y), itemView.frame.size };
259 | } completion:NULL];
260 | }
261 |
262 | [UIView animateWithDuration:animated ? .25f : CGFLOAT_MIN animations:^ { self.alpha = 0.f; } completion:^(BOOL finished) {
263 | [self removeFromSuperview];
264 | }];
265 |
266 | if (self.delegate && [self.delegate respondsToSelector:@selector(floatingListWillHide:)])
267 | [self.delegate floatingListWillHide:self];
268 | }
269 |
270 | #pragma mark - Private
271 |
272 | - (NSArray *)visibleItems {
273 |
274 | NSMutableArray *listItems = [self.listItems mutableCopy];
275 | if (self.delegate && [self.delegate respondsToSelector:@selector(floatingList:isItemVisible:)]) {
276 | for (SEFloatingListItem *listItem in self.listItems) {
277 | if (![self.delegate floatingList:self isItemVisible:listItem.item]) { [listItems removeObject:listItem]; }
278 | }
279 | }
280 | return [listItems copy];
281 | }
282 |
283 | #pragma mark - Actions
284 |
285 | - (void)handleDeleteAction:(UIButton *)button {
286 |
287 | SEFloatingListItem *listItem = (SEFloatingListItem *)button.superview;
288 | if (!listItem || ![listItem isKindOfClass:[SEFloatingListItem class]]) return;
289 | if (!listItem.item) return;
290 | if (!self.delegate || ![self.delegate respondsToSelector:@selector(floatingList:willDeleteItem:)]) return;
291 |
292 | if (![self.delegate floatingList:self willDeleteItem:listItem.item]) return;
293 | // TODO: success delete list item, need reset position of other list items
294 |
295 | CGFloat const SCREEN_WIDTH = UIScreen.mainScreen.bounds.size.width;
296 | BOOL inLeft = (listItem.frame.origin.x <= 0.f);
297 | CGFloat const x = ((inLeft ? listItem.frame.size.width : SCREEN_WIDTH) + 30.f) * (inLeft ? -1.f : 1.f);
298 |
299 |
300 | __block CGRect currentRect = listItem.frame;
301 | [UIView animateWithDuration:0.15f animations:^{
302 | listItem.frame = (CGRect){ CGPointMake(x, listItem.frame.origin.y), listItem.frame.size};
303 | } completion:^(BOOL finished) {
304 | [listItem removeFromSuperview];
305 | }];
306 |
307 | if (self.visibleItems.count <= 1) {
308 | [self dismissWithAnimated:YES];
309 | return;
310 | }
311 |
312 | NSUInteger const currendIdx = [self.visibleItems indexOfObject:listItem];
313 | for (SEFloatingListItem *tempItem in self.visibleItems) {
314 | NSUInteger const idx = [self.visibleItems indexOfObject:tempItem];
315 | if (idx <= currendIdx) { continue; }
316 | CGRect const tempRect = tempItem.frame;
317 | [UIView animateWithDuration:0.15f animations:^{
318 | tempItem.frame = currentRect;
319 | }];
320 | currentRect = tempRect;
321 | }
322 | }
323 |
324 | #pragma mark - Setter
325 |
326 | //- (void)setDelegate:(id)delegate {
327 | // _delegate = delegate;
328 | // [self reloadData];
329 | //}
330 |
331 | - (void)setEditable:(BOOL)editable {
332 | _editable = editable;
333 | for (SEFloatingListItem *itemView in self.listItems) { itemView.editable = editable; }
334 | }
335 |
336 | #pragma mark - Getter
337 |
338 | - (NSArray *)listItems {
339 | NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) {
340 | return [obj isKindOfClass:[SEFloatingListItem class]];
341 | }];
342 | return (NSArray *)[self.subviews filteredArrayUsingPredicate:predicate];
343 | }
344 |
345 | - (CGRect)floatingRect {
346 |
347 | for (SEFloatingListItem *listItem in self.listItems) {
348 | if (listItem.isSelected) return listItem.frame;
349 | if (listItem.isHighlighted) return listItem.frame;
350 | }
351 | return CGRectZero;
352 | }
353 |
354 | @end
355 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SETransitionAnimator.m:
--------------------------------------------------------------------------------
1 | // SETransitionAnimator.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/9
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SETransitionAnimator
7 |
8 | #import "SETransitionAnimator.h"
9 |
10 | #ifndef SCREEN_WIDTH
11 | #define SCREEN_WIDTH UIScreen.mainScreen.bounds.size.width
12 | #endif
13 |
14 | #ifndef SCREEN_HEIGHT
15 | #define SCREEN_HEIGHT UIScreen.mainScreen.bounds.size.height
16 | #endif
17 |
18 | @interface SETransitionAnimator ()
19 | @property (assign, nonatomic) CGRect floatingRect;
20 | @property (strong, nonatomic) UIView *coverView;
21 | @property (assign, nonatomic) CGFloat radius;
22 | @property (strong, nonatomic) id transitionContext;
23 | @end
24 |
25 | @implementation SETransitionAnimator
26 |
27 | #pragma mark - Life
28 |
29 | - (instancetype)initWithStyle:(SETransitionAnimatorStyle)style floatingRect:(CGRect)rect {
30 |
31 | self = [super init];
32 | if (self) {
33 | _style = style;
34 | _radius = CGRectGetHeight(rect) / 2.f;
35 | _floatingRect = rect;
36 | }
37 | return self;
38 | }
39 |
40 | + (instancetype)roundPushAnimatorWithRect:(CGRect)rect {
41 | return [[SETransitionAnimator alloc] initWithStyle:SETransitionAnimatorStyleRoundPush floatingRect:rect];
42 | }
43 |
44 | + (instancetype)roundPopAnimatorWithRect:(CGRect)rect {
45 | return [[SETransitionAnimator alloc] initWithStyle:SETransitionAnimatorStyleRoundPop floatingRect:rect];
46 | }
47 |
48 | + (instancetype)continuousPopAnimatorWithRect:(CGRect)rect {
49 | return [[SETransitionAnimator alloc] initWithStyle:SETransitionAnimatorStyleContinuousPop floatingRect:rect];
50 | }
51 |
52 | #pragma mark - Public
53 |
54 | - (void)finishContinousAnimation {
55 |
56 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
57 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
58 | BOOL const isPresented = fromVC.presentingViewController != nil;
59 | [fromVC.view addSubview:self.coverView];
60 | // 当前fromVC.view有偏移,需要重置
61 | CGFloat const currentOffset = isPresented ? fromVC.view.frame.origin.y : fromVC.view.frame.origin.x;
62 | fromVC.view.frame = (CGRect){ CGPointZero, fromVC.view.frame.size };
63 | // if (isPresented) fromVC.view.frame = (CGRect) { CGPointMake(0.f, fromVC.view.frame.origin.y), fromVC.view.frame.size };
64 | // else fromVC.view.frame = (CGRect) { CGPointMake(fromVC.view.frame.origin.x, 0.f), fromVC.view.frame.size };
65 |
66 | CGRect roundedRect = CGRectMake(currentOffset, -self.radius, SCREEN_WIDTH + self.radius * 2, SCREEN_HEIGHT + self.radius * 2);
67 | if (isPresented) roundedRect = CGRectMake(-self.radius, currentOffset, SCREEN_WIDTH + self.radius * 2, SCREEN_HEIGHT + self.radius * 2);
68 | UIBezierPath *startPath = [UIBezierPath bezierPathWithRoundedRect:roundedRect cornerRadius:self.radius];
69 | UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:self.floatingRect cornerRadius:self.radius];
70 | CAShapeLayer *maskLayer = [CAShapeLayer layer];
71 | maskLayer.fillColor = [UIColor blackColor].CGColor;
72 | maskLayer.path = endPath.CGPath;
73 | maskLayer.frame = fromVC.view.frame;
74 | fromVC.view.layer.mask = maskLayer;
75 | CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
76 | maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
77 | maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath);
78 | maskLayerAnimation.duration = 0.2f;
79 | maskLayerAnimation.delegate = (id)self;
80 | [maskLayer addAnimation:maskLayerAnimation forKey:@"xw_path"];
81 |
82 | CGFloat duration = (1 - (isPresented ? (currentOffset / SCREEN_HEIGHT) : (currentOffset / SCREEN_WIDTH))) * self.animationDuration;
83 | self.coverView.alpha = 0;
84 | [UIView animateWithDuration:duration animations:^{
85 | self.coverView.alpha = isPresented ? 0.f : 0.3;
86 | if (!isPresented) toVC.view.frame = (CGRect){ CGPointMake(0, toVC.view.frame.origin.y), toVC.view.frame.size };
87 | UITabBar *tabBar = toVC.tabBarController.tabBar;
88 | if (tabBar) tabBar.frame = (CGRect) { CGPointMake(0.f, toVC.view.bounds.size.height - tabBar.bounds.size.height), tabBar.bounds.size };
89 | } completion:^(BOOL finished) {
90 | [self.coverView removeFromSuperview];
91 | }];
92 | }
93 |
94 | - (void)cancelContinousAnimation {
95 |
96 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
97 | BOOL isPresented = fromVC.presentingViewController != nil;
98 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
99 | CGFloat percent = isPresented ? fromVC.view.frame.origin.y / SCREEN_HEIGHT : fromVC.view.frame.origin.x / SCREEN_WIDTH;
100 | [UIView animateWithDuration:self.animationDuration * percent animations:^{
101 | if (isPresented) fromVC.view.frame = (CGRect){ CGPointMake(fromVC.view.frame.origin.x, 0.f) , fromVC.view.frame.size };
102 | else {
103 | fromVC.view.frame = (CGRect){ CGPointMake(0.f, fromVC.view.frame.origin.y) , fromVC.view.frame.size };
104 | toVC.view.frame = (CGRect){ CGPointMake(-SCREEN_WIDTH / 3.f, toVC.view.frame.origin.y) , toVC.view.frame.size };
105 | }
106 | } completion:^(BOOL finished) {
107 | if (!isPresented) toVC.view.frame = (CGRect){ CGPointMake(0, toVC.view.frame.origin.y) , toVC.view.frame.size };
108 | [self.transitionContext completeTransition:!self.transitionContext.transitionWasCancelled];
109 | }];
110 | }
111 |
112 | - (void)updateContinousAnimationPercent:(CGFloat)percent {
113 |
114 | percent = MIN(1.f, MAX(0.f, percent));
115 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
116 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
117 | BOOL const isPresented = fromVC.presentingViewController != nil;
118 |
119 | if (isPresented) {
120 | fromVC.view.frame = (CGRect){ CGPointMake(0.f, SCREEN_HEIGHT * percent) , fromVC.view.frame.size };
121 | } else {
122 | fromVC.view.frame = (CGRect){ CGPointMake(SCREEN_WIDTH * percent, fromVC.view.frame.origin.y) , fromVC.view.frame.size };
123 | toVC.view.frame = (CGRect){ CGPointMake((SCREEN_WIDTH / -3.f) * (1 - percent), toVC.view.frame.origin.y) , toVC.view.frame.size };
124 | }
125 |
126 | self.coverView.alpha = (1 - percent) * 0.7f;
127 |
128 | UITabBar *tabBar = toVC.tabBarController.tabBar;
129 | if (tabBar == nil) return;
130 | CGFloat maxY = tabBar.bounds.size.height * percent;
131 | #ifdef __IPHONE_11_0
132 | if (@available(iOS 11.0, *)) maxY += tabBar.safeAreaInsets.bottom;
133 | #endif
134 | maxY = MIN(maxY, tabBar.bounds.size.height);
135 | tabBar.frame = (CGRect) { CGPointMake(0.f, toVC.view.bounds.size.height - maxY), tabBar.bounds.size };
136 | }
137 |
138 | - (void)finishContinousAnimationWithFastAnimating:(BOOL)fast {
139 |
140 | UIViewController *fromVC = [self.transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
141 | UIViewController *toVC = [self.transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
142 | BOOL const isPresented = fromVC.presentingViewController != nil;
143 | CGFloat duration = 0.2f;
144 | if (fast) duration = (1 - fromVC.view.frame.origin.x / SCREEN_WIDTH) * self.animationDuration;
145 |
146 | [UIView animateWithDuration:duration animations:^{
147 | if (isPresented) {
148 | fromVC.view.frame = (CGRect){ CGPointMake(fromVC.view.frame.origin.x, SCREEN_HEIGHT) , fromVC.view.frame.size };
149 | } else {
150 | fromVC.view.frame = (CGRect){ CGPointMake(SCREEN_WIDTH, fromVC.view.frame.origin.y) , fromVC.view.frame.size };
151 | toVC.view.frame = (CGRect){ CGPointMake(0.f, toVC.view.frame.origin.y) , toVC.view.frame.size };
152 | }
153 | UITabBar *tabBar = toVC.tabBarController.tabBar;
154 | if (tabBar) {
155 | tabBar.frame = (CGRect) { CGPointMake(0.f, toVC.view.bounds.size.height - tabBar.bounds.size.height), tabBar.bounds.size };
156 | }
157 | } completion:^(BOOL finished) {
158 | [self.transitionContext completeTransition:!self.transitionContext.transitionWasCancelled];
159 | }];
160 | }
161 |
162 | #pragma mark - UIViewControllerAnimatedTransitioning
163 |
164 | - (NSTimeInterval)transitionDuration:(id)transitionContext {
165 | return self.animationDuration;
166 | }
167 |
168 | - (void)animateTransition:(id)transitionContext {
169 |
170 | self.transitionContext = transitionContext;
171 | switch (self.style) {
172 | case SETransitionAnimatorStyleRoundPop:
173 | [self startRoundPopAnimation:transitionContext];
174 | break;
175 | case SETransitionAnimatorStyleRoundPush:
176 | [self startRoundPushAnimation:transitionContext];
177 | break;
178 | case SETransitionAnimatorStyleContinuousPop:
179 | [self startContinousPopAnimation:transitionContext];
180 | break;
181 | case SETransitionAnimatorStyleUnknown: // fall through
182 | default: break;
183 | }
184 | }
185 |
186 | - (void)animationEnded:(BOOL)transitionCompleted {
187 |
188 | // animation ended
189 |
190 | [self.coverView removeFromSuperview];
191 | // [self.transitionContext completeTransition:transitionCompleted];
192 | self.transitionContext = nil;
193 | }
194 |
195 | #pragma mark - Animations
196 |
197 | - (void)startRoundPushAnimation:(id)transitionContext {
198 |
199 | UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
200 | UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
201 | UIView *containerView = [transitionContext containerView];
202 | [containerView addSubview:self.coverView];
203 | toVC.view.frame = CGRectMake(0.f, 0.f, SCREEN_WIDTH, SCREEN_HEIGHT);
204 | [containerView addSubview:toVC.view];
205 | UIBezierPath *startPath = [UIBezierPath bezierPathWithRoundedRect:self.floatingRect cornerRadius:self.radius];
206 | UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(-self.radius, -self.radius, SCREEN_WIDTH + self.radius * 2, SCREEN_HEIGHT + self.radius * 2) cornerRadius:self.radius];
207 | CAShapeLayer *maskLayer = [CAShapeLayer layer];
208 | maskLayer.path = endPath.CGPath;
209 | toVC.view.layer.mask = maskLayer;
210 |
211 | CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
212 | maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
213 | maskLayerAnimation.toValue = (__bridge id)((endPath.CGPath));
214 | maskLayerAnimation.duration = self.animationDuration;
215 | maskLayerAnimation.delegate = (id)self;
216 | [maskLayer addAnimation:maskLayerAnimation forKey:@"xw_path"];
217 |
218 | self.coverView.alpha = 0.0f;
219 | UITabBar *tabBar = fromVC.tabBarController.tabBar;
220 | tabBar.frame = (CGRect) { CGPointMake(0.f, fromVC.view.bounds.size.height - tabBar.bounds.size.height), tabBar.bounds.size };
221 | [UIView animateWithDuration:self.animationDuration animations:^{
222 | self.coverView.alpha = 0.6f;
223 | tabBar.frame = (CGRect) { CGPointMake(fromVC.view.bounds.size.width, fromVC.view.bounds.size.height), tabBar.bounds.size };
224 | }];
225 | }
226 |
227 | - (void)startRoundPopAnimation:(id)transitionContext {
228 |
229 | UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
230 | UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
231 | UIView *containerView = [transitionContext containerView];
232 | containerView.backgroundColor = [UIColor whiteColor];
233 | [containerView insertSubview:toVC.view atIndex:0];
234 |
235 | [toVC.view addSubview:self.coverView];
236 | UIBezierPath *startPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(-self.radius, -self.radius, SCREEN_WIDTH + self.radius * 2, SCREEN_HEIGHT + self.radius * 2) cornerRadius:self.radius];
237 | UIBezierPath *endPath = [UIBezierPath bezierPathWithRoundedRect:self.floatingRect cornerRadius:self.radius];
238 | CAShapeLayer *maskLayer = [CAShapeLayer layer];
239 | maskLayer.path = endPath.CGPath;
240 | fromVC.view.layer.mask = maskLayer;
241 |
242 | CABasicAnimation *maskLayerAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
243 | maskLayerAnimation.fromValue = (__bridge id)(startPath.CGPath);
244 | maskLayerAnimation.toValue = (__bridge id)(endPath.CGPath);
245 | maskLayerAnimation.duration = self.animationDuration;
246 | maskLayerAnimation.delegate = (id)self;
247 | [maskLayer addAnimation:maskLayerAnimation forKey:@"xw_path"];
248 |
249 | self.coverView.alpha = 0.6f;
250 |
251 | UITabBar *tabBar = toVC.tabBarController.tabBar;
252 | CGPoint origin = CGPointMake(0.f, toVC.view.bounds.size.height - tabBar.bounds.size.height);
253 | tabBar.frame = (CGRect) { CGPointMake(0.f, toVC.view.bounds.size.height), tabBar.bounds.size };
254 | [UIView animateWithDuration:self.animationDuration animations:^{
255 | self.coverView.alpha = 0.0f;
256 | tabBar.frame = (CGRect) { origin, tabBar.bounds.size };
257 | }];
258 | }
259 |
260 | - (void)startContinousPopAnimation:(id)transitionContext {
261 |
262 | UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
263 | UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
264 | UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
265 | UIView *containerView = [transitionContext containerView];
266 | BOOL const isPresented = fromVC.presentingViewController != nil;
267 | if (!isPresented) toVC.view.frame = CGRectMake(toView.bounds.size.width * -1.f / 3.f, 0, toView.bounds.size.width, toView.bounds.size.height);
268 | [containerView insertSubview:toVC.view atIndex:0];
269 |
270 | if (isPresented) {
271 | self.coverView.alpha = 0.7f;
272 | [toVC.view addSubview:self.coverView];
273 | }
274 |
275 | UITabBar *tabBar = toVC.tabBarController.tabBar;
276 | if (tabBar == nil) return;
277 | tabBar.frame = (CGRect) { CGPointMake(0.f, toView.bounds.size.height), tabBar.bounds.size };
278 | }
279 |
280 | #pragma mark - CAAnimationDelegate
281 |
282 | - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
283 | [self.transitionContext completeTransition:!self.transitionContext.transitionWasCancelled];
284 | }
285 |
286 | #pragma mark - Getter
287 |
288 | - (UIView *)coverView {
289 | if (!_coverView) _coverView = [[UIView alloc] initWithFrame:UIScreen.mainScreen.bounds];
290 | _coverView.backgroundColor = [UIColor blackColor];
291 | return _coverView;
292 | }
293 |
294 | - (NSTimeInterval)animationDuration { return 0.3f; }
295 |
296 | @end
297 |
298 | #undef SCREEN_WIDTH
299 | #undef SCREEN_HEIGHT
300 |
--------------------------------------------------------------------------------
/SuspensionEntrance.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 972FD4CB230256F6004CB039 /* SEFloatingList.m in Sources */ = {isa = PBXBuildFile; fileRef = 972FD4CA230256F6004CB039 /* SEFloatingList.m */; };
11 | 9747418D22FBB7F600281B55 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9747418C22FBB7F600281B55 /* AppDelegate.m */; };
12 | 9747419322FBB7F600281B55 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9747419122FBB7F600281B55 /* Main.storyboard */; };
13 | 9747419522FBB7FA00281B55 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9747419422FBB7FA00281B55 /* Assets.xcassets */; };
14 | 9747419822FBB7FA00281B55 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9747419622FBB7FA00281B55 /* LaunchScreen.storyboard */; };
15 | 9747419B22FBB7FA00281B55 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 9747419A22FBB7FA00281B55 /* main.m */; };
16 | 974741A322FBB88300281B55 /* NormalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 974741A222FBB88300281B55 /* NormalViewController.m */; };
17 | 974741A622FBB88E00281B55 /* EntranceViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 974741A522FBB88E00281B55 /* EntranceViewController.m */; };
18 | 974741A922FBB8BE00281B55 /* BaseNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 974741A822FBB8BE00281B55 /* BaseNavigationController.m */; };
19 | 974741AD22FBBAEB00281B55 /* SuspensionEntrance.m in Sources */ = {isa = PBXBuildFile; fileRef = 974741AC22FBBAEB00281B55 /* SuspensionEntrance.m */; };
20 | 974741B022FBBE9200281B55 /* SEFloatingBall.m in Sources */ = {isa = PBXBuildFile; fileRef = 974741AF22FBBE9200281B55 /* SEFloatingBall.m */; };
21 | 974741B322FD074100281B55 /* SETransitionAnimator.m in Sources */ = {isa = PBXBuildFile; fileRef = 974741B222FD074100281B55 /* SETransitionAnimator.m */; };
22 | 974741B922FD3DFF00281B55 /* SEFloatingArea.m in Sources */ = {isa = PBXBuildFile; fileRef = 974741B822FD3DFF00281B55 /* SEFloatingArea.m */; };
23 | 97C4F5F02472803C00236421 /* PresentNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C4F5EF2472803C00236421 /* PresentNavigationController.m */; };
24 | /* End PBXBuildFile section */
25 |
26 | /* Begin PBXFileReference section */
27 | 972FD4C9230256F6004CB039 /* SEFloatingList.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SEFloatingList.h; sourceTree = ""; };
28 | 972FD4CA230256F6004CB039 /* SEFloatingList.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SEFloatingList.m; sourceTree = ""; };
29 | 9747418822FBB7F600281B55 /* SuspensionEntrance.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SuspensionEntrance.app; sourceTree = BUILT_PRODUCTS_DIR; };
30 | 9747418B22FBB7F600281B55 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; };
31 | 9747418C22FBB7F600281B55 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; };
32 | 9747419222FBB7F600281B55 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
33 | 9747419422FBB7FA00281B55 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
34 | 9747419722FBB7FA00281B55 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
35 | 9747419922FBB7FA00281B55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
36 | 9747419A22FBB7FA00281B55 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; };
37 | 974741A122FBB88300281B55 /* NormalViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NormalViewController.h; sourceTree = ""; };
38 | 974741A222FBB88300281B55 /* NormalViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NormalViewController.m; sourceTree = ""; };
39 | 974741A422FBB88E00281B55 /* EntranceViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EntranceViewController.h; sourceTree = ""; };
40 | 974741A522FBB88E00281B55 /* EntranceViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EntranceViewController.m; sourceTree = ""; };
41 | 974741A722FBB8BE00281B55 /* BaseNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BaseNavigationController.h; sourceTree = ""; };
42 | 974741A822FBB8BE00281B55 /* BaseNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BaseNavigationController.m; sourceTree = ""; };
43 | 974741AB22FBBAEB00281B55 /* SuspensionEntrance.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SuspensionEntrance.h; sourceTree = ""; };
44 | 974741AC22FBBAEB00281B55 /* SuspensionEntrance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SuspensionEntrance.m; sourceTree = ""; };
45 | 974741AE22FBBE9200281B55 /* SEFloatingBall.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SEFloatingBall.h; sourceTree = ""; };
46 | 974741AF22FBBE9200281B55 /* SEFloatingBall.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SEFloatingBall.m; sourceTree = ""; };
47 | 974741B122FD074100281B55 /* SETransitionAnimator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SETransitionAnimator.h; sourceTree = ""; };
48 | 974741B222FD074100281B55 /* SETransitionAnimator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SETransitionAnimator.m; sourceTree = ""; };
49 | 974741B722FD3DFF00281B55 /* SEFloatingArea.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SEFloatingArea.h; sourceTree = ""; };
50 | 974741B822FD3DFF00281B55 /* SEFloatingArea.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SEFloatingArea.m; sourceTree = ""; };
51 | 97C4F5EE2472803C00236421 /* PresentNavigationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PresentNavigationController.h; sourceTree = ""; };
52 | 97C4F5EF2472803C00236421 /* PresentNavigationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PresentNavigationController.m; sourceTree = ""; };
53 | /* End PBXFileReference section */
54 |
55 | /* Begin PBXFrameworksBuildPhase section */
56 | 9747418522FBB7F600281B55 /* Frameworks */ = {
57 | isa = PBXFrameworksBuildPhase;
58 | buildActionMask = 2147483647;
59 | files = (
60 | );
61 | runOnlyForDeploymentPostprocessing = 0;
62 | };
63 | /* End PBXFrameworksBuildPhase section */
64 |
65 | /* Begin PBXGroup section */
66 | 9747417F22FBB7F600281B55 = {
67 | isa = PBXGroup;
68 | children = (
69 | 9747418A22FBB7F600281B55 /* SuspensionEntrance */,
70 | 9747418922FBB7F600281B55 /* Products */,
71 | );
72 | sourceTree = "";
73 | };
74 | 9747418922FBB7F600281B55 /* Products */ = {
75 | isa = PBXGroup;
76 | children = (
77 | 9747418822FBB7F600281B55 /* SuspensionEntrance.app */,
78 | );
79 | name = Products;
80 | sourceTree = "";
81 | };
82 | 9747418A22FBB7F600281B55 /* SuspensionEntrance */ = {
83 | isa = PBXGroup;
84 | children = (
85 | 974741AA22FBBAD700281B55 /* OC */,
86 | 9747418B22FBB7F600281B55 /* AppDelegate.h */,
87 | 9747418C22FBB7F600281B55 /* AppDelegate.m */,
88 | 974741A122FBB88300281B55 /* NormalViewController.h */,
89 | 974741A222FBB88300281B55 /* NormalViewController.m */,
90 | 974741A422FBB88E00281B55 /* EntranceViewController.h */,
91 | 974741A522FBB88E00281B55 /* EntranceViewController.m */,
92 | 974741A722FBB8BE00281B55 /* BaseNavigationController.h */,
93 | 974741A822FBB8BE00281B55 /* BaseNavigationController.m */,
94 | 97C4F5EE2472803C00236421 /* PresentNavigationController.h */,
95 | 97C4F5EF2472803C00236421 /* PresentNavigationController.m */,
96 | 9747419122FBB7F600281B55 /* Main.storyboard */,
97 | 9747419422FBB7FA00281B55 /* Assets.xcassets */,
98 | 9747419622FBB7FA00281B55 /* LaunchScreen.storyboard */,
99 | 9747419922FBB7FA00281B55 /* Info.plist */,
100 | 9747419A22FBB7FA00281B55 /* main.m */,
101 | );
102 | path = SuspensionEntrance;
103 | sourceTree = "";
104 | };
105 | 974741AA22FBBAD700281B55 /* OC */ = {
106 | isa = PBXGroup;
107 | children = (
108 | 974741AB22FBBAEB00281B55 /* SuspensionEntrance.h */,
109 | 974741AC22FBBAEB00281B55 /* SuspensionEntrance.m */,
110 | 974741AE22FBBE9200281B55 /* SEFloatingBall.h */,
111 | 974741AF22FBBE9200281B55 /* SEFloatingBall.m */,
112 | 972FD4C9230256F6004CB039 /* SEFloatingList.h */,
113 | 972FD4CA230256F6004CB039 /* SEFloatingList.m */,
114 | 974741B722FD3DFF00281B55 /* SEFloatingArea.h */,
115 | 974741B822FD3DFF00281B55 /* SEFloatingArea.m */,
116 | 974741B122FD074100281B55 /* SETransitionAnimator.h */,
117 | 974741B222FD074100281B55 /* SETransitionAnimator.m */,
118 | );
119 | path = OC;
120 | sourceTree = "";
121 | };
122 | /* End PBXGroup section */
123 |
124 | /* Begin PBXNativeTarget section */
125 | 9747418722FBB7F600281B55 /* SuspensionEntrance */ = {
126 | isa = PBXNativeTarget;
127 | buildConfigurationList = 9747419E22FBB7FA00281B55 /* Build configuration list for PBXNativeTarget "SuspensionEntrance" */;
128 | buildPhases = (
129 | 9747418422FBB7F600281B55 /* Sources */,
130 | 9747418522FBB7F600281B55 /* Frameworks */,
131 | 9747418622FBB7F600281B55 /* Resources */,
132 | );
133 | buildRules = (
134 | );
135 | dependencies = (
136 | );
137 | name = SuspensionEntrance;
138 | productName = SuspensionEntrance;
139 | productReference = 9747418822FBB7F600281B55 /* SuspensionEntrance.app */;
140 | productType = "com.apple.product-type.application";
141 | };
142 | /* End PBXNativeTarget section */
143 |
144 | /* Begin PBXProject section */
145 | 9747418022FBB7F600281B55 /* Project object */ = {
146 | isa = PBXProject;
147 | attributes = {
148 | LastUpgradeCheck = 1020;
149 | ORGANIZATIONNAME = Fraker.XM;
150 | TargetAttributes = {
151 | 9747418722FBB7F600281B55 = {
152 | CreatedOnToolsVersion = 10.2;
153 | };
154 | };
155 | };
156 | buildConfigurationList = 9747418322FBB7F600281B55 /* Build configuration list for PBXProject "SuspensionEntrance" */;
157 | compatibilityVersion = "Xcode 9.3";
158 | developmentRegion = en;
159 | hasScannedForEncodings = 0;
160 | knownRegions = (
161 | en,
162 | Base,
163 | );
164 | mainGroup = 9747417F22FBB7F600281B55;
165 | productRefGroup = 9747418922FBB7F600281B55 /* Products */;
166 | projectDirPath = "";
167 | projectRoot = "";
168 | targets = (
169 | 9747418722FBB7F600281B55 /* SuspensionEntrance */,
170 | );
171 | };
172 | /* End PBXProject section */
173 |
174 | /* Begin PBXResourcesBuildPhase section */
175 | 9747418622FBB7F600281B55 /* Resources */ = {
176 | isa = PBXResourcesBuildPhase;
177 | buildActionMask = 2147483647;
178 | files = (
179 | 9747419822FBB7FA00281B55 /* LaunchScreen.storyboard in Resources */,
180 | 9747419522FBB7FA00281B55 /* Assets.xcassets in Resources */,
181 | 9747419322FBB7F600281B55 /* Main.storyboard in Resources */,
182 | );
183 | runOnlyForDeploymentPostprocessing = 0;
184 | };
185 | /* End PBXResourcesBuildPhase section */
186 |
187 | /* Begin PBXSourcesBuildPhase section */
188 | 9747418422FBB7F600281B55 /* Sources */ = {
189 | isa = PBXSourcesBuildPhase;
190 | buildActionMask = 2147483647;
191 | files = (
192 | 974741AD22FBBAEB00281B55 /* SuspensionEntrance.m in Sources */,
193 | 9747419B22FBB7FA00281B55 /* main.m in Sources */,
194 | 974741B922FD3DFF00281B55 /* SEFloatingArea.m in Sources */,
195 | 9747418D22FBB7F600281B55 /* AppDelegate.m in Sources */,
196 | 97C4F5F02472803C00236421 /* PresentNavigationController.m in Sources */,
197 | 974741A622FBB88E00281B55 /* EntranceViewController.m in Sources */,
198 | 974741A322FBB88300281B55 /* NormalViewController.m in Sources */,
199 | 972FD4CB230256F6004CB039 /* SEFloatingList.m in Sources */,
200 | 974741B022FBBE9200281B55 /* SEFloatingBall.m in Sources */,
201 | 974741A922FBB8BE00281B55 /* BaseNavigationController.m in Sources */,
202 | 974741B322FD074100281B55 /* SETransitionAnimator.m in Sources */,
203 | );
204 | runOnlyForDeploymentPostprocessing = 0;
205 | };
206 | /* End PBXSourcesBuildPhase section */
207 |
208 | /* Begin PBXVariantGroup section */
209 | 9747419122FBB7F600281B55 /* Main.storyboard */ = {
210 | isa = PBXVariantGroup;
211 | children = (
212 | 9747419222FBB7F600281B55 /* Base */,
213 | );
214 | name = Main.storyboard;
215 | sourceTree = "";
216 | };
217 | 9747419622FBB7FA00281B55 /* LaunchScreen.storyboard */ = {
218 | isa = PBXVariantGroup;
219 | children = (
220 | 9747419722FBB7FA00281B55 /* Base */,
221 | );
222 | name = LaunchScreen.storyboard;
223 | sourceTree = "";
224 | };
225 | /* End PBXVariantGroup section */
226 |
227 | /* Begin XCBuildConfiguration section */
228 | 9747419C22FBB7FA00281B55 /* Debug */ = {
229 | isa = XCBuildConfiguration;
230 | buildSettings = {
231 | ALWAYS_SEARCH_USER_PATHS = NO;
232 | CLANG_ANALYZER_NONNULL = YES;
233 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
234 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
235 | CLANG_CXX_LIBRARY = "libc++";
236 | CLANG_ENABLE_MODULES = YES;
237 | CLANG_ENABLE_OBJC_ARC = YES;
238 | CLANG_ENABLE_OBJC_WEAK = YES;
239 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
240 | CLANG_WARN_BOOL_CONVERSION = YES;
241 | CLANG_WARN_COMMA = YES;
242 | CLANG_WARN_CONSTANT_CONVERSION = YES;
243 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
244 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
245 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
246 | CLANG_WARN_EMPTY_BODY = YES;
247 | CLANG_WARN_ENUM_CONVERSION = YES;
248 | CLANG_WARN_INFINITE_RECURSION = YES;
249 | CLANG_WARN_INT_CONVERSION = YES;
250 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
251 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
252 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
253 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
254 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
255 | CLANG_WARN_STRICT_PROTOTYPES = YES;
256 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
257 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
258 | CLANG_WARN_UNREACHABLE_CODE = YES;
259 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
260 | CODE_SIGN_IDENTITY = "iPhone Developer";
261 | COPY_PHASE_STRIP = NO;
262 | DEBUG_INFORMATION_FORMAT = dwarf;
263 | ENABLE_STRICT_OBJC_MSGSEND = YES;
264 | ENABLE_TESTABILITY = YES;
265 | GCC_C_LANGUAGE_STANDARD = gnu11;
266 | GCC_DYNAMIC_NO_PIC = NO;
267 | GCC_NO_COMMON_BLOCKS = YES;
268 | GCC_OPTIMIZATION_LEVEL = 0;
269 | GCC_PREPROCESSOR_DEFINITIONS = (
270 | "DEBUG=1",
271 | "$(inherited)",
272 | );
273 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
274 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
275 | GCC_WARN_UNDECLARED_SELECTOR = YES;
276 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
277 | GCC_WARN_UNUSED_FUNCTION = YES;
278 | GCC_WARN_UNUSED_VARIABLE = YES;
279 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
280 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
281 | MTL_FAST_MATH = YES;
282 | ONLY_ACTIVE_ARCH = YES;
283 | SDKROOT = iphoneos;
284 | };
285 | name = Debug;
286 | };
287 | 9747419D22FBB7FA00281B55 /* Release */ = {
288 | isa = XCBuildConfiguration;
289 | buildSettings = {
290 | ALWAYS_SEARCH_USER_PATHS = NO;
291 | CLANG_ANALYZER_NONNULL = YES;
292 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
293 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
294 | CLANG_CXX_LIBRARY = "libc++";
295 | CLANG_ENABLE_MODULES = YES;
296 | CLANG_ENABLE_OBJC_ARC = YES;
297 | CLANG_ENABLE_OBJC_WEAK = YES;
298 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
299 | CLANG_WARN_BOOL_CONVERSION = YES;
300 | CLANG_WARN_COMMA = YES;
301 | CLANG_WARN_CONSTANT_CONVERSION = YES;
302 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
303 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
304 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
305 | CLANG_WARN_EMPTY_BODY = YES;
306 | CLANG_WARN_ENUM_CONVERSION = YES;
307 | CLANG_WARN_INFINITE_RECURSION = YES;
308 | CLANG_WARN_INT_CONVERSION = YES;
309 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
310 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
311 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
312 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
313 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
314 | CLANG_WARN_STRICT_PROTOTYPES = YES;
315 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
316 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
317 | CLANG_WARN_UNREACHABLE_CODE = YES;
318 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
319 | CODE_SIGN_IDENTITY = "iPhone Developer";
320 | COPY_PHASE_STRIP = NO;
321 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
322 | ENABLE_NS_ASSERTIONS = NO;
323 | ENABLE_STRICT_OBJC_MSGSEND = YES;
324 | GCC_C_LANGUAGE_STANDARD = gnu11;
325 | GCC_NO_COMMON_BLOCKS = YES;
326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
328 | GCC_WARN_UNDECLARED_SELECTOR = YES;
329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
330 | GCC_WARN_UNUSED_FUNCTION = YES;
331 | GCC_WARN_UNUSED_VARIABLE = YES;
332 | IPHONEOS_DEPLOYMENT_TARGET = 12.2;
333 | MTL_ENABLE_DEBUG_INFO = NO;
334 | MTL_FAST_MATH = YES;
335 | SDKROOT = iphoneos;
336 | VALIDATE_PRODUCT = YES;
337 | };
338 | name = Release;
339 | };
340 | 9747419F22FBB7FA00281B55 /* Debug */ = {
341 | isa = XCBuildConfiguration;
342 | buildSettings = {
343 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
344 | CODE_SIGN_STYLE = Automatic;
345 | DEVELOPMENT_TEAM = CY4833TA98;
346 | INFOPLIST_FILE = SuspensionEntrance/Info.plist;
347 | IPHONEOS_DEPLOYMENT_TARGET = 10;
348 | LD_RUNPATH_SEARCH_PATHS = (
349 | "$(inherited)",
350 | "@executable_path/Frameworks",
351 | );
352 | PRODUCT_BUNDLE_IDENTIFIER = com.fraker.SuspensionEntrance;
353 | PRODUCT_NAME = "$(TARGET_NAME)";
354 | TARGETED_DEVICE_FAMILY = "1,2";
355 | };
356 | name = Debug;
357 | };
358 | 974741A022FBB7FA00281B55 /* Release */ = {
359 | isa = XCBuildConfiguration;
360 | buildSettings = {
361 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
362 | CODE_SIGN_STYLE = Automatic;
363 | DEVELOPMENT_TEAM = CY4833TA98;
364 | INFOPLIST_FILE = SuspensionEntrance/Info.plist;
365 | IPHONEOS_DEPLOYMENT_TARGET = 10;
366 | LD_RUNPATH_SEARCH_PATHS = (
367 | "$(inherited)",
368 | "@executable_path/Frameworks",
369 | );
370 | PRODUCT_BUNDLE_IDENTIFIER = com.fraker.SuspensionEntrance;
371 | PRODUCT_NAME = "$(TARGET_NAME)";
372 | TARGETED_DEVICE_FAMILY = "1,2";
373 | };
374 | name = Release;
375 | };
376 | /* End XCBuildConfiguration section */
377 |
378 | /* Begin XCConfigurationList section */
379 | 9747418322FBB7F600281B55 /* Build configuration list for PBXProject "SuspensionEntrance" */ = {
380 | isa = XCConfigurationList;
381 | buildConfigurations = (
382 | 9747419C22FBB7FA00281B55 /* Debug */,
383 | 9747419D22FBB7FA00281B55 /* Release */,
384 | );
385 | defaultConfigurationIsVisible = 0;
386 | defaultConfigurationName = Release;
387 | };
388 | 9747419E22FBB7FA00281B55 /* Build configuration list for PBXNativeTarget "SuspensionEntrance" */ = {
389 | isa = XCConfigurationList;
390 | buildConfigurations = (
391 | 9747419F22FBB7FA00281B55 /* Debug */,
392 | 974741A022FBB7FA00281B55 /* Release */,
393 | );
394 | defaultConfigurationIsVisible = 0;
395 | defaultConfigurationName = Release;
396 | };
397 | /* End XCConfigurationList section */
398 | };
399 | rootObject = 9747418022FBB7F600281B55 /* Project object */;
400 | }
401 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SEFloatingBall.m:
--------------------------------------------------------------------------------
1 | // SEFloatingView.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SEFloatingView
7 |
8 | #import "SEFloatingBall.h"
9 | #import "SuspensionEntrance.h"
10 |
11 | static NSString * const kSEFloatingBallFrameKey = @"com.fraker.xm.se.ball.frame";
12 |
13 | static const CGFloat kSEFloatingBallRadius = 30.f;
14 | static const CGFloat kSEFloatingBallPadding = 9.f;
15 | static const CGFloat kSEScreenWidth() { return UIScreen.mainScreen.bounds.size.width; }
16 | static const CGFloat kSEScreenHeight() { return UIScreen.mainScreen.bounds.size.height; }
17 |
18 | @interface SEFloatingBallItem : UIImageView
19 | @property (nonatomic, weak) id item;
20 | @end
21 |
22 | @implementation SEFloatingBallItem
23 |
24 | #pragma mark - Life
25 |
26 | - (instancetype)initWithFrame:(CGRect)frame {
27 |
28 | self = [super initWithFrame:frame];
29 | if (self) {
30 | self.contentMode = UIViewContentModeScaleAspectFill;
31 | }
32 | return self;
33 | }
34 |
35 | - (instancetype)initWithItem:(id)item {
36 | self = [super initWithFrame:CGRectZero];
37 | if (self) { self->_item = item; }
38 | return self;
39 | }
40 |
41 | + (Class)layerClass { return [CAShapeLayer class]; }
42 |
43 | #pragma mark - Override
44 |
45 | - (void)willMoveToSuperview:(UIView *)newSuperview {
46 | [super willMoveToSuperview:newSuperview];
47 | if (newSuperview) {
48 | self.transform = CGAffineTransformMakeScale(0.3, 0.3);
49 | [UIView animateWithDuration:0.25 animations:^{ self.transform = CGAffineTransformMakeScale(1.f, 1.f); }];
50 | }
51 | }
52 |
53 | #pragma mark - Private
54 |
55 | - (void)updateMaskWithAngle:(CGFloat)angle isRound:(BOOL)isRound {
56 |
57 | CAShapeLayer *maskLayer = [CAShapeLayer layer];
58 | maskLayer.frame = self.bounds;
59 | maskLayer.path = [self maskPathWithRound:isRound].CGPath;
60 | maskLayer.transform = CATransform3DRotate(CATransform3DIdentity, angle, 0, 0, 1);
61 | self.layer.mask = maskLayer;
62 | }
63 |
64 | - (UIBezierPath *)maskPathWithRound:(BOOL)isRound {
65 |
66 | CGFloat radius = CGRectGetHeight(self.bounds) / 2.f;
67 | if (isRound) return [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:radius];
68 |
69 | CGFloat startAngle = 45.f / 180.f * M_PI;
70 | CGFloat endAngle = 315.f / 180.f * M_PI;
71 |
72 | CGPoint centerA = CGPointMake(self.bounds.size.width / 2.f, self.bounds.size.height / 2.f);
73 | CGFloat value = ceil((sqrt(pow(radius, 2) / 2)));
74 | CGPoint centerB = CGPointMake(centerA.x + value, centerA.y + value);
75 | UIBezierPath *path = [UIBezierPath bezierPath];
76 | [path addArcWithCenter:centerA radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
77 | [path addQuadCurveToPoint:centerB controlPoint:CGPointMake(centerA.x * 5 / 4.f, centerA.y)];
78 | [path closePath];
79 | return path;
80 | }
81 |
82 | @end
83 |
84 | @interface SEFloatingBallEffectView : UIView
85 | @property (weak, nonatomic) UIImageView *imageView;
86 | @property (weak, nonatomic) CAShapeLayer *blackLayer;
87 | @property (weak, nonatomic) CAShapeLayer *whiteLayer;
88 | @property (assign, nonatomic, getter=isHighlighted) BOOL highlighted;
89 | @end
90 | @implementation SEFloatingBallEffectView
91 |
92 | - (instancetype)initWithFrame:(CGRect)frame {
93 | self = [super initWithFrame:frame];
94 | if (self) {
95 |
96 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
97 | UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
98 | effectView.frame = self.bounds;
99 | effectView.alpha = 0.875f;
100 | [self addSubview:effectView];
101 |
102 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.bounds];
103 | imageView.backgroundColor = [UIColor colorWithWhite:1.f alpha:0.5f];
104 | imageView.contentMode = UIViewContentModeScaleAspectFill;
105 | imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
106 | [self addSubview:_imageView = imageView];
107 |
108 | { // add white border
109 | CAShapeLayer *borderLayer = [CAShapeLayer layer];
110 | borderLayer.frame = self.bounds;
111 | borderLayer.lineWidth = 2.f;
112 | borderLayer.fillColor = [UIColor clearColor].CGColor;
113 | borderLayer.strokeColor = [UIColor whiteColor].CGColor;
114 | [self.layer addSublayer:_whiteLayer = borderLayer];
115 | }
116 |
117 | { // add black border later
118 | CAShapeLayer *borderLayer = [CAShapeLayer layer];
119 | borderLayer.frame = self.bounds;
120 | borderLayer.lineWidth = 0.5f;
121 | borderLayer.fillColor = [UIColor clearColor].CGColor;
122 | borderLayer.strokeColor = [UIColor colorWithWhite:0.8f alpha:1.0f].CGColor;
123 | [self.layer addSublayer:_blackLayer = borderLayer];
124 | }
125 | }
126 | return self;
127 | }
128 |
129 | - (void)setHighlighted:(BOOL)highlighted {
130 | if (_highlighted == highlighted) return;
131 | _highlighted = highlighted;
132 | self.imageView.backgroundColor = [UIColor colorWithWhite:highlighted ? 0.8f : 1.f alpha:0.5f];
133 | }
134 |
135 | - (UIBezierPath *)updateMaskCorners:(UIRectCorner)corners {
136 |
137 | CGRect rect = self.bounds;
138 | CGSize size = CGSizeApplyAffineTransform(rect.size, CGAffineTransformMakeScale(0.5f, 0.5f));
139 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:size];
140 | CAShapeLayer *layer = [CAShapeLayer layer];
141 | layer.frame = rect;
142 | layer.path = path.CGPath;
143 | layer.fillColor = [UIColor blackColor].CGColor;
144 | self.layer.mask = layer;
145 |
146 | self.blackLayer.path = self.whiteLayer.path = path.CGPath;
147 |
148 | return path;
149 | }
150 |
151 | @end
152 |
153 | @interface SEFloatingBall ()
154 |
155 | @property (assign, nonatomic, getter=isHighlighted) BOOL highlighted;
156 | @property (strong, nonatomic) CAShapeLayer *blackLayer;
157 | @property (strong, nonatomic) CAShapeLayer *whiteLayer;
158 | @property (strong, nonatomic) SEFloatingBallEffectView *effectView;
159 | @property (strong, nonatomic) NSMutableArray *iconViews;
160 |
161 | @property (assign, nonatomic, readonly) CGFloat radius;
162 | @property (strong, nonatomic, readonly) NSArray> *oldItems;
163 |
164 | @end
165 |
166 | @implementation SEFloatingBall
167 |
168 | - (instancetype)initWithFrame:(CGRect)frame {
169 | self = [super initWithFrame:frame];
170 | if (self) {
171 |
172 | _radius = kSEFloatingBallRadius;
173 | _iconViews = [NSMutableArray array];
174 |
175 | [self setupUI];
176 | [self setupGestures];
177 | }
178 | return self;
179 | }
180 |
181 | #pragma mark - Override
182 |
183 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
184 | [super touchesBegan:touches withEvent:event];
185 | self.layer.shadowOpacity = 1.f;
186 | self.effectView.highlighted = YES;
187 | }
188 |
189 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
190 | [super touchesEnded:touches withEvent:event];
191 | self.layer.shadowOpacity = 0.5f;
192 | self.effectView.highlighted = NO;
193 |
194 | CGPoint point = [[touches anyObject] locationInView:self];
195 | point = [self.superview convertPoint:point fromView:self];
196 | if (!CGRectContainsPoint(self.frame, point)) return;
197 | if (!self.delegate || ![self.delegate respondsToSelector:@selector(floatingBallDidClicked:)]) return;
198 | [self.delegate floatingBallDidClicked:self];
199 | }
200 |
201 | - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
202 | [super touchesCancelled:touches withEvent:event];
203 | self.layer.shadowOpacity = 0.5f;
204 | }
205 |
206 | #pragma mark - Public
207 |
208 | - (void)reloadIconViews:(NSArray> *)items {
209 |
210 | NSMutableSet> *newItems = [NSMutableSet setWithArray:items];
211 | NSMutableSet> *oldItems = [NSMutableSet setWithArray:self.oldItems];
212 | if ([newItems isEqualToSet:oldItems]) return;
213 |
214 | for (id item in items) {
215 | SEFloatingBallItem *theBall = nil;
216 | for (SEFloatingBallItem *ball in self.iconViews) {
217 | if (ball.item == item) { theBall = ball; break; }
218 | }
219 | if (theBall == nil) theBall = [[SEFloatingBallItem alloc] initWithItem:item];
220 | theBall.item = item;
221 | if (![self.iconViews containsObject:theBall]) {
222 | [self.iconViews addObject:theBall];
223 | [self addSubview:theBall];
224 | theBall.center = CGPointMake(CGRectGetMidX(self.floatingRect), CGRectGetMidY(self.floatingRect));
225 | }
226 | }
227 |
228 | // remove unnecessary ballItem
229 | for (SEFloatingBallItem *ball in [self.iconViews copy]) {
230 | if (![items containsObject:ball.item] || ball.item == nil) {
231 | [ball removeFromSuperview];
232 | [self.iconViews removeObject:ball];
233 | }
234 | }
235 |
236 | if (self.iconViews.count <= 0) return;
237 | NSArray *frames = [self.frames objectAtIndex:self.iconViews.count - 1];
238 |
239 | [self.iconViews enumerateObjectsUsingBlock:^(SEFloatingBallItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
240 |
241 | CGRect const origin = obj.frame;
242 | NSDictionary *info = [frames objectAtIndex:idx];
243 | obj.transform = CGAffineTransformIdentity;
244 | obj.frame = CGRectFromString([info objectForKey:@"frame"]);
245 | obj.center = CGPointFromString([info objectForKey:@"center"]);
246 | BOOL isRound = ![[info objectForKey:@"mask"] boolValue];
247 | CGFloat angle = [[info objectForKey:@"angle"] intValue] * M_PI / 180.f;
248 | [obj updateMaskWithAngle:angle isRound:isRound];
249 |
250 | CABasicAnimation *position = [CABasicAnimation animationWithKeyPath:@"position"];
251 | position.fromValue = @(CGPointMake(CGRectGetMidX(origin), CGRectGetMidY(origin)));
252 | position.toValue = @(CGPointMake(CGRectGetMidX(obj.frame), CGRectGetMidY(obj.frame)));
253 | position.duration = .25f;
254 | [obj.layer addAnimation:position forKey:@"position"];
255 |
256 | [SuspensionEntrance shared].iconHandler(obj, [items objectAtIndex:idx]);
257 | }];
258 | }
259 |
260 | - (NSArray *> *)frames {
261 | CGFloat const maxWidth = self.floatingRect.size.width;
262 | CGPoint const center = CGPointMake(CGRectGetMidX(self.floatingRect), CGRectGetMidY(self.floatingRect));
263 | return @[
264 | [self single],
265 | [self twoWithMaxWidth:maxWidth center:center],
266 | [self threeWithMaxWidth:maxWidth center:center],
267 | [self fourWithMaxWidth:maxWidth center:center],
268 | [self fiveWithMaxWidth:maxWidth center:center]
269 | ];
270 | }
271 |
272 | - (NSArray *)single {
273 | return @[
274 | @{
275 | @"frame" : NSStringFromCGRect(CGRectInset(self.bounds, 7.5f, 7.5f)),
276 | @"center" : NSStringFromCGPoint(CGPointMake(CGRectGetMidX(self.floatingRect), CGRectGetMidY(self.floatingRect)))
277 | }
278 | ];
279 | }
280 |
281 | - (NSArray *)twoWithMaxWidth:(CGFloat const)maxWidth center:(CGPoint)center {
282 |
283 | // the padding between each ball item
284 | CGFloat const half = 2.f;
285 | CGFloat const width = maxWidth / 2.f + half;
286 | CGRect const frame = CGRectMake(0, 0, width, width);
287 | return @[
288 | @{
289 | @"center" : NSStringFromCGPoint(CGPointMake(center.x - width / 2.f + half, center.y)),
290 | @"frame" : NSStringFromCGRect(frame),
291 | @"mask" : @YES
292 | },
293 | @{
294 | @"frame" : NSStringFromCGRect(frame),
295 | @"center" : NSStringFromCGPoint(CGPointMake(center.x + width / 2.f - half, center.y)),
296 | }
297 | ];
298 | }
299 |
300 | - (NSArray *)threeWithMaxWidth:(CGFloat const)maxWidth center:(CGPoint)center {
301 |
302 | // the padding between each ball item
303 | CGFloat const half = 3.f;
304 | CGFloat const width = maxWidth / 2.f;
305 | CGRect const frame = CGRectMake(0, 0, width, width);
306 | return @[
307 | @{
308 | @"center" : NSStringFromCGPoint(CGPointMake(center.x - width / 2.f + half / 2.f, center.y + width / 2.f - half)),
309 | @"frame" : NSStringFromCGRect(frame),
310 | @"mask" : @YES
311 | },
312 | @{
313 | @"center" : NSStringFromCGPoint(CGPointMake(center.x + width / 2.f - half / 2.f, center.y + width / 2.f - half)),
314 | @"frame" : NSStringFromCGRect(frame),
315 | @"mask" : @YES,
316 | @"angle" : @(240.f)
317 | },
318 | @{
319 | @"center" : NSStringFromCGPoint(CGPointMake(center.x, center.y - width / 2.f + half)),
320 | @"frame" : NSStringFromCGRect(frame),
321 | @"mask" : @YES,
322 | @"angle" : @(120.f)
323 | }
324 | ];
325 | }
326 |
327 | - (NSArray *)fourWithMaxWidth:(CGFloat const)maxWidth center:(CGPoint)center {
328 |
329 | // the padding between each ball item
330 | CGFloat const half = 1.5f;
331 | CGFloat const width = (maxWidth - half * 4.f) / 2.f;
332 | CGRect const frame = CGRectMake(0, 0, width, width);
333 | return @[
334 | @{
335 | @"center" : NSStringFromCGPoint(CGPointMake(center.x - width/2.f - half, center.y)),
336 | @"frame" : NSStringFromCGRect(frame),
337 | @"mask" : @YES,
338 | @"angle" : @(45.f)
339 | },
340 | @{
341 | @"center" : NSStringFromCGPoint(CGPointMake(center.x, center.y + width/2.f + half)),
342 | @"frame" : NSStringFromCGRect(frame),
343 | @"mask" : @YES,
344 | @"angle" : @(315.f)
345 | },
346 | @{
347 | @"center" : NSStringFromCGPoint(CGPointMake(center.x + width/2.f + half, center.y)),
348 | @"frame" : NSStringFromCGRect(frame),
349 | @"mask" : @YES,
350 | @"angle" : @(225.f)
351 | },
352 | @{
353 | @"center" : NSStringFromCGPoint(CGPointMake(center.x, center.y - width/2.f - half)),
354 | @"frame" : NSStringFromCGRect(frame),
355 | @"mask" : @YES,
356 | @"angle" : @(135.f)
357 | }
358 | ];
359 | }
360 |
361 | - (NSArray *)fiveWithMaxWidth:(CGFloat const)maxWidth center:(CGPoint)center {
362 |
363 | CGFloat const half = 2.f;
364 | CGFloat const width = maxWidth / 2.f - half;
365 | // CGFloat const distance = ceil(sqrt(pow(width / 2.f, 2) / 2.f) - half);
366 | CGRect const frame = CGRectMake(0, 0, width, width);
367 | return @[
368 | @{
369 | @"center" : NSStringFromCGPoint(CGPointMake(center.x, center.y - width / 2.f - half)),
370 | @"frame" : NSStringFromCGRect(frame),
371 | @"mask" : @YES,
372 | @"angle" : @(144.f)
373 | },
374 | @{
375 | @"center" : NSStringFromCGPoint(CGPointMake(center.x - width / 2.f - half - 0.5f, center.y - half)),
376 | @"frame" : NSStringFromCGRect(frame),
377 | @"mask" : @YES,
378 | @"angle" : @(72.f)
379 | },
380 | @{
381 | @"center" : NSStringFromCGPoint(CGPointMake(center.x - width/2.f + half + 0.5f, center.y + width/2.f + half + 1.f)),
382 | @"frame" : NSStringFromCGRect(frame),
383 | @"mask" : @YES,
384 | @"angle" : @(0.f)
385 | },
386 | @{
387 | @"center" : NSStringFromCGPoint(CGPointMake(center.x + width/2.f - half/2.f, center.y + width/2.f + half + 1.f)),
388 | @"frame" : NSStringFromCGRect(frame),
389 | @"mask" : @YES,
390 | @"angle" : @(288.f)
391 | },
392 | @{
393 | @"center" : NSStringFromCGPoint(CGPointMake(center.x + width/2.f + half*1.5f, center.y - half - 0.5f)),
394 | @"frame" : NSStringFromCGRect(frame),
395 | @"mask" : @YES,
396 | @"angle" : @(216.f)
397 | }
398 | ];
399 | }
400 |
401 |
402 | #pragma mark - Private
403 |
404 | - (void)setupUI {
405 |
406 | NSString *rectValue = [[NSUserDefaults standardUserDefaults] stringForKey:kSEFloatingBallFrameKey];
407 | if (rectValue.length > 0) {
408 | self.frame = CGRectFromString(rectValue);
409 | } else {
410 | CGPoint origin = CGPointMake(kSEScreenWidth() - self.radius * 2.f, kSEScreenHeight() / 2.f - self.radius);
411 | self.frame = (CGRect){ origin, CGSizeMake(self.radius * 2.f, self.radius * 2.f) };
412 | }
413 |
414 | _effectView = [[SEFloatingBallEffectView alloc] initWithFrame:self.bounds];
415 | _effectView.contentMode = UIViewContentModeScaleAspectFill;
416 | [_effectView updateMaskCorners:self.corners];
417 | [self addSubview:_effectView];
418 |
419 | self.layer.shadowPath = [(CAShapeLayer *)_effectView.layer.mask path];
420 | self.layer.shadowColor = [UIColor colorWithRed:0.75f green:0.75f blue:0.75f alpha:1.0].CGColor;
421 | self.layer.shadowOpacity = 0.5f;
422 | self.layer.shadowOffset = CGSizeZero;
423 | self.layer.shadowRadius = 7.5f;
424 | }
425 |
426 | - (void)setupGestures {
427 |
428 | UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
429 | longPress.minimumPressDuration = 0.5;
430 | longPress.allowableMovement = 5.f;
431 | longPress.delaysTouchesBegan = NO;
432 | [self addGestureRecognizer:longPress];
433 |
434 | UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
435 | pan.delaysTouchesBegan = NO;
436 | [self addGestureRecognizer:pan];
437 | [pan requireGestureRecognizerToFail:longPress];
438 | }
439 |
440 | #pragma mark - Actions
441 |
442 | - (void)handlePan:(UIPanGestureRecognizer *)pan {
443 |
444 | if (pan.state == UIGestureRecognizerStateBegan) {
445 | // pan start, record some position
446 | self.effectView.highlighted = YES;
447 | UIBezierPath *path = [self.effectView updateMaskCorners:UIRectCornerAllCorners];
448 | self.layer.shadowPath = path.CGPath;
449 | } else if (pan.state == UIGestureRecognizerStateChanged) {
450 | // pan changed, update self.position
451 | CGPoint transition = [pan translationInView:pan.view];
452 | CGFloat transitionX = MAX(self.radius, MIN(self.center.x + transition.x, kSEScreenWidth() - self.radius));
453 | CGFloat transitionY = MAX(self.radius, MIN(self.center.y + transition.y, kSEScreenHeight() - self.radius));
454 | self.center = CGPointMake(transitionX, transitionY);
455 | [pan setTranslation:CGPointZero inView:pan.view];
456 | } else if (pan.state == UIGestureRecognizerStateEnded || pan.state == UIGestureRecognizerStateChanged) {
457 | [self showMoveBorderAnimation];
458 | self.effectView.highlighted = NO;
459 | }
460 | }
461 |
462 | - (void)handleLongPress:(UILongPressGestureRecognizer *)longPress {
463 |
464 | SEL selector = NULL;
465 | switch (longPress.state) {
466 | case UIGestureRecognizerStateBegan:
467 | self.effectView.highlighted = YES;
468 | selector = @selector(floatingBall:pressDidBegan:);
469 | break;
470 | case UIGestureRecognizerStateChanged:
471 | selector = @selector(floatingBall:pressDidChanged:);
472 | break;
473 | case UIGestureRecognizerStateEnded:
474 | case UIGestureRecognizerStateCancelled:
475 | self.effectView.highlighted = NO;
476 | selector = @selector(floatingBall:pressDidEnded:);
477 | break;
478 | default: break;
479 | }
480 | if (self.delegate && [self.delegate respondsToSelector:selector]) {
481 | #pragma clang diagnostic push
482 | #pragma clang diagnostic ignored"-Weverything"
483 | [self.delegate performSelector:selector withObject:self withObject:longPress];
484 | #pragma clang diagnostic pop
485 | }
486 | }
487 |
488 | #pragma mark - Animations
489 |
490 | - (void)showMoveBorderAnimation {
491 |
492 | CGFloat minX = 0.f;
493 | CGFloat maxX = kSEScreenWidth() - self.bounds.size.height;
494 | CGFloat minY = 0.f;
495 | if (@available(iOS 11.0, *)) minY = UIApplication.sharedApplication.keyWindow.safeAreaInsets.top;
496 | CGFloat maxY = kSEScreenHeight() - self.bounds.size.height;
497 | if (@available(iOS 11.0, *)) maxY = kSEScreenHeight() - self.bounds.size.height - UIApplication.sharedApplication.keyWindow.safeAreaInsets.bottom;
498 | BOOL isLeft = (self.center.x < kSEScreenWidth() / 2.0);
499 | CGPoint point = CGPointMake(isLeft ? minX : maxX, MIN(MAX(minY, self.frame.origin.y), maxY));
500 | [UIView animateWithDuration:0.15 animations:^{
501 | self.frame = (CGRect){ point, self.frame.size };
502 | } completion:^(BOOL finished) {
503 | UIBezierPath *path = [self.effectView updateMaskCorners:self.corners];
504 | self.layer.shadowPath = path.CGPath;
505 | [[NSUserDefaults standardUserDefaults] setObject:NSStringFromCGRect(self.frame) forKey:kSEFloatingBallFrameKey];
506 | }];
507 | }
508 |
509 | #pragma mark - Helpers
510 |
511 | - (CAShapeLayer *)maskLayerWithRectCorners:(UIRectCorner)corners {
512 |
513 | CGRect rect = self.bounds;
514 | CGSize size = CGSizeApplyAffineTransform(rect.size, CGAffineTransformMakeScale(0.5f, 0.5f));
515 | UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:size];
516 | CAShapeLayer *layer = [CAShapeLayer layer];
517 | layer.frame = rect;
518 | layer.path = path.CGPath;
519 | layer.fillColor = [UIColor blackColor].CGColor;
520 | return layer;
521 | }
522 |
523 | #pragma mark - Setter
524 |
525 | - (void)setFrame:(CGRect)frame {
526 | [super setFrame:frame];
527 | self.effectView.frame = self.bounds;
528 | [self.effectView updateMaskCorners:self.corners];
529 | BOOL isLeft = (self.center.x < kSEScreenWidth() / 2.0);
530 | self.autoresizingMask = isLeft ? UIViewAutoresizingFlexibleRightMargin : UIViewAutoresizingFlexibleLeftMargin;
531 | }
532 |
533 | #pragma mark - Getter
534 |
535 | - (BOOL)isAtLeft { return (self.center.x < kSEScreenWidth() / 2.0); }
536 |
537 | - (UIRectCorner)corners {
538 | return self.isAtLeft ? (UIRectCornerBottomRight | UIRectCornerTopRight) : (UIRectCornerBottomLeft | UIRectCornerTopLeft);
539 | }
540 |
541 | - (CGRect)floatingRect { return CGRectInset(self.bounds, kSEFloatingBallPadding, kSEFloatingBallPadding); }
542 |
543 | - (NSArray> *)oldItems {
544 | NSMutableArray> *oldItems = [NSMutableArray arrayWithCapacity:self.iconViews.count];
545 | for (SEFloatingBallItem *ball in self.iconViews) {
546 | if (ball.item != nil) [oldItems insertObject:ball.item atIndex:0];
547 | }
548 | return [oldItems copy];
549 | }
550 |
551 | @end
552 |
--------------------------------------------------------------------------------
/SuspensionEntrance/OC/SuspensionEntrance.m:
--------------------------------------------------------------------------------
1 | // SuspensionEntrance.m
2 | // SuspensionEntrance
3 | //
4 | // Created by XMFraker on 2019/8/8
5 | // Copyright © XMFraker All rights reserved. (https://github.com/ws00801526)
6 | // @class SuspensionEntrance
7 |
8 | #import "SuspensionEntrance.h"
9 | #import "SETransitionAnimator.h"
10 |
11 | #import "SEFloatingBall.h"
12 | #import "SEFloatingArea.h"
13 | #import "SEFloatingList.h"
14 |
15 | #import
16 |
17 |
18 | static NSString *const kSEItemClassKey = @"class";
19 | static NSString *const kSEItemTitleKey = @"title";
20 | static NSString *const kSEItemIconUrlKey = @"iconUrl";
21 | static NSString *const kSEItemUserInfoKey = @"userInfo";
22 |
23 | @interface SuspensionEntrance ()
24 |
25 | @property (strong, nonatomic) SEFloatingBall *floatingBall;
26 | @property (strong, nonatomic) SEFloatingArea *floatingArea;
27 | @property (strong, nonatomic) SEFloatingList *floatingList;
28 |
29 | @property (strong, nonatomic) SETransitionAnimator *animator;
30 | @property (strong, nonatomic) UIPercentDrivenInteractiveTransition *interactive;
31 |
32 | @property (strong, nonatomic, readwrite) NSMutableSet *ignoredClasses;
33 | @property (strong, nonatomic, readwrite) NSMutableSet *disabledClasses;
34 | @property (strong, nonatomic, readwrite) NSMutableArray *> *items;
35 | @property (strong, nonatomic, readonly) NSArray *> *unusedItems;
36 | @property (strong, nonatomic, readonly) UINavigationController *navigationController;
37 |
38 | - (void)handleKeyboardWillShow:(NSNotification *)note;
39 | - (void)handleKeyboardWillHide:(NSNotification *)note;
40 | @end
41 |
42 | @interface UIViewController (SEPrivate)
43 | @property (assign, nonatomic, readonly) BOOL se_isUsed;
44 | @property (assign, nonatomic, readonly) BOOL se_isEntrance;
45 | @property (assign, nonatomic, readonly) BOOL se_canBeEntrance;
46 | @end
47 |
48 | @implementation UIViewController (SEPrivate)
49 |
50 | - (BOOL)se_isUsed {
51 | if (!self.se_isEntrance) return false;
52 | NSInteger index = [[self.navigationController viewControllers] indexOfObject:self];
53 | if (index == NSNotFound) return false;
54 | // !!!: first index should'd be entrance. fix bug while set viewControllers contains the removed controller on iOS14.
55 | return self.presentingViewController || index != 0;
56 | }
57 |
58 | - (BOOL)se_canBeEntrance {
59 | return [[self class] conformsToProtocol:@protocol(SEItem)];
60 | }
61 |
62 | - (BOOL)se_isEntrance {
63 | if (!self.se_canBeEntrance) return NO;
64 | return [[SuspensionEntrance shared].items containsObject:(UIViewController *)self];
65 | }
66 |
67 | @end
68 |
69 | @interface NSDictionary (SEPrivate)
70 | @end
71 |
72 | @implementation NSDictionary (SEPrivate)
73 | @dynamic entranceTitle;
74 | @dynamic entranceIconUrl;
75 | @dynamic entranceUserInfo;
76 |
77 | - (Class)entranceClass {
78 |
79 | NSString *clazz = [self objectForKey:kSEItemClassKey];
80 | if (clazz.length <= 0) return NULL;
81 | return NSClassFromString(clazz);
82 | }
83 |
84 | - (NSString *)entranceTitle { return [self objectForKey:kSEItemTitleKey]; }
85 | - (NSURL *)entranceIconUrl { return [self objectForKey:kSEItemIconUrlKey]; }
86 | - (NSDictionary *)entranceUserInfo { return [self objectForKey:kSEItemUserInfoKey]; }
87 |
88 | @end
89 |
90 | #import
91 | static NSString *const kSEItemIconTask;
92 |
93 | @implementation UIImageView (SEPrivate)
94 |
95 | - (void)se_setImageWithItem:(id)item {
96 |
97 | NSURLSessionDataTask *task = objc_getAssociatedObject(self, &kSEItemIconTask);
98 | if (task && [task.originalRequest.URL isEqual:item.entranceIconUrl]) { return; }
99 | if (task) [task cancel];
100 |
101 | __weak typeof(self) wSelf = self;
102 | self.backgroundColor = [UIColor colorWithWhite:0.90f alpha:1.f];
103 | task = [[NSURLSession sharedSession] dataTaskWithURL:item.entranceIconUrl completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
104 | if (!data) return;
105 | UIImage *image = [UIImage imageWithData:data scale:2.f];
106 | if (!image) return;
107 | dispatch_async(dispatch_get_main_queue(), ^{
108 | __strong typeof(wSelf) self = wSelf;
109 | self.image = image;
110 | self.backgroundColor = [UIColor colorWithWhite:0.90f alpha:1.f];
111 | });
112 | }];
113 | [task resume];
114 | objc_setAssociatedObject(self, &kSEItemIconTask, task, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
115 | }
116 |
117 | @end
118 |
119 | @implementation UIViewController (Private)
120 |
121 | - (void)se_viewWillAppear:(BOOL)animated {
122 |
123 | [self se_viewWillAppear:animated];
124 |
125 | SuspensionEntrance *entrance = [SuspensionEntrance shared];
126 | if (!entrance.isAvailable || entrance.interactive) return;
127 | if ([entrance.ignoredClasses containsObject:[self class]]) return;
128 | if ([NSStringFromClass([self class]) containsString:@"JXSegmentedView"]) return;
129 |
130 | // UITabBarController *vc = (UITabBarController *)[UIApplication sharedApplication].keyWindow.rootViewController;
131 | // if ([vc isKindOfClass:[UITabBarController class]]) {
132 | // vc = [vc selectedViewController];
133 | // if ([vc isKindOfClass:[UINavigationController class]]) vc = [(UINavigationController *)vc visibleViewController];
134 | // if (vc == self) return;
135 | // }
136 |
137 | BOOL visible = entrance.isAvailable && entrance.floatingBall.superview && (entrance.unusedItems.count >= 1);
138 | if (self.presentingViewController != nil || self.navigationController == nil) visible = NO;
139 | if ([entrance.disabledClasses containsObject:[self class]]) visible = NO;
140 | if (visible) {
141 | if (entrance.floatingBall.alpha < 1.f) [UIView animateWithDuration:0.25f animations:^{ entrance.floatingBall.alpha = 1.f; }];
142 | #ifdef __IPHONE_13_0
143 | if (@available(iOS 13.0, *)) [entrance.floatingBall.superview bringSubviewToFront:entrance.floatingBall];
144 | #endif
145 | } else { entrance.floatingBall.alpha = .0f; }
146 | }
147 |
148 | @end
149 |
150 | @implementation SuspensionEntrance
151 | @synthesize window = _window;
152 |
153 | #pragma mark - Life
154 |
155 | + (void)initialize {
156 | if (self == [SuspensionEntrance class]) {
157 | SEL originalSelector = @selector(viewWillAppear:);
158 | SEL swizzledSelector = @selector(se_viewWillAppear:);
159 | Method originalMethod = class_getInstanceMethod([UIViewController class], originalSelector);
160 | Method swizzledMethod = class_getInstanceMethod([UIViewController class], swizzledSelector);
161 | method_exchangeImplementations(originalMethod, swizzledMethod);
162 | }
163 | }
164 |
165 | - (instancetype)init {
166 |
167 | self = [super init];
168 | if (self) {
169 |
170 | _maxCount = 5;
171 | _vibratable = YES;
172 | _available = YES;
173 |
174 | _items = [NSMutableArray array];
175 | _ignoredClasses = [NSMutableSet set];
176 | _disabledClasses = [NSMutableSet set];
177 | _archivedPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"entrance.items"];
178 |
179 | _floatingBall = [[SEFloatingBall alloc] initWithFrame:CGRectZero];
180 | _floatingBall.delegate = (id)self;
181 |
182 | _floatingArea = [[SEFloatingArea alloc] initWithFrame:CGRectZero];
183 |
184 | _floatingList = [[SEFloatingList alloc] initWithFrame:CGRectZero];
185 | _floatingList.delegate = (id)self;
186 |
187 | _iconHandler = ^(UIImageView *iconView, id item) {
188 | [iconView se_setImageWithItem:item];
189 | };
190 |
191 | // register keyboard notification to hide floating ball
192 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
193 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
194 | }
195 |
196 | // need to get in next main loop, otherwise self.window may be nil
197 | dispatch_async(dispatch_get_main_queue(), ^ { [self unarchiveEntranceItems]; });
198 | return self;
199 | }
200 |
201 | #pragma mark - Public
202 |
203 | - (BOOL)isEntranceItem:(__kindof UIViewController *)item {
204 |
205 | if (![item conformsToProtocol:@protocol(SEItem)]) return NO;
206 | return [self.items containsObject:(UIViewController *)item];
207 | }
208 |
209 | - (void)addEntranceItem:(__kindof UIViewController *)item {
210 |
211 | if ([self isEntranceItem:item]) return;
212 | if (self->_items.count >= self.maxCount) {
213 | [self showItemsFullAlert];
214 | return;
215 | }
216 | [self->_items addObject:item];
217 | [self.floatingBall reloadIconViews:self.items];
218 | [self.floatingList reloadData];
219 | [self archiveEntranceItems];
220 | // !!! Fixed: add first entrance item, the floating ball doesnot has superview
221 | if (self.floatingBall.superview == nil) { [self.window addSubview:self.floatingBall]; self.floatingBall.alpha = 1.f; }
222 | if (self.navigationController.viewControllers.lastObject == item)
223 | [self.navigationController popViewControllerAnimated:YES];
224 | }
225 |
226 | - (void)cancelEntranceItem:(__kindof UIViewController *)item {
227 |
228 | if (![self isEntranceItem:item]) return;
229 | [self->_items removeObject:item];
230 | [self.floatingBall reloadIconViews:self.items];
231 | [self.floatingList reloadData];
232 | [self archiveEntranceItems];
233 | }
234 |
235 | - (void)clearEntranceItems {
236 | [self->_items removeAllObjects];
237 | [self.floatingBall reloadIconViews:self.items];
238 | [self.floatingList reloadData];
239 | [self.floatingBall removeFromSuperview];
240 | [[NSFileManager defaultManager] removeItemAtPath:self.archivedPath error:nil];
241 | }
242 |
243 | #pragma mark - Private
244 |
245 | - (void)pushEntranceItem:(UIViewController *)item {
246 |
247 | if (![self.items containsObject:item]) return;
248 | if ([item isKindOfClass:[UINavigationController class]]) {
249 | [self.navigationController presentViewController:item animated:YES completion:NULL];
250 | } else {
251 | NSMutableArray *viewControllers = [self.navigationController.viewControllers mutableCopy];
252 | if ([viewControllers containsObject:item]) {
253 | [self.navigationController popToViewController:item animated:YES];
254 | } else {
255 | if (viewControllers.lastObject.se_isEntrance) { [viewControllers removeLastObject]; }
256 | [viewControllers addObject:item];
257 | if (@available(iOS 17, *)) {
258 | [self.navigationController setViewControllers:[viewControllers copy] animated:false];
259 | } else {
260 | [self.navigationController setViewControllers:[viewControllers copy] animated:YES];
261 | }
262 | }
263 | }
264 | }
265 |
266 | - (CGRect)floatingRectOfOperation:(UINavigationControllerOperation)operation {
267 | CGRect rect = CGRectZero;
268 | switch (operation) {
269 | case UINavigationControllerOperationPush:
270 | rect = [self.window convertRect:self.floatingList.floatingRect fromView:self.floatingList];
271 | break;
272 | case UINavigationControllerOperationPop:
273 | rect = [self.window convertRect:self.floatingBall.floatingRect fromView:self.floatingBall];
274 | break;
275 | default: break;
276 | }
277 | if (CGRectIsEmpty(rect)) rect = self.floatingBall.frame;
278 | return rect;
279 | }
280 |
281 | - (void)showItemsFullAlert {
282 |
283 | NSString *message = [NSString stringWithFormat:@"最多设置%d个浮窗", (int)self.maxCount];
284 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert];
285 | __weak typeof(self) wSelf = self;
286 | UIAlertAction *confirm = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
287 | __strong typeof(wSelf) self = wSelf;
288 | [self handleKeyboardWillHide:nil];
289 | }];
290 | [alert addAction:confirm];
291 | [self.navigationController.visibleViewController showDetailViewController:alert sender:nil];
292 | }
293 |
294 | - (void)archiveEntranceItems {
295 |
296 | NSMutableArray *infos = [NSMutableArray array];
297 | for (UIViewController *item in self.items) {
298 | [infos addObject:@{
299 | kSEItemClassKey : NSStringFromClass(item.class),
300 | kSEItemTitleKey : item.entranceTitle ? : @"",
301 | kSEItemIconUrlKey : item.entranceIconUrl ? : [NSURL URLWithString:@""],
302 | kSEItemUserInfoKey : item.entranceUserInfo ? : @{}
303 | }];
304 | }
305 |
306 | #if DEBUG
307 | BOOL succ = [NSKeyedArchiver archiveRootObject:infos toFile:self.archivedPath];
308 | if (!succ) { NSLog(@"archive entrance items failed :%@", self.archivedPath); }
309 | #else
310 | [NSKeyedArchiver archiveRootObject:infos toFile:self.archivedPath];
311 | #endif
312 | }
313 |
314 | - (void)unarchiveEntranceItems {
315 |
316 | NSArray *infos = [NSKeyedUnarchiver unarchiveObjectWithFile:self.archivedPath];
317 | if (infos.count <= 0) return;
318 |
319 | for (NSDictionary *info in infos) {
320 |
321 | if (info.entranceClass == NULL) continue;
322 | if (info.entranceTitle.length <= 0) continue;
323 | if (![info.entranceClass respondsToSelector:@selector(entranceWithItem:)]) continue;
324 |
325 | UIViewController *item = [info.entranceClass entranceWithItem:info];
326 | if (!item || ![item isKindOfClass:[UIViewController class]]) continue;
327 |
328 | [self->_items addObject:item];
329 | }
330 |
331 | [self.floatingList reloadData];
332 | [self.floatingBall reloadIconViews:self.items];
333 | if (self.items.count <= 0 || !self.isAvailable) { [self.floatingBall removeFromSuperview]; }
334 | else if (!self.floatingBall.superview) { self.floatingBall.alpha = 1.f; [self.window addSubview:self.floatingBall]; }
335 | else { self.floatingBall.alpha = 1.f; [self.window bringSubviewToFront:self.floatingBall]; }
336 | }
337 |
338 | - (void)handleKeyboardWillShow:(NSNotification *)note {
339 |
340 | BOOL visible = self.floatingBall.superview && self.floatingBall.alpha >= 1.f;
341 | if (!visible) return;
342 | [UIView animateWithDuration:.25f animations:^ { self.floatingBall.alpha = .0f; }];
343 | }
344 |
345 | - (void)handleKeyboardWillHide:(NSNotification *)note {
346 |
347 | BOOL visible = self.floatingBall.superview && self.unusedItems.count >= 1;
348 | if (!visible || self.floatingBall.alpha >= 1.f) return;
349 | [UIView animateWithDuration:.25f animations:^ { self.floatingBall.alpha = 1.0f; }];
350 | }
351 |
352 | #pragma mark - Actions
353 |
354 | - (void)handleTransition:(UIScreenEdgePanGestureRecognizer *)pan {
355 |
356 | CGFloat const SCREEN_WIDTH = UIScreen.mainScreen.bounds.size.width;
357 | CGFloat const SCREEN_HEIGHT = UIScreen.mainScreen.bounds.size.height;
358 | UIViewController *tempItem = (UIViewController *)pan.view.nextResponder;
359 | BOOL isPresented = tempItem.presentingViewController != nil;
360 | switch (pan.state) {
361 |
362 | case UIGestureRecognizerStateBegan:
363 | self.interactive = [[UIPercentDrivenInteractiveTransition alloc] init];
364 | if (isPresented) [tempItem dismissViewControllerAnimated:YES completion:NULL];
365 | else [tempItem.navigationController popViewControllerAnimated:YES];
366 | if (tempItem.se_isEntrance) [self.floatingArea removeFromSuperview];
367 | else if (!self.floatingArea.superview && self.isAvailable) [self.window addSubview:self.floatingArea];
368 | self.floatingArea.enabled = self.items.count < self.maxCount;
369 | break;
370 | case UIGestureRecognizerStateChanged:
371 | {
372 | CGPoint tPoint = [pan translationInView:self.window];
373 | if (self.floatingArea.superview) {
374 | CGFloat x = MAX(SCREEN_WIDTH - tPoint.x - kSEFloatingAreaWidth / 4.f, SCREEN_WIDTH - kSEFloatingAreaWidth);
375 | CGFloat y = MAX(SCREEN_HEIGHT - tPoint.x - kSEFloatingAreaWidth / 4.f, SCREEN_HEIGHT - kSEFloatingAreaWidth);
376 | self.floatingArea.frame = (CGRect){ CGPointMake(x, y), self.floatingArea.bounds.size };
377 |
378 | CGPoint innerPoint = [pan locationInView:self.window];
379 | self.floatingArea.highlighted = kSEFloatAreaContainsPoint(innerPoint);
380 | }
381 |
382 | [self.animator updateContinousAnimationPercent:tPoint.x / SCREEN_WIDTH];
383 | [self.interactive updateInteractiveTransition:tPoint.x / SCREEN_WIDTH];
384 |
385 | if (self.floatingBall.alpha < 1.f && self.unusedItems.count >= 1) self.floatingBall.alpha = tPoint.x / SCREEN_WIDTH;
386 | }
387 | break;
388 | case UIGestureRecognizerStateEnded: // fall through
389 | case UIGestureRecognizerStateCancelled:
390 | {
391 | CGPoint point = [pan locationInView:self.window];
392 | CGPoint vPoint = [pan velocityInView:self.window];
393 | CGFloat vPointX = vPoint.x * [self.animator animationDuration];
394 | // 判断快速滑动是否超过屏幕1/2
395 | if (fmax(vPointX, point.x) >= SCREEN_WIDTH / 2.f) {
396 | if (self.floatingArea.superview && self.floatingArea.isHighlighted) {
397 | if (self.floatingArea.isEnabled) {
398 | // floating is available
399 | if (!self.floatingBall.superview && self.isAvailable) { [self.window addSubview:self.floatingBall]; }
400 | if (![self.items containsObject:tempItem]) { [self->_items addObject:tempItem]; }
401 | [self archiveEntranceItems];
402 | [self.animator finishContinousAnimation];
403 | [self.interactive finishInteractiveTransition];
404 | [self.floatingList reloadData];
405 | [self.floatingBall reloadIconViews:self.items];
406 | } else {
407 | // floating is full
408 | [self.animator cancelContinousAnimation];
409 | [self.interactive cancelInteractiveTransition];
410 | [self showItemsFullAlert];
411 | }
412 | } else if (tempItem.se_isEntrance) {
413 | // just ended
414 | [self.animator finishContinousAnimation];
415 | [self.interactive finishInteractiveTransition];
416 | } else {
417 | // just ended
418 | [self.animator finishContinousAnimationWithFastAnimating:YES];
419 | [self.interactive finishInteractiveTransition];
420 | }
421 | } else {
422 | [self.animator cancelContinousAnimation];
423 | [self.interactive cancelInteractiveTransition];
424 | }
425 | self.interactive = nil;
426 | [self.floatingArea removeFromSuperview];
427 | [UIView animateWithDuration:.25 animations:^{ self.floatingBall.alpha = (self.unusedItems.count >= 1) ? 1.f : .0f; }];
428 | }
429 | break;
430 | default: break;
431 | }
432 | }
433 |
434 | #pragma mark - SEFloatingBallDelegate
435 |
436 | - (void)floatingBallDidClicked:(SEFloatingBall *)floatingBall {
437 | // will show floating list
438 | self.floatingList.editable = YES;
439 | if (!self.floatingList.superview) [self.window addSubview:self.floatingList];
440 | [self.floatingList showAtRect:floatingBall.frame animated:YES];
441 | }
442 |
443 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidBegan:(UILongPressGestureRecognizer *)gesture {
444 | // will show floating list
445 | self.floatingList.editable = NO;
446 | if (!self.floatingList.superview) [self.window addSubview:self.floatingList];
447 | [self.floatingList showAtRect:floatingBall.frame animated:YES];
448 | }
449 |
450 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidChanged:(UILongPressGestureRecognizer *)gesture {
451 | // will highlight the item in floating list
452 | CGPoint point = [gesture locationInView:gesture.view];
453 | point = [self.floatingList convertPoint:point fromView:gesture.view];
454 | for (SEFloatingListItem *listItem in self.floatingList.listItems) {
455 | listItem.selected = CGRectContainsPoint(listItem.frame, point);
456 | }
457 | }
458 |
459 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidEnded:(UILongPressGestureRecognizer *)gesture {
460 | // will end, check floating list is selected
461 | for (SEFloatingListItem *listItem in self.floatingList.listItems) {
462 | if (listItem.isSelected) {
463 | [self pushEntranceItem:(UIViewController *)listItem.item];
464 | break;
465 | }
466 | }
467 | [self.floatingList dismissWithAnimated:YES];
468 | }
469 |
470 | #pragma mark - SEFloatingListDelegate
471 |
472 | - (NSUInteger)numberOfItemsInFloatingList:(SEFloatingList *)list {
473 | return self.items.count;
474 | }
475 |
476 | - (id)floatingList:(SEFloatingList *)list itemAtIndex:(NSUInteger)index {
477 | return [self.items objectAtIndex:index];
478 | }
479 |
480 | - (void)floatingList:(SEFloatingList *)list didSelectItem:(id)item {
481 | [self pushEntranceItem:(UIViewController *)item];
482 | }
483 |
484 | - (BOOL)floatingList:(SEFloatingList *)list willDeleteItem:(id)item {
485 | if (![self.items containsObject:(UIViewController *)item]) return NO;
486 | [self->_items removeObject:(UIViewController *)item];
487 | [self archiveEntranceItems];
488 | return YES;
489 | }
490 |
491 | - (BOOL)floatingList:(SEFloatingList *)list isItemVisible:(id)item {
492 | return !((UIViewController *)item).se_isUsed;
493 | }
494 |
495 | - (void)floatingListWillShow:(SEFloatingList *)list {
496 | [UIView animateWithDuration:0.25 animations:^{ self.floatingBall.alpha = .0f; }];
497 | }
498 |
499 | - (void)floatingListWillHide:(SEFloatingList *)list {
500 |
501 | NSArray *> *unusedItems = self.unusedItems;
502 | // [self.floatingBall reloadIconViews:unusedItems];
503 |
504 | CGFloat alpha = unusedItems.count >= 1 ? 1.f : self.floatingBall.alpha;
505 | [UIView animateWithDuration:0.25 animations:^{ self.floatingBall.alpha = alpha; }];
506 | }
507 |
508 | #pragma mark - Setter
509 |
510 | - (void)setMaxCount:(NSUInteger)maxCount {
511 | _maxCount = MAX(1, MIN(5, maxCount));
512 | }
513 |
514 | - (void)setWindow:(UIWindow *)window {
515 | if (_window == window) return;
516 | _window = window;
517 | if (self.floatingBall.superview) [self.floatingBall removeFromSuperview];
518 | if (self.isAvailable && self.items.count) { [window addSubview:self.floatingBall]; }
519 | }
520 |
521 | - (void)setAvailable:(BOOL)available {
522 |
523 | _available = available;
524 | if (!available) {
525 | [self.floatingBall removeFromSuperview];
526 | } else {
527 | if (self.floatingBall.superview) [self.floatingBall.superview bringSubviewToFront:self.floatingBall];
528 | else if (self.items.count >= 1) [self.window addSubview:self.floatingBall];
529 | }
530 | }
531 |
532 | - (void)setArchivedPath:(NSString *)archivedPath {
533 |
534 | if ([_archivedPath isEqualToString:archivedPath]) return;
535 | _archivedPath = archivedPath;
536 | [self->_items removeAllObjects];
537 | [self.floatingBall removeFromSuperview];
538 | [self unarchiveEntranceItems];
539 | }
540 |
541 | #pragma mark - Getter
542 |
543 | - (UIWindow *)window { return _window ? : [UIApplication sharedApplication].keyWindow; }
544 |
545 | - (UINavigationController *)navigationController {
546 |
547 | __kindof UIViewController *controller = self.window.rootViewController;
548 | if ([controller isKindOfClass:[UITabBarController class]]) { controller = [(UITabBarController *)controller selectedViewController]; }
549 | while (controller.presentedViewController) { controller = controller.presentedViewController; }
550 |
551 | if ([controller isKindOfClass:[UINavigationController class]]) return controller;
552 | if (controller.navigationController) return controller.navigationController;
553 | return nil;
554 | }
555 |
556 | - (NSArray *> *)unusedItems {
557 | return [self.items filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF.se_isUsed = NO"]];
558 | }
559 |
560 | #pragma mark - Class
561 |
562 | + (instancetype)shared {
563 | static SuspensionEntrance *instance;
564 | static dispatch_once_t onceToken;
565 | dispatch_once(&onceToken, ^{
566 | instance = [[super allocWithZone:nil] init];
567 | });
568 | return instance;
569 | }
570 |
571 | + (instancetype)allocWithZone:(struct _NSZone *)zone { return [SuspensionEntrance shared]; }
572 |
573 | @end
574 |
575 | @implementation SuspensionEntrance (NavigationControllerDelegate)
576 |
577 | - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
578 |
579 | self.animator = nil;
580 | self.interactive = nil;
581 | [self.floatingBall reloadIconViews:self.unusedItems];
582 |
583 | if (![viewController se_canBeEntrance]) return;
584 | if (navigationController.viewControllers.count <= 1) return;
585 |
586 | NSArray *gestures = [viewController.view.gestureRecognizers copy];
587 | for (UIGestureRecognizer *gesture in gestures) {
588 | if ([gesture isKindOfClass:[UIScreenEdgePanGestureRecognizer class]] && gesture.delegate == self) {
589 | // may be this gesture is add before, remove it
590 | [viewController.view removeGestureRecognizer:gesture];
591 | }
592 | }
593 |
594 | UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTransition:)];
595 | pan.edges = UIRectEdgeLeft;
596 | pan.delegate = self;
597 | [viewController.view addGestureRecognizer:pan];
598 | }
599 |
600 | - (id)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id)animationController {
601 |
602 | return self.interactive;
603 | }
604 |
605 | - (id)navigationController:(UINavigationController *)navigationController
606 | animationControllerForOperation:(UINavigationControllerOperation)operation
607 | fromViewController:(UIViewController *)fromVC
608 | toViewController:(UIViewController *)toVC {
609 |
610 | if (operation == UINavigationControllerOperationPop) {
611 | if (self.interactive) {
612 | CGRect const floatingRect = [self floatingRectOfOperation:operation];
613 | self.animator = [SETransitionAnimator continuousPopAnimatorWithRect:floatingRect];
614 | } else if (fromVC.se_isEntrance) {
615 | CGRect const floatingRect = [self floatingRectOfOperation:operation];
616 | self.animator = [SETransitionAnimator roundPopAnimatorWithRect:floatingRect];
617 | } else {
618 | self.animator = nil;
619 | }
620 | } else if (operation == UINavigationControllerOperationPush) {
621 | if ((toVC.se_isEntrance && fromVC.se_isEntrance) || (toVC.se_isEntrance && !fromVC.se_isEntrance)) {
622 | CGRect const floatingRect = [self floatingRectOfOperation:operation];
623 | self.animator = [SETransitionAnimator roundPushAnimatorWithRect:floatingRect];
624 | } else {
625 | self.animator = nil;
626 | }
627 | // [self.floatingBall reloadIconViews:self.unusedItems];
628 | }
629 | return self.animator;
630 | }
631 |
632 | @end
633 |
634 | @implementation SuspensionEntrance (TransitioningDelegate)
635 |
636 | - (id)animationControllerForDismissedController:(UIViewController *)dismissed {
637 |
638 | if (self.interactive) {
639 | CGRect const floatingRect = [self floatingRectOfOperation:UINavigationControllerOperationPop];
640 | self.animator = [SETransitionAnimator continuousPopAnimatorWithRect:floatingRect];
641 | } else if (dismissed.se_isEntrance) {
642 | CGRect const floatingRect = [self floatingRectOfOperation:UINavigationControllerOperationPop];
643 | self.animator = [SETransitionAnimator roundPopAnimatorWithRect:floatingRect];
644 | } else {
645 | self.animator = nil;
646 | }
647 | return self.animator;
648 | }
649 |
650 | - (id)animationControllerForPresentedController:(UIViewController *)toVC presentingController:(UIViewController *)fromVC sourceController:(UIViewController *)source {
651 |
652 | if ((toVC.se_isEntrance && fromVC.se_isEntrance) || (toVC.se_isEntrance && !fromVC.se_isEntrance)) {
653 | CGRect const floatingRect = [self floatingRectOfOperation:UINavigationControllerOperationPush];
654 | self.animator = [SETransitionAnimator roundPushAnimatorWithRect:floatingRect];
655 | } else {
656 | self.animator = nil;
657 | }
658 |
659 | // NSArray *> *unusedItems = self.unusedItems;
660 | // [self.floatingBall reloadIconViews:unusedItems];
661 |
662 | NSArray *gestures = [toVC.view.gestureRecognizers copy];
663 | for (UIGestureRecognizer *gesture in gestures) {
664 | if ([gesture isKindOfClass:[UIScreenEdgePanGestureRecognizer class]] && gesture.delegate == self) {
665 | // may be this gesture is add before, remove it
666 | [toVC.view removeGestureRecognizer:gesture];
667 | }
668 | }
669 |
670 | UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTransition:)];
671 | pan.edges = UIRectEdgeLeft;
672 | pan.delegate = self;
673 | [toVC.view addGestureRecognizer:pan];
674 |
675 | return self.animator;
676 | }
677 |
678 | //- (nullable id )interactionControllerForPresentation:(id )animator;
679 |
680 | - (nullable id )interactionControllerForDismissal:(id )animator {
681 | return self.interactive;
682 | }
683 |
684 | @end
685 |
--------------------------------------------------------------------------------