Initial commit

This commit is contained in:
Guilherme Rambo 2019-08-28 11:49:52 -03:00
commit 24ad2a1d02
No known key found for this signature in database
GPG Key ID: 87A5DFB1FFAB6724
16 changed files with 1804 additions and 0 deletions

106
.gitignore vendored Normal file
View File

@ -0,0 +1,106 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData/
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xcuserstate
.DS_Store
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
fastlane/report.xml
fastlane/screenshots
fastlane/Preview.html
fastlane/test_output
# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
iOSInjectionProject/
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# SVN
.svn/
*.xcbkptlist
Vendor/Dependencies
*.todo

View File

@ -0,0 +1,395 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
DDF6E75A2316CA9000251A21 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7592316CA9000251A21 /* AppDelegate.swift */; };
DDF6E75C2316CA9000251A21 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E75B2316CA9000251A21 /* ViewController.swift */; };
DDF6E75F2316CA9000251A21 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDF6E75D2316CA9000251A21 /* Main.storyboard */; };
DDF6E7612316CA9100251A21 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDF6E7602316CA9100251A21 /* Assets.xcassets */; };
DDF6E7642316CA9100251A21 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DDF6E7622316CA9100251A21 /* LaunchScreen.storyboard */; };
DDF6E7742316CAA300251A21 /* SheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E76E2316CAA300251A21 /* SheetViewController.swift */; };
DDF6E7752316CAA300251A21 /* SheetPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7702316CAA300251A21 /* SheetPresenter.swift */; };
DDF6E7762316CAA300251A21 /* SheetPresentationWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7712316CAA300251A21 /* SheetPresentationWindow.swift */; };
DDF6E7772316CAA300251A21 /* SheetContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E7722316CAA300251A21 /* SheetContainerViewController.swift */; };
DDF6E77F2316CAEE00251A21 /* FluidTimingCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6E77B2316CAEE00251A21 /* FluidTimingCurve.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
DDF6E7562316CA9000251A21 /* Sheet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sheet.app; sourceTree = BUILT_PRODUCTS_DIR; };
DDF6E7592316CA9000251A21 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
DDF6E75B2316CA9000251A21 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
DDF6E75E2316CA9000251A21 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
DDF6E7602316CA9100251A21 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
DDF6E7632316CA9100251A21 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
DDF6E7652316CA9100251A21 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DDF6E76E2316CAA300251A21 /* SheetViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetViewController.swift; sourceTree = "<group>"; };
DDF6E7702316CAA300251A21 /* SheetPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetPresenter.swift; sourceTree = "<group>"; };
DDF6E7712316CAA300251A21 /* SheetPresentationWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetPresentationWindow.swift; sourceTree = "<group>"; };
DDF6E7722316CAA300251A21 /* SheetContainerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetContainerViewController.swift; sourceTree = "<group>"; };
DDF6E77B2316CAEE00251A21 /* FluidTimingCurve.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FluidTimingCurve.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
DDF6E7532316CA9000251A21 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
DDF6E74D2316CA8F00251A21 = {
isa = PBXGroup;
children = (
DDF6E7582316CA9000251A21 /* Sheet */,
DDF6E7572316CA9000251A21 /* Products */,
);
sourceTree = "<group>";
};
DDF6E7572316CA9000251A21 /* Products */ = {
isa = PBXGroup;
children = (
DDF6E7562316CA9000251A21 /* Sheet.app */,
);
name = Products;
sourceTree = "<group>";
};
DDF6E7582316CA9000251A21 /* Sheet */ = {
isa = PBXGroup;
children = (
DDF6E7782316CAEE00251A21 /* Animation */,
DDF6E76B2316CAA300251A21 /* Bottom Sheet */,
DDF6E7592316CA9000251A21 /* AppDelegate.swift */,
DDF6E75B2316CA9000251A21 /* ViewController.swift */,
DDF6E75D2316CA9000251A21 /* Main.storyboard */,
DDF6E7602316CA9100251A21 /* Assets.xcassets */,
DDF6E7622316CA9100251A21 /* LaunchScreen.storyboard */,
DDF6E7652316CA9100251A21 /* Info.plist */,
);
path = Sheet;
sourceTree = "<group>";
};
DDF6E76B2316CAA300251A21 /* Bottom Sheet */ = {
isa = PBXGroup;
children = (
DDF6E76C2316CAA300251A21 /* Chrome */,
DDF6E76F2316CAA300251A21 /* Presentation */,
);
path = "Bottom Sheet";
sourceTree = "<group>";
};
DDF6E76C2316CAA300251A21 /* Chrome */ = {
isa = PBXGroup;
children = (
DDF6E76E2316CAA300251A21 /* SheetViewController.swift */,
);
path = Chrome;
sourceTree = "<group>";
};
DDF6E76F2316CAA300251A21 /* Presentation */ = {
isa = PBXGroup;
children = (
DDF6E7702316CAA300251A21 /* SheetPresenter.swift */,
DDF6E7712316CAA300251A21 /* SheetPresentationWindow.swift */,
DDF6E7722316CAA300251A21 /* SheetContainerViewController.swift */,
);
path = Presentation;
sourceTree = "<group>";
};
DDF6E7782316CAEE00251A21 /* Animation */ = {
isa = PBXGroup;
children = (
DDF6E77B2316CAEE00251A21 /* FluidTimingCurve.swift */,
);
path = Animation;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
DDF6E7552316CA9000251A21 /* Sheet */ = {
isa = PBXNativeTarget;
buildConfigurationList = DDF6E7682316CA9100251A21 /* Build configuration list for PBXNativeTarget "Sheet" */;
buildPhases = (
DDF6E7522316CA9000251A21 /* Sources */,
DDF6E7532316CA9000251A21 /* Frameworks */,
DDF6E7542316CA9000251A21 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = Sheet;
productName = Sheet;
productReference = DDF6E7562316CA9000251A21 /* Sheet.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
DDF6E74E2316CA8F00251A21 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1030;
LastUpgradeCheck = 1030;
ORGANIZATIONNAME = "Guilherme Rambo";
TargetAttributes = {
DDF6E7552316CA9000251A21 = {
CreatedOnToolsVersion = 10.3;
};
};
};
buildConfigurationList = DDF6E7512316CA8F00251A21 /* Build configuration list for PBXProject "Sheet" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = DDF6E74D2316CA8F00251A21;
productRefGroup = DDF6E7572316CA9000251A21 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
DDF6E7552316CA9000251A21 /* Sheet */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
DDF6E7542316CA9000251A21 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DDF6E7642316CA9100251A21 /* LaunchScreen.storyboard in Resources */,
DDF6E7612316CA9100251A21 /* Assets.xcassets in Resources */,
DDF6E75F2316CA9000251A21 /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
DDF6E7522316CA9000251A21 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DDF6E7742316CAA300251A21 /* SheetViewController.swift in Sources */,
DDF6E7752316CAA300251A21 /* SheetPresenter.swift in Sources */,
DDF6E7762316CAA300251A21 /* SheetPresentationWindow.swift in Sources */,
DDF6E77F2316CAEE00251A21 /* FluidTimingCurve.swift in Sources */,
DDF6E7772316CAA300251A21 /* SheetContainerViewController.swift in Sources */,
DDF6E75C2316CA9000251A21 /* ViewController.swift in Sources */,
DDF6E75A2316CA9000251A21 /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
DDF6E75D2316CA9000251A21 /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
DDF6E75E2316CA9000251A21 /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
DDF6E7622316CA9100251A21 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
DDF6E7632316CA9100251A21 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
DDF6E7662316CA9100251A21 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
DDF6E7672316CA9100251A21 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.4;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
DDF6E7692316CA9100251A21 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 6ABEVHK7CE;
INFOPLIST_FILE = Sheet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.Sheet;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
DDF6E76A2316CA9100251A21 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = 6ABEVHK7CE;
INFOPLIST_FILE = Sheet/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = codes.rambo.Sheet;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
DDF6E7512316CA8F00251A21 /* Build configuration list for PBXProject "Sheet" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DDF6E7662316CA9100251A21 /* Debug */,
DDF6E7672316CA9100251A21 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
DDF6E7682316CA9100251A21 /* Build configuration list for PBXNativeTarget "Sheet" */ = {
isa = XCConfigurationList;
buildConfigurations = (
DDF6E7692316CA9100251A21 /* Debug */,
DDF6E76A2316CA9100251A21 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = DDF6E74E2316CA8F00251A21 /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Sheet.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,67 @@
//
// FluidTimingCurve.swift
// Sheet
//
// Created by Guilherme Rambo on 05/08/19.
// Copyright © 2019 Peixe Urbano. All rights reserved.
//
import UIKit
public final class FluidTimingCurve: NSObject, UITimingCurveProvider {
public let initialVelocity: CGVector
let mass: CGFloat
let stiffness: CGFloat
let damping: CGFloat
public init(velocity: CGVector, stiffness: CGFloat = 400, damping: CGFloat = 30, mass: CGFloat = 1.0) {
self.initialVelocity = velocity
self.stiffness = stiffness
self.damping = damping
self.mass = mass
super.init()
}
public func encode(with aCoder: NSCoder) {
fatalError("Not supported")
}
public init?(coder aDecoder: NSCoder) {
fatalError("Not supported")
}
public func copy(with zone: NSZone? = nil) -> Any {
return FluidTimingCurve(velocity: initialVelocity)
}
public var timingCurveType: UITimingCurveType {
return .composed
}
public var cubicTimingParameters: UICubicTimingParameters? {
return .init(animationCurve: .easeIn)
}
public var springTimingParameters: UISpringTimingParameters? {
return UISpringTimingParameters(mass: mass, stiffness: stiffness, damping: damping, initialVelocity: initialVelocity)
}
}
public extension UISpringTimingParameters {
/// A design-friendly way to create a spring timing curve.
///
/// - Parameters:
/// - damping: The 'bounciness' of the animation. Value must be between 0 and 1.
/// - response: The 'speed' of the animation.
/// - initialVelocity: The vector describing the starting motion of the property. Optional, default is `.zero`.
convenience init(damping: CGFloat, response: CGFloat, initialVelocity: CGVector = .zero) {
let stiffness = pow(2 * .pi / response, 2)
let damp = 4 * .pi * damping / response
self.init(mass: 1, stiffness: stiffness, damping: damp, initialVelocity: initialVelocity)
}
}

46
Sheet/AppDelegate.swift Normal file
View File

@ -0,0 +1,46 @@
//
// AppDelegate.swift
// Sheet
//
// Created by Guilherme Rambo on 28/08/19.
// Copyright © 2019 Guilherme Rambo. All rights reserved.
//
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}

View File

@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "20x20",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"scale" : "3x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "20x20",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "29x29",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "40x40",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "1x"
},
{
"idiom" : "ipad",
"size" : "76x76",
"scale" : "2x"
},
{
"idiom" : "ipad",
"size" : "83.5x83.5",
"scale" : "2x"
},
{
"idiom" : "ios-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_1" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="Sheet" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CQn-it-quH">
<rect key="frame" x="166" y="438" width="82" height="30"/>
<state key="normal" title="Show Sheet"/>
<connections>
<action selector="showSheet:" destination="BYZ-38-t0r" eventType="touchUpInside" id="iEs-xO-1Lh"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="CQn-it-quH" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="kE8-Hq-2Oy"/>
<constraint firstItem="CQn-it-quH" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="uPD-oZ-sli"/>
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,217 @@
//
// SheetViewController.swift
// Sheet
//
// Created by Guilherme Rambo on 05/08/19.
// Copyright © 2019 Peixe Urbano. All rights reserved.
//
import UIKit
class SheetViewController: UIViewController {
let metrics: SheetMetrics
init(metrics: SheetMetrics) {
self.metrics = metrics
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
private var container: SheetContainerViewController? {
return parent as? SheetContainerViewController
}
var rubberBandingStartHandler: (() -> Void)?
var rubberBandingUpdateHandler: ((CGFloat) -> Void)?
var rubberBandingFinishedHandler: (() -> Void)?
var isScrollingEnabled = true
private let scrollViewAtTheTopDeltaThreshold: CGFloat = 3
var isScrollViewAtTheTop: Bool {
return abs(scrollView.contentOffset.y - scrollView.contentInset.top * -1) < scrollViewAtTheTopDeltaThreshold
}
private lazy var contentView: SheetContentView = {
let v = SheetContentView(metrics: self.metrics)
v.layer.cornerRadius = view.layer.cornerRadius
v.layer.maskedCorners = view.layer.maskedCorners
v.autoresizingMask = [.flexibleWidth, .flexibleHeight]
v.clipsToBounds = true
return v
}()
private(set) lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.translatesAutoresizingMaskIntoConstraints = false
v.delegate = self
return v
}()
override func loadView() {
view = UIView()
view.backgroundColor = #colorLiteral(red: 0.9411764706, green: 0.9411764706, blue: 0.9411764706, alpha: 1)
view.layer.cornerRadius = metrics.cornerRadius
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = Float(metrics.shadowOpacity)
view.layer.shadowRadius = metrics.shadowRadius
view.layer.shadowOffset = CGSize(width: 0, height: -1)
contentView.frame = view.bounds
view.addSubview(contentView)
contentView.addSubview(scrollView)
NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
private weak var contentController: UIViewController?
func installContent(_ content: UIViewController) {
contentController = content
addChild(content)
content.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(content.view)
content.didMove(toParent: self)
NSLayoutConstraint.activate([
content.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
content.view.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
content.view.topAnchor.constraint(equalTo: scrollView.topAnchor),
content.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
content.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
content.view.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
])
}
var availableHeight: CGFloat {
guard let parentView = parent?.view else { return 0 }
return parentView.bounds.intersection(view.frame).height
}
private var scrolledUpOnFirstContentInsetUpdate = false
private var initialContentOffset: CGPoint = .zero
private var previousContentOffset: CGPoint = .zero
func updateContentInsets() {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: metrics.trueSheetHeight - availableHeight, right: 0)
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(scrollUpOnFirstContentInsetUpdateIfNeeded), object: nil)
perform(#selector(scrollUpOnFirstContentInsetUpdateIfNeeded), with: nil, afterDelay: 0)
}
@objc private func scrollUpOnFirstContentInsetUpdateIfNeeded() {
guard !scrollView.contentInset.top.isZero else { return }
guard !scrolledUpOnFirstContentInsetUpdate else { return }
scrolledUpOnFirstContentInsetUpdate = true
scrollView.setContentOffset(CGPoint(x: 0, y: -scrollView.contentInset.top), animated: false)
}
deinit {
print("\(String(describing: type(of: self))) DEINIT")
}
}
final class SheetContentView: UIView {
let metrics: SheetMetrics
init(metrics: SheetMetrics) {
self.metrics = metrics
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError()
}
}
extension SheetViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
initialContentOffset = scrollView.contentOffset
}
func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
rubberBandingStartHandler?()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
rubberBandingFinishedHandler?()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
defer { previousContentOffset = scrollView.contentOffset }
var isRubberBandingUp = false
var bandOffset: CGFloat = 0
if scrollView.isDecelerating {
let effectiveOffset = scrollView.contentOffset.y + scrollView.contentInset.top
if scrollView.contentOffset.y < initialContentOffset.y {
if effectiveOffset < 0 {
isRubberBandingUp = true
bandOffset = effectiveOffset
rubberBandingUpdateHandler?(effectiveOffset)
}
}
}
if isRubberBandingUp {
// Counteract rubber banding by shifting contents so that they are flush with the top.
// We can't use setContentOffset here because that kills the rubber banding.
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setAnimationDuration(0)
contentController?.view.layer.transform = CATransform3DMakeTranslation(0, bandOffset, 0)
CATransaction.commit()
} else {
let currentTransform = contentController?.view.layer.transform ?? CATransform3DIdentity
if !CATransform3DIsIdentity(currentTransform) {
CATransaction.begin()
CATransaction.setDisableActions(true)
CATransaction.setAnimationDuration(0)
contentController?.view.layer.transform = CATransform3DIdentity
CATransaction.commit()
}
}
guard isScrollingEnabled else {
scrollView.setContentOffset(previousContentOffset, animated: false)
return
}
}
}

View File

@ -0,0 +1,555 @@
//
// SheetContainerViewController.swift
// Sheet
//
// Created by Guilherme Rambo on 05/08/19.
// Copyright © 2019 Peixe Urbano. All rights reserved.
//
import UIKit
public struct SheetMetrics {
public static let `default` = SheetMetrics()
public let bufferHeight: CGFloat = 400
public let cornerRadius: CGFloat = 10
public let shadowRadius: CGFloat = 10
public let shadowOpacity: CGFloat = 0.12
public var trueSheetHeight: CGFloat {
return UIScreen.main.bounds.height + bufferHeight
}
}
/// Defines snapping positions for the sheet.
public enum SheetDetent: String, CaseIterable {
/// A detent where the sheet will have its maximum height and have
/// its top edge close to the top edge of the screen.
case maximum
/// A detent where the sheet's height will be about half the height
/// of the screen, with its top edge close to the middle of the screen.
case middle
/// A detent at which the sheet's contents are effectively hidden,
/// but the sheet's header still peek's through the bottom of the screen,
/// allowing the user to expand it.
case minimum
/// The velocity at which the sheet will ignore the middle detent and transition directly
/// from the maximum detent to the minimum detent when swiping down.
static let thresholdVelocityForSkippingMiddleDetent: CGFloat = 2000
/// The velocity at which the sheet will be dismissed instead of snapping
/// to a detent when flung down.
static let thresholdVelocityForFlingDismissal: CGFloat = 4000
/// The velocity at which the sheet will snap to the middle detent when flung upwards from
/// the minimum detent, ignoring the distance between the current position and the minimum detent.
static let thresholdVelocityForEnforcingMinimumToMiddleTransition: CGFloat = 900
}
extension SheetDetent: CustomDebugStringConvertible {
public var debugDescription: String {
switch self {
case .maximum: return "<Maximum Detent>"
case .middle: return "<Middle Detent>"
case .minimum: return "<Minimum Detent>"
}
}
}
class SheetContainerViewController: UIViewController {
var transitionToMaximumDetentProgressDidChange: ((CGFloat) -> Void)?
var performSnapCompanionAnimations: ((SheetDetent) -> Void)?
let sheetContentController: UIViewController
let initialDetent: SheetDetent
let metrics: SheetMetrics
let allowedDetents: [SheetDetent]
let dismissWhenFlungDown: Bool
weak var presentingSheetPresenter: SheetPresenter?
init(sheetContentController: UIViewController,
presentingSheetPresenter: SheetPresenter?,
initialDetent: SheetDetent = .middle,
allowedDetents: [SheetDetent] = SheetDetent.allCases,
metrics: SheetMetrics = .default,
dismissWhenFlungDown: Bool = false)
{
self.sheetContentController = sheetContentController
self.presentingSheetPresenter = presentingSheetPresenter
self.initialDetent = initialDetent
self.metrics = metrics
self.allowedDetents = allowedDetents
self.dismissWhenFlungDown = dismissWhenFlungDown
super.init(nibName: nil, bundle: nil)
}
private var overrideStatusBarStyle: UIStatusBarStyle? {
didSet {
setNeedsStatusBarAppearanceUpdate()
}
}
override var preferredStatusBarStyle: UIStatusBarStyle {
return overrideStatusBarStyle ?? super.preferredStatusBarStyle
}
override var childForStatusBarStyle: UIViewController? {
return overrideStatusBarStyle != nil ? nil : sheetContentController
}
override var childForStatusBarHidden: UIViewController? {
return sheetContentController
}
required init?(coder: NSCoder) {
fatalError()
}
#warning("TODO: Rubber band and limit maximum sheet height while interactively moving")
private var maximumSheetHeight: CGFloat {
return value(for: .maximum) - view.safeAreaInsets.top
}
#warning("TODO: Rubber band and limit minimum sheet height while interactively moving")
private var minimumSheetHeight: CGFloat {
return metrics.shadowRadius + view.safeAreaInsets.bottom
}
private func heightToBottom(constant: CGFloat) -> CGFloat {
return metrics.trueSheetHeight - constant
}
private func normalize(_ value: CGFloat, range: ClosedRange<CGFloat>) -> CGFloat {
return (value - range.lowerBound) / (range.upperBound - range.lowerBound)
}
private lazy var availableHeightOnMiddleDetent = abs(value(for: .middle) - metrics.trueSheetHeight)
private lazy var availableHeightOnMaximumDetent = abs(value(for: .maximum) - metrics.trueSheetHeight)
var maximumDetentAnimationProgress: CGFloat {
guard sheetBottomConstraint.constant < value(for: .middle) else { return 0 }
let currentAvailableHeight = sheetController.availableHeight
let rawMin = availableHeightOnMiddleDetent / availableHeightOnMaximumDetent
let rawValue = currentAvailableHeight / availableHeightOnMaximumDetent
return normalize(rawValue, range: rawMin...1)
}
private func value(for detent: SheetDetent) -> CGFloat {
switch detent {
case .maximum: return heightToBottom(constant: UIScreen.main.bounds.height * 0.92)
case .middle: return heightToBottom(constant: UIScreen.main.bounds.height * 0.54)
case .minimum: return heightToBottom(constant: UIScreen.main.bounds.height * 0.16)
}
}
private var flingDownDismissVelocity: CGFloat?
private var snappingCancelled = false {
didSet {
if snappingCancelled { view.isUserInteractionEnabled = false }
}
}
private func closestSnappingDetent(for height: CGFloat, velocity: CGPoint) -> SheetDetent {
// print("SNAP velocity = \(velocity)")
var winner: SheetDetent = .maximum
let validDetents: [SheetDetent]
if dismissWhenFlungDown, velocity.y > 0, abs(velocity.y) > SheetDetent.thresholdVelocityForFlingDismissal {
snappingCancelled = true
flingDownDismissVelocity = velocity.y
presentingSheetPresenter?.dismiss()
return .minimum
}
if abs(velocity.y) > SheetDetent.thresholdVelocityForSkippingMiddleDetent {
if velocity.y < 0 {
// Swiping up really hard, force maximum detent
validDetents = [.maximum]
} else {
// Swiping hard in any direction, ignore middle detent
validDetents = allowedDetents.filter({ $0 != .middle })
}
} else if velocity.y < 0,
abs(velocity.y) > SheetDetent.thresholdVelocityForEnforcingMinimumToMiddleTransition,
sheetBottomConstraint.constant > value(for: .middle)
{
// Swiping up hard in between minimum and medium detent, force middle detent
validDetents = [.middle]
} else {
validDetents = allowedDetents
}
for detent in validDetents {
if abs(height - value(for: detent)) < abs(height - value(for: winner)) {
winner = detent
}
}
return winner
}
private weak var currentAnimator: UIViewPropertyAnimator?
private func timingCurve(with velocity: CGFloat) -> FluidTimingCurve {
let damping: CGFloat = velocity.isZero ? 100 : 30
return FluidTimingCurve(
velocity: CGVector(dx: velocity, dy: velocity),
stiffness: 400,
damping: damping
)
}
private func estimateTargetDetent(with velocity: CGFloat) -> SheetDetent {
return .maximum
}
private func snap(to detent: SheetDetent, with velocity: CGPoint = .zero, completion: (() -> Void)? = nil) {
guard !snappingCancelled else { return }
if currentAnimator?.state == .some(.active) {
currentAnimator?.stopAnimation(true)
}
let targetValue = value(for: detent)
// the 0.5 is to ensure there's always some distance for the gesture to work with
let distanceY = (sheetBottomConstraint.constant - 0.5) - targetValue
let effectiveVelocity = velocity.y.isInfinite || velocity.y.isNaN ? 2000 : velocity.y
let initialVelocityY = distanceY.isZero ? 0 : effectiveVelocity/distanceY * -1
let timing = timingCurve(with: initialVelocityY)
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: timing)
animator.isUserInteractionEnabled = true
self.sheetBottomConstraint.constant = targetValue
animator.addAnimations {
self.performSnapCompanionAnimations?(detent)
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
self.sheetController.updateContentInsets()
if detent == .maximum {
self.dimmingView.alpha = self.maximumDimmingAlpha
self.overrideStatusBarStyle = .lightContent
} else {
if detent == .minimum {
self.dimmingView.alpha = 0
} else {
self.dimmingView.alpha = self.minimumDimmingAlpha
}
self.overrideStatusBarStyle = nil
}
}
animator.addCompletion { pos in
guard pos == .end else { return }
completion?()
}
currentAnimator = animator
animator.startAnimation()
}
func dismissSheet(coordinator: UIViewControllerTransitionCoordinator? = nil, duration: TimeInterval = 0.3, completion: (() -> Void)? = nil) {
let targetValue = metrics.trueSheetHeight
let animationBlock = {
self.dimmingView.alpha = 0
self.performSnapCompanionAnimations?(.minimum)
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
self.sheetController.updateContentInsets()
self.overrideStatusBarStyle = nil
}
if let coordinator = coordinator {
coordinator.animate(alongsideTransition: { _ in
animationBlock()
}, completion: { _ in
completion?()
})
} else {
let distanceY = sheetBottomConstraint.constant - targetValue
let effectiveVelocity: CGFloat
if let flingVelocity = flingDownDismissVelocity {
effectiveVelocity = flingVelocity.isInfinite || flingVelocity.isNaN ? 2000 : flingVelocity
} else {
effectiveVelocity = 0
}
let initialVelocityY = distanceY.isZero ? 0 : effectiveVelocity/distanceY * -1
let timing = timingCurve(with: initialVelocityY)
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: timing)
animator.isUserInteractionEnabled = true
self.sheetBottomConstraint.constant = targetValue
animator.addAnimations {
animationBlock()
}
animator.addCompletion { pos in
guard pos == .end else { return }
completion?()
}
animator.startAnimation()
}
}
private lazy var sheetBottomConstraint: NSLayoutConstraint = {
return sheetController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: metrics.trueSheetHeight)
}()
private(set) lazy var sheetController: SheetViewController = {
let v = SheetViewController(metrics: self.metrics)
v.rubberBandingStartHandler = { [weak self] in
self?.registerRubberBandingStart()
}
v.rubberBandingUpdateHandler = { [weak self] offset in
self?.followSheetScrollViewRubberBanding(with: offset)
}
v.rubberBandingFinishedHandler = { [weak self] in
self?.rubberBandingFinished()
}
return v
}()
private lazy var dimmingView: UIView = {
let v = UIView()
v.backgroundColor = .black
v.alpha = 0
v.autoresizingMask = [.flexibleWidth, .flexibleHeight]
v.frame = view.bounds
return v
}()
private lazy var panGesture: UIPanGestureRecognizer = {
let g = UIPanGestureRecognizer(target: self, action: #selector(handlePan))
g.delegate = self
return g
}()
override func loadView() {
view = SheetContainerView(metrics: metrics)
view.addSubview(dimmingView)
addChild(sheetController)
view.addSubview(sheetController.view)
sheetController.didMove(toParent: self)
NSLayoutConstraint.activate([
sheetController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
sheetController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
sheetController.view.heightAnchor.constraint(equalToConstant: metrics.trueSheetHeight),
sheetBottomConstraint
])
sheetController.installContent(sheetContentController)
sheetController.view.addGestureRecognizer(panGesture)
}
private var snappedToInitialDetent = false
private func snapToInitialDetent() {
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(doSnapToInitialDetent), with: nil, afterDelay: 0)
}
@objc private func doSnapToInitialDetent() {
snap(to: initialDetent)
}
override func viewDidLoad() {
super.viewDidLoad()
snapToInitialDetent()
}
private var isDraggingSheet = false
private var lastTranslationY: CGFloat = 0
private var initialSheetHeightConstant: CGFloat = 0
private var minimumDimmingAlpha: CGFloat = 0.1
private var maximumDimmingAlpha: CGFloat = 0.5
private func snapToClosestDetent(with velocity: CGPoint) {
let target = closestSnappingDetent(for: sheetBottomConstraint.constant, velocity: velocity)
snap(to: target, with: velocity)
}
@objc private func handlePan(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: view)
let velocity = recognizer.velocity(in: view)
switch recognizer.state {
case .began:
isDraggingSheet = true
initialSheetHeightConstant = sheetBottomConstraint.constant
case .ended, .cancelled, .failed:
isDraggingSheet = false
if !sheetController.isScrollingEnabled {
snapToClosestDetent(with: velocity)
}
lastTranslationY = 0
case .changed:
let newConstant = sheetBottomConstraint.constant + (translation.y - lastTranslationY)
if sheetController.isScrollViewAtTheTop {
if newConstant > value(for: .maximum) || translation.y > 0 {
sheetBottomConstraint.constant = newConstant
sheetController.updateContentInsets()
sheetController.isScrollingEnabled = false
progressMaximumDetentInteractiveAnimation()
} else {
sheetController.isScrollingEnabled = true
}
} else {
sheetController.isScrollingEnabled = true
}
lastTranslationY = translation.y
default:
break
}
}
private var sheetBottomConstantAtRubberBandingStart: CGFloat = 0
private func registerRubberBandingStart() {
sheetBottomConstantAtRubberBandingStart = sheetBottomConstraint.constant
}
private func rubberBandingFinished() {
guard currentAnimator?.state != .active else { return }
snapToClosestDetent(with: .zero)
}
private func followSheetScrollViewRubberBanding(with offset: CGFloat) {
guard offset < 0 else { return } // only follow rubber banding when at the top
sheetBottomConstraint.constant = sheetBottomConstantAtRubberBandingStart - offset
progressMaximumDetentInteractiveAnimation()
}
private func progressMaximumDetentInteractiveAnimation() {
let progressToMaxDetent = maximumDetentAnimationProgress
if progressToMaxDetent >= 0.5 {
overrideStatusBarStyle = .lightContent
} else {
overrideStatusBarStyle = nil
}
if sheetBottomConstraint.constant < value(for: .minimum) {
let duration: TimeInterval = dimmingView.alpha == 0 ? 0.3 : 0
UIView.animate(withDuration: duration) {
self.dimmingView.alpha = self.minimumDimmingAlpha + self.maximumDimmingAlpha * progressToMaxDetent
}
} else {
dimmingView.alpha = 0
}
transitionToMaximumDetentProgressDidChange?(progressToMaxDetent)
}
deinit {
print("\(String(describing: type(of: self))) DEINIT")
}
}
private final class SheetContainerView: UIView {
let metrics: SheetMetrics
init(metrics: SheetMetrics) {
self.metrics = metrics
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let result = super.hitTest(point, with: event) else { return nil }
return result.isSheetDescendant ? result : nil
}
}
extension UIView {
var isSheetDescendant: Bool {
var currentView: UIView? = self
repeat {
if currentView is SheetContentView { return true }
currentView = currentView?.superview
} while currentView != nil
return false
}
}
extension SheetContainerViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}

