Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bottom sheet improvements #112

Merged
merged 7 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading