Skip to content

Commit

Permalink
Implement FoundationExtensions package
Browse files Browse the repository at this point in the history
  • Loading branch information
ns-vasilev committed Jul 16, 2021
1 parent 0666ed5 commit 8863b07
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ playground.xcworkspace
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.swiftpm

.build/

Expand Down
30 changes: 30 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "FoundationExtensions",
platforms: [
.macOS(.v10_10),
.iOS(.v8),
.watchOS(.v2),
.tvOS(.v9),
],
products: [
.library(
name: "FoundationExtensions",
targets: ["FoundationExtensions"]
),
],
targets: [
.target(
name: "FoundationExtensions",
dependencies: []
),
.testTarget(
name: "FoundationExtensionsTests",
dependencies: ["FoundationExtensions"]
),
]
)
8 changes: 8 additions & 0 deletions Sources/FoundationExtensions/Array/Array+safe.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

public extension Array {
/// Safe getting element from array by index.
subscript(safe index: Int) -> Element? {
(0 ..< count).contains(index) ? self[index] : nil
}
}
20 changes: 20 additions & 0 deletions Sources/FoundationExtensions/Debouncer/Debouncable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

/// Represent general interface of `Debouncable` object.
public protocol Debouncable: AnyObject {
typealias ActionHandler = () -> Void

/// The `Bool` value that indicates that closure is running.
var isRunning: Bool { get }

/// Execute closure with delay.
///
/// - Parameters:
/// - delay: The amount of time (measured in seconds) to wait before beginning the action.
/// Specify a value of 0 to begin the action immediately.
/// - action: A block object to be executed.
func run(delay: TimeInterval, action: @escaping ActionHandler)

/// Cancel all running tasks.
func cancel()
}
33 changes: 33 additions & 0 deletions Sources/FoundationExtensions/Debouncer/Debouncer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

public final class Debouncer: Debouncable {
// MARK: Public

public var isRunning: Bool {
!(dispatchWorkItem?.isCancelled ?? true)
}

public func run(delay: TimeInterval, action: @escaping ActionHandler) {
cancel()
_run(delay: delay, action)
}

public func cancel() {
dispatchWorkItem?.cancel()
}

// MARK: Private

private var dispatchWorkItem: DispatchWorkItem?

private func _run(delay: TimeInterval, _ closure: @escaping ActionHandler) {
let dispatchWorkItem = DispatchWorkItem(block: closure)

DispatchQueue.main.asyncAfter(
deadline: .now() + delay,
execute: dispatchWorkItem
)

self.dispatchWorkItem = dispatchWorkItem
}
}
49 changes: 49 additions & 0 deletions Sources/FoundationExtensions/Thread/Thread+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation

public extension Thread {
/// Name of the current thread.
var threadName: String {
if let currentOperationQueue = OperationQueue.current?.name {
return "OperationQueue: \(currentOperationQueue)"
} else if let underlyingDispatchQueue = OperationQueue.current?.underlyingQueue?.label {
return "DispatchQueue: \(underlyingDispatchQueue)"
} else {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8) ?? Thread.current.description
}
}
}

/// Run closure on main thread.
///
/// - Parameter run: A completion block that will executed on main thread.
public func onMain(run: @escaping () -> Void) {
DispatchQueue.main.async {
run()
}
}

/// Background serial queue.
let backgroundThread = DispatchQueue(label: "com.foundation-extensions.thread")

/// Run closure on background
///
/// - Parameter run: A completion block that will executed on background thread.
public func onBackground(run: @escaping () -> Void) {
backgroundThread.async {
run()
}
}

/// Execute closure on main thread.
///
/// - Parameter work: A block based object to be executed on main thread.
public func ensureMainThread(execute work: @escaping @convention(block) () -> Swift.Void) {
if Thread.isMainThread {
work()
} else {
onMain {
work()
}
}
}
10 changes: 10 additions & 0 deletions Tests/FoundationExtensionsTests/Array/ArrayTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@testable import FoundationExtensions
import XCTest

final class ArrayTests: XCTestCase {
func testThatGettingElementByIndexIsSafe() {
let array = [1, 2, 3]
XCTAssertNil(array[safe: 5], "Element at index 5 should be nil")
XCTAssertEqual(array[safe: 1], 2, "Elements should have the same values")
}
}
38 changes: 38 additions & 0 deletions Tests/FoundationExtensionsTests/Debouncer/DebouncerTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
@testable import FoundationExtensions
import XCTest

final class DebouncerTests: XCTestCase {
// MARK: Internal

func testThatDebouncerExecuteClosure() {
let delay: TimeInterval = 2.0
let startTime = CFAbsoluteTimeGetCurrent()

let expectation = XCTestExpectation(description: "Run delayed closure")

debouncer.run(delay: delay) {
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
XCTAssertEqual(timeElapsed, delay, accuracy: 0.1)

expectation.fulfill()
}

wait(for: [expectation], timeout: 3.0)
}

func testThatDebouncerCancelExecutedClosure() {
let delay: TimeInterval = 0.0

debouncer.run(delay: delay) {
XCTFail("The closure must not be executed")
}

debouncer.cancel()

XCTAssertFalse(debouncer.isRunning)
}

// MARK: Private

private let debouncer: Debouncable = Debouncer()
}
17 changes: 17 additions & 0 deletions Tests/FoundationExtensionsTests/Thread/ThreadTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@testable import FoundationExtensions
import XCTest

final class ThreadTests: XCTestCase {
func testThatThreadRunningClosureOnMainThread() {
onMain {
XCTAssertTrue(Thread.isMainThread, "The closure must be running on the main thread")
}
}

func testThatThreadRunningClosureOnBackgroundThread() {
onBackground {
XCTAssertFalse(!Thread.isMainThread, "The closure must be running on the background thread")
XCTAssertEqual(Thread.current.threadName, backgroundThread.label, "The threads must be equal")
}
}
}

0 comments on commit 8863b07

Please sign in to comment.