Skip to content
Open
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
17 changes: 17 additions & 0 deletions Sources/General/CGSize+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// CGSize+Extensions.swift
// UtiliKit iOS
//
// Created by Nathan Chiu on 5/18/22.
// Copyright © 2022 Bottle Rocket Studios. All rights reserved.
//

import UIKit

extension CGSize {

/// The size whose width and height are both one.
static var one: CGSize {
Self.init(width: 1, height: 1)
}
}
35 changes: 35 additions & 0 deletions Sources/General/Comparable+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// Comparable+Extensions.swift
// UtiliKit iOS
//
// Created by Nathan Chiu on 5/19/22.
// Copyright © 2022 Bottle Rocket Studios. All rights reserved.
//

import Foundation

extension Comparable {

/// Keeps the value within the specified `min` and `max` values
/// - Parameters:
/// - minValue: The minimum value to return
/// - maxValue: The maximum value to return
/// - Returns: The `minValue` if less than `minValue`, else `maxValue` if greater than `maxValue`, else the value itself
func clamped(min minValue: Self, max maxValue: Self) -> Self {
min(max(minValue, self), maxValue)
}

/// Keeps the value within the specified `range` of values
/// - Parameter range: The range of values to return
/// - Returns: The `range.lowerBound` if less than the range, else `range.upperBound` if greater than the range, else the value itself
func clamped(in range: Range<Self>) -> Self {
clamped(min: range.lowerBound, max: range.upperBound)
}

/// Keeps the value within the specified `range` of values
/// - Parameter range: The range of values to return
/// - Returns: The `range.lowerBound` if less than the range, else `range.upperBound` if greater than the range, else the value itself
func clamped(in range: ClosedRange<Self>) -> Self {
clamped(min: range.lowerBound, max: range.upperBound)
}
}
67 changes: 67 additions & 0 deletions Sources/General/UIScrollView+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// UIScrollView+Extensions.swift
// UtiliKit iOS
//
// Created by Nathan Chiu on 5/18/22.
// Copyright © 2022 Bottle Rocket Studios. All rights reserved.
//

import UIKit

// MARK: - Scroll position
extension UIScrollView {

/// Content offset including `adjustedContentInset`
var insetAdjustedContentOffset: CGPoint {
.init(x: contentOffset.x + adjustedContentInset.left,
y: contentOffset.y + adjustedContentInset.top)
}

/// Content size including `adjustedContentInset`
var insetAdjustedContentSize: CGSize {
.init(width: contentSize.width + adjustedContentInset.left + adjustedContentInset.right,
height: contentSize.height + adjustedContentInset.top + adjustedContentInset.bottom)
}

/// Percentage the user has scrolled
var scrollPercentage: (horizontal: CGFloat, vertical: CGFloat) {
var totalScrollableHeight = insetAdjustedContentSize.height - frame.height
if insetAdjustedContentSize.height <= frame.height {
totalScrollableHeight = frame.height
}
let verticalPercentage = insetAdjustedContentOffset.y / totalScrollableHeight

var totalScrollableWidth = insetAdjustedContentSize.width - frame.width
if insetAdjustedContentSize.width <= frame.width {
totalScrollableWidth = frame.width
}
let horizontalPercentage = insetAdjustedContentOffset.x / totalScrollableWidth

return (horizontal: horizontalPercentage.clamped(min: 0, max: 1), vertical: verticalPercentage.clamped(min: 0, max: 1))
}

/// `true` if the scroll view is scrolled to the top, else `false`
var isScrolledToTop: Bool {
guard insetAdjustedContentSize.height > frame.height else {
return insetAdjustedContentOffset.y <= 0
}
return scrollPercentage.vertical <= .leastNormalMagnitude
}

/// `true` if the scroll view is scrolled to the bottom, else `false`
var isScrolledToBottom: Bool {
guard insetAdjustedContentSize.height > frame.height else {
return insetAdjustedContentOffset.y > insetAdjustedContentSize.height - frame.height
}

// Ideally this wouldn't be hard coded, but `1 - .leastNormalMagnitude` doesn't always recognize when the bottom is reached. This could potentially be a problem on a very tall contentSize
return 1.0 - scrollPercentage.vertical <= .leastNormalMagnitude
}

/// This method scrolls the content view to the top. If already at the top, the method does nothing.
/// - Parameter animated: `true` if the scrolling should be animated, `false` if it should be immediate.
func scrollToTop(animated: Bool) {
scrollRectToVisible(.init(origin: .zero, size: .one), animated: animated)
delegate?.scrollViewDidScroll?(self)
}
}
18 changes: 18 additions & 0 deletions Tests/CGSizeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// CGSizeTests.swift
// UtiliKit iOS
//
// Created by Nathan Chiu on 5/19/22.
// Copyright © 2022 Bottle Rocket Studios. All rights reserved.
//