View File

@ -0,0 +1,21 @@
//
// SheetPresentationWindow.swift
// Sheet
//
// Created by Guilherme Rambo on 05/08/19.
// Copyright © 2019 Peixe Urbano. All rights reserved.
//
import UIKit
final class SheetPresentationWindow: UIWindow {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return rootViewController?.view.hitTest(point, with: event) != nil
}
deinit {
print("\(String(describing: type(of: self))) DEINIT")
}
}

View File

@ -0,0 +1,142 @@
//
// SheetPresenter.swift
// Sheet
//
// Created by Guilherme Rambo on 05/08/19.
// Copyright © 2019 Peixe Urbano. All rights reserved.
//
import UIKit
/// Allows a controller to present another controller as a sheet that can be
/// snapped to different positions.
public final class SheetPresenter: NSObject {
private var window: SheetPresentationWindow?
private var container: SheetContainerViewController?
private weak var presenter: UIViewController?
private var presenterWindow: UIWindow? {
return presenter?.view.window
}
/// Whether the sheet is currently being presented.
private(set) var isPresentingSheet = false
/// Starts the presentation of a controller as a sheet.
/// - Parameter presenter: The view controller that's presenting the sheet.
/// - Parameter content: The view controller that will be inside the sheet.
/// - Parameter initialDetent: The initial position of the sheet (defaults to `.middle`)
/// - Parameter allowedDetents: The allowed snapping positions for the sheet (defaults to all positions).
/// - Parameter dismissWhenFlungDown: Whether the sheet can be dismissed when flung down by the user.
/// - Parameter metrics: Metrics defining the look of the sheet (can be ommited to use default metrics).
public func presentSheet(from presenter: UIViewController,
with content: UIViewController,
initialDetent: SheetDetent = .middle,
allowedDetents: [SheetDetent] = SheetDetent.allCases,
dismissWhenFlungDown: Bool = false,
metrics: SheetMetrics = .default)
{
guard !isPresentingSheet else { return }
assert(presenter.view.window != nil, "Tried to present a sheet from a view controller that's not currently on screen!")
guard window == nil else { return }
self.presenter = presenter
presenterWindow?.clipsToBounds = true
let w = SheetPresentationWindow(frame: presenter.view.bounds)
let c = SheetContainerViewController(
sheetContentController: content,
presentingSheetPresenter: self,
initialDetent: initialDetent,
allowedDetents: allowedDetents,
metrics: metrics,
dismissWhenFlungDown: dismissWhenFlungDown
)
w.rootViewController = c
w.windowLevel = .alert
w.makeKeyAndVisible()
self.window = w
self.container = c
c.performSnapCompanionAnimations = { [weak self] detent in
guard let self = self else { return }
switch detent {
case .maximum:
self.animateToMaximumDetent()
default:
self.animateToNonMaximumDetent()
}
}
c.transitionToMaximumDetentProgressDidChange = { [weak self] progress in
self?.updateSheetAnimationStateToMaximumDetent(with: progress)
}
isPresentingSheet = true
}
/// Dismisses the sheet.
/// - Parameter coordinator: Perform the dismissal together with an animated transition.
/// - Parameter completion: Called when the dismissal animation has completed.
public func dismiss(with coordinator: UIViewControllerTransitionCoordinator? = nil, completion: (() -> Void)? = nil) {
container?.dismissSheet(duration: 0.4) { [weak self] in
completion?()
self?.window?.resignKey()
self?.window?.isHidden = true
self?.window?.removeFromSuperview()
self?.container = nil
self?.presenter = nil
self?.window = nil
self?.isPresentingSheet = false
}
}
private var presenterTranslationWhenAtMaximumDetent: CGFloat {
let safeAreaTop = container?.view.safeAreaInsets.top ?? 0
return safeAreaTop <= 20 ? safeAreaTop + 22 : safeAreaTop + 8
}
private let presenterHorizontalScaleWhenAtMaximumDetent: CGFloat = 0.914
private let presenterCornerRadiusWhenAtMaximumDetent: CGFloat = 10
private let presenterScaleWhenAtMaximumDetent: CGFloat = 0.9
private func updateSheetAnimationStateToMaximumDetent(with progress: CGFloat) {
let translation = presenterTranslationWhenAtMaximumDetent * progress
let radius = presenterCornerRadiusWhenAtMaximumDetent * progress
let scale = min(1, 1 - progress + presenterScaleWhenAtMaximumDetent)
let translationTransform = CATransform3DMakeTranslation(0, translation, 0)
let scaleTransform = CATransform3DMakeScale(scale, 1, 1)
presenterWindow?.layer.transform = CATransform3DConcat(translationTransform, scaleTransform)
presenterWindow?.layer.cornerRadius = radius
}
private func animateToMaximumDetent() {
let translationTransform = CATransform3DMakeTranslation(0, presenterTranslationWhenAtMaximumDetent, 0)
let scaleTransform = CATransform3DMakeScale(presenterScaleWhenAtMaximumDetent, 1, 1)
presenterWindow?.layer.transform = CATransform3DConcat(translationTransform, scaleTransform)
presenterWindow?.layer.cornerRadius = presenterCornerRadiusWhenAtMaximumDetent
}
private func animateToNonMaximumDetent() {
presenterWindow?.layer.transform = CATransform3DIdentity
presenterWindow?.layer.cornerRadius = 0
}
}

43
Sheet/Info.plist Normal file
View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1,28 @@
//
// ViewController.swift
// Sheet
//
// Created by Guilherme Rambo on 28/08/19.
// Copyright © 2019 Guilherme Rambo. All rights reserved.
//
import UIKit
class ViewController: UIViewController {
private lazy var sheetContentController: UIViewController = {
let c = UIViewController()
c.view.backgroundColor = .red
return c
}()
private lazy var sheetPresenter = SheetPresenter()
@IBAction func showSheet(_ sender: UIButton) {
sheetPresenter.presentSheet(from: self, with: sheetContentController)
}
}