ICSDrawerController.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. //
  2. // ICSDrawerController.m
  3. //
  4. // Created by Vito Modena
  5. //
  6. // Copyright (c) 2016 ice cream studios s.r.l. - http://icecreamstudios.com
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy of
  9. // this software and associated documentation files (the "Software"), to deal in
  10. // the Software without restriction, including without limitation the rights to
  11. // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  12. // the Software, and to permit persons to whom the Software is furnished to do so,
  13. // subject to the following conditions:
  14. //
  15. // The above copyright notice and this permission notice shall be included in all
  16. // copies or substantial portions of the Software.
  17. //
  18. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  19. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  20. // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  21. // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  22. // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  23. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  24. #import "ICSDrawerController.h"
  25. #import "ICSDropShadowView.h"
  26. static const CGFloat kICSDrawerControllerDrawerDepth = 260.0f;
  27. static const CGFloat kICSDrawerControllerLeftViewInitialOffset = -60.0f;
  28. static const NSTimeInterval kICSDrawerControllerAnimationDuration = 0.5;
  29. static const CGFloat kICSDrawerControllerOpeningAnimationSpringDamping = 0.7f;
  30. static const CGFloat kICSDrawerControllerOpeningAnimationSpringInitialVelocity = 0.1f;
  31. static const CGFloat kICSDrawerControllerClosingAnimationSpringDamping = 1.0f;
  32. static const CGFloat kICSDrawerControllerClosingAnimationSpringInitialVelocity = 0.5f;
  33. typedef NS_ENUM(NSUInteger, ICSDrawerControllerState)
  34. {
  35. ICSDrawerControllerStateClosed = 0,
  36. ICSDrawerControllerStateOpening,
  37. ICSDrawerControllerStateOpen,
  38. ICSDrawerControllerStateClosing
  39. };
  40. @interface ICSDrawerController () <UIGestureRecognizerDelegate>
  41. @property(nonatomic, strong, readwrite) UIViewController<ICSDrawerControllerChild, ICSDrawerControllerPresenting> *leftViewController;
  42. @property(nonatomic, strong, readwrite) UIViewController<ICSDrawerControllerChild, ICSDrawerControllerPresenting> *centerViewController;
  43. @property(nonatomic, strong) UIView *leftView;
  44. @property(nonatomic, strong) ICSDropShadowView *centerView;
  45. @property(nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer;
  46. @property(nonatomic, strong) UIPanGestureRecognizer *panGestureRecognizer;
  47. @property(nonatomic, assign) CGPoint panGestureStartLocation;
  48. @property(nonatomic, assign) ICSDrawerControllerState drawerState;
  49. @end
  50. @implementation ICSDrawerController
  51. - (id)initWithLeftViewController:(UIViewController<ICSDrawerControllerChild, ICSDrawerControllerPresenting> *)leftViewController
  52. centerViewController:(UIViewController<ICSDrawerControllerChild, ICSDrawerControllerPresenting> *)centerViewController
  53. {
  54. NSParameterAssert(leftViewController);
  55. NSParameterAssert(centerViewController);
  56. self = [super init];
  57. if (self) {
  58. _leftViewController = leftViewController;
  59. _centerViewController = centerViewController;
  60. if ([_leftViewController respondsToSelector:@selector(setDrawer:)]) {
  61. _leftViewController.drawer = self;
  62. }
  63. if ([_centerViewController respondsToSelector:@selector(setDrawer:)]) {
  64. _centerViewController.drawer = self;
  65. }
  66. }
  67. return self;
  68. }
  69. - (void)addCenterViewController
  70. {
  71. NSParameterAssert(self.centerViewController);
  72. NSParameterAssert(self.centerView);
  73. [self addChildViewController:self.centerViewController];
  74. self.centerViewController.view.frame = self.view.bounds;
  75. [self.centerView addSubview:self.centerViewController.view];
  76. [self.centerViewController didMoveToParentViewController:self];
  77. }
  78. #pragma mark - Managing the view
  79. - (void)viewDidLoad
  80. {
  81. [super viewDidLoad];
  82. self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  83. // Initialize left and center view containers
  84. self.leftView = [[UIView alloc] initWithFrame:self.view.bounds];
  85. self.centerView = [[ICSDropShadowView alloc] initWithFrame:self.view.bounds];
  86. self.leftView.autoresizingMask = self.view.autoresizingMask;
  87. self.centerView.autoresizingMask = self.view.autoresizingMask;
  88. // Add the center view container
  89. [self.view addSubview:self.centerView];
  90. // Add the center view controller to the container
  91. [self addCenterViewController];
  92. [self setupGestureRecognizers];
  93. }
  94. #pragma mark - Configuring the view’s layout behavior
  95. - (UIViewController *)childViewControllerForStatusBarHidden
  96. {
  97. NSParameterAssert(self.leftViewController);
  98. NSParameterAssert(self.centerViewController);
  99. if (self.drawerState == ICSDrawerControllerStateOpening) {
  100. return self.leftViewController;
  101. }
  102. return self.centerViewController;
  103. }
  104. - (UIViewController *)childViewControllerForStatusBarStyle
  105. {
  106. NSParameterAssert(self.leftViewController);
  107. NSParameterAssert(self.centerViewController);
  108. if (self.drawerState == ICSDrawerControllerStateOpening) {
  109. return self.leftViewController;
  110. }
  111. return self.centerViewController;
  112. }
  113. #pragma mark - Gesture recognizers
  114. - (void)setupGestureRecognizers
  115. {
  116. NSParameterAssert(self.centerView);
  117. self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGestureRecognized:)];
  118. self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureRecognized:)];
  119. self.panGestureRecognizer.maximumNumberOfTouches = 1;
  120. self.panGestureRecognizer.delegate = self;
  121. [self.centerView addGestureRecognizer:self.panGestureRecognizer];
  122. }
  123. - (void)addClosingGestureRecognizers
  124. {
  125. NSParameterAssert(self.centerView);
  126. NSParameterAssert(self.panGestureRecognizer);
  127. [self.centerView addGestureRecognizer:self.tapGestureRecognizer];
  128. }
  129. - (void)removeClosingGestureRecognizers
  130. {
  131. NSParameterAssert(self.centerView);
  132. NSParameterAssert(self.panGestureRecognizer);
  133. [self.centerView removeGestureRecognizer:self.tapGestureRecognizer];
  134. }
  135. #pragma mark Tap to close the drawer
  136. - (void)tapGestureRecognized:(UITapGestureRecognizer *)tapGestureRecognizer
  137. {
  138. if (tapGestureRecognizer.state == UIGestureRecognizerStateEnded) {
  139. [self close];
  140. }
  141. }
  142. #pragma mark Pan to open/close the drawer
  143. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
  144. {
  145. NSParameterAssert([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]);
  146. CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self.view];
  147. if (self.drawerState == ICSDrawerControllerStateClosed && velocity.x > 0.0f) {
  148. return YES;
  149. }
  150. else if (self.drawerState == ICSDrawerControllerStateOpen && velocity.x < 0.0f) {
  151. return YES;
  152. }
  153. return NO;
  154. }
  155. - (void)panGestureRecognized:(UIPanGestureRecognizer *)panGestureRecognizer
  156. {
  157. NSParameterAssert(self.leftView);
  158. NSParameterAssert(self.centerView);
  159. UIGestureRecognizerState state = panGestureRecognizer.state;
  160. CGPoint location = [panGestureRecognizer locationInView:self.view];
  161. CGPoint velocity = [panGestureRecognizer velocityInView:self.view];
  162. switch (state) {
  163. case UIGestureRecognizerStateBegan:
  164. self.panGestureStartLocation = location;
  165. if (self.drawerState == ICSDrawerControllerStateClosed) {
  166. [self willOpen];
  167. }
  168. else {
  169. [self willClose];
  170. }
  171. break;
  172. case UIGestureRecognizerStateChanged:
  173. {
  174. CGFloat delta = 0.0f;
  175. if (self.drawerState == ICSDrawerControllerStateOpening) {
  176. delta = location.x - self.panGestureStartLocation.x;
  177. }
  178. else if (self.drawerState == ICSDrawerControllerStateClosing) {
  179. delta = kICSDrawerControllerDrawerDepth - (self.panGestureStartLocation.x - location.x);
  180. }
  181. CGRect l = self.leftView.frame;
  182. CGRect c = self.centerView.frame;
  183. if (delta > kICSDrawerControllerDrawerDepth) {
  184. l.origin.x = 0.0f;
  185. c.origin.x = kICSDrawerControllerDrawerDepth;
  186. }
  187. else if (delta < 0.0f) {
  188. l.origin.x = kICSDrawerControllerLeftViewInitialOffset;
  189. c.origin.x = 0.0f;
  190. }
  191. else {
  192. // While the centerView can move up to kICSDrawerControllerDrawerDepth points, to achieve a parallax effect
  193. // the leftView has move no more than kICSDrawerControllerLeftViewInitialOffset points
  194. l.origin.x = kICSDrawerControllerLeftViewInitialOffset
  195. - (delta * kICSDrawerControllerLeftViewInitialOffset) / kICSDrawerControllerDrawerDepth;
  196. c.origin.x = delta;
  197. }
  198. self.leftView.frame = l;
  199. self.centerView.frame = c;
  200. break;
  201. }
  202. case UIGestureRecognizerStateEnded:
  203. if (self.drawerState == ICSDrawerControllerStateOpening) {
  204. CGFloat centerViewLocation = self.centerView.frame.origin.x;
  205. if (centerViewLocation == kICSDrawerControllerDrawerDepth) {
  206. // Open the drawer without animation, as it has already being dragged in its final position
  207. [self setNeedsStatusBarAppearanceUpdate];
  208. [self didOpen];
  209. }
  210. else if (centerViewLocation > self.view.bounds.size.width / 3
  211. && velocity.x > 0.0f) {
  212. // Animate the drawer opening
  213. [self animateOpening];
  214. }
  215. else {
  216. // Animate the drawer closing, as the opening gesture hasn't been completed or it has
  217. // been reverted by the user
  218. [self didOpen];
  219. [self willClose];
  220. [self animateClosing];
  221. }
  222. } else if (self.drawerState == ICSDrawerControllerStateClosing) {
  223. CGFloat centerViewLocation = self.centerView.frame.origin.x;
  224. if (centerViewLocation == 0.0f) {
  225. // Close the drawer without animation, as it has already being dragged in its final position
  226. [self setNeedsStatusBarAppearanceUpdate];
  227. [self didClose];
  228. }
  229. else if (centerViewLocation < (2 * self.view.bounds.size.width) / 3
  230. && velocity.x < 0.0f) {
  231. // Animate the drawer closing
  232. [self animateClosing];
  233. }
  234. else {
  235. // Animate the drawer opening, as the opening gesture hasn't been completed or it has
  236. // been reverted by the user
  237. [self didClose];
  238. // Here we save the current position for the leftView since
  239. // we want the opening animation to start from the current position
  240. // and not the one that is set in 'willOpen'
  241. CGRect l = self.leftView.frame;
  242. [self willOpen];
  243. self.leftView.frame = l;
  244. [self animateOpening];
  245. }
  246. }
  247. break;
  248. default:
  249. break;
  250. }
  251. }
  252. #pragma mark - Animations
  253. #pragma mark Opening animation
  254. - (void)animateOpening
  255. {
  256. NSParameterAssert(self.drawerState == ICSDrawerControllerStateOpening);
  257. NSParameterAssert(self.leftView);
  258. NSParameterAssert(self.centerView);
  259. // Calculate the final frames for the container views
  260. CGRect leftViewFinalFrame = self.view.bounds;
  261. CGRect centerViewFinalFrame = self.view.bounds;
  262. centerViewFinalFrame.origin.x = kICSDrawerControllerDrawerDepth;
  263. [UIView animateWithDuration:kICSDrawerControllerAnimationDuration
  264. delay:0
  265. usingSpringWithDamping:kICSDrawerControllerOpeningAnimationSpringDamping
  266. initialSpringVelocity:kICSDrawerControllerOpeningAnimationSpringInitialVelocity
  267. options:UIViewAnimationOptionCurveLinear
  268. animations:^{
  269. self.centerView.frame = centerViewFinalFrame;
  270. self.leftView.frame = leftViewFinalFrame;
  271. [self setNeedsStatusBarAppearanceUpdate];
  272. }
  273. completion:^(BOOL finished) {
  274. [self didOpen];
  275. }];
  276. }
  277. #pragma mark Closing animation
  278. - (void)animateClosing
  279. {
  280. NSParameterAssert(self.drawerState == ICSDrawerControllerStateClosing);
  281. NSParameterAssert(self.leftView);
  282. NSParameterAssert(self.centerView);
  283. // Calculate final frames for the container views
  284. CGRect leftViewFinalFrame = self.leftView.frame;
  285. leftViewFinalFrame.origin.x = kICSDrawerControllerLeftViewInitialOffset;
  286. CGRect centerViewFinalFrame = self.view.bounds;
  287. [UIView animateWithDuration:kICSDrawerControllerAnimationDuration
  288. delay:0
  289. usingSpringWithDamping:kICSDrawerControllerClosingAnimationSpringDamping
  290. initialSpringVelocity:kICSDrawerControllerClosingAnimationSpringInitialVelocity
  291. options:UIViewAnimationOptionCurveLinear
  292. animations:^{
  293. self.centerView.frame = centerViewFinalFrame;
  294. self.leftView.frame = leftViewFinalFrame;
  295. [self setNeedsStatusBarAppearanceUpdate];
  296. }
  297. completion:^(BOOL finished) {
  298. [self didClose];
  299. }];
  300. }
  301. #pragma mark - Opening the drawer
  302. - (void)open
  303. {
  304. NSParameterAssert(self.drawerState == ICSDrawerControllerStateClosed);
  305. [self willOpen];
  306. [self animateOpening];
  307. }
  308. - (void)willOpen
  309. {
  310. NSParameterAssert(self.drawerState == ICSDrawerControllerStateClosed);
  311. NSParameterAssert(self.leftView);
  312. NSParameterAssert(self.centerView);
  313. NSParameterAssert(self.leftViewController);
  314. NSParameterAssert(self.centerViewController);
  315. // Keep track that the drawer is opening
  316. self.drawerState = ICSDrawerControllerStateOpening;
  317. // Position the left view
  318. CGRect f = self.view.bounds;
  319. f.origin.x = kICSDrawerControllerLeftViewInitialOffset;
  320. NSParameterAssert(f.origin.x < 0.0f);
  321. self.leftView.frame = f;
  322. // Start adding the left view controller to the container
  323. [self addChildViewController:self.leftViewController];
  324. self.leftViewController.view.frame = self.leftView.bounds;
  325. [self.leftView addSubview:self.leftViewController.view];
  326. // Add the left view to the view hierarchy
  327. [self.view insertSubview:self.leftView belowSubview:self.centerView];
  328. // Notify the child view controllers that the drawer is about to open
  329. if ([self.leftViewController respondsToSelector:@selector(drawerControllerWillOpen:)]) {
  330. [self.leftViewController drawerControllerWillOpen:self];
  331. }
  332. if ([self.centerViewController respondsToSelector:@selector(drawerControllerWillOpen:)]) {
  333. [self.centerViewController drawerControllerWillOpen:self];
  334. }
  335. }
  336. - (void)didOpen
  337. {
  338. NSParameterAssert(self.drawerState == ICSDrawerControllerStateOpening);
  339. NSParameterAssert(self.leftViewController);
  340. NSParameterAssert(self.centerViewController);
  341. // Complete adding the left controller to the container
  342. [self.leftViewController didMoveToParentViewController:self];
  343. [self addClosingGestureRecognizers];
  344. // Keep track that the drawer is open
  345. self.drawerState = ICSDrawerControllerStateOpen;
  346. // Notify the child view controllers that the drawer is open
  347. if ([self.leftViewController respondsToSelector:@selector(drawerControllerDidOpen:)]) {
  348. [self.leftViewController drawerControllerDidOpen:self];
  349. }
  350. if ([self.centerViewController respondsToSelector:@selector(drawerControllerDidOpen:)]) {
  351. [self.centerViewController drawerControllerDidOpen:self];
  352. }
  353. }
  354. #pragma mark - Closing the drawer
  355. - (void)close
  356. {
  357. NSParameterAssert(self.drawerState == ICSDrawerControllerStateOpen);
  358. [self willClose];
  359. [self animateClosing];
  360. }
  361. - (void)willClose
  362. {
  363. NSParameterAssert(self.drawerState == ICSDrawerControllerStateOpen);
  364. NSParameterAssert(self.leftViewController);
  365. NSParameterAssert(self.centerViewController);
  366. // Start removing the left controller from the container
  367. [self.leftViewController willMoveToParentViewController:nil];
  368. // Keep track that the drawer is closing
  369. self.drawerState = ICSDrawerControllerStateClosing;
  370. // Notify the child view controllers that the drawer is about to close
  371. if ([self.leftViewController respondsToSelector:@selector(drawerControllerWillClose:)]) {
  372. [self.leftViewController drawerControllerWillClose:self];
  373. }
  374. if ([self.centerViewController respondsToSelector:@selector(drawerControllerWillClose:)]) {
  375. [self.centerViewController drawerControllerWillClose:self];
  376. }
  377. }
  378. - (void)didClose
  379. {
  380. NSParameterAssert(self.drawerState == ICSDrawerControllerStateClosing);
  381. NSParameterAssert(self.leftView);
  382. NSParameterAssert(self.centerView);
  383. NSParameterAssert(self.leftViewController);
  384. NSParameterAssert(self.centerViewController);
  385. // Complete removing the left view controller from the container
  386. [self.leftViewController.view removeFromSuperview];
  387. [self.leftViewController removeFromParentViewController];
  388. // Remove the left view from the view hierarchy
  389. [self.leftView removeFromSuperview];
  390. [self removeClosingGestureRecognizers];
  391. // Keep track that the drawer is closed
  392. self.drawerState = ICSDrawerControllerStateClosed;
  393. // Notify the child view controllers that the drawer is closed
  394. if ([self.leftViewController respondsToSelector:@selector(drawerControllerDidClose:)]) {
  395. [self.leftViewController drawerControllerDidClose:self];
  396. }
  397. if ([self.centerViewController respondsToSelector:@selector(drawerControllerDidClose:)]) {
  398. [self.centerViewController drawerControllerDidClose:self];
  399. }
  400. }
  401. #pragma mark - Reloading/Replacing the center view controller
  402. - (void)reloadCenterViewControllerUsingBlock:(void (^)(void))reloadBlock
  403. {
  404. NSParameterAssert(self.drawerState == ICSDrawerControllerStateOpen);
  405. NSParameterAssert(self.centerViewController);
  406. [self willClose];
  407. CGRect f = self.centerView.frame;
  408. f.origin.x = self.view.bounds.size.width;
  409. [UIView animateWithDuration: kICSDrawerControllerAnimationDuration / 2
  410. animations:^{
  411. self.centerView.frame = f;
  412. }
  413. completion:^(BOOL finished) {
  414. // The center view controller is now out of sight
  415. if (reloadBlock) {
  416. reloadBlock();
  417. }
  418. // Finally, close the drawer
  419. [self animateClosing];
  420. }];
  421. }
  422. - (void)replaceCenterViewControllerWithViewController:(UIViewController<ICSDrawerControllerChild, ICSDrawerControllerPresenting> *)viewController
  423. {
  424. NSParameterAssert(self.drawerState == ICSDrawerControllerStateOpen);
  425. NSParameterAssert(viewController);
  426. NSParameterAssert(self.centerView);
  427. NSParameterAssert(self.centerViewController);
  428. [self willClose];
  429. CGRect f = self.centerView.frame;
  430. f.origin.x = self.view.bounds.size.width;
  431. [self.centerViewController willMoveToParentViewController:nil];
  432. [UIView animateWithDuration: kICSDrawerControllerAnimationDuration / 2
  433. animations:^{
  434. self.centerView.frame = f;
  435. }
  436. completion:^(BOOL finished) {
  437. // The center view controller is now out of sight
  438. // Remove the current center view controller from the container
  439. if ([self.centerViewController respondsToSelector:@selector(setDrawer:)]) {
  440. self.centerViewController.drawer = nil;
  441. }
  442. [self.centerViewController.view removeFromSuperview];
  443. [self.centerViewController removeFromParentViewController];
  444. // Set the new center view controller
  445. self.centerViewController = viewController;
  446. if ([self.centerViewController respondsToSelector:@selector(setDrawer:)]) {
  447. self.centerViewController.drawer = self;
  448. }
  449. // Add the new center view controller to the container
  450. [self addCenterViewController];
  451. // Finally, close the drawer
  452. [self animateClosing];
  453. }];
  454. }
  455. @end