import XCTest
@testable import UtiliKit

class CGSizeTests: XCTestCase {

func test_one_correctSize() {
XCTAssertEqual(CGSize.one.width, 1)
XCTAssertEqual(CGSize.one.height, 1)
}
}
59 changes: 59 additions & 0 deletions Tests/ComparableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// ComparableTests.swift
// UtiliKit iOS Tests
//
// Created by Nathan Chiu on 5/19/22.
// Copyright © 2022 Bottle Rocket Studios. All rights reserved.
//

import XCTest
@testable import UtiliKit

class ComparableTests: XCTestCase {

let minValue = 0
let maxValue = 10
let middleValue = 5
let lessThanValue = -5
let greaterThanValue = 15

func test_clamping_lessThanMin() {
XCTAssertEqual(lessThanValue.clamped(min: minValue, max: maxValue), minValue)
}

func test_clamping_equalToMin() {
XCTAssertEqual(minValue.clamped(min: minValue, max: maxValue), minValue)
}

func test_clamping_inMinMax() {
XCTAssertEqual(middleValue.clamped(min: minValue, max: maxValue), middleValue)
}

func test_clamping_equalToMax() {
XCTAssertEqual(maxValue.clamped(min: minValue, max: maxValue), maxValue)
}

func test_clamping_greaterThanMax() {
XCTAssertEqual(greaterThanValue.clamped(min: minValue, max: maxValue), maxValue)
}

func test_clamping_lessThanMinRange() {
XCTAssertEqual(lessThanValue.clamped(in: minValue...maxValue), minValue)
}

func test_clamping_equalToMinRange() {
XCTAssertEqual(minValue.clamped(in: minValue...maxValue), minValue)
}

func test_clamping_inRange() {
XCTAssertEqual(middleValue.clamped(in: minValue...maxValue), middleValue)
}

func test_clamping_equalToMaxRange() {
XCTAssertEqual(maxValue.clamped(in: minValue...maxValue), maxValue)
}

func test_clamping_greaterThanMaxRange() {
XCTAssertEqual(greaterThanValue.clamped(in: minValue...maxValue), maxValue)
}
}
24 changes: 22 additions & 2 deletions UtiliKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
0EC7F0A9279231D500F712F3 /* UtiliKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC7F031279231A300F712F3 /* UtiliKitTests.swift */; };
0EC7F0AA279231D500F712F3 /* ObfuscationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC7F034279231A300F712F3 /* ObfuscationTests.swift */; };
0EC7F0ED27923DD400F712F3 /* SnapshotTesting.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EC7F0EC27923DD400F712F3 /* SnapshotTesting.xcframework */; };
5919D94228354BFC00E1626E /* UIScrollView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5919D94128354BFC00E1626E /* UIScrollView+Extensions.swift */; };
5919D94428354EE600E1626E /* CGSize+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5919D94328354EE600E1626E /* CGSize+Extensions.swift */; };
5919D94628369E1400E1626E /* Comparable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5919D94528369E1300E1626E /* Comparable+Extensions.swift */; };
5919D94928369F3C00E1626E /* CGSizeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5919D94728369EA400E1626E /* CGSizeTests.swift */; };
5919D94B28369F6D00E1626E /* ComparableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5919D94A28369F6D00E1626E /* ComparableTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -122,6 +127,11 @@
0EC7F0A3279231C600F712F3 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = SOURCE_ROOT; };
0EC7F0A4279231C600F712F3 /* Dangerfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dangerfile.swift; sourceTree = SOURCE_ROOT; };
0EC7F0EC27923DD400F712F3 /* SnapshotTesting.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = SnapshotTesting.xcframework; path = Carthage/Build/SnapshotTesting.xcframework; sourceTree = "<group>"; };
5919D94128354BFC00E1626E /* UIScrollView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScrollView+Extensions.swift"; sourceTree = "<group>"; };
5919D94328354EE600E1626E /* CGSize+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGSize+Extensions.swift"; sourceTree = "<group>"; };
5919D94528369E1300E1626E /* Comparable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comparable+Extensions.swift"; sourceTree = "<group>"; };
5919D94728369EA400E1626E /* CGSizeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeTests.swift; sourceTree = "<group>"; };
5919D94A28369F6D00E1626E /* ComparableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparableTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -199,9 +209,12 @@
0EC7F006279231A300F712F3 /* General */ = {
isa = PBXGroup;
children = (
0EC7F007279231A300F712F3 /* UIView+Extensions.swift */,
0EC7F008279231A300F712F3 /* FileManager+Extensions.swift */,
5919D94328354EE600E1626E /* CGSize+Extensions.swift */,
0E666BDD27A074E7001082D0 /* CLLocationCoordinate2D+Extensions.swift */,
5919D94528369E1300E1626E /* Comparable+Extensions.swift */,
0EC7F008279231A300F712F3 /* FileManager+Extensions.swift */,
5919D94128354BFC00E1626E /* UIScrollView+Extensions.swift */,
0EC7F007279231A300F712F3 /* UIView+Extensions.swift */,
0EC7F009279231A300F712F3 /* URL+Extensions.swift */,
);
path = General;
Expand Down Expand Up @@ -322,6 +335,8 @@
0EC7F02E279231A300F712F3 /* Tests */ = {
isa = PBXGroup;
children = (
5919D94728369EA400E1626E /* CGSizeTests.swift */,
5919D94A28369F6D00E1626E /* ComparableTests.swift */,
0EC7F02F279231A300F712F3 /* ScrollingPageControlTests.swift */,
0EC7F030279231A300F712F3 /* DateTests.swift */,
0EC7F031279231A300F712F3 /* UtiliKitTests.swift */,
Expand Down Expand Up @@ -475,10 +490,12 @@
0EC7F060279231A400F712F3 /* ScrollingPageControl.swift in Sources */,
0EC7F06A279231A400F712F3 /* VersionConfig.swift in Sources */,
0EC7F068279231A400F712F3 /* ContainerViewController.swift in Sources */,
5919D94228354BFC00E1626E /* UIScrollView+Extensions.swift in Sources */,
0EC7F066279231A400F712F3 /* DefaultContainerTransitionAnimator.swift in Sources */,
0E666BDE27A074E7001082D0 /* CLLocationCoordinate2D+Extensions.swift in Sources */,
0EC7F05F279231A400F712F3 /* URL+Extensions.swift in Sources */,
0EC7F05B279231A400F712F3 /* UICollectionView+Extensions.swift in Sources */,
5919D94628369E1400E1626E /* Comparable+Extensions.swift in Sources */,
0EC7F05C279231A400F712F3 /* NibLoadable.swift in Sources */,
0EC7F057279231A400F712F3 /* Configurable.swift in Sources */,
0EC7F06F279231A400F712F3 /* Date+Extensions.swift in Sources */,
Expand All @@ -496,6 +513,7 @@
0EC7F061279231A400F712F3 /* ContainerViewController+ManagedChildren.swift in Sources */,
0EC7F056279231A400F712F3 /* StoryboardIdentifiable.swift in Sources */,
0EC7F055279231A400F712F3 /* ReuseIdentifiable.swift in Sources */,
5919D94428354EE600E1626E /* CGSize+Extensions.swift in Sources */,
0EC7F06D279231A400F712F3 /* ObfuscatedKey.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand All @@ -505,10 +523,12 @@
buildActionMask = 2147483647;
files = (
0EC7F0A5279231D500F712F3 /* ScrollingPageControlTests.swift in Sources */,
5919D94B28369F6D00E1626E /* ComparableTests.swift in Sources */,
0EC7F0A8279231D500F712F3 /* ContainerTests.swift in Sources */,
0EC7F0A9279231D500F712F3 /* UtiliKitTests.swift in Sources */,
0EC7F0AA279231D500F712F3 /* ObfuscationTests.swift in Sources */,
0EC7F0A7279231D500F712F3 /* ActiveLabelTests.swift in Sources */,
5919D94928369F3C00E1626E /* CGSizeTests.swift in Sources */,
0EC7F0A6279231D500F712F3 /* DateTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down