Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/uwaisalqadri/giffy

GIF app build with The Composable Architecture, XcodeGen, and Generic Protocol
https://github.com/uwaisalqadri/giffy

composable-architecture coredata modularization protocol-oriented-programming swiftui swinject xcodegen

Last synced: 2 months ago
JSON representation

GIF app build with The Composable Architecture, XcodeGen, and Generic Protocol

Awesome Lists containing this project

README

        


A

Giffy





A
B
C
D

## 🤖 Introduction

Giphy Client App built with some of the interesting iOS tech such as **TCA (The Composable Architecture by Point-Free)**, **Swinject**, Beautiful UI built with **SwiftUI**, **Clean Architecture with Generic Protocol Approach**, **SPM Modularization** and **XcodeGen!**

**Module**

* **`Giffy`**: the main app with presentation layer
* **`Common`**: domain and data layer
* **`CommonUI`**: common utils and assets
* **`Core`**: generic protocol for _DataSource_ and _Interactor_

## Table of Contents

- [Introduction](#introduction)
- [Features](#features)
- [Installation](#installation)
- [Libraries](#libraries)
- [The Composable Architecture](#composable-architecture)
- [Dependency Injection](#dependency-injection)
- [Project Structure](#project-structure)

## 🦾 Features

- Sharing, Copy-Pasting, and AirDropping GIFs and Stickers
- Search GIFs from various sources (Giphy and Tenor
- Save Favorite GIFs
- Widget, Live Activty, and Dynamic Island
- Animations!

⚠️ **`This project have no concern about backward compatibility, and only support the very latest or experimental api`** ⚠️

## 💿 Installation

With the greatness of _**XcodeGen**_ you can simply execute :

```
xcodegen
```

Rate my [XcodeGen setup!](https://github.com/uwaisalqadri/GiphyGIF/blob/master/project.yml)

## 💡 Libraries

* [Swift's New Concurrency](https://developer.apple.com/news/?id=2o3euotz)
* [SDWebImage](https://github.com/SDWebImage/SDWebImage)
* [SwiftUI](https://developer.apple.com/documentation/swiftui)
* [The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)
* [XcodeGen](https://github.com/yonaskolb/XcodeGen)
* [SwiftLint](https://github.com/realm/SwiftLint)
* [Swinject](https://github.com/Swinject/Swinject)
* [CoreData](https://developer.apple.com/documentation/coredata)

## 💨 TCA: Reducer, Action, State, and Store

Define your screen's _**State**_ and _**Action**_

```swift
public struct State: Equatable {
public var list: [Giphy] = []
public var errorMessage: String = ""
public var isLoading: Bool = false
public var isError: Bool = false
}

public enum Action {
case fetch(request: String)
case removeFavorite(item: Giphy, request: String)

case success(response: [Giphy])
case failed(error: Error)
}
```

Setup the _**Reducer**_

```swift
public struct FavoriteReducer: Reducer {

private let useCase: FavoriteInteractor
private let removeUseCase: RemoveFavoriteInteractor

init(useCase: FavoriteInteractor, removeUseCase: RemoveFavoriteInteractor) {
self.useCase = useCase
self.removeUseCase = removeUseCase
}

public var body: some ReducerOf {
Reduce { state, action in
switch action {
case .fetch(let request):
state.isLoading = true
return .run { send in
do {
let response = try await self.useCase.execute(request: request)
await send(.success(response: response))
} catch {
await send(.failed(error: error))
}
}

case .success(let data):
state.list = data
state.isLoading = false
return .none

case .failed:
state.isError = true
state.isLoading = false
return .none

case .removeFavorite(let item, let request):
return .run { send in
do {
let response = try await self.removeUseCase.execute(request: item)
await send(.fetch(request: request))
} catch {
await send(.failed(error: error))
}
}

}
}
}
}
```

**Composing** the Reducer
```swift
struct MainTabView: View {
let store: StoreOf

var body: some View {
WithViewStore(store, observe: \.selectedTab) { viewStore in
NavigationView {
ZStack {
switch viewStore.state {
case .home:
HomeView(
store: store.scope(
state: \.home,
action: \.home
)
)

case .search:
SearchView(
store: store.scope(
state: \.search,
action: \.search
)
)
}

. . . .

}
}
}
}
}
```

_"consistent and understandable"_ **- Point-Free**

Let your _**Store**_(d) _**Reducer**_ update the View

```swift
struct FavoriteView: View {
let store: StoreOf

var body: some View {
WithViewStore(store, observe: { $0 }) { viewStore in
ScrollView {
SearchField { query in
viewStore.send(.fetch(request: query))
}.padding(.vertical, 20)

if viewStore.state.list.isEmpty {
FavoriteEmptyView()
.padding(.top, 50)
}

LazyVStack {
ForEach(viewStore.state.list, id: \.id) { item in
GiphyItemRow(
isFavorite: true,
giphy: item,
onTapRow: { giphy in
viewStore.send(.showDetail(item: giphy))
},
onFavorite: { giphy in
viewStore.send(.removeFavorite(item: giphy, request: ""))
}
)
.padding(.horizontal, 20)
.padding(.bottom, 20)
}
}
}
.padding(.horizontal, 10)
.navigationTitle(FavoriteString.titleFavorite.localized)
.onAppear {
viewStore.send(.fetch(request: ""))
}
}
}
}
```

Read more about [**The Composable Architecture**](https://github.com/pointfreeco/swift-composable-architecture)

## 🚀 Dependency Injection

Here i'm using _**Swinject**_ for Dependency Injection

```swift
import Swinject

class Injection {
static let shared = Injection()
private let container = Container()

init() {
registerSearchFeature()
}

. . . .

private func registerSearchFeature() {
container.register(SearchInteractor.self) { [unowned self] _ in
Interactor(repository: self.resolve())
}
container.register(SearchGiphyRepository.self) { [unowned self] _ in
SearchGiphyRepository(remoteDataSource: self.resolve())
}
container.register(SearchRemoteDataSource.self) { _ in
SearchRemoteDataSource()
}
}

public static func resolve() -> T {
Injection().resolve()
}

public static func resolve(argument: A) -> T {
Injection().resolve(argument: argument)
}

public static func resolve(name: String) -> T {
Injection().resolve(name: name)
}

private func resolve() -> T {
guard let result = container.resolve(T.self) else {
fatalError("This type is not registered: \(T.self)")
}
return result
}

private func resolve(argument: A) -> T {
guard let result = container.resolve(T.self, argument: argument) else {
fatalError("This type is not registered: \(T.self)")
}
return result
}

private func resolve(name: String) -> T {
guard let result = container.resolve(T.self, name: name) else {
fatalError("This type is not registered: \(T.self)")
}
return result
}
}
```

### Usage:
```
Injection.resolve()
```

Read more about [**Swinject**](https://github.com/Swinject/Swinject)

## ☕️ Buy Me a Coffee
If you like this project please support me by Buy Me A Coffee ;-)

## 🏛 Project Structure

**`Giffy`**:

- `Dependency`
- `App`
- `Module`
- `Home`
- `Detail`
- `Favorite`
- `Search`

- `**GiffyWidget**`
- `**GiffyTests**`

**`Modules`**:

**`Common`**:
- `Data`
- `API`
- `DB`
- `DataSource`
- `Local`
- `Remote`
- `Entity`
- `Repository`
- `Domain`
- `Model`
- `Mapper`

**`CommonUI`**:
- `Assets`
- `Extensions`
- `Modifier`
- `Utils`

[**`Core`**](https://github.com/uwaisalqadri/CoreModule):
- `DataSource`
- `Extension`
- `Repository`
- `UseCase`