Skip to content

Commit

Permalink
Bottom sheet improvements (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
almazrafi authored Jan 29, 2024
1 parent 983c306 commit 3a10209
Show file tree
Hide file tree
Showing 16 changed files with 150 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .xcode-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
14.3
15.2
6 changes: 6 additions & 0 deletions Nivelir.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
8E41C80628AD38B400E3E1B2 /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 8E41C80528AD38B400E3E1B2 /* Documentation.docc */; };
8E41C80728AD38B400E3E1B2 /* Documentation.docc in Sources */ = {isa = PBXBuildFile; fileRef = 8E41C80528AD38B400E3E1B2 /* Documentation.docc */; };
C002FF202B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = C002FF1F2B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift */; };
C002FF212B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = C002FF1F2B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift */; };
C01294B62859B2C80039D06B /* SharingActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01294B52859B2C80039D06B /* SharingActivity.swift */; };
C01294B82859B2DA0039D06B /* SharingCustomActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01294B72859B2DA0039D06B /* SharingCustomActivity.swift */; };
C01294BA2859B2EC0039D06B /* SharingCustomItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C01294B92859B2EC0039D06B /* SharingCustomItem.swift */; };
Expand Down Expand Up @@ -675,6 +677,7 @@
/* Begin PBXFileReference section */
8E41C80528AD38B400E3E1B2 /* Documentation.docc */ = {isa = PBXFileReference; lastKnownFileType = folder.documentationcatalog; path = Documentation.docc; sourceTree = "<group>"; };
C0007FFA23CBAAA5005F95E0 /* .swift-version */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".swift-version"; sourceTree = "<group>"; };
C002FF1F2B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetRubberBandEffect.swift; sourceTree = "<group>"; };
C010082725EEA6B700A2FF1A /* Nivelir.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = Nivelir.podspec; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
C01294B52859B2C80039D06B /* SharingActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingActivity.swift; sourceTree = "<group>"; };
C01294B72859B2DA0039D06B /* SharingCustomActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingCustomActivity.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1957,6 +1960,7 @@
C089D9102A0B1AC300B6400C /* BottomSheetCard.swift */,
C089D90F2A0B1AC300B6400C /* BottomSheetController.swift */,
C089D9092A0B1AC300B6400C /* BottomSheetGrabber.swift */,
C002FF1F2B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift */,
C089D9112A0B1AC300B6400C /* BottomSheetShadow.swift */,
C089D9062A0B1AC300B6400C /* BottomSheetStackController.swift */,
C089D90D2A0B1AC300B6400C /* BottomSheetTransitionView.swift */,
Expand Down Expand Up @@ -2708,6 +2712,7 @@
C078FD5D2861A1DA0008FC6E /* ProgressFailureIndicator.swift in Sources */,
C0959148285256CE00729E93 /* UNNotificationResponse+Extensions.swift in Sources */,
C0A728EE279EF148000BFBF9 /* Character+Extensions.swift in Sources */,
C002FF202B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift in Sources */,
C089D9412A0B1AC300B6400C /* BottomSheetBorder.swift in Sources */,
C088603726B6C60B00CA0A77 /* UnavailableMediaPickerSourceError.swift in Sources */,
C078FD882861AAEC0008FC6E /* ScreenHideHUDAction.swift in Sources */,
Expand Down Expand Up @@ -2917,6 +2922,7 @@
C0866B5B2853A59D00B85C54 /* ScreenGetAction.swift in Sources */,
C05F488C26B68B7F0002DF52 /* AnyScreenActionBaseBox.swift in Sources */,
C088602226B6C60B00CA0A77 /* InvalidMailParametersError.swift in Sources */,
C002FF212B67FE0900D07C8B /* BottomSheetRubberBandEffect.swift in Sources */,
C0238F0527A7F77D00148E91 /* ShortcutDeeplinkUserInfoOptions.swift in Sources */,
C0A728C1279AD889000BFBF9 /* DictionaryUnkeyedDecodingContainer.swift in Sources */,
C052C7D12823A94500132875 /* ScreenObserverPredicate.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public struct ScreenShowActionSheetAction<Container: UIViewController>: ScreenAc
let alertContainer = makeAlertContainer()

switch UIDevice.current.userInterfaceIdiom {
case .pad, .mac:
case .pad, .mac, .vision:
showAlertContainerUsingPopover(
alertContainer,
from: actionSheet.anchor,
Expand Down
5 changes: 5 additions & 0 deletions Sources/Addons/BottomSheet/BottomSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public struct BottomSheet {
public let presentAnimationOptions: BottomSheetAnimationOptions
public let dismissAnimationOptions: BottomSheetAnimationOptions

public let rubberBandEffect: BottomSheetRubberBandEffect?

public let canEndEditing: (() -> Bool)?
public let shouldDismiss: (() -> Bool)?

Expand All @@ -44,6 +46,7 @@ public struct BottomSheet {
changesAnimationOptions: BottomSheetAnimationOptions = .changes,
presentAnimationOptions: BottomSheetAnimationOptions = .transition,
dismissAnimationOptions: BottomSheetAnimationOptions = .transition,
rubberBandEffect: BottomSheetRubberBandEffect? = .default,
canEndEditing: (() -> Bool)? = nil,
shouldDismiss: (() -> Bool)? = nil,
didAttemptToDismiss: (() -> Void)? = nil,
Expand All @@ -67,6 +70,8 @@ public struct BottomSheet {
self.presentAnimationOptions = presentAnimationOptions
self.dismissAnimationOptions = dismissAnimationOptions

self.rubberBandEffect = rubberBandEffect

self.canEndEditing = canEndEditing
self.shouldDismiss = shouldDismiss

Expand Down
9 changes: 7 additions & 2 deletions Sources/Addons/BottomSheet/BottomSheetController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public class BottomSheetController: NSObject {
public var presentAnimationOptions: BottomSheetAnimationOptions
public var dismissAnimationOptions: BottomSheetAnimationOptions

public var rubberBandEffect: BottomSheetRubberBandEffect?

public var canEndEditing: (() -> Bool)?
public var shouldDismiss: (() -> Bool)?

Expand Down Expand Up @@ -115,6 +117,8 @@ public class BottomSheetController: NSObject {
self.presentAnimationOptions = bottomSheet.presentAnimationOptions
self.dismissAnimationOptions = bottomSheet.dismissAnimationOptions

self.rubberBandEffect = bottomSheet.rubberBandEffect

self.canEndEditing = bottomSheet.canEndEditing
self.shouldDismiss = bottomSheet.shouldDismiss

Expand Down Expand Up @@ -192,8 +196,6 @@ extension BottomSheetController: UIViewControllerTransitioningDelegate {
presentation.delegate = self
presentation.detention.delegate = self

presentation.changesAnimationOptions = changesAnimationOptions

presentation.detents = detents
presentation.selectedDetentKey = selectedDetentKey

Expand All @@ -206,6 +208,9 @@ extension BottomSheetController: UIViewControllerTransitioningDelegate {
presentation.prefersWidthFollowsPreferredContentSize = prefersWidthFollowsPreferredContentSize
presentation.prefersEdgeAttachedInCompactHeight = prefersEdgeAttachedInCompactHeight

presentation.changesAnimationOptions = changesAnimationOptions
presentation.rubberBandEffect = rubberBandEffect

self.presentation = presentation

return presentation
Expand Down
23 changes: 23 additions & 0 deletions Sources/Addons/BottomSheet/BottomSheetRubberBandEffect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#if canImport(UIKit)
import UIKit

public struct BottomSheetRubberBandEffect {

public let handler: (_ delta: CGFloat) -> CGFloat

public init(handler: @escaping (_ delta: CGFloat) -> CGFloat) {
self.handler = handler
}

public func callAsFunction(value: CGFloat, limit: CGFloat) -> CGFloat {
handler(abs(limit - value))
}
}

extension BottomSheetRubberBandEffect {

public static let `default` = Self { delta in
2.0 * delta.squareRoot()
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,17 @@ internal final class BottomSheetDismissingInteraction: BottomSheetInteraction {

if gestureValue > largestDetentValue + .leastNonzeroMagnitude {
let largestDetentDelta = largestDetentValue - currentDetentValue
let largestDetentExcess = gestureValue - largestDetentValue

return simultaneousScrollView?.canScrollVertically ?? false
? largestDetentDelta
: largestDetentDelta + 2.0 * largestDetentExcess.squareRoot()
if simultaneousScrollView?.canScrollVertically ?? false {
return largestDetentDelta
}

let rubberBandEffect = presentationController.rubberBandEffect?(
value: gestureValue,
limit: largestDetentValue
) ?? .zero

return largestDetentDelta + rubberBandEffect
}

return gestureValue - currentDetentValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,17 @@ internal final class BottomSheetPresentedInteraction: BottomSheetInteraction {

if gestureValue > largestDetentValue + .leastNonzeroMagnitude {
let largestDetentDelta = largestDetentValue - currentDetentValue
let largestDetentExcess = gestureValue - largestDetentValue

return simultaneousScrollView?.canScrollVertically ?? false
? largestDetentDelta
: largestDetentDelta + 2.0 * largestDetentExcess.squareRoot()
if simultaneousScrollView?.canScrollVertically ?? false {
return largestDetentDelta
}

let rubberBandEffect = presentationController.rubberBandEffect?(
value: gestureValue,
limit: largestDetentValue
) ?? .zero

return largestDetentDelta + rubberBandEffect
}

let smallestDetentValue = presentationController
Expand All @@ -66,9 +72,13 @@ internal final class BottomSheetPresentedInteraction: BottomSheetInteraction {

if gestureValue < smallestDetentValue - .leastNonzeroMagnitude {
let smallestDetentDelta = smallestDetentValue - currentDetentValue
let smallestDetentExcess = smallestDetentValue - gestureValue

return smallestDetentDelta - 2.0 * smallestDetentExcess.squareRoot()
let rubberBandEffect = presentationController.rubberBandEffect?(
value: gestureValue,
limit: smallestDetentValue
) ?? .zero

return smallestDetentDelta - rubberBandEffect
}

return gestureValue - currentDetentValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ internal final class BottomSheetPresentingInteraction: BottomSheetInteraction {

if gestureValue > largestDetentValue + .leastNonzeroMagnitude {
let largestDetentDelta = largestDetentValue - currentDetentValue
let largestDetentExcess = gestureValue - largestDetentValue

return largestDetentDelta + 2.0 * largestDetentExcess.squareRoot()
let rubberBandEffect = presentationController.rubberBandEffect?(
value: gestureValue,
limit: largestDetentValue
) ?? .zero

return largestDetentDelta + rubberBandEffect
}

return gestureValue - currentDetentValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ internal final class BottomSheetPresentationController: UIPresentationController
didSet { interaction.handlePresentationState(state) }
}

internal var changesAnimationOptions: BottomSheetAnimationOptions = .changes {
didSet { transition.completionCurve = changesAnimationOptions.curve }
}

internal var detents: [BottomSheetDetent] {
get { detention.detents }

Expand Down Expand Up @@ -97,6 +93,12 @@ internal final class BottomSheetPresentationController: UIPresentationController
}
}

internal var changesAnimationOptions: BottomSheetAnimationOptions = .changes {
didSet { transition.completionCurve = changesAnimationOptions.curve }
}

internal var rubberBandEffect: BottomSheetRubberBandEffect? = .default

internal var isEdgeAttached: Bool {
prefersEdgeAttachedInCompactHeight || (traitCollection.verticalSizeClass != .compact)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Addons/Safari/Safari.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public struct Safari: CustomStringConvertible {
/// The URL to navigate to. The URL must use the http or https scheme.
public let url: URL

/// The color to tint the background of the navigation bar and the toolbar.
/// The color to tint the background of the navigation bar and th toolbar.
public let preferredBarTintColor: UIColor?

/// The color to tint the control buttons on the navigation bar and the toolbar.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Addons/Sharing/ScreenShareAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public struct ScreenShareAction<Container: UIViewController>: ScreenAction {
let activitiesContainer = makeActivitiesContainer(navigator: navigator)

switch UIDevice.current.userInterfaceIdiom {
case .pad, .mac:
case .pad, .mac, .vision:
showActivitiesContainerInPopover(
activitiesContainer,
on: container,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ extension DictionaryComponentDecoder {
}

private func decodeURL(from component: Any?, at codingPath: [CodingKey]) throws -> URL {
if let url = component as? URL {
return url
}

guard let url = URL(string: try decodePrimitiveValue(from: component, at: codingPath)) else {
let errorContext = DecodingError.Context(
codingPath: codingPath,
Expand Down
50 changes: 44 additions & 6 deletions Tests/Tools/DictionaryDecoder/DictionaryDecoderTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ extension DictionaryDecoderTesting {
return try jsonDecoder.decode(T.self, from: data)
}

func assertDecoderSucceeds<Key: Hashable & Decodable, Value: Decodable & FloatingPoint & Equatable>(
decoding valueType: [Key: Value].Type,
func assertDecoderSucceeds<Key: Decodable & Hashable, Value: Decodable & FloatingPoint & Equatable>(
decoding expectedValue: [Key: Value],
from dictionary: [String: Any],
file: StaticString = #file,
line: UInt = #line
) {
do {
let expectedValue = try makeExpectedValue(valueType, from: dictionary)
let value = try decoder.decode(valueType, from: dictionary)
let value = try decoder.decode([Key: Value].self, from: dictionary)

XCTAssertEqual(
NSDictionary(dictionary: value),
Expand All @@ -46,6 +45,41 @@ extension DictionaryDecoderTesting {
}
}

func assertDecoderSucceeds<Key: Decodable & Hashable, Value: Decodable & FloatingPoint & Equatable>(
decoding valueType: [Key: Value].Type,
from dictionary: [String: Any],
file: StaticString = #file,
line: UInt = #line
) {
do {
let expectedValue = try makeExpectedValue(valueType, from: dictionary)

assertDecoderSucceeds(
decoding: expectedValue,
from: dictionary,
file: file,
line: line
)
} catch {
XCTFail("Test encountered unexpected error: \(error)", file: file, line: line)
}
}

func assertDecoderSucceeds<T: Decodable & Equatable>(
decoding expectedValue: T,
from dictionary: [String: Any],
file: StaticString = #file,
line: UInt = #line
) {
do {
let value = try decoder.decode(T.self, from: dictionary)

XCTAssertEqual(value, expectedValue, file: file, line: line)
} catch {
XCTFail("Test encountered unexpected error: \(error)", file: file, line: line)
}
}

func assertDecoderSucceeds<T: Decodable & Equatable>(
decoding valueType: T.Type,
from dictionary: [String: Any],
Expand All @@ -54,9 +88,13 @@ extension DictionaryDecoderTesting {
) {
do {
let expectedValue = try makeExpectedValue(valueType, from: dictionary)
let value = try decoder.decode(valueType, from: dictionary)

XCTAssertEqual(value, expectedValue, file: file, line: line)
assertDecoderSucceeds(
decoding: expectedValue,
from: dictionary,
file: file,
line: line
)
} catch {
XCTFail("Test encountered unexpected error: \(error)", file: file, line: line)
}
Expand Down
19 changes: 18 additions & 1 deletion Tests/Tools/DictionaryDecoder/DictionaryDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,23 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting {
assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary)
}

func testThatDecoderSucceedsWhenDecodingStructWithURL() {
struct DecodableStruct: Decodable, Equatable {
let foobar: URL?
}

let url = URL(string: "https://apple.com")!

let dictionary = [
"foobar": "https://apple.com"
]

assertDecoderSucceeds(
decoding: DecodableStruct(foobar: url),
from: dictionary
)
}

func testThatDecoderSucceedsWhenDecodingStructWithMultipleProperties() {
struct DecodableStruct: Decodable, Equatable {
let foo: Bool
Expand Down Expand Up @@ -562,7 +579,7 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting {
}

func testThatDecoderFailsWhenDecodingInvalidURL() {
let dictionary = ["foobar": "invalid url"]
let dictionary = ["foobar": "//invalid url"]

assertDecoderFails(decoding: [String: URL].self, from: dictionary) { error in
switch error {
Expand Down
2 changes: 1 addition & 1 deletion Tests/Tools/URLQueryDecoder/URLQueryDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ final class URLQueryDecoderTests: XCTestCase, URLQueryDecoderTesting {
}

func testThatDecoderFailsWhenDecodingInvalidURL() {
let url = "invalid url"
let url = "//invalid url"
let query = "foobar=\(url.urlQueryEncoded!)"

assertDecoderFails(decoding: [String: URL].self, from: query) { error in
Expand Down

0 comments on commit 3a10209

Please sign in to comment.