Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
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
- Host: GitHub
- URL: https://github.com/uwaisalqadri/giffy
- Owner: uwaisalqadri
- Created: 2021-05-23T03:42:51.000Z (over 3 years ago)
- Default Branch: master
- Last Pushed: 2024-11-09T01:04:20.000Z (3 months ago)
- Last Synced: 2024-11-09T02:18:10.594Z (3 months ago)
- Topics: composable-architecture, coredata, modularization, protocol-oriented-programming, swiftui, swinject, xcodegen
- Language: Swift
- Homepage: https://medium.com/@uwaisalqadri/the-composable-architecture-tca-in-a-nutshell-3c574708542c
- Size: 6.53 MB
- Stars: 23
- Watchers: 1
- Forks: 4
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
Giffy
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)- 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`** ⚠️
With the greatness of _**XcodeGen**_ you can simply execute :
```
xcodegen
```Rate my [XcodeGen setup!](https://github.com/uwaisalqadri/GiphyGIF/blob/master/project.yml)
* [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: StoreOfvar 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)
Here i'm using _**Swinject**_ for Dependency Injection
```swift
import Swinjectclass 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 ;-)**`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`