Skip to content

Releases: AlaskaAirlines/atom

4.7.0

29 Dec 23:04
9a21f6f

Choose a tag to compare

Introduction:

This Pull Request enhances support for the Proof Key for Code Exchange (PKCE) protocol in Atom's token refresh mechanism, as defined in RFC 7636. PKCE enhances security for public clients (e.g., mobile apps) by allowing client authentication without a client secret. To achieve this, the ClientCredential struct now treats the secret property as optional, defaulting to nil. This enables clients to omit the client_secret parameter in token refresh requests when using PKCE, while still supporting legacy flows that require it.
Additionally, this PR improves error handling by distinguishing between general "Bad Request" (HTTP 400) errors from regular API calls and those specifically from authentication endpoints (e.g., during token refresh). This ensures that a failed token refresh due to an invalid or expired refresh token triggers the appropriate notification (Atom.didFailToRefreshAccessToken), allowing observers to handle error gracefully.
These changes are additive and do not break existing source compatibility. They build upon the optimizations introduced in #20 by refining authentication flows without altering queuing or serialization logic.

Key Changes:

  • PKCE Support: Improves security for public clients by eliminating the need for a shared secret, reducing the risk of secret exposure.
  • Flexible Client Credentials: Allows seamless integration with both PKCE-enabled and legacy authorization servers.
  • Error Notification: Ensures notifications are only posted for authentic token refresh failures, improving app reliability and user experience.

Proposed Solution:

  1. Update ClientCredential to Support Optional Secret (PKCE):
    • Made secret optional with a default of nil in the initializer.
    • Updated documentation to reference RFC 6749 and emphasize optional usage.
    • No changes to equality or sendability.
    /// The `ClientCredential` type declares an object used by Atom for automated refreshing of the access token.
    /// See https://tools.ietf.org/html/RFC6749
    public struct ClientCredential: Sendable, Equatable {
        // MARK: - Properties
    
        /// The authorization grant type as described in Sections 4.1.3, 4.3.2, 4.4.2, RFC 6749. Starting with
        /// version 4.0, Atom supports automated token refresh using `refresh_token` grant type as defined in RFC 6749.
        public let grantType: GrantType
    
        /// The client identifier issued to the client during the registration process described in Section 2.2, RFC 6749.
        public let id: String
    
        /// The client secret. The client MAY omit the parameter if the client secret is an empty string. See RFC 6749.
        public let secret: String?
    
        // MARK: - Lifecycle
    
        /// Creates a `ClientCredential` instance given the provided parameter(s).
        ///
        /// - Parameters:
        /// - grantType: The authorization grant type as described in Sections 4.1.3, 4.3.2, 4.4.2, RFC 6749.
        /// - id:        The client identifier issued to the client during the registration process described by Section 2.2, RFC 6749.
        /// - secret:    The client secret. The client MAY omit the parameter if the client secret is an empty string. See RFC 6749.
        public init(grantType: GrantType = .refreshToken, id: String, secret: String? = nil) {
            self.id = id
            self.grantType = grantType
            self.secret = secret
        }
    }
  2. Conditional Inclusion of Client Secret in Token Refresh Request:
    • In the RefreshTokenEndpoint updated the method property to append client_secret only if credential.secret is non-nil.
    • This supports PKCE by omitting the secret when nil, while including it for backward compatibility.
    var method: HTTPMethod {
        let grantType = Identifier.grantType.appending(credential.grantType.rawValue)
        let clientID = Identifier.clientID.appending(credential.id)
        let refreshToken = Identifier.refreshToken.appending(writable.tokenCredential.refreshToken)
        var bodyString = grantType + "&" + clientID + "&" + refreshToken
    
        if let secret = credential.secret {
            bodyString.append("&")
            bodyString.append(Identifier.clientSecret.appending(secret))
        }
    
        return .post(Data(bodyString.utf8))
    }
  3. Add isAuthenticationEndpoint to Requestable:
    • Extended Requestable with a computed property to check if the request targets an authentication endpoint (via type check against AuthenticationEndpoint).
    • This enables quick identification of auth-related requests for specialized handling.
    extension Requestable {
        /// Returns a `Bool` indicating whether the current requestable is targeted at an authentication endpoint.
        ///
        /// This property is `true` when the conforming type is `AuthenticationEndpoint`. It allows Atom to quickly identify
        /// whether the request involves authentication-related operations, such as token refresh.
        var isAuthenticationEndpoint: Bool {
            self is AuthenticationEndpoint
        }
    }
  4. Fix Error Handling with Notification Distinction:
    • In the error handling flow added a check using isAuthenticationEndpoint and isBadRequest to post Atom.didFailToRefreshAccessToken specifically for auth endpoint 400 errors.
    • Falls back to Atom.didFailToAuthorizeRequest for authorization failures (e.g., 401).
    if requestable.isAuthenticationEndpoint, error.isBadRequest {
        // Notify observers that a token refresh has failed.
        NotificationCenter.default.post(name: Atom.didFailToRefreshAccessToken, object: nil, userInfo: ["error": error])
    } else if error.isAuthorizationFailure {
        // Notify observers that request authorization has failed.
        NotificationCenter.default.post(name: Atom.didFailToAuthorizeRequest, object: nil, userInfo: ["error": error])
    }

