Skip to content
This repository was archived by the owner on Aug 30, 2023. It is now read-only.

Commit 735726a

Browse files
Add interactive sliding menu as an example
#19
1 parent 320d7f9 commit 735726a

File tree

4 files changed

+252
-0
lines changed

4 files changed

+252
-0
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import UIKit
18+
import Transitioning
19+
20+
class MenuGestureViewController: ExampleViewController {
21+
var call: (() -> Void)! = nil
22+
23+
public func setCall(call: @escaping ()->Void) {
24+
self.call = call
25+
}
26+
27+
override func viewDidLoad() {
28+
super.viewDidLoad()
29+
30+
let tap = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(edgePanGesture))
31+
tap.edges = .left
32+
view.addGestureRecognizer(tap)
33+
34+
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
35+
}
36+
37+
var percentage = CGFloat(0.01)
38+
func edgePanGesture(_ sender: UIScreenEdgePanGestureRecognizer) {
39+
let translation = sender.location(in: sender.view?.superview)
40+
switch sender.state {
41+
case .began:
42+
call()
43+
case .changed:
44+
percentage = translation.x / ((sender.view!.frame.width)/2)
45+
percentage = min(percentage, 0.99)
46+
interactiveTransitionContext?.updatePercent(percentage)
47+
case .ended:
48+
if percentage > 0.8 {
49+
interactiveTransitionContext?.finishInteractiveTransition()
50+
} else {
51+
interactiveTransitionContext?.cancelInteractiveTransition()
52+
}
53+
interactiveTransitionContext = nil
54+
percentage = CGFloat(0.01)
55+
default:
56+
break
57+
}
58+
}
59+
}
60+
61+
// This example demonstrates the minimal path to building a custom transition using the Material
62+
// Motion Transitioning APIs in Swift. The essential steps have been documented below.
63+
64+
class MenuInteractiveExampleViewController: MenuGestureViewController {
65+
66+
func didTap() {
67+
let modalViewController = ModalInteractiveViewController()
68+
69+
// The transition controller is an associated object on all UIViewController instances that
70+
// allows you to customize the way the view controller is presented. The primary API on the
71+
// controller that you'll make use of is the `transition` property. Setting this property will
72+
// dictate how the view controller is presented. For this example we've built a custom
73+
// FadeTransition, so we'll make use of that now:
74+
modalViewController.transitionController.transition = MenuTransition()
75+
76+
// Note that once we assign the transition object to the view controller, the transition will
77+
// govern all subsequent presentations and dismissals of that view controller instance. If we
78+
// want to use a different transition (e.g. to use an edge-swipe-to-dismiss transition) then we
79+
// can simply change the transition object before initiating the transition.
80+
81+
present(modalViewController, animated: true)
82+
}
83+
84+
override func viewDidLoad() {
85+
super.viewDidLoad()
86+
87+
let label = UILabel(frame: view.bounds)
88+
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
89+
label.textColor = .white
90+
label.textAlignment = .center
91+
label.text = "Swipe from left edge to start the transition"
92+
view.addSubview(label)
93+
94+
setCall(call: didTap)
95+
}
96+
97+
override func exampleInformation() -> ExampleInfo {
98+
return .init(title: type(of: self).catalogBreadcrumbs().last!,
99+
instructions: "Tap to present a modal transition.")
100+
}
101+
}
102+
103+
// Transitions must be NSObject types that conform to the Transition protocol.
104+
private final class MenuTransition: NSObject, Transition, InteractiveTransition {
105+
106+
// The sole method we're expected to implement, start is invoked each time the view controller is
107+
// presented or dismissed.
108+
func start(with context: TransitionContext) {
109+
let fromVC = context.backViewController
110+
let toVC = context.foreViewController
111+
let containerView = context.containerView
112+
113+
if(context.direction == .forward) {
114+
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
115+
toVC.view.frame.origin.x = -1 * toVC.view.frame.width
116+
UIView.animate(
117+
withDuration: context.duration,
118+
delay: 0,
119+
options: .curveLinear,
120+
animations: {
121+
toVC.view.frame.origin.x = -1 * (toVC.view.frame.width / 2)
122+
},
123+
completion: { _ in
124+
let deadlineTime = DispatchTime.now() + .milliseconds(10)
125+
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
126+
context.transitionDidEnd()
127+
}
128+
}
129+
)
130+
if let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false) {
131+
snapshot.isUserInteractionEnabled = false
132+
containerView.insertSubview(snapshot, belowSubview: toVC.view)
133+
snapshot.tag = 2000
134+
}
135+
} else {
136+
UIView.animate(
137+
withDuration: context.duration,
138+
delay: 0,
139+
options: .curveLinear,
140+
animations: {
141+
toVC.view.frame.origin.x = -1 * toVC.view.frame.width
142+
},
143+
completion: { _ in
144+
145+
let deadlineTime = DispatchTime.now() + .milliseconds(10)
146+
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
147+
context.transitionDidEnd()
148+
}
149+
150+
if(context.wasCancelled == false) {
151+
containerView.viewWithTag(2000)?.removeFromSuperview()
152+
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
153+
}
154+
}
155+
)
156+
}
157+
}
158+
159+
func isInteractive(_ context: TransitionContext) -> Bool {
160+
return true
161+
}
162+
163+
func start(withInteractiveContext context: InteractiveTransitionContext) {
164+
context.sourceViewController!.interactiveTransitionContext = context
165+
context.foreViewController.interactiveTransitionContext = context
166+
}
167+
}

