Pigeon is a SwiftUI and UIKit library that relies on Combine to deal with asynchronous data.

Overview

Pigeon 🐦

CI Status Version License Platform Slack

Introduction

Pigeon is a SwiftUI and UIKit library that relies on Combine to deal with asynchronous data. It is heavily inspired by React Query.

In a nutshell

With Pigeon you can:

  • Fetch server side APIs.
  • Cache server responses using interchangeable and configurable cache providers.
  • Share server data among different, unconnected components in your app.
  • Mutate server side resources.
  • Invalidate cache and refetch data.
  • Manage paginated data sources
  • Pigeon is agnostic on what you use for fetching data.

All of that working against a very simple interface that uses the very convenient ObservableObject Combine protocol.

What is Pigeon?

Pigeon is all about Queries and Mutations. Queries are objects that are responsible of fetching server data, and Mutations are objects that are responsible of modifying server data. Both Queries and Mutations are ObservableObject conforming, meaning both of them are fully compatible with SwiftUI and that their states are observable.

Queries are identified by a QueryKey. Pigeon uses QueryKey objects to cache query results, link them internally and invalidate queries when they need to be refetched.

A very important thing in Pigeon is that you can use whatever you want to fetch data from wherever you need. Pigeon don't force you to use Alamofire or URLSession or GraphQL or even CoreData. You can fetch the data from where you need using the most appropriate tool. The only thing you need to use is Combine publishers.

The last thing I want to note and then we can go straight to code. Pigeon can optionally cache your responses: you can let Pigeon store the responses for your fetches and it will populate your app with data with almost zero-config.

Quick Start

In the core of Pigeon is the Query ObservableObject. Let's explore the 'hello world' of Pigeon:

// 1
struct User: Codable, Identifiable {
    let id: Int
    let name: String
}