Testing

  • Verified PKCE flow: Token refresh succeeds without client_secret when secret is nil.
  • Tested legacy flow: Token refresh includes client_secret when provided.
  • Simulated 400 errors: Confirmed notification posts only for auth endpoints; regular API 400s do not trigger it.
  • Ensured no regressions in queuing from #20 by running high-throughput scenarios.

Source Compatibility:

Please check the box to indicate the impact of this proposal on source compatibility.

  • This change is additive and does not impact existing source code.
  • This change breaks existing source code.

Optimize Token Refresh with Conditional Queuing and Typed Error Mapping

21 Oct 22:54
352b5eb

Choose a tag to compare

Introduction:

This PR enhances AtomNetworking by optimizing bearer token refresh handling. It introduces conditional serialization of network calls - bypassing the queue when no refresh is needed for better performance - while maintaining strict FIFO ordering and deduplication during refresh scenarios.

Motivation:

Previously, all network operations were serialized through RequestableQueueManager, even when the access token was valid. This caused unnecessary delays in high-throughput scenarios. Additionally, error propagation from generic Error to typed AtomError was verbose and repetitive.

The goal is to:

  • Only serialize calls when token refresh is needed or in progress.
  • Enqueue calls during refresh and execute them immediately after completion.
  • Preserve FIFO order and race safety during refresh.

Proposed Solution:

Conditional Serialization in Service**:

  • Added needsSerialization() to SessionActor to check both .requiresRefresh and isRefreshing.
  • Introduced awaitOrEnqueue(_:) in Service to:
    • Bypass the queue and execute directly if no refresh needed.
    • Enqueue if refresh is required or ongoing.

Code Changes

SessionActor+Authorization.swift

/// Determines whether the access token should be refreshed based on the service configuration.
///
/// This asynchronous function checks the authentication method in the service configuration. It only proceeds if the method is a
/// bearer token with a writable component.
///
/// - Returns: A `Bool` value indicating if the access token needs refreshing. Returns `true` if refresh is required; otherwise, `false`.
func needsSerialization() async -> Bool {
    guard case let .bearer(_, _, writable) = serviceConfiguration.authenticationMethod else {
        return false
    }

    return writable.tokenCredential.requiresRefresh || isRefreshing
}

Service+ConditionalQueuing.swift

/// Executes or enqueues an asynchronous task based on whether the access token requires refreshing.
///
/// This function creates a task to check if the session's access token needs to be refreshed using `sessionActor.shouldRefreshAccessToken()`. If a
/// refresh is required, the provided task is enqueued in the `requestableQueueManager` for later execution. If no refresh is needed, the task is
/// executed immediately within the detached task context.
///
/// This mechanism ensures that tasks dependent on a valid access token are properly sequenced without blocking the caller.
///
/// - Parameter task: An escaping, sendable asynchronous closure that performs the desired operation. The closure takes no parameters and returns `Void`.
func awaitOrEnqueue(_ task: @escaping @Sendable () async -> Void) {
    Task {
        if await sessionActor.shouldRefreshAccessToken() {
            requestableQueueManager.enqueue(task)
        } else {
            await task()
        }
    }
}

Additional Info:

  • Optimize access token refresh with conditional queuing.
  • Updated documentation.

Source Compatibility:

Please check the box to indicate the impact of this proposal on source compatibility.

  • This change is additive and does not impact existing source code.
  • This change breaks existing source code.

Add Serialized FIFO Network Operations & Completion-Based APIs

