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

Add interactive sliding menu as an example #22

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions examples/MenuInteractiveExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
Copyright 2017-present The Material Motion Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import UIKit
import Transitioning

class MenuGestureViewController: ExampleViewController {
var call: (() -> Void)! = nil

public func setCall(call: @escaping ()->Void) {
self.call = call
}

override func viewDidLoad() {
super.viewDidLoad()

let tap = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(edgePanGesture))
tap.edges = .left
view.addGestureRecognizer(tap)

navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}

var percentage = CGFloat(0.01)
func edgePanGesture(_ sender: UIScreenEdgePanGestureRecognizer) {
let translation = sender.location(in: sender.view?.superview)
switch sender.state {
case .began:
call()
case .changed:
percentage = translation.x / ((sender.view!.frame.width)/2)
percentage = min(percentage, 0.99)
interactiveTransitionContext?.updatePercent(percentage)
case .ended:
if percentage > 0.8 {
interactiveTransitionContext?.finishInteractiveTransition()
} else {
interactiveTransitionContext?.cancelInteractiveTransition()
}
interactiveTransitionContext = nil
percentage = CGFloat(0.01)
default:
break
}
}
}

// This example demonstrates the minimal path to building a custom transition using the Material
// Motion Transitioning APIs in Swift. The essential steps have been documented below.

class MenuInteractiveExampleViewController: MenuGestureViewController {

func didTap() {
let modalViewController = ModalInteractiveViewController()

// The transition controller is an associated object on all UIViewController instances that
// allows you to customize the way the view controller is presented. The primary API on the
// controller that you'll make use of is the `transition` property. Setting this property will
// dictate how the view controller is presented. For this example we've built a custom
// FadeTransition, so we'll make use of that now:
modalViewController.transitionController.transition = MenuTransition()

// Note that once we assign the transition object to the view controller, the transition will
// govern all subsequent presentations and dismissals of that view controller instance. If we
// want to use a different transition (e.g. to use an edge-swipe-to-dismiss transition) then we
// can simply change the transition object before initiating the transition.

present(modalViewController, animated: true)
}

override func viewDidLoad() {
super.viewDidLoad()

let label = UILabel(frame: view.bounds)
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
label.textColor = .white
label.textAlignment = .center
label.text = "Swipe from left edge to start the transition"
view.addSubview(label)

setCall(call: didTap)
}

override func exampleInformation() -> ExampleInfo {
return .init(title: type(of: self).catalogBreadcrumbs().last!,
instructions: "Tap to present a modal transition.")
}
}

// Transitions must be NSObject types that conform to the Transition protocol.
private final class MenuTransition: NSObject, Transition, InteractiveTransition {

// The sole method we're expected to implement, start is invoked each time the view controller is
// presented or dismissed.
func start(with context: TransitionContext) {
let fromVC = context.backViewController
let toVC = context.foreViewController
let containerView = context.containerView

if(context.direction == .forward) {
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
toVC.view.frame.origin.x = -1 * toVC.view.frame.width
UIView.animate(
withDuration: context.duration,
delay: 0,
options: .curveLinear,
animations: {
toVC.view.frame.origin.x = -1 * (toVC.view.frame.width / 2)
},
completion: { _ in
let deadlineTime = DispatchTime.now() + .milliseconds(10)
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
context.transitionDidEnd()
}
}
)
if let snapshot = fromVC.view.snapshotView(afterScreenUpdates: false) {
snapshot.isUserInteractionEnabled = false
containerView.insertSubview(snapshot, belowSubview: toVC.view)
snapshot.tag = 2000
}
} else {
UIView.animate(
withDuration: context.duration,
delay: 0,
options: .curveLinear,
animations: {
toVC.view.frame.origin.x = -1 * toVC.view.frame.width
},
completion: { _ in

let deadlineTime = DispatchTime.now() + .milliseconds(10)
DispatchQueue.main.asyncAfter(deadline: deadlineTime) {
context.transitionDidEnd()
}

if(context.wasCancelled == false) {
containerView.viewWithTag(2000)?.removeFromSuperview()
containerView.insertSubview(toVC.view, aboveSubview: fromVC.view)
}
}
)
}
}

