Skip to content

Latest commit

 

History

History
343 lines (273 loc) · 9.03 KB

README.md

File metadata and controls

343 lines (273 loc) · 9.03 KB

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

🦾 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!

💡 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
      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

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

🚀 Dependency Injection

Here i'm using Swinject for Dependency Injection

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<SearchRemoteDataSource>.self) { [unowned self] _ in
      SearchGiphyRepository(remoteDataSource: self.resolve())
    }
    container.register(SearchRemoteDataSource.self) { _ in
      SearchRemoteDataSource()
    }
  }

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

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

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

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

  private 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
  }

  private 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
  }
}

Usage:

Injection.resolve()

Read more about 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:

  • DataSource
  • Extension
  • Repository
  • UseCase