15 Oct 18:51
7f078b9

Choose a tag to compare

Introduction & Motivation:

In AtomNetworking, we need to support both async/await and completion-based APIs for flexibility in iOS apps, especially in legacy or UIKit contexts where sync calls are required. Direct actor calls to SessionActor allow reentrancy during awaits, leading to potential races in concurrent flows (e.g., multiple updates/resumes interleaving). To address this, we introduce a serialized queue for FIFO processing, ensuring tasks execute one at a time in enqueue order.

Proposed Solution:

These changes are additive and enhance the API without breaking existing async flows:

  • Change Atom from actor to struct to enable non-async enqueue returning Service.
  • Add withAtomCheckedContinuation function for mapping Error to AtomError in checked continuations.
  • Add extension on Task for typedValue() to map Error to AtomError during awaits.
  • Improve .bearer token refresh in applyAuthorizationHeader with deduplication, polling for race windows, and typed error mapping.
  • Introduce RequestableQueueManager class for GCD-based serial queuing with on-demand timer for stall recovery.
  • Add async resume overloads to Service extension for bridging to completion-based APIs.
  • Add completion-based resume overloads to Service extension for serialized execution.

For the improved .bearer implementation in SessionActor's applyAuthorizationHeader (example snippet):

case let .bearer(endpoint, credential, writable):
	// Ensure the requestable requires an authorization header to be applied to it. A client has a
	// way to opt out of including the authorization header on a per-request basis even if the `authenticationMethod` is set.
	guard requestable.requiresAuthorization else {
		return requestable
	}

	// Ensure the existing credential requires a refresh.
	if writable.tokenCredential.requiresRefresh {
		if let refreshTask {
			// Await the ongoing refresh Task to ensure completion before proceeding.
			writable.tokenCredential = try await refreshTask.typedValue()
		}

		else {
			// If isRefreshing but no task yet (race window), wait briefly.
			var pollCount = 0

			// Poll loop to handle potential race where flag is set but Task not yet assigned.
			while isRefreshing, refreshTask == nil, pollCount < 10 {
				pollCount += 1

				// Sleep briefly to allow the other call to assign the refreshTask.
				try? await Task.sleep(for: .milliseconds(10))
			}

			// After polling, check if the refreshTask is now available and await it if so.
			if let refreshTask {
				writable.tokenCredential = try await refreshTask.typedValue()
			}

			// If no Task after polling, initiate a new refresh.
			else {
				// Set the flag to block other concurrent calls from starting a duplicate refresh.
				isRefreshing = true

				// Create a new Task for the background refresh operation.
				refreshTask = Task {
					// Ensure the flag and task reference are reset after completion or failure.
					defer {
						isRefreshing = false
						refreshTask = nil
					}

					// Perform the actual token refresh and return the new credential.
					return try await refreshAccessToken(using: endpoint, credential: credential, writable: writable)
				}

				// Await the new Task's result and assign if successful, or throw on failure.
				if let value = try await refreshTask?.typedValue() {
					writable.tokenCredential = value
				}
			}
		}
	}

	// Apply access token/credential authorization header.
	return AuthorizedRequestable(requestable: requestable, authorizationHeaderItems: [method.authorizationHeaderItem])

For the RequestableQueueManager class (full code with documentation):

/// A manager for serializing and processing a FIFO queue of asynchronous tasks in Swift 6 iOS apps.
/// 
/// This class uses a serial `DispatchQueue` for thread-safe mutations to shared state (`pendingTasks`, `isProcessing`), ensuring no data races. Tasks are enqueued synchronously (non-blocking for callers beyond brief sync), processed asynchronously in a detached `Task` for background execution, and drained serially with `await` to maintain FIFO order. An on-demand timer provides a "nudge" to recover from potential stalls (e.g., if the processor Task fails to reset due to errors or suspension).
/// 
/// Use this for network-heavy flows (e.g., in Atom) where order matters but callers need non-async APIs. `@unchecked Sendable` is used as the queue protects state for cross-concurrency safety.
/// 
/// - Note: Enqueues apply backpressure via `sync` (minor blocking on contention), suitable for moderate loads. For high-throughput, monitor with Instruments to avoid overload.
final class RequestableQueueManager: @unchecked Sendable {
    // MARK: - Properties

    /// A flag indicating if the processing loop is active. Mutated only within the serialized queue context to prevent races.
    private var isProcessing: Bool

