munkit is a Swift library designed to streamline network operations by providing a flexible and extensible approach to handling API requests. Built on top of Moya, it introduces features such as access token management and mock data support.
- iOS 16.0+
- macOS 15.0+
- Swift 6.1+
- Flexible API Targets: Define API endpoints with support for access token requirements and mock data.
- Access Token Management: Optional automatic handling of token refresh.
- Replica System: Manage network data with caching, state observation, and automatic refresh for reactive and efficient data handling.
- Mock Data: Seamlessly switch between real and mock data for testing.
- Extensibility: Leverage Moya plugins to customize behavior.
- Logging: Inject custom loggers for network operations.
To integrate munkit into your Swift project, add it as a dependency in your Package.swift
file:
dependencies: [
.package(url: "https://github.com/MobileUpLLC/munkit.git", from: "1.0.0")
]
Then, include it in your target:
targets: [
.target(
name: "YourTarget",
dependencies: ["munkit"]
)
]
Alternatively, add the library using the Swift Package Manager interface in Xcode.
Your API targets must conform to the MUNAPITarget
protocol, which extends Moya’s TargetType
and AccessTokenAuthorizable
. Below is an example using nested enumerations for v1
and v2
APIs:
enum MyAPI: MUNAPITarget {
enum V1: MUNAPITarget {
case getData(endpoint: String)
case postData(endpoint: String)
// Additional cases...
var baseURL: URL { /* Implementation */ }
var path: String { /* Implementation */ }
var method: Moya.Method { /* Implementation */ }
var task: Moya.Task { /* Implementation */ }
var headers: [String: String]? { /* Implementation */ }
var parameters: [String: Any] { /* Implementation */ }
var isAccessTokenRequired: Bool { /* Implementation */ }
var isRefreshTokenRequest: Bool { /* Implementation */ }
var isMockEnabled: Bool { /* Implementation */ }
var mockFileName: String? { /* Implementation */ }
var authorizationType: Moya.AuthorizationType? { /* Implementation */ }
}
enum V2: MUNAPITarget {
case fetchItems(endpoint: String)
case updateItem(endpoint: String)
// Additional cases...
var baseURL: URL { /* Implementation */ }
var path: String { /* Implementation */ }
var method: Moya.Method { /* Implementation */ }
var task: Moya.Task { /* Implementation */ }
var headers: [String: String]? { /* Implementation */ }
var parameters: [String: Any] { /* Implementation */ }
var isAccessTokenRequired: Bool { /* Implementation */ }
var isRefreshTokenRequest: Bool { /* Implementation */ }
var isMockEnabled: Bool { /* Implementation */ }
var mockFileName: String? { /* Implementation */ }
var authorizationType: Moya.AuthorizationType? { /* Implementation */ }
}
case v1(V1)
case v2(V2)
var baseURL: URL {
switch self {
case .v1(let target): return target.baseURL
case .v2(let target): return target.baseURL
}
}
var path: String { /* Implementation */ }
var method: Moya.Method { /* Implementation */ }
var task: Moya.Task { /* Implementation */ }
var headers: [String: String]? { /* Implementation */ }
var parameters: [String: Any] { /* Implementation */ }
var isAccessTokenRequired: Bool { /* Implementation */ }
var isRefreshTokenRequest: Bool { /* Implementation */ }
var isMockEnabled: Bool { /* Implementation */ }
var mockFileName: String? { /* Implementation */ }
var authorizationType: Moya.AuthorizationType? { /* Implementation */ }
}
Create an instance of MUNNetworkService
with your target type:
let networkService = MUNNetworkService<MyAPI>(
session: /* Optional custom session */,
plugins: /* Optional additional plugins */
)
Access token management is facilitated through two protocols: MUNAccessTokenProvider
and MUNAccessTokenRefresher
. These can be implemented by separate classes or structs, or by a single entity implementing both protocols:
MUNAccessTokenProvider
: Supplies the current access token.MUNAccessTokenRefresher
: Handles token refresh when needed.
Configure these in MUNNetworkService
after initialization:
let tokenProvider: MUNAccessTokenProvider = TokenProvider()
let tokenRefresher: MUNAccessTokenRefresher = TokenRefresher()
await networkService.setAuthorizationObjects(
provider: tokenProvider,
refresher: tokenRefresher,
tokenRefreshFailureHandler: { /* Handle refresh failure */ }
)
Use the executeRequest
method to perform API calls:
do {
let response = try await networkService.executeRequest(target: .v1(.getData(endpoint: "data")))
// Process the response
} catch {
// Handle the error
}
To enable mock data for a target, set isMockEnabled
to true
and specify a mockFileName
. Mock data should be provided as a JSON file in your bundle.
For paginated APIs, use the MUNMockablePaginationAPITarget
protocol and specify pageIndexParameterName
and pageSizeParameterName
.
The munkit library allows clients to inject a custom logger to handle logging for network operations. To enable logging, you need:
- Implement the MUNLoggable protocol to define how messages are logged.
class CustomLoggerAdapter: MUNLoggable {
func log(type: OSLogType, _ message: String) {
...
}
}
- Configure the MUNLogger with your custom logger implementation.
- Initialize the NetworkService with the MUNLoggerPlugin.
MUNLogger.setupLogger(CustomLoggerAdapter())
let networkService = NetworkService(plugins: [MUNLoggerPlugin.instance])
munkit provides a powerful Replica
system. Replicas encapsulate data fetching, storage, and observation logic, making it easy to handle API responses in a reactive and efficient manner.
SingleReplica
: An actor-based protocol for managing a single data type, supporting fetching, refreshing, and state observation.ReplicaState
: Tracks the loading state, data, errors, and observer status for a replica.ReplicaSettings
: Configures replica behavior, including stale time, data/error clearing, and revalidation policies.ReplicaStorage
: An optional protocol for persisting replica data to disk.ReplicaObserver
: Monitors replica state changes and observer activity via async streams.
Define a repository to manage a replica for API data:
import munkit
public actor DNDClassesRepository {
private let networkService: NetworkService
private var dndClassesListReplica: (any SingleReplica<DNDClassesListModel>)?
public init(networkService: NetworkService) {
self.networkService = networkService
}
public func getDNDClassesListReplica() async -> any SingleReplica<DNDClassesListModel> {
if let replica = dndClassesListReplica { return replica }
dndClassesListReplica = await ReplicasHolder.shared.getReplica(
name: "DNDClassesListReplica",
settings: .init(
staleTime: 10,
clearTime: 5,
clearErrorTime: 1,
cancelTime: 0.05,
revalidateOnActiveObserverAdded: true
),
storage: nil,
fetcher: { [weak self] in
guard let networkService = self?.networkService else { throw CancellationError() }
return try await networkService.executeRequest(target: .classes)
}
)
return dndClassesListReplica!
}
}
In a SwiftUI view, observe the replica's state:
import SwiftUI
import munkit
struct DNDClassesListView: View {
@Environment(DNDClassesRepository.self) private var dndClassesRepository
@State private var replicaState: ReplicaState<DNDClassesListModel>?
@State private var replicaSetupped = false
private let activityStream = AsyncStream<Bool>.makeStream()
var body: some View {
ZStack {
if let state = replicaState, let data = state.data?.value.results, !data.isEmpty {
List(data, id: \.index) { dndClass in
Text(dndClass.name)
}
.refreshable { Task { await dndClassesRepository.getDNDClassesListReplica().revalidate() } }
}
}
.onAppear {
guard !replicaSetupped else { activityStream.continuation.yield(true); return }
replicaSetupped = true
Task {
let observer = await dndClassesRepository.getDNDClassesListReplica().observe(
activityStream: activityStream.stream
)
activityStream.continuation.yield(true)
for await state in await observer.stateStream {
replicaState = state
}
}
}
}
}
Contributions are welcome! Please create an issue or submit a pull request on GitHub.