{"id":13903105,"url":"https://github.com/QuickBirdEng/FlippingNotch","last_synced_at":"2025-07-18T00:33:29.321Z","repository":{"id":115596617,"uuid":"117989207","full_name":"QuickBirdEng/FlippingNotch","owner":"QuickBirdEng","description":"FlippingNotch 🤙 - Dribble inspired animation https://dribbble.com/shots/4089014-Pull-To-Refresh-iPhone-X","archived":false,"fork":false,"pushed_at":"2018-03-02T09:57:03.000Z","size":9641,"stargazers_count":835,"open_issues_count":0,"forks_count":43,"subscribers_count":16,"default_branch":"master","last_synced_at":"2024-08-07T22:36:01.798Z","etag":null,"topics":["animation","dribble","notch","swift"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/QuickBirdEng.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-01-18T13:52:21.000Z","updated_at":"2024-07-29T10:13:05.000Z","dependencies_parsed_at":null,"dependency_job_id":"fd55138a-26cd-4a0f-b3d7-5c42071dec8d","html_url":"https://github.com/QuickBirdEng/FlippingNotch","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/QuickBirdEng%2FFlippingNotch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/QuickBirdEng%2FFlippingNotch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/QuickBirdEng%2FFlippingNotch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/QuickBirdEng%2FFlippingNotch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/QuickBirdEng","download_url":"https://codeload.github.com/QuickBirdEng/FlippingNotch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226320901,"owners_count":17606375,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["animation","dribble","notch","swift"],"created_at":"2024-08-06T22:01:37.294Z","updated_at":"2024-11-25T11:31:18.148Z","avatar_url":"https://github.com/QuickBirdEng.png","language":"Swift","readme":"# FlippingNotch 🤙\n[![Platform](https://img.shields.io/badge/Platform-iOS-lightgrey.svg)]()\n[![Swift 3.2](https://img.shields.io/badge/Swift-4.0-orange.svg)](https://swift.org)\n\nFlippingNotch is \"pull to refresh/add/show\" custom animation written Swift, using the iPhone X Notch. Heavily inspired by this Dribble project: https://dribbble.com/shots/4089014-Pull-To-Refresh-iPhone-X\n\n![alt text](https://cdn.dribbble.com/users/793057/screenshots/4089014/iphone-x-pull-to-refresh.gif)\n\n### What FlippingNotch is not\nIt is not a framework, it is just an Xcode project, embracing the notch.\n\n### Requirements\nFlippingNotch is written in Swift 4.0 and requires an iPhone X Simulator/Device.\n\n### Tutorial\n1. **Put a UICollectionView and constraint it in a ViewController.**\n\nThe image below shows an example how to constraint it.\n\n\u003cimg src=\"https://github.com/jdisho/FlippingNotch/blob/master/Screenshots/cv_constrains.png\" width=\"30%\"\u003e\n\n2. **Add a cell in the UICollectionView.**\n\n\u003cimg src=\"https://github.com/jdisho/FlippingNotch/blob/master/Screenshots/cv_cell.png\" width=\"40%\"\u003e\n\n3. **Set up the UICollectionView in the ViewController by conforming to UICollectionViewDataSource.**\n\n``` swift\nclass ViewController: UIViewController {\n\n    // MARK: IBOutlets\n\n    @IBOutlet var collectionView: UICollectionView!\n    \n    // MARK: Fileprivates\n    fileprivate var numberOfItemsInSection = 1\n\n    \n    // MARK: Overrides\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        collectionView.dataSource = self\n    }\n    \n}\n    \n// MARK: UICollectionViewDataSource\n\nextension ViewController: UICollectionViewDataSource {\n    \n    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -\u003e Int {\n        return numberOfItemsInSection\n    }\n\n    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -\u003e UICollectionViewCell {\n        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: \"Cell\", for: indexPath)\n        \n        cell.layer.cornerRadius = 10\n        cell.layer.masksToBounds = true\n        \n        return cell\n    }\n}\n\n```\n\n4. **The Notch View**\n- Instantiate a view that represents the notch. The `notchViewBottomConstraint` is used to position the notchView into the view.\n\n``` swift \n   fileprivate var notchView = UIView()\n   fileprivate var notchViewBottomConstraint: NSLayoutConstraint!\n   fileprivate var numberOfItemsInSection = 1\n```\n- After instantiating the notchView, add it as a subview its parent view. \n  The notchView have a black background and rounded corners. \n  `translatesAutoResizingMaskIntoConstraints` needs to be set to `false` because we want to use auto layout for this view rather than frame-based layout.\n  Then, the notchView is constrained to the center of its parent view, with the same width as the notch, a height of `(notch height - maximum scrolling offset what we want to give)` and a bottom constrained to its parent view `topAnchor` + notch height.\n\n``` swift\n\nprivate func configureNotchView() {\n        self.view.addSubview(notchView)\n        \n        notchView.translatesAutoresizingMaskIntoConstraints = false\n        notchView.backgroundColor = UIColor.black\n        notchView.layer.cornerRadius = 20\n        \n        notchView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).activate()\n        notchView.widthAnchor.constraint(equalToConstant: Constants.notchWidth).activate()\n        notchView.heightAnchor.constraint(equalToConstant: Constants.notchHeight - \n                                                           Constants.maxScrollOffset).activate()\n        notchViewBottomConstraint = notchView.bottomAnchor.constraint(equalTo: self.view.topAnchor, \n                                                                      constant: Constants.notchHeight)\n        notchViewBottomConstraint.activate()\n    }\n```\nThe result in an iPhone 8:\n\n\u003cimg src=\"https://github.com/jdisho/FlippingNotch/blob/master/Screenshots/notch_iphone8.png\" width=\"40%\"\u003e\n\n5. **Reacting while scrolling**\n\n (Looks clearer in an iPhone 8 what we are trying to do)\n \n- We want to move down the notchView while scrolling\n\u003cimg src=\"https://github.com/jdisho/FlippingNotch/blob/master/Screenshots/notch_stretching.gif\" width=\"40%\"\u003e\n\n- To do this, first we have to conform our ViewController to UICollectionViewDelegate and call `scrollViewDidScroll` delegate function. In there we write the logic to move the notchView down.\n- The scrollView should scroll until it reaches `the maximum scrolling offset what we want to give`\n- The bottom constrained of the notchView should be increased while scrolling.\n``` swift \n  extension ViewController: UICollectionViewDelegate {\n\n    func scrollViewDidScroll(_ scrollView: UIScrollView) {\n    \n        // Making sure that we contentOffset of the scrollView is max to maxScrollOffset\n        scrollView.contentOffset.y = max(Constants.maxScrollOffset, scrollView.contentOffset.y)\n        \n        // Move down the notchView until we have reached our threshold\n        notchViewTopConstraint.constant = Constants.notchTopOffset - min(0, scrollView.contentOffset.y)\n    }\n```\n\n6. **Drop the view from the notch**\n- When the scroll did end dragging we want to create the view that will be part of the flipping animation.\n\u003cimg src=\"https://github.com/jdisho/FlippingNotch/blob/master/Screenshots/notch_drop.gif\" width=\"40%\"\u003e\n\n- We create the animatableView, reset `notchBottomConstraint`, and move down the `collectionView` and drop the animatableView (notchView clone) with an animation and we round its corners.\n\n\n``` swift \n  private func animateView() {\n    \n        // Create animatableView (notch clone)\n        let animatableView = UIImageView(frame: notchView.frame)\n        animatableView.backgroundColor = UIColor.black\n        animatableView.layer.cornerRadius = self.notchView.layer.cornerRadius\n        animatableView.layer.masksToBounds = true\n        animatableView.frame = self.notchView.frame\n        self.view.addSubview(animatableView)\n        \n        // Reset notchView bottom constraint\n        notchViewBottomConstraint.constant = Constants.notchHeight\n        \n        // Move the collectionView down\n        let flowLayout = collectionView.collectionViewLayout as! UICollectionViewFlowLayout\n        let height = flowLayout.itemSize.height + flowLayout.minimumInteritemSpacing\n        \n        self.collectionView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: -Constants.maxScrollOffset)\n\n        // Dropping animation\n        UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: {\n            let itemSize = flowLayout.itemSize\n            animatableView.frame.size = CGSize(width: Constants.notchWidth, \n                                               height: (itemSize.height / itemSize.width) * Constants.notchWidth)\n\n            // UIImage.fromColor(color), returns an image in a certain color\n            animatableView.image = UIImage.fromColor(self.view.backgroundColor?.withAlphaComponent(0.2) ?? UIColor.black)\n            animatableView.frame.origin.y = Constants.notchViewTopInset\n            self.collectionView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: height * 0.5)\n        }) \n        \n        // Animate the corners\n        let cornerRadiusAnimation = CABasicAnimation(keyPath: \"cornerRadius\")\n        cornerRadiusAnimation.fromValue = 16\n        cornerRadiusAnimation.toValue = 10\n        cornerRadiusAnimation.duration = 0.3\n        animatableView.layer.add(cornerRadiusAnimation, forKey: \"cornerRadius\")\n        animatableView.layer.cornerRadius = 10\n    }\n    \nextension ViewController: UICollectionViewDelegate {\n   \n   ...\n\n    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {\n        if scrollView.contentOffset.y \u003c= Constants.maxScrollOffset {\n            animateView()\n        }\n    }\n}\n```\n7. **Flip it**\n\n- After dropping the view, a snapshot of the `collectionview cell` is taken, the image is set on the `animatableView` and it is flipped with an animation.\n\n\u003cimg src=\"https://github.com/jdisho/FlippingNotch/blob/master/Screenshots/notch_flip.gif\" width=\"40%\"\u003e\n\n``` swift \n  private func animateView() {\n   ...\n   \n        UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: {\n            \n            ...\n            \n        }) { _ in\n            \n            // Snapshot the collectionView cell. \n            // It is easier to deal with an image of the cell than the cell itself\n            // This is the reason why animatableView is an UIImageView and not a UIView.\n            let item = self.collectionView.cellForItem(at: IndexPath(row: 0, section: 0))\n            animatableView.image = item?.snapshotImage()\n            \n            // Flipping transition\n            UIView.transition(with: animatableView, duration: 0.6, options: UIViewAnimationOptions.transitionFlipFromBottom, animations: {\n                animatableView.frame.size = flowLayout.itemSize\n                animatableView.frame.origin = CGPoint(x: (self.collectionView.frame.width - flowLayout.itemSize.width) / 2.0, \n                                                      y: self.collectionView.frame.origin.y - height * 0.5)\n                self.collectionView.transform = CGAffineTransform.identity.translatedBy(x: 0, y: height)\n                }, completion: { _ in\n                    // Remove the animatableView\n                    self.collectionView.transform = CGAffineTransform.identity\n                    animatableView.removeFromSuperview()\n                    \n                    // Add an item in section\n                    self.numberOfItemsInSection += 1\n                    self.collectionView.reloadData()\n                }\n            )\n        }\n        \n      ...\n    }\n```\n\n### Limitations\nThe animation works as expected only in iPhone X in portrait mode\n\n### TODO\n- Include the case when a NavigationBar is implemented.\n\n## Authors\n\n* **Joan Disho** - [QuickBird Studios](http://www.quickbirdstudios.com)\n\n","funding_links":[],"categories":["Swift"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FQuickBirdEng%2FFlippingNotch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FQuickBirdEng%2FFlippingNotch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FQuickBirdEng%2FFlippingNotch/lists"}