Giter VIP home page Giter VIP logo

uwaisalqadri / giphygif Goto Github PK

View Code? Open in Web Editor NEW
21.0 1.0 3.0 6.15 MB

Giphy Client App build with The Composable Architecture, Coordinator Pattern, XcodeGen, and Generic Protocol

Home Page: https://medium.com/@uwaisalqadri/the-composable-architecture-tca-in-a-nutshell-3c574708542c

Swift 100.00%
swinject protocol-oriented-programming swiftui modularization composable-architecture coredata xcodegen

giphygif's Introduction

GiphyGIF


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, Coordinator Pattern, Beautiful UI built with SwiftUI, Clean Architecture with Generic Protocol Approach, SPM Modularization and XcodeGen!

Module

  • GiphyGIF: the main app with presentation layer
  • Giphy: domain and data layer
  • Common: common utils and assets
  • Core: generic protocol for DataSource and Interactor

Table of Contents

๐Ÿฆพ Features

  • Sharing, Copy-Pasting, and AirDropping GIFs and Stickers
  • Search GIFs
  • 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!

๐Ÿ’ก Libraries

๐Ÿ’จ TCA: Reducer, Action, State, and Store

Define your screen's State and Action

 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

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<Self> {
    Reduce<State, Action> { 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

struct MainTabView: View {
  let store: StoreOf<MainTabReducer>
  
  var body: some View {
    WithViewStore(store, observe: \.selectedTab) { viewStore in
      ZStack {
        switch viewStore.state {
        case .home:
          AppCoordinatorView(
            coordinator: store.scope(
              state: \.homeTab,
              action: { .homeTab($0) }
            )
          )
        case .search:
          AppCoordinatorView(
            coordinator: store.scope(
              state: \.searchTab,
              action: { .searchTab($0) }
            )
          )
        }

        VStack {
          Spacer()
          TabView(currentTab: viewStore.binding(send: MainTabReducer.Action.selectedTabChanged))
            .padding(.bottom, 20)
        }
      }
    }
  }
}

"consistent and understandable" - Point-Free

Let your Store(d) Reducer update the View

struct FavoriteView: View {
  let store: StoreOf<FavoriteReducer>
  
  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

โš™๏ธ Navigation Between Screens Done with Coordinator Pattern supported by TCACoodinators!

Screenshot 2023-10-17 at 7 19 53 PM
struct AppCoordinatorView: View {
  let coordinator: StoreOf<AppCoordinator>
  
  var body: some View {
    TCARouter(coordinator) { screen in
      SwitchStore(screen) { screen in
        switch screen {
        case .detail:
          CaseLet(
            /AppScreen.State.detail,
             action: AppScreen.Action.detail,
             then: DetailView.init
          )
        case .favorite:
          CaseLet(
            /AppScreen.State.favorite,
             action: AppScreen.Action.favorite,
             then: FavoriteView.init
          )
        case .home:
          CaseLet(
            /AppScreen.State.home,
             action: AppScreen.Action.home,
             then: HomeView.init
          )
        case .search:
          CaseLet(
            /AppScreen.State.search,
             action: AppScreen.Action.search,
             then: SearchView.init
          )
        }
      }
    }
  }
}
public struct AppScreen: Reducer {
  public enum State: Equatable {
    case detail(DetailReducer.State)
    case favorite(FavoriteReducer.State)
    case home(HomeReducer.State)
    case search(SearchReducer.State)
  }
  
  public enum Action {
    case detail(DetailReducer.Action)
    case favorite(FavoriteReducer.Action)
    case home(HomeReducer.Action)
    case search(SearchReducer.Action)
  }
  
  public var body: some ReducerOf<Self> {
    Scope(state: /State.detail, action: /Action.detail) {
      DetailReducer(checkUseCase: Injection.shared.resolve(), addUseCase: Injection.shared.resolve(), removeUseCase: Injection.shared.resolve())
    }
    
    Scope(state: /State.favorite, action: /Action.favorite) {
      FavoriteReducer(useCase: Injection.shared.resolve(), removeUseCase: Injection.shared.resolve())
    }
    
    Scope(state: /State.home, action: /Action.home) {
      HomeReducer(useCase: Injection.shared.resolve())
    }
    
    Scope(state: /State.search, action: /Action.search) {
      SearchReducer(useCase: Injection.shared.resolve())
    }
  }
}
public struct AppCoordinator: Reducer {
  public struct State: Equatable, IndexedRouterState {
    public static let rootHomeState = AppCoordinator.State(
      routes: [.root(.home(.init()), embedInNavigationView: true)]
    )
    
    public static let rootSearchState = AppCoordinator.State(
      routes: [.root(.search(.init()), embedInNavigationView: true)]
    )
    
    public var routes: [Route<AppScreen.State>]
  }
  
  public enum Action: IndexedRouterAction {
    case routeAction(Int, action: AppScreen.Action)
    case updateRoutes([Route<AppScreen.State>])
  }
  
  public var body: some ReducerOf<Self> {
    Reduce<State, Action> { state, action in
      switch action {
      case let .routeAction(_, action: .home(.showDetail(item))):
        state.routes.presentSheet(.detail(.init(item: item)))
        
      case .routeAction(_, action: .home(.openFavorite)):
        state.routes.push(.favorite(.init()))
        
      case let .routeAction(_, action: .search(.showDetail(item))):
        state.routes.presentSheet(.detail(.init(item: item)))
        
      case .routeAction(_, action: .search(.openFavorite)):
        state.routes.push(.favorite(.init()))
        
      case let .routeAction(_, action: .favorite(.showDetail(item))):
        state.routes.presentSheet(.detail(.init(item: item)))
        
      default:
        break
      }
      
      return .none
      
    }.forEachRoute {
      AppScreen()
    }
  }
}

๐Ÿš€ Dependency Injection

Here i'm using Swinject for Dependency Injection

import Swinject

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

  init() {
    registerFavoriteFeature()
  }

  . . . .

  private func registerFavoriteFeature() {
    container.register(FavoriteView.self) { [unowned self] _ in
      FavoriteView(holder: self.resolve(), router: self.resolve(), store: self.resolve())
    }
    
    container.register(StoreOf<FavoriteReducer>.self) { [unowned self] _ in
      Store(initialState: FavoriteReducer.State()) {
        FavoriteReducer(useCase: self.resolve(), removeUseCase: self.resolve())
      }
    }

    . . . .
  }

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

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

Read more about Swinject

โ˜•๏ธ Buy Me a Coffee

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

๐Ÿ› Project Structure

GiphyGIF:

  • Dependency

  • App

  • Module

    • Home
    • Detail
    • Favorite
    • Search
  • **GiphyWidget**

Modules:

Giphy:

  • Data
    • API
    • DB
    • DataSource
      • Local
      • Remote
    • Entity
    • Repository
  • Domain
    • Model
    • Mapper

Common:

  • Assets
  • Extensions
  • Modifier
  • Utils

Core:

  • DataSource
  • Extension
  • Repository
  • UseCase

giphygif's People

Contributors

uwais-nbs avatar uwaisalqadri avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.