Browse Source

Initial commit

master
Guilherme Rambo 2 years ago
commit
24ad2a1d02
No known key found for this signature in database GPG Key ID: 87A5DFB1FFAB6724
  1. 106
      .gitignore
  2. 395
      Sheet.xcodeproj/project.pbxproj
  3. 7
      Sheet.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  4. 8
      Sheet.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  5. 67
      Sheet/Animation/FluidTimingCurve.swift
  6. 46
      Sheet/AppDelegate.swift
  7. 98
      Sheet/Assets.xcassets/AppIcon.appiconset/Contents.json
  8. 6
      Sheet/Assets.xcassets/Contents.json
  9. 25
      Sheet/Base.lproj/LaunchScreen.storyboard
  10. 40
      Sheet/Base.lproj/Main.storyboard
  11. 217
      Sheet/Bottom Sheet/Chrome/SheetViewController.swift
  12. 555
      Sheet/Bottom Sheet/Presentation/SheetContainerViewController.swift
  13. 21
      Sheet/Bottom Sheet/Presentation/SheetPresentationWindow.swift
  14. 142
      Sheet/Bottom Sheet/Presentation/SheetPresenter.swift
  15. 43
      Sheet/Info.plist
  16. 28
      Sheet/ViewController.swift

106
.gitignore

@ -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

395
Sheet.xcodeproj/project.pbxproj

@ -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 */;
}

7
Sheet.xcodeproj/project.xcworkspace/contents.xcworkspacedata

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

8
Sheet.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -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>

67
Sheet/Animation/FluidTimingCurve.swift

@ -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

@ -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:.
}
}

98
Sheet/Assets.xcassets/AppIcon.appiconset/Contents.json

@ -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"
}
}

6
Sheet/Assets.xcassets/Contents.json

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

25
Sheet/Base.lproj/LaunchScreen.storyboard

@ -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>

40
Sheet/Base.lproj/Main.storyboard

@ -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>

217
Sheet/Bottom Sheet/Chrome/SheetViewController.swift

@ -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
}
}
}

555
Sheet/Bottom Sheet/Presentation/SheetContainerViewController.swift

@ -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
}
}

21
Sheet/Bottom Sheet/Presentation/SheetPresentationWindow.swift

@ -0,0 +1,21 @@
//
// SheetPresentationWindow.swift
// Sheet
//
// Created by Guilherme Rambo on 05/08/19.
// Copyright © 2019 Peixe Ur