func isInteractive(_ context: TransitionContext) -> Bool {
return true
}

func start(withInteractiveContext context: InteractiveTransitionContext) {
context.sourceViewController!.interactiveTransitionContext = context
context.foreViewController.interactiveTransitionContext = context
}
}
4 changes: 4 additions & 0 deletions examples/apps/Catalog/TableOfContents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ extension MenuExampleViewController {
class func catalogBreadcrumbs() -> [String] { return ["2. Menu transition"] }
}

extension MenuInteractiveExampleViewController {
class func catalogBreadcrumbs() -> [String] { return ["2. Menu transition (interactive)"] }
}

extension CustomPresentationExampleViewController {
class func catalogBreadcrumbs() -> [String] { return ["3. Custom presentation transitions"] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

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

/* Begin PBXFileReference section */
072A063A1EEE26A900B9B5FC /* MenuExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuExample.swift; sourceTree = "<group>"; };
07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuInteractiveExample.swift; sourceTree = "<group>"; };
07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalInteractiveViewController.swift; sourceTree = "<group>"; };
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>"; };
2408A4B72C0BA93CC963452F /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
Expand Down Expand Up @@ -172,6 +176,7 @@
66BBC7731ED729A70015CB9B /* FadeExample.h */,
66BBC7741ED729A70015CB9B /* FadeExample.m */,
072A063A1EEE26A900B9B5FC /* MenuExample.swift */,
07730EDC1F02B586007BAEFC /* MenuInteractiveExample.swift */,
);
name = examples;
path = ../..;
Expand Down Expand Up @@ -223,6 +228,7 @@
66BBC76B1ED4C8790015CB9B /* HexColor.swift */,
66BBC76C1ED4C8790015CB9B /* Layout.swift */,
6629151F1ED5E137002B9A5D /* ModalViewController.swift */,
07730EDE1F02B58F007BAEFC /* ModalInteractiveViewController.swift */,
);
name = supplemental;
path = ../../supplemental;
Expand Down Expand Up @@ -475,9 +481,11 @@
666FAA841D384A6B000363DA /* AppDelegate.swift in Sources */,
66BBC76F1ED4C8790015CB9B /* HexColor.swift in Sources */,
66BBC7751ED729A80015CB9B /* FadeExample.m in Sources */,
07730EDF1F02B58F007BAEFC /* ModalInteractiveViewController.swift in Sources */,
072A063B1EEE26A900B9B5FC /* MenuExample.swift in Sources */,
66BBC76D1ED4C8790015CB9B /* ExampleViewController.swift in Sources */,
667A3F541DEE273000CB3A99 /* TableOfContents.swift in Sources */,
07730EDD1F02B586007BAEFC /* MenuInteractiveExample.swift in Sources */,
66BBC7701ED4C8790015CB9B /* Layout.swift in Sources */,
6629151E1ED5E0E0002B9A5D /* CustomPresentationExample.swift in Sources */,
66BBC76E1ED4C8790015CB9B /* ExampleViews.swift in Sources */,
Expand Down
73 changes: 73 additions & 0 deletions examples/supplemental/ModalInteractiveViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Copyright 2017-present The Material Motion Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Foundation
import UIKit

class ModalGestureViewController: ExampleViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handleGesture)))
}

var percentage = CGFloat(0.01)
func handleGesture(_ sender: UIPanGestureRecognizer) {
let translation = sender.location(in: sender.view?.superview)
switch sender.state {
case .began:
dismiss(animated: true, completion: nil)
case .changed:
percentage = 1-(translation.x / (sender.view!.frame.width/2))
print(percentage)
percentage = min(percentage, 0.99)
print(translation.x)

interactiveTransitionContext?.updatePercent(percentage)
case .ended:
if percentage > 0.8 {
interactiveTransitionContext?.finishInteractiveTransition()
print("finished")
} else {
interactiveTransitionContext?.cancelInteractiveTransition()
print("canceled")
}
interactiveTransitionContext = nil
percentage = CGFloat(0.01)
default:
break
}
}
}