    /// An array of pending async tasks (closures), appended in FIFO order and drained serially.
    private var pendingTasks: [() async -> Void]

    /// A serial dispatch queue for synchronizing access to `pendingTasks` and `isProcessing`, ensuring atomicity in Swift 6.
    private let queue: DispatchQueue

    /// An optional timer for periodic checks to nudge draining if the queue stalls (e.g., due to unhandled Task issues). Started on-demand and stopped when idle to minimize battery impact in iOS.
    private var checkTimer: DispatchSourceTimer?

    // MARK: - Lifecycle

    /// Initializes the queue manager with empty state.
    init() {
        self.isProcessing = false
        self.pendingTasks = .init()
        self.queue = .init(label: "com.alaskaair.atom.requestable.queue.manager")
    }

    /// Cleans up the timer on deallocation to prevent leaks or continued firing.
    deinit {
        checkTimer?.cancel()
    }

    // MARK: - Functions

    /// Enqueues a task for FIFO processing (callable synchronously from non-actors).
    ///
    /// Appends the task atomically via `queue.sync` and spawns a detached processor if not already processing. Starts the timer on-demand if the queue was empty before appending, to handle potential stalls.
    ///
    /// - Parameter task: The async closure to enqueue (e.g., network operations).
    func enqueue(_ task: @escaping @Sendable () async -> Void) {
        queue.sync {
            let wasEmpty = pendingTasks.isEmpty

            pendingTasks.append(task)

            if !isProcessing {
                isProcessing = true
                Task.detached { [weak self] in
                    await self?.processQueue()
                }
            }

            if wasEmpty {
                startCheckTimer()
            }
        }
    }
}

// MARK: - Private Properties and Methods

extension RequestableQueueManager {
    /// Processes the queue asynchronously, draining tasks in FIFO order.
    ///
    /// Runs in a detached Task, awaiting each task serially to maintain order. Resets `isProcessing` after draining and stops the timer if the queue is now empty.
    private func processQueue() async {
        while let nextTask = dequeue() {
            await nextTask()
        }
        queue.sync {
            isProcessing = false
            if pendingTasks.isEmpty {
                stopCheckTimer()
            }
        }
    }

    /// Safely dequeues the first task, or nil if empty.
    ///
    /// Uses `queue.sync` for atomic access, ensuring no races in Swift 6.
    ///
    /// - Returns: The next task closure, or nil if the queue is empty.
    private func dequeue() -> (() async -> Void)? {
        queue.sync {
            pendingTasks.isEmpty ? nil : pendingTasks.removeFirst()
        }
    }

    /// Starts the on-demand timer for periodic queue checks.
    ///
    /// Creates a DispatchSourceTimer on a background queue for power efficiency, scheduling checks every 1 second (adjustable). Uses leeway to allow iOS to optimize timing.
    private func startCheckTimer() {
        guard checkTimer == nil else {
            return
        }

        let timer = DispatchSource.makeTimerSource(queue: .global(qos: .background))
        timer.schedule(deadline: .now(), repeating: .milliseconds(1000), leeway: .milliseconds(100))
        timer.setEventHandler { [weak self] in
            self?.checkAndDrainIfNeeded()
        }

        timer.resume()

        checkTimer = timer
    }

    /// Stops and clears the timer to prevent unnecessary firing when the queue is idle.
    private func stopCheckTimer() {
        checkTimer?.cancel()
        checkTimer = nil
    }

    /// Checks if tasks are pending and not processing, then spawns a processor if needed.
    ///
    /// Called periodically by the timer to recover from potential stalls (e.g., if a previous processor Task failed to reset the flag).
    private func checkAndDrainIfNeeded() {
        queue.sync {
            if !pendingTasks.isEmpty, !isProcessing {
                isProcessing = true

                Task.detached { [weak self] in
                    await self?.processQueue()
                }
            }
        }
    }
}

Detailed Design

See changed files.

Source compatibility

This implementation is additive and does not introduce breaking changes. It enhances the existing Service API with a wrapper for serialized operations, maintaining compatibility with current source code. Consumers can opt-in to the wrapper for completion-based flows without affecting async/await usage.

Source Compatibility:

Please check the box to indicate the impact of this proposal on source compatibility.

  • This change is additive and does not impact existing source code.
  • This change breaks existing source code.

Removed Irrelevant Cancelation Methods

15 Apr 00:38
03c7369