struct UsersList: View {
    // 2
    @ObservedObject var users = Query<Void, [User]>(
        // 3    
        key: QueryKey(value: "users"),
        // 4
        fetcher: {
            URLSession.shared
                .dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/users")!)
                .map(\.data)
                .decode(type: [User].self, decoder: JSONDecoder())
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
    )
    
    var body: some View {
        // 5
        List(users.state.value ?? []) { user in
            Text(user.name)
        }.onAppear(perform: {
            // 6
            self.users.refetch(request: ())
        })
    }
}
  1. We start by defining a Codable structure that will store our server side data. This is not related to Pigeon itself, but is still needed for our example to work.
  2. We define a Query that will store our array of User. Query takes two generic parameters: Request (Void in this example, since the fetch action won't receive any parameters) and Response which is the type of our data ([User] in this example).
  3. Data is cached by default in Pigeon. The QueryKey is a simple wrapper around the String that identifies our piece of state.
  4. Query also receives a fetcher, which is a function that we have to define. fetcher takes the Request and returns a Combine Publisher holding the Response. Note that we can put whatever custom logic in the fetcher. In this case, we use URLSession to get an array of User from an API.
  5. Query contains a state, that is either: idle (if it just starts), loading (if the fetcher is running), failed (which also contains an Error), or succeed (which also contains the Response). value is just a convenience property that returns a Response in case it exists, or nil otherwise.
// ...
    var body: some View {
        // 5
        switch users.state {
            case .idle, .loading:
                return AnyView(Text("Loading..."))
            case .failed:
                return AnyView(Text("Oops..."))
            case let .succeed(users):
                return AnyView(
                    List(users) { user in
                        Text(user.name)
                    }
                )
        }
    }
// ...

Note: If you find this ugly, then you might be interested in QueryRenderer. Keep scrolling!

  1. In this example, we are firing our Query manually, using refetch. However, we can also configure our Query so it fires immediately like this:
struct UsersList: View {
    @ObservedObject var users = Query<Void, [User]>(
        key: QueryKey(value: "users"),
        // Changing the query behavior, we can tell the query to 
        // start fetching as soon as it initializes. 
        behavior: .startImmediately(()),
        fetcher: {
            URLSession.shared
                .dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/users")!)
                .map(\.data)
                .decode(type: [User].self, decoder: JSONDecoder())
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
    )
    
    var body: some View {
        List(users.state.value ?? []) { user in
            Text(user.name)
        }
    }
}

Queries and Query Consumers

In addition to Queries, Pigeon has another type, Consumer that doesn't provide any kind of fetching capability, but just provides the capability to consume, and react to changes in Queries with the same QueryKey that it subscribes to. Please note that the Query dependency injection is done internally, and that the state is not duplicated.

.Consumer(key: QueryKey(value: "users")) var body: some View { List(users.state.value ?? []) { user in Text(user.name) } } } struct User: Codable, Identifiable { let id: Int let name: String } ">
struct ContentView: View {
    @ObservedObject var users = Query<Void, [User]>(
        key: QueryKey(value: "users"),
        behavior: .startImmediately(()),
        fetcher: {
            URLSession.shared
                .dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/users/")!)
                .map(\.data)
                .decode(type: [User].self, decoder: JSONDecoder())
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
    )
    
    var body: some View {
        UsersList()
    }
}

struct UsersList: View {
    @ObservedObject var users = Query<Void, [User]>.Consumer(key: QueryKey(value: "users"))
    
    var body: some View {
        List(users.state.value ?? []) { user in
            Text(user.name)
        }
    }
}

struct User: Codable, Identifiable {
    let id: Int
    let name: String
}

Polling

Pigeon provides a way to fetching data using the fetcher every N seconds. That's achieved with the pollingBehavior property in the Query class. Default is .noPolling. Let's see an example:

@ObservedObject var users = Query<Void, [User]>(
    key: QueryKey(value: "users"),
    behavior: .startImmediately(()),
    pollingBehavior: .pollEvery(2),
    fetcher: {
        URLSession.shared
            .dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/users")!)
            .map(\.data)
            .decode(type: [User].self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
)

That query will trigger its fetcher every 2 seconds.

Mutations

In addition to allow queries, Pigeon also provides a way to mutate server data, and force to refetch affected queries.

@ObservedObject var sampleMutation = Mutation<Int, User> { (number) -> AnyPublisher<User, Error> in
    Just(User(id: number, name: "Pepe"))
        .tryMap({ $0 })
        .eraseToAnyPublisher()
}

// ...

sampleMutation.execute(with: 10) { (user: User, invalidate) in
    // Invalidate triggers a new query on the "users" key
    invalidate(QueryKey(value: "users"), .lastData)
}

Convenient keys

You can also define more convenient keys by extending QueryKey like this:

extension QueryKey {
    static let users: QueryKey = QueryKey(value: "users")
}

So then you can use it like this:

struct UsersList: View {
    @ObservedObject var users = Query<Void, [User]>.Consumer(key: .users)
    
    var body: some View {
        List(users.state.value ?? []) { user in
            Text(user.name)
        }
    }
}

Key adapters

There are some times where you need to cache values not only depending on your Query type, but also on the parameters of your request. For instance, maybe you want to cache the response for user with id=1 in a separate cache value than user with id=2. That is the problem key adapters solve. Key Adapters are available both in Query and in PaginatedQuery and are optional. Key adapters are sent under the keyAdapter parameter for the constructor and are functions with (QueryKey, Request) -> QueryKey signature.

@ObservedObject private var user = Query<Int, [User]>(
    key: QueryKey(value: "users"),
    keyAdapter: { key, id in
        key.appending(id.description)
    },
    behavior: .startImmediately(1),
    cache: UserDefaultsQueryCache.shared,
    fetcher: { id in
        // ...
    }
)

Paginated Queries

A very frequent scenario when fetching server data is pagination. Pigeon provides a special type of Query for this use case: PaginatedQuery. PaginatedQuery is generic on three types:

  • Request: The type that is required in order to perform the fetch
  • PageIdentifier: a PaginatedQueryKey conforming type, that identifies the current page. By default, Pigeon provides two PaginatedQueryKey alternatives: NumericPaginatedQueryKey (page 1, page 2, ...) and LimitOffsetPaginatedQueryKey (limit: 20, offset: 40, for instance). If these don't match your needs, then you can create a new type that implements PaginatedQueryKey and customize its behavior.
  • Response: The response type. This type needs to conform Sequence in order to be suitable for use in PaginatedQuery.

Let's jump on an example:

@ObservedObject private var users = PaginatedQuery<Void, LimitOffsetPaginatedQueryKey, [User]>(
    key: QueryKey(value: "users"),
    firstPage: LimitOffsetPaginatedQueryKey(
        limit: 20,
        offset: 0
    ),
    fetcher: { (request, page) in
        // ...
    }
)

This is an example of a PaginatedQuery. There are a couple of important things to note here:

  • key works in the exact same way as in the regular Query type.
  • firstPage should receive the first possible page for your fetcher.
  • fetcher works exactly the same way as in Query BUT it also receives the page to be fetched.

On top of all the functionality that Query provides, PaginatedQuery allow you a couple of more things:

// If you want to fetch the next page.
users.fetchNextPage()

// If you need to fetch the first page again (this will reset the current state for your query)
users.refetch(request /* some Request */)

An important thing to note is that PaginatedQuery can not be cached at this moment.

Dependency on Codable

An important restriction in Pigeon Query type is that the Response must be Codable. That is because of the cachable nature of server side data. Data can be cached, and in order to be cached, we need it to be Codable.

Cache

Cache is deeply integrated into Pigeon mechanics. All data in Pigeon Query objects can be cached since it's codable, and then used for state rehydration in the next app startup.

Let's see an example:

@ObservedObject private var cards = PaginatedQuery<Void, NumericPaginatedQueryKey, [Card]>(
    key: QueryKey(value: "cards"),
    firstPage: NumericPaginatedQueryKey(current: 0),
    behavior: .startImmediately(()),
    cache: UserDefaultsQueryCache.shared,
    cacheConfig: QueryCacheConfig(
        invalidationPolicy: .expiresAfter(1000),
        usagePolicy: .useInsteadOfFetching
    ),
    fetcher: { request, page in
        print("Fetching page no. \(page)")
        return GetCardsRequest()
            .execute()
            .map(\.cards)
            .eraseToAnyPublisher()
    }
)

This is from the Example folder in this project. If you see in the cacheConfig:

cacheConfig: QueryCacheConfig(
    invalidationPolicy: .expiresAfter(1000),
    usagePolicy: .useInsteadOfFetching
),

It's almost self-explanatory: Pigeon will use the cache if possible and if its data is valid, instead of fetching. And the data will be considered valid until 1000 seconds from saved.

Pigeon provides two invalidation policies:

public enum InvalidationPolicy {
    case notExpires
    case expiresAfter(TimeInterval)
}

and three usage policies:

public enum UsagePolicy {
    case useInsteadOfFetching
    case useIfFetchFails
    case useAndThenFetch
}

Right now, two cache providers are included in the project: InMemoryQueryCache and UserDefaultsQueryCache, but you can create your own cache by implementing QueryCacheType in a custom type.

Query Renderers

If you saw the state rendering in the Quick Start section:

// ...
    var body: some View {
        // 5
        switch users.state {
            case .idle, .loading:
                return AnyView(Text("Loading..."))
            case .failed:
                return AnyView(Text("Oops..."))
            case let .succeed(users):
                return AnyView(
                    List(users) { user in
                        Text(user.name)
                    }
                )
        }
    }
// ...

Then you probably felt it could have been done in a much better way. What is all that AnyView thing? Weird...

Well, Pigeon provides an alternative way to do this: QueryRenderer. It's a protocol with three requirements:

// When Query is in loading state
var loadingView: some View { get }

// When Query is in succeed state
func successView(for response: Response) -> some View

// When Query is in failure state
func failureView(for failure: Error) -> some View

In exchange of that, QueryRenderer provides a method for rendering a QueryState. Let's see a full example:

some View { List(response) { user in Text(user.name) } } func failureView(for failure: Error) -> some View { Text("It failed...") } } struct User: Codable, Identifiable { let id: Int let name: String } ">
struct UsersList: View {
    @ObservedObject private var users = Query<Void, [User]>(
        key: QueryKey(value: "users"),
        behavior: .startImmediately(()),
        fetcher: {
            URLSession.shared
                .dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/users/")!)
                .map(\.data)
                .decode(type: [User].self, decoder: JSONDecoder())
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
    )
    
    var body: some View {
        self.view(for: users.state)
    }
}

extension UsersList: QueryRenderer {
    var loadingView: some View {
        Text("Loading...")
    }
    
    func successView(for response: [User]) -> some View {
        List(response) { user in
            Text(user.name)
        }
    }
    
    func failureView(for failure: Error) -> some View {
        Text("It failed...")
    }
}

struct User: Codable, Identifiable {
    let id: Int
    let name: String
}

Please note that you aren't forced to put implement QueryRenderer in your View. You can always create a different structure for the rendering logic, and make that structure reusable for different contexts. Check this full example:

() init(id: String) { self.id = id } var body: some View { renderer.view(for: card.state) .navigationBarTitle("Card Detail") } } protocol NameRepresentable { var name: String { get } } extension Card: NameRepresentable {} struct NameRepresentableRenderer : QueryRenderer { var loadingView: some View { Text("Loading...") } func failureView(for failure: Error) -> some View { EmptyView() } func successView(for response: T) -> some View { Text(response.name) } } ">
struct CardDetailView: View {
    @ObservedObject private var card = Query<String, Card>(
        key: QueryKey(value: "card_detail"),
        keyAdapter: { key, id in
            key.appending(id)
        },
        cache: UserDefaultsQueryCache.shared,
        cacheConfig: QueryCacheConfig(
            invalidationPolicy: .expiresAfter(500),
            usagePolicy: .useInsteadOfFetching
        ),
        fetcher: { id in
            CardDetailRequest(cardId: id)
                .execute()
                .map(\.card)
                .eraseToAnyPublisher()
        }
    )
    private let id: String
    
    let renderer = NameRepresentableRenderer<Card>()
    
    init(id: String) {
        self.id = id
    }
    
    var body: some View {
        renderer.view(for: card.state)
            .navigationBarTitle("Card Detail")
    }
}

protocol NameRepresentable {
    var name: String { get }
}

extension Card: NameRepresentable {}

struct NameRepresentableRenderer<T: NameRepresentable>: QueryRenderer {
    var loadingView: some View {
        Text("Loading...")
    }
    
    func failureView(for failure: Error) -> some View {
        EmptyView()
    }
    
    func successView(for response: T) -> some View {
        Text(response.name)
    }
}

Global defaults

You can change QueryCacheType and QueryCacheConfig global data by calling to setGlobal on either type.

Best Practices

You are not forced to mix networking logic with the views. You can always define your queries externally and inject them as a dependency. You can even embed Queries and Mutations in your own view models or ObservableObject instances. Query, Consumer and PaginatedQuery have three interesting properties:

var state: QueryState
    { 
   get }

   var statePublisher: AnyPublisher
   
    
     , 
     Never> { 
     get }

     var valuePublisher: AnyPublisher
     
      Never>
     
    
   
  

You can observe statePublisher or valuePublisher, so you can add abstract your views from the QueryType objects, or even create dependent queries. You can chain queries by listening to changes in their state or success values.

Example

To run the example project, clone the repo, and run pod install from the Example directory first.

Requirements

Pigeon works with SwiftUI and UIKit as well. As it has a dependency in Combine, it required a minimum iOS version of 13.0.

Installation

Using Cocoapods

Pigeon is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Pigeon'

Using Swift Package Manager

Pigeon is also available through Swift Package Manager. To install it:

  1. In Xcode, open File > Swift Packages > Add Package Dependency...
  2. In the window that opens, paste https://github.com/fmo91/Pigeon.git into the package repository URL text field.
  3. Click next and accept the defaults.

Author

fmo91, [email protected]

License

Pigeon is available under the MIT license. See the LICENSE file for more info.

Comments
  • How to invalidate and refetch query?

    How to invalidate and refetch query?

    Hi!

    Thanks for this great library. I have it mostly working, but I'm struggling with one issue.

    I'm trying to invalidate and refetch data when a user does an action. I can see there is a way to do with this with mutations in the docs but I can't seem to do it with a Query.

    I have tried using UserDefaultsQueryCache.shared.invalidate(for:), but nothing seems to happen.

    Here's what I have so far, I'm using a pull down library which I'm trying to trigger the refresh.

    import SwiftUI
    import Combine
    import Pigeon
    import SwiftUIRefresh
    
    struct UnreadView: View {
        @State private var showPull = false
        private var queryInvalidator = UserDefaultsQueryCache.shared
        
        @ObservedObject var query = Query<Void, [Pin]>(
                key: QueryKey(value: "all"),
                behavior: .startWhenRequested,
                cache: UserDefaultsQueryCache.shared,
                cacheConfig: QueryCacheConfig(
                    invalidationPolicy: .expiresAfter(1000),
                    usagePolicy: .useInsteadOfFetching
                ),
                fetcher: {
                    print("FETCHING All Pins")
                    return GetAllPinsRequest()
                        .execute()
                        .map { return $0}
                        .eraseToAnyPublisher()
                }
            )
        
        var unreadPins: [Pin] {
            switch query.state {
            case .succeed(let pins):
                return pins.filter { pin in
                    return pin.toRead == "yes"
                }
            default:
                return []
            }
        }
        
        var body: some View {
            view(for: query.state)
                .navigationTitle("Unread")
                .onAppear(perform: {
                    query.refetch(request: ())
                })
                .pullToRefresh(isShowing: $showPull) {
                      queryInvalidator.invalidate(for: QueryKey(value: "all"))
                      query.refetch(request: ())
                     DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                        self.showPull = false
                    }
                }
        }
    }
    
    extension UnreadView: QueryRenderer {
        var loadingView: some View {
            Text("Loading...")
        }
        
        func successView(for pins: [Pin]) -> some View {
            List(unreadPins) { pin in
                NavigationLink(destination: PinPreviewView(title: pin.title, url: pin.url)) {
                    VStack(alignment: .leading, spacing: 10) {
                        Text("\(pin.title)").font(.headline)
                        Text("\(pin.url)").lineLimit(1).truncationMode(.tail)
                        TagListView(tags: pin.tags)
                    }.padding(.vertical)
                }
            }.listStyle(InsetGroupedListStyle())
        }
        
        func failureView(for failure: DecodingError) -> some View {
            Text("Error loading pins")
        }
        func failureView(for failure: Error) -> some View {
            Text("Error")
               
        }
    }
    

    Thanks for any guidance you can give.

    question 
    opened by acoustep 2
  • StateObject vs. ObservedObject usage

    StateObject vs. ObservedObject usage

    Hey @fmo91,

    Amazing library. Good to see the learnings from the React ecosystem making it to the ever so expressive Swift.

    Reaching out because I spent quite a bit of time getting the library to work. I set up a Query following your examples quite closely, but I couldn't get my views to update. It all finally worked out when instead of using @ObservedObject, I switched to @StateObject.

    I am completely new to Swift/SwiftUI so my code is very vanilla, was wondering if this is something you encountered before or would know why there would be a difference between the two in this use case.

    Couple of sites that led me to this discovery:

    • https://stackoverflow.com/questions/60956270/swiftui-view-not-updating-based-on-observedobject
    • https://www.hackingwithswift.com/forums/macos/observed-object-not-updating-view/5935
    opened by rnmp 0
  • Flag package supporting watchOS 6+

    Flag package supporting watchOS 6+

    I'm currently developing an app using Pigeon. I had to copy the sources of Pigeon into my project in order to use Pigeon, because it's not marked to support watchOS. So far everything works just fine with watchOS, so I think it's safe to flag the package as supporting watchOS 6+ (that's the minimum supported version according to Xcode).

    opened by Manc 0
  • Fix Swiftlint warnings

    Fix Swiftlint warnings

    Steps to do this

    1. Open the example project
    2. Build the project Cmd + B
    3. You should now see all the Swiftlint warnings.

    The goal of this issue is to fix them without changing any functionality.

    good first issue Code Quality 
    opened by fmo91 2
  • Include @ViewBuilder in state switch in the Readme

    Include @ViewBuilder in state switch in the Readme

    In your readme the following sample code is shown:

    
    // ...
        var body: some View {
            // 5
            switch users.state {
                case .idle, .loading:
                    return AnyView(Text("Loading..."))
                case .failed:
                    return AnyView(Text("Oops..."))
                case let .succeed(users):
                    return AnyView(
                        List(users) { user in
                            Text(user.name)
                        }
                    )
            }
        }
    

    As far as I can see you can use the ViewBuilder feature in this case. The QueryRenderer protocol of course is useful too but this would make the code a little but more readable imo and provides another alternative for your users:

    
    // ...
        @ViewBuilder var body: some View {
            // 5
            switch users.state {
                case .idle, .loading:
                    Text("Loading...")
                case .failed:
                    Text("Oops...")
                case let .succeed(users):
                    List(users) { user in
                        Text(user.name)
                    }
            }
        }
    

    Please also note that @ViewBuilder is automatically added to the body on iOS 14.

    documentation 
    opened by dehlen 7
Owner
Fernando Martín Ortiz
Senior iOS Engineer at Parser Digital
Fernando Martín Ortiz
A Swift micro-framework to easily deal with weak references to self inside closures

WeakableSelf Context Closures are one of Swift must-have features, and Swift developers are aware of how tricky they can be when they capture the refe

Vincent Pradeilles 72 Sep 1, 2022
Testable Combine Publishers - An easy, declarative way to unit test Combine Publishers in Swift

Testable Combine Publishers An easy, declarative way to unit test Combine Publishers in Swift About Combine Publishers are notoriously verbose to unit

Albert Bori 6 Sep 26, 2022
A simple Pokedex app written in Swift that implements the PokeAPI, using Combine and data driven UI.

SwiftPokedex SwiftPokedex is a simple Pokedex app written by Viktor Gidlöf in Swift that implements the PokeAPI. For full documentation and implementa

Viktor G 26 Dec 14, 2022
Swift extensions for asynchronous CloudKit record processing

⛅️ AsyncCloudKit Swift extensions for asynchronous CloudKit record processing. D

Chris Araman 17 Dec 8, 2022
Project shows how to unit test asynchronous API calls in Swift using Mocking without using any 3rd party software

UnitTestingNetworkCalls-Swift Project shows how to unit test asynchronous API ca

Gary M 0 May 6, 2022
Customize and resize sheets in SwiftUI with SheeKit. Utilise the power of `UISheetPresentationController` and other UIKit features.

SheeKit Customize and resize sheets in SwiftUI with SheeKit. Utilise the power of UISheetPresentationController and other UIKit features. Overview She

Eugene Dudnyk 67 Dec 31, 2022
🟣 Verge is a very tunable state-management engine on iOS App (UIKit / SwiftUI) and built-in ORM.

Verge is giving the power of state-management in muukii/Brightroom v2 development! Verge.swift ?? An effective state management architecture for iOS -

VergeGroup 478 Dec 29, 2022
DGPreview - Make UIKit project enable preview feature of SwiftUI

DGPreview Make UIKit project enable preview feature of SwiftUI Requirements iOS

donggyu 5 Feb 14, 2022
Handy Combine extensions on NSObject, including Set.

Storable Description If you're using Combine, you've probably encountered the following code more than a few times. class Object: NSObject { var c

hcrane 23 Dec 13, 2022
AnalyticsKit for Swift is designed to combine various analytical services into one simple tool.

?? AnalyticsKit AnalyticsKit for Swift is designed to combine various analytical services into one simple tool. To send information about a custom eve

Broniboy 6 Jan 14, 2022
Use this package in order to ease up working with Combine URLSession.

Use this package in order to ease up working with Combine URLSession. We support working with Codable for all main HTTP methods GET, POST, PUT and DELETE. We also support MultipartUpload

Daniel Mandea 1 Jan 6, 2022
Swifty closures for UIKit and Foundation

Closures is an iOS Framework that adds closure handlers to many of the popular UIKit and Foundation classes. Although this framework is a substitute f

Vinnie Hesener 1.7k Dec 21, 2022
This is a app developed in Swift, using Object Oriented Programing, UIKit user interface programmatically, API Request and Kingfisher to load remote images

iOS NOW ⭐ This is a app developed in Swift, using Object Oriented Programing, UIKit user interface programmatically, API Request and Kingfisher to loa

William Tristão de Paula 1 Dec 7, 2021
SwiftExtensionKit - SwiftExtensionKit is to contain generic extension helpers for UIKit and Foundation

RichAppz PureSwiftExtensionKit SwiftExtensionKit is to contain generic extension

Rich Mucha 0 Jan 31, 2022
FastLayout - A UIKit or AppKit package for fast UI design

FastLayout FastLayout is a UIKit or AppKit package for fast UI design. Layout Ex

null 1 Feb 19, 2022
Swift-HorizontalPickerView - Customizable horizontal picker view component written in Swift for UIKit/iOS

Horizontal Picker View Customizable horizontal picker view component written in

Afraz Siddiqui 8 Aug 1, 2022
swift-highlight a pure-Swift data structure library designed for server applications that need to store a lot of styled text

swift-highlight is a pure-Swift data structure library designed for server applications that need to store a lot of styled text. The Highlight module is memory-efficient and uses slab allocations and small-string optimizations to pack large amounts of styled text into a small amount of memory, while still supporting efficient traversal through the Sequence protocol.

kelvin 4 Aug 14, 2022
Data Mapping library for Objective C

OCMapper is a data mapping library for Objective C that converts NSDictionary to NSObject

Aryan Ghassemi 346 Dec 8, 2022
RandomKit is a Swift framework that makes random data generation simple and easy.

RandomKit is a Swift framework that makes random data generation simple and easy. Build Status Installation Compatibility Swift Package Manager CocoaP

Nikolai Vazquez 1.5k Dec 29, 2022