├── LICENSE ├── README.md ├── SuspensionEntrance.podspec ├── SuspensionEntrance.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── XMFraker.xcuserdatad │ │ └── UserInterfaceState.xcuserstate ├── xcshareddata │ └── xcschemes │ │ └── SuspensionEntrance.xcscheme └── xcuserdata │ └── XMFraker.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── SuspensionEntrance ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── blur_background.imageset │ │ ├── Contents.json │ │ ├── 矩形@2x.png │ │ └── 矩形@3x.png │ ├── favicon.imageset │ │ ├── Contents.json │ │ └── favicon.png │ └── web_entrance_close.imageset │ │ ├── Contents.json │ │ ├── 关闭@2x.png │ │ └── 关闭@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── BaseNavigationController.h ├── BaseNavigationController.m ├── EntranceViewController.h ├── EntranceViewController.m ├── Info.plist ├── NormalViewController.h ├── NormalViewController.m ├── OC │ ├── SEFloatingArea.h │ ├── SEFloatingArea.m │ ├── SEFloatingBall.h │ ├── SEFloatingBall.m │ ├── SEFloatingList.h │ ├── SEFloatingList.m │ ├── SETransitionAnimator.h │ ├── SETransitionAnimator.m │ ├── SuspensionEntrance.h │ └── SuspensionEntrance.m ├── PresentNavigationController.h ├── PresentNavigationController.m └── main.m └── preview.gif /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SuspensionEntrance 2 | 3 | 仿微信新版的悬浮窗入口功能 4 | 5 | ![Preview](https://github.com/ws00801526/SuspensionEntrance/blob/master/preview.gif) 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.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.2' 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.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.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SuspensionEntrance.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SuspensionEntrance.xcodeproj/project.xcworkspace/xcuserdata/XMFraker.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/de9d70ead75937b316e982ae70b93503f4602494/SuspensionEntrance.xcodeproj/project.xcworkspace/xcuserdata/XMFraker.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /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.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/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/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/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/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /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/blur_background.imageset/矩形@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/de9d70ead75937b316e982ae70b93503f4602494/SuspensionEntrance/Assets.xcassets/blur_background.imageset/矩形@2x.png -------------------------------------------------------------------------------- /SuspensionEntrance/Assets.xcassets/blur_background.imageset/矩形@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/de9d70ead75937b316e982ae70b93503f4602494/SuspensionEntrance/Assets.xcassets/blur_background.imageset/矩形@3x.png -------------------------------------------------------------------------------- /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/Assets.xcassets/favicon.imageset/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/de9d70ead75937b316e982ae70b93503f4602494/SuspensionEntrance/Assets.xcassets/favicon.imageset/favicon.png -------------------------------------------------------------------------------- /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/Assets.xcassets/web_entrance_close.imageset/关闭@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/de9d70ead75937b316e982ae70b93503f4602494/SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/关闭@2x.png -------------------------------------------------------------------------------- /SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/关闭@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/de9d70ead75937b316e982ae70b93503f4602494/SuspensionEntrance/Assets.xcassets/web_entrance_close.imageset/关闭@3x.png -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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.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 | -------------------------------------------------------------------------------- /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/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/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/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/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, padding, frame.size.width - 55.f - padding - 50.f, 40.f)]; 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.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/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/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/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 | [self.navigationController setViewControllers:[viewControllers copy] animated:YES]; 258 | } 259 | } 260 | } 261 | 262 | - (CGRect)floatingRectOfOperation:(UINavigationControllerOperation)operation { 263 | CGRect rect = CGRectZero; 264 | switch (operation) { 265 | case UINavigationControllerOperationPush: 266 | rect = [self.window convertRect:self.floatingList.floatingRect fromView:self.floatingList]; 267 | break; 268 | case UINavigationControllerOperationPop: 269 | rect = [self.window convertRect:self.floatingBall.floatingRect fromView:self.floatingBall]; 270 | break; 271 | default: break; 272 | } 273 | if (CGRectIsEmpty(rect)) rect = self.floatingBall.frame; 274 | return rect; 275 | } 276 | 277 | - (void)showItemsFullAlert { 278 | 279 | NSString *message = [NSString stringWithFormat:@"最多设置%d个浮窗", (int)self.maxCount]; 280 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert]; 281 | __weak typeof(self) wSelf = self; 282 | UIAlertAction *confirm = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) { 283 | __strong typeof(wSelf) self = wSelf; 284 | [self handleKeyboardWillHide:nil]; 285 | }]; 286 | [alert addAction:confirm]; 287 | [self.navigationController.visibleViewController showDetailViewController:alert sender:nil]; 288 | } 289 | 290 | - (void)archiveEntranceItems { 291 | 292 | NSMutableArray *infos = [NSMutableArray array]; 293 | for (UIViewController *item in self.items) { 294 | [infos addObject:@{ 295 | kSEItemClassKey : NSStringFromClass(item.class), 296 | kSEItemTitleKey : item.entranceTitle ? : @"", 297 | kSEItemIconUrlKey : item.entranceIconUrl ? : [NSURL URLWithString:@""], 298 | kSEItemUserInfoKey : item.entranceUserInfo ? : @{} 299 | }]; 300 | } 301 | 302 | #if DEBUG 303 | BOOL succ = [NSKeyedArchiver archiveRootObject:infos toFile:self.archivedPath]; 304 | if (!succ) { NSLog(@"archive entrance items failed :%@", self.archivedPath); } 305 | #else 306 | [NSKeyedArchiver archiveRootObject:infos toFile:self.archivedPath]; 307 | #endif 308 | } 309 | 310 | - (void)unarchiveEntranceItems { 311 | 312 | NSArray *infos = [NSKeyedUnarchiver unarchiveObjectWithFile:self.archivedPath]; 313 | if (infos.count <= 0) return; 314 | 315 | for (NSDictionary *info in infos) { 316 | 317 | if (info.entranceClass == NULL) continue; 318 | if (info.entranceTitle.length <= 0) continue; 319 | if (![info.entranceClass respondsToSelector:@selector(entranceWithItem:)]) continue; 320 | 321 | UIViewController *item = [info.entranceClass entranceWithItem:info]; 322 | if (!item || ![item isKindOfClass:[UIViewController class]]) continue; 323 | 324 | [self->_items addObject:item]; 325 | } 326 | 327 | [self.floatingList reloadData]; 328 | [self.floatingBall reloadIconViews:self.items]; 329 | if (self.items.count <= 0 || !self.isAvailable) { [self.floatingBall removeFromSuperview]; } 330 | else if (!self.floatingBall.superview) { self.floatingBall.alpha = 1.f; [self.window addSubview:self.floatingBall]; } 331 | else { self.floatingBall.alpha = 1.f; [self.window bringSubviewToFront:self.floatingBall]; } 332 | } 333 | 334 | - (void)handleKeyboardWillShow:(NSNotification *)note { 335 | 336 | BOOL visible = self.floatingBall.superview && self.floatingBall.alpha >= 1.f; 337 | if (!visible) return; 338 | [UIView animateWithDuration:.25f animations:^ { self.floatingBall.alpha = .0f; }]; 339 | } 340 | 341 | - (void)handleKeyboardWillHide:(NSNotification *)note { 342 | 343 | BOOL visible = self.floatingBall.superview && self.unusedItems.count >= 1; 344 | if (!visible || self.floatingBall.alpha >= 1.f) return; 345 | [UIView animateWithDuration:.25f animations:^ { self.floatingBall.alpha = 1.0f; }]; 346 | } 347 | 348 | #pragma mark - Actions 349 | 350 | - (void)handleTransition:(UIScreenEdgePanGestureRecognizer *)pan { 351 | 352 | CGFloat const SCREEN_WIDTH = UIScreen.mainScreen.bounds.size.width; 353 | CGFloat const SCREEN_HEIGHT = UIScreen.mainScreen.bounds.size.height; 354 | UIViewController *tempItem = (UIViewController *)pan.view.nextResponder; 355 | BOOL isPresented = tempItem.presentingViewController != nil; 356 | switch (pan.state) { 357 | 358 | case UIGestureRecognizerStateBegan: 359 | self.interactive = [[UIPercentDrivenInteractiveTransition alloc] init]; 360 | if (isPresented) [tempItem dismissViewControllerAnimated:YES completion:NULL]; 361 | else [tempItem.navigationController popViewControllerAnimated:YES]; 362 | if (tempItem.se_isEntrance) [self.floatingArea removeFromSuperview]; 363 | else if (!self.floatingArea.superview && self.isAvailable) [self.window addSubview:self.floatingArea]; 364 | self.floatingArea.enabled = self.items.count < self.maxCount; 365 | break; 366 | case UIGestureRecognizerStateChanged: 367 | { 368 | CGPoint tPoint = [pan translationInView:self.window]; 369 | if (self.floatingArea.superview) { 370 | CGFloat x = MAX(SCREEN_WIDTH - tPoint.x - kSEFloatingAreaWidth / 4.f, SCREEN_WIDTH - kSEFloatingAreaWidth); 371 | CGFloat y = MAX(SCREEN_HEIGHT - tPoint.x - kSEFloatingAreaWidth / 4.f, SCREEN_HEIGHT - kSEFloatingAreaWidth); 372 | self.floatingArea.frame = (CGRect){ CGPointMake(x, y), self.floatingArea.bounds.size }; 373 | 374 | CGPoint innerPoint = [pan locationInView:self.window]; 375 | self.floatingArea.highlighted = kSEFloatAreaContainsPoint(innerPoint); 376 | } 377 | 378 | [self.animator updateContinousAnimationPercent:tPoint.x / SCREEN_WIDTH]; 379 | [self.interactive updateInteractiveTransition:tPoint.x / SCREEN_WIDTH]; 380 | 381 | if (self.floatingBall.alpha < 1.f && self.unusedItems.count >= 1) self.floatingBall.alpha = tPoint.x / SCREEN_WIDTH; 382 | } 383 | break; 384 | case UIGestureRecognizerStateEnded: // fall through 385 | case UIGestureRecognizerStateCancelled: 386 | { 387 | CGPoint point = [pan locationInView:self.window]; 388 | CGPoint vPoint = [pan velocityInView:self.window]; 389 | CGFloat vPointX = vPoint.x * [self.animator animationDuration]; 390 | // 判断快速滑动是否超过屏幕1/2 391 | if (fmax(vPointX, point.x) >= SCREEN_WIDTH / 2.f) { 392 | if (self.floatingArea.superview && self.floatingArea.isHighlighted) { 393 | if (self.floatingArea.isEnabled) { 394 | // floating is available 395 | if (!self.floatingBall.superview && self.isAvailable) { [self.window addSubview:self.floatingBall]; } 396 | if (![self.items containsObject:tempItem]) { [self->_items addObject:tempItem]; } 397 | [self archiveEntranceItems]; 398 | [self.animator finishContinousAnimation]; 399 | [self.interactive finishInteractiveTransition]; 400 | [self.floatingList reloadData]; 401 | [self.floatingBall reloadIconViews:self.items]; 402 | } else { 403 | // floating is full 404 | [self.animator cancelContinousAnimation]; 405 | [self.interactive cancelInteractiveTransition]; 406 | [self showItemsFullAlert]; 407 | } 408 | } else if (tempItem.se_isEntrance) { 409 | // just ended 410 | [self.animator finishContinousAnimation]; 411 | [self.interactive finishInteractiveTransition]; 412 | } else { 413 | // just ended 414 | [self.animator finishContinousAnimationWithFastAnimating:YES]; 415 | [self.interactive finishInteractiveTransition]; 416 | } 417 | } else { 418 | [self.animator cancelContinousAnimation]; 419 | [self.interactive cancelInteractiveTransition]; 420 | } 421 | self.interactive = nil; 422 | [self.floatingArea removeFromSuperview]; 423 | [UIView animateWithDuration:.25 animations:^{ self.floatingBall.alpha = (self.unusedItems.count >= 1) ? 1.f : .0f; }]; 424 | } 425 | break; 426 | default: break; 427 | } 428 | } 429 | 430 | #pragma mark - SEFloatingBallDelegate 431 | 432 | - (void)floatingBallDidClicked:(SEFloatingBall *)floatingBall { 433 | // will show floating list 434 | self.floatingList.editable = YES; 435 | if (!self.floatingList.superview) [self.window addSubview:self.floatingList]; 436 | [self.floatingList showAtRect:floatingBall.frame animated:YES]; 437 | } 438 | 439 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidBegan:(UILongPressGestureRecognizer *)gesture { 440 | // will show floating list 441 | self.floatingList.editable = NO; 442 | if (!self.floatingList.superview) [self.window addSubview:self.floatingList]; 443 | [self.floatingList showAtRect:floatingBall.frame animated:YES]; 444 | } 445 | 446 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidChanged:(UILongPressGestureRecognizer *)gesture { 447 | // will highlight the item in floating list 448 | CGPoint point = [gesture locationInView:gesture.view]; 449 | point = [self.floatingList convertPoint:point fromView:gesture.view]; 450 | for (SEFloatingListItem *listItem in self.floatingList.listItems) { 451 | listItem.selected = CGRectContainsPoint(listItem.frame, point); 452 | } 453 | } 454 | 455 | - (void)floatingBall:(SEFloatingBall *)floatingBall pressDidEnded:(UILongPressGestureRecognizer *)gesture { 456 | // will end, check floating list is selected 457 | for (SEFloatingListItem *listItem in self.floatingList.listItems) { 458 | if (listItem.isSelected) { 459 | [self pushEntranceItem:(UIViewController *)listItem.item]; 460 | break; 461 | } 462 | } 463 | [self.floatingList dismissWithAnimated:YES]; 464 | } 465 | 466 | #pragma mark - SEFloatingListDelegate 467 | 468 | - (NSUInteger)numberOfItemsInFloatingList:(SEFloatingList *)list { 469 | return self.items.count; 470 | } 471 | 472 | - (id)floatingList:(SEFloatingList *)list itemAtIndex:(NSUInteger)index { 473 | return [self.items objectAtIndex:index]; 474 | } 475 | 476 | - (void)floatingList:(SEFloatingList *)list didSelectItem:(id)item { 477 | [self pushEntranceItem:(UIViewController *)item]; 478 | } 479 | 480 | - (BOOL)floatingList:(SEFloatingList *)list willDeleteItem:(id)item { 481 | if (![self.items containsObject:(UIViewController *)item]) return NO; 482 | [self->_items removeObject:(UIViewController *)item]; 483 | [self archiveEntranceItems]; 484 | return YES; 485 | } 486 | 487 | - (BOOL)floatingList:(SEFloatingList *)list isItemVisible:(id)item { 488 | return !((UIViewController *)item).se_isUsed; 489 | } 490 | 491 | - (void)floatingListWillShow:(SEFloatingList *)list { 492 | [UIView animateWithDuration:0.25 animations:^{ self.floatingBall.alpha = .0f; }]; 493 | } 494 | 495 | - (void)floatingListWillHide:(SEFloatingList *)list { 496 | 497 | NSArray *> *unusedItems = self.unusedItems; 498 | // [self.floatingBall reloadIconViews:unusedItems]; 499 | 500 | CGFloat alpha = unusedItems.count >= 1 ? 1.f : self.floatingBall.alpha; 501 | [UIView animateWithDuration:0.25 animations:^{ self.floatingBall.alpha = alpha; }]; 502 | } 503 | 504 | #pragma mark - Setter 505 | 506 | - (void)setMaxCount:(NSUInteger)maxCount { 507 | _maxCount = MAX(1, MIN(5, maxCount)); 508 | } 509 | 510 | - (void)setWindow:(UIWindow *)window { 511 | if (_window == window) return; 512 | _window = window; 513 | if (self.floatingBall.superview) [self.floatingBall removeFromSuperview]; 514 | if (self.isAvailable && self.items.count) { [window addSubview:self.floatingBall]; } 515 | } 516 | 517 | - (void)setAvailable:(BOOL)available { 518 | 519 | _available = available; 520 | if (!available) { 521 | [self.floatingBall removeFromSuperview]; 522 | } else { 523 | if (self.floatingBall.superview) [self.floatingBall.superview bringSubviewToFront:self.floatingBall]; 524 | else if (self.items.count >= 1) [self.window addSubview:self.floatingBall]; 525 | } 526 | } 527 | 528 | - (void)setArchivedPath:(NSString *)archivedPath { 529 | 530 | if ([_archivedPath isEqualToString:archivedPath]) return; 531 | _archivedPath = archivedPath; 532 | [self->_items removeAllObjects]; 533 | [self.floatingBall removeFromSuperview]; 534 | [self unarchiveEntranceItems]; 535 | } 536 | 537 | #pragma mark - Getter 538 | 539 | - (UIWindow *)window { return _window ? : [UIApplication sharedApplication].keyWindow; } 540 | 541 | - (UINavigationController *)navigationController { 542 | 543 | __kindof UIViewController *controller = self.window.rootViewController; 544 | if ([controller isKindOfClass:[UITabBarController class]]) { controller = [(UITabBarController *)controller selectedViewController]; } 545 | while (controller.presentedViewController) { controller = controller.presentedViewController; } 546 | 547 | if ([controller isKindOfClass:[UINavigationController class]]) return controller; 548 | if (controller.navigationController) return controller.navigationController; 549 | return nil; 550 | } 551 | 552 | - (NSArray *> *)unusedItems { 553 | return [self.items filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"SELF.se_isUsed = NO"]]; 554 | } 555 | 556 | #pragma mark - Class 557 | 558 | + (instancetype)shared { 559 | static SuspensionEntrance *instance; 560 | static dispatch_once_t onceToken; 561 | dispatch_once(&onceToken, ^{ 562 | instance = [[super allocWithZone:nil] init]; 563 | }); 564 | return instance; 565 | } 566 | 567 | + (instancetype)allocWithZone:(struct _NSZone *)zone { return [SuspensionEntrance shared]; } 568 | 569 | @end 570 | 571 | @implementation SuspensionEntrance (NavigationControllerDelegate) 572 | 573 | - (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated { 574 | 575 | self.animator = nil; 576 | self.interactive = nil; 577 | [self.floatingBall reloadIconViews:self.unusedItems]; 578 | 579 | if (![viewController se_canBeEntrance]) return; 580 | if (navigationController.viewControllers.count <= 1) return; 581 | 582 | NSArray *gestures = [viewController.view.gestureRecognizers copy]; 583 | for (UIGestureRecognizer *gesture in gestures) { 584 | if ([gesture isKindOfClass:[UIScreenEdgePanGestureRecognizer class]] && gesture.delegate == self) { 585 | // may be this gesture is add before, remove it 586 | [viewController.view removeGestureRecognizer:gesture]; 587 | } 588 | } 589 | 590 | UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTransition:)]; 591 | pan.edges = UIRectEdgeLeft; 592 | pan.delegate = self; 593 | [viewController.view addGestureRecognizer:pan]; 594 | } 595 | 596 | - (id)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id)animationController { 597 | 598 | return self.interactive; 599 | } 600 | 601 | - (id)navigationController:(UINavigationController *)navigationController 602 | animationControllerForOperation:(UINavigationControllerOperation)operation 603 | fromViewController:(UIViewController *)fromVC 604 | toViewController:(UIViewController *)toVC { 605 | 606 | if (operation == UINavigationControllerOperationPop) { 607 | if (self.interactive) { 608 | CGRect const floatingRect = [self floatingRectOfOperation:operation]; 609 | self.animator = [SETransitionAnimator continuousPopAnimatorWithRect:floatingRect]; 610 | } else if (fromVC.se_isEntrance) { 611 | CGRect const floatingRect = [self floatingRectOfOperation:operation]; 612 | self.animator = [SETransitionAnimator roundPopAnimatorWithRect:floatingRect]; 613 | } else { 614 | self.animator = nil; 615 | } 616 | } else if (operation == UINavigationControllerOperationPush) { 617 | if ((toVC.se_isEntrance && fromVC.se_isEntrance) || (toVC.se_isEntrance && !fromVC.se_isEntrance)) { 618 | CGRect const floatingRect = [self floatingRectOfOperation:operation]; 619 | self.animator = [SETransitionAnimator roundPushAnimatorWithRect:floatingRect]; 620 | } else { 621 | self.animator = nil; 622 | } 623 | // [self.floatingBall reloadIconViews:self.unusedItems]; 624 | } 625 | return self.animator; 626 | } 627 | 628 | @end 629 | 630 | @implementation SuspensionEntrance (TransitioningDelegate) 631 | 632 | - (id)animationControllerForDismissedController:(UIViewController *)dismissed { 633 | 634 | if (self.interactive) { 635 | CGRect const floatingRect = [self floatingRectOfOperation:UINavigationControllerOperationPop]; 636 | self.animator = [SETransitionAnimator continuousPopAnimatorWithRect:floatingRect]; 637 | } else if (dismissed.se_isEntrance) { 638 | CGRect const floatingRect = [self floatingRectOfOperation:UINavigationControllerOperationPop]; 639 | self.animator = [SETransitionAnimator roundPopAnimatorWithRect:floatingRect]; 640 | } else { 641 | self.animator = nil; 642 | } 643 | return self.animator; 644 | } 645 | 646 | - (id)animationControllerForPresentedController:(UIViewController *)toVC presentingController:(UIViewController *)fromVC sourceController:(UIViewController *)source { 647 | 648 | if ((toVC.se_isEntrance && fromVC.se_isEntrance) || (toVC.se_isEntrance && !fromVC.se_isEntrance)) { 649 | CGRect const floatingRect = [self floatingRectOfOperation:UINavigationControllerOperationPush]; 650 | self.animator = [SETransitionAnimator roundPushAnimatorWithRect:floatingRect]; 651 | } else { 652 | self.animator = nil; 653 | } 654 | 655 | // NSArray *> *unusedItems = self.unusedItems; 656 | // [self.floatingBall reloadIconViews:unusedItems]; 657 | 658 | NSArray *gestures = [toVC.view.gestureRecognizers copy]; 659 | for (UIGestureRecognizer *gesture in gestures) { 660 | if ([gesture isKindOfClass:[UIScreenEdgePanGestureRecognizer class]] && gesture.delegate == self) { 661 | // may be this gesture is add before, remove it 662 | [toVC.view removeGestureRecognizer:gesture]; 663 | } 664 | } 665 | 666 | UIScreenEdgePanGestureRecognizer *pan = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleTransition:)]; 667 | pan.edges = UIRectEdgeLeft; 668 | pan.delegate = self; 669 | [toVC.view addGestureRecognizer:pan]; 670 | 671 | return self.animator; 672 | } 673 | 674 | //- (nullable id )interactionControllerForPresentation:(id )animator; 675 | 676 | - (nullable id )interactionControllerForDismissal:(id )animator { 677 | return self.interactive; 678 | } 679 | 680 | @end 681 | -------------------------------------------------------------------------------- /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/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/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 | // -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ws00801526/SuspensionEntrance/de9d70ead75937b316e982ae70b93503f4602494/preview.gif --------------------------------------------------------------------------------