Choose a tag to compare

Introduction:

In Atom 4, the underlying mechanism for handling data tasks has undergone a significant overhaul. Previous versions of Atom relied on URLDataTasks to manage network requests, which supported task cancellation through public methods exposed to clients. However, Atom 4 has transitioned to using an asynchronous version of the data function (async/await-compatible data(from:) method). This new approach leverages asynchronous programming paradigms to improve performance, readability, and maintainability.

A key consequence of this architectural shift is that task cancellation is no longer supported via public methods. In the URLDataTasks model, clients could explicitly cancel tasks using methods like cancel(), which was useful for scenarios such as aborting long-running network requests. In contrast, the asynchronous data function operates differently, with cancellation typically handled internally through mechanisms like Swift’s structured concurrency (e.g., Task cancellation) or cooperative cancellation patterns. These mechanisms are not exposed as public APIs in the same way, meaning clients cannot directly invoke cancellation through Atom’s public interface.

Motivation:

To avoid confusion and ensure a consistent developer experience, we must deprecate and remove the legacy task cancellation methods from Atom’s public API. Retaining these methods could mislead developers into expecting cancellation functionality that is no longer supported, potentially leading to errors, deprecated code usage, or incorrect assumptions about Atom’s behavior.

By removing these methods, we clarify that task cancellation is no longer a feature clients can rely on, aligning the API with the new asynchronous architecture.

Proposed Solution:

We removed the following method from Atom:

/// Cancels all currently running and suspended tasks.
///
/// Calling `cancelAllSessionTasks()` method will not invalide `URLSession`
/// instance nor will it empty cookies, cache or credential stores.
///
/// All subsequent tasks will occur on the same TCP connection.
public func cancelAllSessionTasks() async {
    await service.cancelAllSessionTasks()
}

Additional Info:

  • Removed irrelevant methods to prevent confusion.
  • Fixed Build Pre-action in the Example application.
  • Updated documentation.

Tests:

N/A.

Source Compatibility:

Please check the box to indicate the impact of this proposal on source compatibility.

  • This change is additive and does not impact existing source code.
  • This change breaks existing source code.

v4.0.1

22 Jan 04:56
a480609

Choose a tag to compare

  • Disable library evolution support.
  • Update documentation (We are working on adding completion-based APIs and extending the Combine framework).

Full Changelog: v4.0.0...v4.0.1

v4.0.0

14 Jan 19:53
104917c

Choose a tag to compare

Introduction & Motivation

Swift 6 introduces several pivotal enhancements that significantly elevate its capabilities over previous versions, focusing on safety, performance, and broader platform support. One of the standout features is the introduction of compile-time data race safety in its concurrency model.

In Atom 4.0, we utilized the compile-time data race safety checks as a guiding principle to rewrite the underlying implementation of the networking code and access token refresh logic.

Swift 6 includes various compiler optimizations that enhance the efficiency of common operations, from collection handling to memory management.

In terms of language structure, Swift 6 brings improvements in error handling by allowing functions to specify exact error types, leading to more precise error management. This, combined with the introduction of noncopyable types, allowed us to update our implementation to throw an AtomError where errors could occur within the package implementation.
This means the client no longer needs to check the type of error returned by AtomNetworking, as it will always be an AtomError.

Moving to Swift 6 is really compelling if you're looking to future-proof your applications. We’ve rewritten AtomNetworking to make full use of these Swift 6 features.

Proposed Solution

As part of our move to Swift 6, we are implementing a series of significant upgrades to the AtomNetworking Swift package to ensure it aligns with the latest language features and standards. Here's a detailed look at the changes we're making:

  • Add support for Swift Tools version 6.0: We're updating our toolchain to take advantage of the new features and improvements in Swift 6, ensuring compatibility and leveraging the latest compiler optimizations.
swift-tools-version:6.0
  • Add support for Swift language version 6.0: This change not only includes adopting the new syntax and features of Swift 6 but also ensures our code benefits from the enhanced type safety and compile-time checks introduced in this version.
.swiftLanguageMode(.v6)
  • Enable BUILD_LIBRARY_FOR_DISTRIBUTION flag: This flag is set to prepare our library for distribution, ensuring it can be used across different platforms or within larger frameworks with optimal performance.

  • Enable library evolution: By turning this on, we're allowing our library to evolve over time without breaking ABI compatibility, which is crucial for maintaining stability when updating or extending the library's functionality.