examples/apps/Catalog/TableOfContents.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ extension MenuExampleViewController {
2424
class func catalogBreadcrumbs() -> [String] { return ["2. Menu transition"] }
2525
}
2626

27+
extension MenuInteractiveExampleViewController {
28+
class func catalogBreadcrumbs() -> [String] { return ["2. Menu transition (interactive)"] }
29+
}
30+
2731
extension CustomPresentationExampleViewController {
2832
class func catalogBreadcrumbs() -> [String] { return ["3. Custom presentation transitions"] }
2933
}

examples/apps/Catalog/TransitionsCatalog.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/* Begin PBXBuildFile section */
1010
072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072A063A1EEE26A900B9B5FC /* MenuExample.swift */; };
11+
07730EDD1F02B586007BAEFC /* MenuInteractiveExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */; };
12+
07730EDF1F02B58F007BAEFC /* ModalInteractiveViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */; };
1113
6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6629151D1ED5E0E0002B9A5D /* CustomPresentationExample.swift */; };
1214
662915201ED5E137002B9A5D /* ModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6629151F1ED5E137002B9A5D /* ModalViewController.swift */; };
1315
662915231ED64A10002B9A5D /* TransitionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662915221ED64A10002B9A5D /* TransitionTests.swift */; };
@@ -48,6 +50,8 @@
4850