class ModalInteractiveViewController: ModalGestureViewController {

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .primaryColor

let label = UILabel(frame: view.bounds)
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
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."
label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(label)
}

override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
7 changes: 7 additions & 0 deletions src/MDMTransition.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import <UIKit/UIKit.h>

@protocol MDMTransitionContext;
@protocol MDMInteractiveTransitionContext;

/**
A transition coordinates the animated presentation or dismissal of a view controller.
Expand Down Expand Up @@ -111,3 +112,9 @@ NS_SWIFT_NAME(presentationController(forPresented:presenting:source:));
// clang-format on

@end

NS_SWIFT_NAME(InteractiveTransition)
@protocol MDMInteractiveTransition <NSObject>
- (Boolean)isInteractive:(nonnull id<MDMTransitionContext>)context;
- (void)startWithInteractiveContext:(nonnull id<MDMInteractiveTransitionContext>)context;
@end
10 changes: 10 additions & 0 deletions src/MDMTransitionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ NS_SWIFT_NAME(TransitionContext)
*/
- (void)transitionDidEnd;

@property(nonatomic, readonly) BOOL wasCancelled;

/**
The direction this transition is moving in.
*/
Expand Down Expand Up @@ -84,3 +86,11 @@ NS_SWIFT_NAME(TransitionContext)
*/
@property(nonatomic, strong, readonly, nullable) UIPresentationController *presentationController;
@end

NS_SWIFT_NAME(InteractiveTransitionContext)
@protocol MDMInteractiveTransitionContext <MDMTransitionContext>
- (UIPercentDrivenInteractiveTransition *_Nonnull)getPercentIT;
- (void)updatePercent:(CGFloat)percent;
- (void)finishInteractiveTransition;
- (void)cancelInteractiveTransition;
@end
2 changes: 1 addition & 1 deletion src/MDMTransitionController.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import <Foundation/Foundation.h>

@protocol MDMTransition;
@protocol MDMInteractiveTransition;

/**
A transition controller is a bridge between UIKit's view controller transitioning APIs and
Expand Down Expand Up @@ -44,5 +45,4 @@ NS_SWIFT_NAME(TransitionController)
This may be non-nil while a transition is active.
*/
@property(nonatomic, strong, nullable, readonly) id<MDMTransition> activeTransition;

@end
2 changes: 2 additions & 0 deletions src/UIViewController+TransitionController.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#import <UIKit/UIKit.h>

@protocol MDMTransitionController;
@protocol MDMInteractiveTransitionContext;

@interface UIViewController (MDMTransitionController)

Expand All @@ -32,4 +33,5 @@
@property(nonatomic, strong, readonly, nonnull) id<MDMTransitionController> mdm_transitionController
NS_SWIFT_NAME(transitionController);

@property(nonatomic, strong, nullable) id<MDMInteractiveTransitionContext> interactiveTransitionContext;
@end
10 changes: 10 additions & 0 deletions src/UIViewController+TransitionController.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ @implementation UIViewController (MDMTransitionController)

#pragma mark - Public

- (id<MDMInteractiveTransition>)interactiveTransitionContext {
//const void *key = [self mdm_transitionControllerKey];
return objc_getAssociatedObject(self, "interactions");
}

- (void)setInteractiveTransitionContext:(id<MDMInteractiveTransition>)interactiveTransition {
//const void *key = [self mdm_transitionControllerKey];
objc_setAssociatedObject(self, "interactions", interactiveTransition, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id<MDMTransitionController>)mdm_transitionController {
const void *key = [self mdm_transitionControllerKey];

Expand Down
Loading