.unsafeFlags(["-enable-library-evolution", "-O"]),
  • Clean up code: This involves removing outdated code, fixing minor issues, and generally tidying up the codebase to improve maintainability and performance.

  • Conform all necessary types to Sendable protocol: With the new concurrency model in Swift 6, we're making sure our types can be safely used in concurrent contexts, reducing the risk of data races and improving the robustness of our package in multi-threaded environments.

  • Move away from Retryable: We moved away from Retryable operations by transitioning both Atom and Service to actors, which ensured more robust and predictable system behavior.

  • Remove unnecessary availability checks: After updating our deployment target to iOS 15, we removed unnecessary availability checks to streamline our codebase and improve development efficiency.

  • Rename the Atom package to AtomNetworking: This renaming clarifies the package's purpose. It also addresses the compiler warning where the public class Atom was shadowing the module Atom, which could cause failures when importing Atom in some configurations.

  • Rewrite the AtomResponse object: By rewriting the AtomResponse object to support the new concurrency model, we ensure propert integration with Swift's async/await, enhancing both performance and developer experience.

  • Rewrite BaseURL and BaseURLScheme, and all internal types to throw typed errors: Updating BaseURL, BaseURLScheme, and our internal types to throw AtomError types enhances error management, making it easier to pinpoint and resolve issues without the need to check for error type first.

  • Update copyright headers: We're refreshing these headers to reflect the current year and to ensure all intellectual property aspects are up to date.

// Copyright (c) 2025 Alaska Airlines
  • Update deployment targets to iOS 15: By setting this target, we ensure our package leverages the latest capabilities of iOS while still being accessible to a broad user base on newer devices.

  • Update documentation script & documentation: New features and changes in Swift 6 necessitate updated documentation. We're enhancing our documentation tools and content to reflect these changes accurately.

  • Update the Example application: We've enhanced the Example application to demonstrate improved performance and stability.

  • Update MARK markers: We're organizing our code with updated MARK comments, which help in navigating the codebase more efficiently, especially in larger projects.

  • Update projects to recommended settings: We're aligning our project settings with Apple's latest recommendations for Swift 6, which includes settings for debugging, testing, and code signing.

  • Update UNIT tests: Testing is critical, especially with such significant updates. We're revising our unit tests to ensure they cover all new functionalities and conform to Swift 6's testing environment, ensuring our package remains robust and reliable.

    These changes collectively aim to make the AtomNetworking Swift package not just compatible with Swift 6 but also more efficient, safer, and easier to use, setting a good foundation for future development and broader platform support.

Detailed Design

See files changed.

Source compatibility

This implementation introduces a breaking change and will affect compatibility with existing source code.

  • Add support for Swift tools version 6.0.
  • Add support for Swift language version 6.0.
  • Enable the BUILD_LIBRARY_FOR_DISTRIBUTION flag.
  • Move away from Retryable.
  • Remove completion-based APIs.
  • Remove unnecessary availability checks.
  • Rename the Atom package to AtomNetworking to adress the compiler warning where the public class Atom was shadowing the module Atom.
  • Rewrite the AtomResponse object to support the new concurrency model.
  • Rewrite BaseURL and BaseURLScheme, and all internal types to throw typed errors.
  • Update the copyright headers.
  • Update the deployment targets to iOS 15.
  • Update the documentation script.
  • Update the Example application.
  • Update MARK markers.
  • Update the project to recommended settings.
  • Update the UNIT tests.

Time-out support

04 Jan 19:01
80715af

Choose a tag to compare

Allows clients to set timeout as part of service configuration to there desired interval.

Async-Await support

10 Nov 00:20
6f02530

Choose a tag to compare

  • Introduce async-await support.
  • Update protocol requirements from class to AnyObject.

README

21 Dec 18:30
b30d5cc

Choose a tag to compare

  • README.md updates.

Version 2.0

21 Dec 17:58
93ecd72

Choose a tag to compare

  • Add Combine support.
  • Introduce RequestableItem.
  • Introduce HeaderItem and QueryItem types. Conform and implement ExpressibleByDictionaryLiteral for cleaner Requestable conformance.
  • Move Atom's nested types to be their own (resolve nesting issues with SPM, etc).
  • Remove Carthage support (sorry). Please use SPM.
  • Update Example to use SwiftUI.
  • Update UNIT tests.
  • Update documentation.