4951
/* Begin PBXFileReference section */
5052
072A063A1EEE26A900B9B5FC /* MenuExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuExample.swift; sourceTree = "<group>"; };
53+
07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuInteractiveExample.swift; sourceTree = "<group>"; };
54+
07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalInteractiveViewController.swift; sourceTree = "<group>"; };
5155
0C2327F961D4F16DEBF0EEB8 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "../../../Pods/Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = "<group>"; };
5256
2408A4B72C0BA93CC963452F /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5357
3734DFFD1C84494E48784617 /* Pods-TransitionsCatalog.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TransitionsCatalog.release.xcconfig"; path = "../../../Pods/Target Support Files/Pods-TransitionsCatalog/Pods-TransitionsCatalog.release.xcconfig"; sourceTree = "<group>"; };
@@ -172,6 +176,7 @@
172176
66BBC7731ED729A70015CB9B /* FadeExample.h */,
173177
66BBC7741ED729A70015CB9B /* FadeExample.m */,
174178
072A063A1EEE26A900B9B5FC /* MenuExample.swift */,
179+
07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */,
175180
);
176181
name = examples;
177182
path = ../..;
@@ -223,6 +228,7 @@
223228
66BBC76B1ED4C8790015CB9B /* HexColor.swift */,
224229
66BBC76C1ED4C8790015CB9B /* Layout.swift */,
225230
6629151F1ED5E137002B9A5D /* ModalViewController.swift */,
231+
07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */,
226232
);
227233
name = supplemental;
228234
path = ../../supplemental;
@@ -475,9 +481,11 @@
475481
666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */,
476482
66BBC76F1ED4C8790015CB9B /* HexColor.swift in Sources */,
477483
66BBC7751ED729A80015CB9B /* FadeExample.m in Sources */,
484+
07730EDF1F02B58F007BAEFC /* ModalInteractiveViewController.swift in Sources */,
478485
072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */,
479486
66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */,
480487
667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */,
488+
07730EDD1F02B586007BAEFC /* MenuInteractiveExample.swift in Sources */,
481489
66BBC7701ED4C8790015CB9B /* Layout.swift in Sources */,
482490
6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */,
483491
66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
Copyright 2017-present The Material Motion Authors. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import Foundation
18+
import UIKit
19+
20+
class ModalGestureViewController: ExampleViewController {
21+
override func viewDidLoad() {
22+
super.viewDidLoad()
23+
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleGesture)))
24+
}
25+
26+
var percentage = CGFloat(0.01)
27+
func handleGesture(_ sender: UIPanGestureRecognizer) {
28+
let translation = sender.location(in: sender.view?.superview)
29+
switch sender.state {
30+
case .began:
31+
dismiss(animated: true, completion: nil)
32+
case .changed:
33+
percentage = 1-(translation.x / (sender.view!.frame.width/2))
34+
print(percentage)
35+
percentage = min(percentage, 0.99)
36+
print(translation.x)
37+
38+
interactiveTransitionContext?.updatePercent(percentage)
39+
case .ended:
40+
if percentage > 0.8 {
41+
interactiveTransitionContext?.finishInteractiveTransition()
42+
print("finished")
43+
} else {
44+
interactiveTransitionContext?.cancelInteractiveTransition()
45+
print("canceled")
46+
}
47+
interactiveTransitionContext = nil
48+
percentage = CGFloat(0.01)
49+
default:
50+
break
51+
}
52+
}
53+
}
54+
55+
class ModalInteractiveViewController: ModalGestureViewController {
56+
57+
override func viewDidLoad() {
58+
super.viewDidLoad()
59+
60+
view.backgroundColor = .primaryColor
61+
62+
let label = UILabel(frame: view.bounds)
63+
label.numberOfLines = 0
64+
label.lineBreakMode = .byWordWrapping
65+
label.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. In aliquam dolor eget orci condimentum, eu blandit metus dictum. Suspendisse vitae metus pellentesque, sagittis massa vel, sodales velit. Aliquam placerat nibh et posuere interdum. Etiam fermentum purus vel turpis lobortis auctor. Curabitur auctor maximus purus, ac iaculis mi. In ac hendrerit sapien, eget porttitor risus. Integer placerat cursus viverra. Proin mollis nulla vitae nisi posuere, eu rutrum mauris condimentum. Nullam in faucibus nulla, non tincidunt lectus. Maecenas mollis massa purus, in viverra elit molestie eu. Nunc volutpat magna eget mi vestibulum pharetra. Suspendisse nulla ligula, laoreet non ante quis, vehicula facilisis libero. Morbi faucibus, sapien a convallis sodales, leo quam scelerisque leo, ut tincidunt diam velit laoreet nulla. Proin at quam vel nibh varius ultrices porta id diam. Pellentesque pretium consequat neque volutpat tristique. Sed placerat a purus ut molestie. Nullam laoreet venenatis urna non pulvinar. Proin a vestibulum nulla, eu placerat est. Morbi molestie aliquam justo, ut aliquet neque tristique consectetur. In hac habitasse platea dictumst. Fusce vehicula justo in euismod elementum. Ut vel malesuada est. Aliquam mattis, ex vel viverra eleifend, mauris nibh faucibus nibh, in fringilla sem purus vitae elit. Donec sed dapibus orci, ut vulputate sapien. Integer eu magna efficitur est pellentesque tempor. Sed ac imperdiet ex. Maecenas congue quis lacus vel dictum. Phasellus dictum mi at sollicitudin euismod. Mauris laoreet, eros vitae euismod commodo, libero ligula pretium massa, in scelerisque eros dui eu metus. Fusce elementum mauris velit, eu tempor nulla congue ut. In at tellus id quam feugiat semper eget ut felis. Nulla quis varius quam. Nullam tincidunt laoreet risus, ut aliquet nisl gravida id. Nulla iaculis mauris velit, vitae feugiat nunc scelerisque ac. Vivamus eget ligula porta, pulvinar ex vitae, sollicitudin erat. Maecenas semper ornare suscipit. Ut et neque condimentum lectus pulvinar maximus in sit amet odio. Aliquam congue purus erat, eu rutrum risus placerat a."
66+
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
67+
view.addSubview(label)
68+
}
69+
70+
override var preferredStatusBarStyle: UIStatusBarStyle {
71+
return .lightContent
72+
}
73+
}

0 commit comments

Comments
 (0)