Skip to content

Commit

Permalink
[Issue-26] Customize animations (#28)
Browse files Browse the repository at this point in the history
* [Issue-26] Customize animations

* Remove source code from README
  • Loading branch information
Mark Pospesel committed May 5, 2023
1 parent 7c61299 commit 2b0f317
Show file tree
Hide file tree
Showing 12 changed files with 143 additions and 81 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Expand Up @@ -21,7 +21,7 @@ let package = Package(
),
.package(
url: "https://github.com/yml-org/YMatterType.git",
from: "1.6.0"
from: "1.7.0"
)
],
targets: [
Expand Down
53 changes: 13 additions & 40 deletions README.md
Expand Up @@ -21,19 +21,6 @@ Usage
### Initializers
The Bottom sheet controller can be initialized with either a title and a view or else with a view controller.

```swift
init(
title: String,
childView: UIView,
appearance: BottomSheetController.Appearance = .default
)

init(
childController: UIViewController,
appearance: BottomSheetController.Appearance = .default
)
```

When initializing with a view controller, the title is drawn from `UIViewController.title`. When the view controller is a `UINavigationController`, the header appearance options are ignored and the navigation controller's navigation bar is displayed as the sheet's header. In this situation, if you wish to have a close button, then that should be set using the view controller's `navigationItem.rightBarButtonItem` or `.leftBarButtonItem`.

Both initializers include an appearance parameter that allows you to fully customize the sheet's appearance. You can also update the sheet's appearance at any time.
Expand Down Expand Up @@ -84,33 +71,15 @@ final class ViewController: UIViewController {
### Customization
`BottomSheetController` has an `appearance` property of type `Appearance`.

`Appearance` lets you customize the bottom sheet appearance. We can customize the appearance of the indicator view, the header view, dimmer color, animation etc.
`Appearance` lets you customize how the bottom sheet both appears and behaves. You can customize:

```swift
/// Determines the appearance of the bottom sheet.
public struct Appearance {
/// Appearance of the drag indicator.
public var indicatorAppearance: DragIndicatorView.Appearance?
/// Appearance of the sheet header view.
public var headerAppearance: SheetHeaderView.Appearance?
/// Bottom sheet layout properties such as corner radius. Default is `.default`.
public let layout: Layout
/// Bottom sheet's shadow. Default is `nil` (no shadow).
public let elevation: Elevation?
/// Dimmer view color. Default is 'UIColor.black.withAlphaComponent(0.5)'.
public let dimmerColor: UIColor?
/// Animation duration on bottom sheet. Default is `0.3`.
public let animationDuration: TimeInterval
/// Animation type during presenting. Default is `curveEaseIn`.
public let presentAnimationCurve: UIView.AnimationOptions
/// Animation type during dismissing. Default is `curveEaseOut`.
public let dismissAnimationCurve: UIView.AnimationOptions
/// (Optional) Minimum content view height. Default is `nil`.
///
/// Only applicable for resizable sheets. `nil` means to use the content view's intrinsic height as the minimum.
public var minimumContentHeight: CGFloat?
}
```
* drag indicator (whether you have one at all or what its size and color are)
* header (whether you have one at all or what its text color, typography, and optional close button image are)
* layout (corner radius and minimum, maximum, and ideal sizes for the sheet's contents)
* drop shadow (if any)
* dimmer color
* present animation
* dismiss animation

**Update or customize appearance**

Expand All @@ -133,8 +102,12 @@ sheet.appearance.elevation = Elevation(
color: .black,
opacity: 0.4
)
sheet.appearance.presentAnimation = Animation(
duration: 0.4,
curve: .spring(damping: 0.6, velocity: 0.4)
)

// Present the sheet.
// Present the sheet with a spring animation.
present(sheet, animated: true)
```

Expand Down
18 changes: 18 additions & 0 deletions Sources/YBottomSheet/Animation/Animation+BottomSheet.swift
@@ -0,0 +1,18 @@
//
// Animation+BottomSheet.swift
// YBottomSheet
//
// Created by Mark Pospesel on 5/4/23.
// Copyright © 2023 Y Media Labs. All rights reserved.
//

import YCoreUI

/// Default animations for bottom sheets
public extension Animation {
/// Default animation for presenting a bottom sheet
static let defaultPresent = Animation(curve: .regular(options: .curveEaseIn))

/// Default animation for dismissing a bottom sheet
static let defaultDismiss = Animation(curve: .regular(options: .curveEaseOut))
}
22 changes: 19 additions & 3 deletions Sources/YBottomSheet/Animation/BottomSheetAnimator.swift
Expand Up @@ -13,6 +13,14 @@ class BottomSheetAnimator: NSObject {
/// Bottom sheet controller.
let sheetViewController: BottomSheetController

enum Direction {
case present
case dismiss
}

/// Animation direction (present or dismiss)
let direction: Direction

/// Override for isReduceMotionEnabled. Default is `nil`.
///
/// For unit testing. When non-`nil` it will be returned instead of
Expand All @@ -25,16 +33,24 @@ class BottomSheetAnimator: NSObject {
}

/// Initializes a bottom sheet animator.
/// - Parameter sheetViewController: the sheet being animated.
init(sheetViewController: BottomSheetController) {
/// - Parameters:
/// - sheetViewController: the sheet being animated.
/// - direction: animation direction
init(sheetViewController: BottomSheetController, direction: Direction) {
self.sheetViewController = sheetViewController
self.direction = direction
super.init()
}
}

extension BottomSheetAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
sheetViewController.appearance.animationDuration
switch direction {
case .present:
return sheetViewController.appearance.presentAnimation.duration
case .dismiss:
return sheetViewController.appearance.dismissAnimation.duration
}
}

// Override this method and perform the animations
Expand Down
14 changes: 8 additions & 6 deletions Sources/YBottomSheet/Animation/BottomSheetDismissAnimator.swift
Expand Up @@ -10,6 +10,12 @@ import UIKit

/// Performs the sheet dismiss animation.
class BottomSheetDismissAnimator: BottomSheetAnimator {
/// Initializes a bottom sheet animator.
/// - Parameter sheetViewController: the sheet being animated.
required init(sheetViewController: BottomSheetController) {
super.init(sheetViewController: sheetViewController, direction: .dismiss)
}

override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from),
let toViewController = transitionContext.viewController(forKey: .to) else {
Expand All @@ -31,12 +37,8 @@ class BottomSheetDismissAnimator: BottomSheetAnimator {
) {
sheet.dimmerView.alpha = 0
}

UIView.animate(
withDuration: duration,
delay: .zero,
options: [.beginFromCurrentState, sheet.appearance.dismissAnimationCurve]
) {

UIView.animate(with: sheet.appearance.dismissAnimation) {
if self.isReduceMotionEnabled {
sheet.sheetView.alpha = 0
} else {
Expand Down
12 changes: 7 additions & 5 deletions Sources/YBottomSheet/Animation/BottomSheetPresentAnimator.swift
Expand Up @@ -10,6 +10,12 @@ import UIKit

/// Performs the sheet present animation.
class BottomSheetPresentAnimator: BottomSheetAnimator {
/// Initializes a bottom sheet animator.
/// - Parameter sheetViewController: the sheet being animated.
required init(sheetViewController: BottomSheetController) {
super.init(sheetViewController: sheetViewController, direction: .present)
}

override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) else {
transitionContext.completeTransition(false)
Expand Down Expand Up @@ -44,11 +50,7 @@ class BottomSheetPresentAnimator: BottomSheetAnimator {
sheet.dimmerView.alpha = 1
}

UIView.animate(
withDuration: duration,
delay: .zero,
options: [.beginFromCurrentState, sheet.appearance.presentAnimationCurve]
) {
UIView.animate(with: sheet.appearance.presentAnimation) {
if self.isReduceMotionEnabled {
sheet.sheetView.alpha = 1
} else {
Expand Down
28 changes: 11 additions & 17 deletions Sources/YBottomSheet/BottomSheetController+Appearance.swift
Expand Up @@ -22,13 +22,10 @@ extension BottomSheetController {
public var elevation: Elevation?
/// Dimmer view color. Default is 'UIColor.black.withAlphaComponent(0.5)'.
public var dimmerColor: UIColor?
/// Animation duration on bottom sheet. Default is `0.3`.
public var animationDuration: TimeInterval
/// Animation type during presenting. Default is `curveEaseIn`.
public var presentAnimationCurve: UIView.AnimationOptions
/// Animation type during dismissing. Default is `curveEaseOut`.
public var dismissAnimationCurve: UIView.AnimationOptions
/// Whether the sheet can be dismissed by swiping down or tapping on the dimmer. Default is `true`.
/// Animation for presenting the bottom sheet. Default = `.defaultPresent`.
public var presentAnimation: Animation
/// Animation for dismissing the bottom sheet. Default = `.defaultDismiss`.
public var dismissAnimation: Animation
///
/// The user can always dismiss the sheet from the close button if it is visible.
public var isDismissAllowed: Bool
Expand All @@ -43,31 +40,28 @@ extension BottomSheetController {
/// - indicatorAppearance: appearance of the drag indicator or pass `nil` to hide.
/// - headerAppearance: appearance of the sheet header view or pass `nil` to hide.
/// - layout: bottom sheet layout properties such as corner radius.
/// - elevation: bottom sheet's shadow or pass `nil` to hide
/// - elevation: bottom sheet's shadow or pass `nil` to hide.
/// - dimmerColor: dimmer view color or pass `nil` to hide.
/// - animationDuration: animation duration for bottom sheet. Default is `0.3`.
/// - presentAnimationCurve: animation type during presenting.
/// - dismissAnimationCurve: animation type during dismiss.
/// - presentAnimation: animation for presenting the bottom sheet.
/// - dismissAnimation: animation for dismissing the bottom sheet.
/// - isDismissAllowed: whether the sheet can be dismissed by swiping down or tapping on the dimmer.
public init(
indicatorAppearance: DragIndicatorView.Appearance? = nil,
headerAppearance: SheetHeaderView.Appearance? = .default,
layout: Layout = .default,
elevation: Elevation? = nil,
dimmerColor: UIColor? = .black.withAlphaComponent(0.5),
animationDuration: TimeInterval = 0.3,
presentAnimationCurve: UIView.AnimationOptions = .curveEaseIn,
dismissAnimationCurve: UIView.AnimationOptions = .curveEaseOut,
presentAnimation: Animation = .defaultPresent,
dismissAnimation: Animation = .defaultDismiss,
isDismissAllowed: Bool = true
) {
self.indicatorAppearance = indicatorAppearance
self.headerAppearance = headerAppearance
self.layout = layout
self.elevation = elevation
self.dimmerColor = dimmerColor
self.animationDuration = animationDuration
self.presentAnimationCurve = presentAnimationCurve
self.dismissAnimationCurve = dismissAnimationCurve
self.presentAnimation = presentAnimation
self.dismissAnimation = dismissAnimation
self.isDismissAllowed = isDismissAllowed
}
}
Expand Down
33 changes: 33 additions & 0 deletions Tests/YBottomSheetTests/Animation/Animation+BottomSheetTests.swift
@@ -0,0 +1,33 @@
//
// Animation+BottomSheetTests.swift
// YBottomSheet
//
// Created by Mark Pospesel on 5/4/23.
// Copyright © 2023 Y Media Labs. All rights reserved.
//

import XCTest
import YCoreUI
@testable import YBottomSheet

final class AnimationBottomSheetTests: XCTestCase {
func test_defaultPresent() {
// Given
let sut = Animation.defaultPresent

// Then
XCTAssertEqual(sut.duration, 0.3)
XCTAssertEqual(sut.delay, 0.0)
XCTAssertEqual(sut.curve, .regular(options: .curveEaseIn))
}

func test_defaultDismiss() {
// Given
let sut = Animation.defaultDismiss

// Then
XCTAssertEqual(sut.duration, 0.3)
XCTAssertEqual(sut.delay, 0.0)
XCTAssertEqual(sut.curve, .regular(options: .curveEaseOut))
}
}
18 changes: 14 additions & 4 deletions Tests/YBottomSheetTests/Animation/BottomSheetAnimatorTests.swift
Expand Up @@ -17,13 +17,22 @@ final class BottomSheetAnimatorTests: XCTestCase {
XCTAssertEqual(sut.sheetViewController, sheetController)
}

func test_duration() {
func test_presentDuration() {
let main = UIViewController()
let sheetController = BottomSheetController(title: "Bottom Sheet", childView: UIView())
let sut = makeSUT(sheetViewController: sheetController)
let sut = makeSUT(sheetViewController: sheetController, direction: .present)
let context = MockAnimationContext(from: main, to: sheetController)

XCTAssertEqual(sut.transitionDuration(using: context), sheetController.appearance.presentAnimation.duration)
}

func test_dismissDuration() {
let main = UIViewController()
let sheetController = BottomSheetController(title: "Bottom Sheet", childView: UIView())
let sut = makeSUT(sheetViewController: sheetController, direction: .dismiss)
let context = MockAnimationContext(from: main, to: sheetController)

XCTAssertEqual(sut.transitionDuration(using: context), sheetController.appearance.animationDuration)
XCTAssertEqual(sut.transitionDuration(using: context), sheetController.appearance.dismissAnimation.duration)
}

func test_animate() {
Expand All @@ -42,10 +51,11 @@ final class BottomSheetAnimatorTests: XCTestCase {
private extension BottomSheetAnimatorTests {
func makeSUT(
sheetViewController: BottomSheetController,
direction: BottomSheetAnimator.Direction = .present,
file: StaticString = #filePath,
line: UInt = #line
) -> BottomSheetAnimator {
let sut = BottomSheetAnimator(sheetViewController: sheetViewController)
let sut = BottomSheetAnimator(sheetViewController: sheetViewController, direction: direction)
trackForMemoryLeak(sut, file: file, line: line)
return sut
}
Expand Down
Expand Up @@ -7,13 +7,15 @@
//

import XCTest
import YCoreUI
@testable import YBottomSheet

final class BottomSheetDismissAnimatorTests: XCTestCase {
func test_animate() throws {
let sheetController = makeSheet()
let (sut, context) = try makeSUT(sheetViewController: sheetController, to: sheetController)

XCTAssertEqual(sut.transitionDuration(using: context), 0.0)
XCTAssertTrue(sut is BottomSheetDismissAnimator)
XCTAssertFalse(context.wasCompleteCalled)
sut.animateTransition(using: context)
Expand All @@ -32,6 +34,7 @@ final class BottomSheetDismissAnimatorTests: XCTestCase {
isReduceMotionEnabled: false
)

XCTAssertEqual(sut.transitionDuration(using: context), 0.0)
sut.animateTransition(using: context)

// Wait for the run loop to tick (animate keyboard)
Expand All @@ -49,6 +52,7 @@ final class BottomSheetDismissAnimatorTests: XCTestCase {
isReduceMotionEnabled: true
)

XCTAssertEqual(sut.transitionDuration(using: context), 0.0)
sut.animateTransition(using: context)

// Wait for the run loop to tick (animate keyboard)
Expand All @@ -62,6 +66,7 @@ final class BottomSheetDismissAnimatorTests: XCTestCase {
let sheetController = makeSheet()
let (sut, context) = try makeSUT(sheetViewController: sheetController, to: nil)

XCTAssertEqual(sut.transitionDuration(using: context), 0.0)
XCTAssertFalse(context.wasCompleteCalled)
sut.animateTransition(using: context)

Expand Down Expand Up @@ -96,7 +101,9 @@ private extension BottomSheetDismissAnimatorTests {
let sheet = BottomSheetController(
title: "Bottom Sheet",
childView: UIView(),
appearance: BottomSheetController.Appearance(animationDuration: 0.0)
appearance: BottomSheetController.Appearance(
dismissAnimation: Animation(duration: 0.0, curve: .regular(options: .curveEaseOut))
)
)
trackForMemoryLeak(sheet)
return sheet
Expand Down

0 comments on commit 2b0f317

Please sign in to comment.