This project is a comprehensive infrastructure study that combines Server-Driven UI (SDUI) principles, one of the most advanced techniques in modern iOS development, with a highly scalable Modular VIPER architecture.
The primary goal is to dynamically manage the application's home screen structure, component layouts, and navigation flows through backend-provided metadata (JSON), without the need for a new App Store version.
-
Postgresql
-
Spring Boot
Protocol-Oriented Modular Structure The application is divided into independent modules that communicate entirely via protocols rather than concrete classes. This structure minimizes coupling while enhancing testability:
-
HomeModule: Homepage logic and the SDUI engine.
-
AllListModule: Dynamic listing screens.
-
CommonKit: Shared UI components and utility tools.
-
ManagerKit: Service and dependency management.
-
ModularProtocols: Interface contracts facilitating inter-module communication.
A lightweight and reusable framework for UICollectionView written in Swift.
It provides a generic, type-safe, and highly customizable way to manage collection view data, section headers, delegates, and layouts.
Generic Collection View Kit
The visual layer of the project consists of highly customizable and modular UI components. These components encapsulate their own presentation logic in accordance with SOLID principles.
- CampaingBanner
- CourierView
- RatingAndCommentView
import UIKit
// MARK: - Home Module Protocol
/// Defines the contract for creating the Home module’s view controller.
/// Used to assemble and return the main screen of the application.
public protocol HomeModuleProtocol {
/// Creates and returns the Home module’s main view controller.
/// - Returns: A configured `UIViewController` representing the Home screen.
func createHomeModule() -> UIViewController
}
// MARK: - All List Module Protocol
/// Defines the contract for creating the All List module’s view controller.
/// Used to assemble and return the All List screen of the application.
public protocol AllListModuleProtocol{
/// Creates and returns the All List module’s main view controller.
/// - Returns: A configured `UIViewController` representing the All List screen.
func createAllListModule(id:Int) -> UIViewController
}
import UIKit
import ModularProtocols
import DependencyKit
import CommonKit
/// `HomeRouter` is responsible for navigation in the Home module.
final class HomeRouter: PresenterToRouterHomeProtocol {
// MARK: - Navigation to All List Page
/// Navigates to the All List screen based on the selected section type.
/// - Parameters:
/// - view: The current view conforming to `PresenterToViewHomeProtocol`.
func toAllList(
view: (any PresenterToViewHomeProtocol)?,
id: Int
) {
// Resolve AllList module dependency from DependencyRegister
let allListModule =
DependencyRegister.shared.resolve(
ModularProtocols.AllListModuleProtocol.self
)
// Create the AllList view controller using module
let allListVC = allListModule.createAllListModule(id: id)
view?.pushViewControllerAble(allListVC, animated: true)
}
}import Foundation
// MARK: - NetworkRequest Protocol
/// A protocol that defines the blueprint for building API requests.
/// Each request specifies its endpoint, HTTP method, headers, and parameters.
public protocol NetworkRequest {
/// The expected response type of the request.
/// It must conform to both `Decodable` (for JSON parsing)
associatedtype Response: Decodable
/// The API endpoint associated with this request, defined using `NetworkPath`.
var path: NetworkPath { get }
/// The HTTP method used for the request (e.g., GET, POST, PUT).
var method: AlamofireMethod { get }
/// Optional HTTP headers to be included in the request.
/// Commonly used for authentication tokens or custom headers.
var headers: [String: String]? { get }
/// Optional parameters to send with the request.
/// These can be encoded as query items or JSON depending on the HTTP method.
var parameters: [String: Any]? { get }
}[
{
"id": 1,
"title": "Camping",
"headerImageDTO": null,
"layoutTemplateDTO": {
"layoutTemplate": "card_02",
"scrollDirection": "horizontal",
"groupOrientation": "horizontal"
},
"cellType": "card_image",
"buttonTypes": [
"alllist"
],
"data": [
{
"id": 1,
"url": "https://cdn.tgoapps.com/mnresize/640/-/local-commerce-promotion/mars/prod/banner/f078c002-f7b7-4ac8-ac36-8dd9dfe44d5e.jpg"
}
]
},
{
"id": 2,
"title": "Campaing Restaurant",
"headerImageDTO": {
"id": 1,
"type": "SYSTEM",
"imageText": "tag.fill",
"color": "#FFA500"
},
"layoutTemplateDTO": {
"layoutTemplate": "card_03",
"scrollDirection": "horizontal",
"groupOrientation": "horizontal"
},
"cellType": "card_info",
"buttonTypes": [
"alllist"
],
"data": [
{
"id": 9,
"imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTIVl8bx-sqjynOaPbHDvVZkhafZ0wtmZui2Q&s",
"rating": 4.5,
"reviewCount": 120,
"name": "Burger House",
"minimumPrice": 50.0,
"distanceKm": 1.2,
"category": "Burger",
"deliveryTime": 25,
"deliveryType": "restaurantCourier",
"campaignList": [
"%20 Discount",
"Free Drink"
]
}
]
},
{
"id": 3,
"title": "Kitchens",
"headerImageDTO": null,
"layoutTemplateDTO": {
"layoutTemplate": "card_01",
"scrollDirection": "horizontal",
"groupOrientation": "horizontal"
},
"cellType": "card_image_title",
"buttonTypes": [
"alllist"
],
"data": [
{
"id": 1,
"url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSOX2YyS0W1LnrD5eyd-7Q9kvO1JGvE80AXXA&s",
"title": "Burger"
}
]
},
{
"id": 5,
"title": "Go Kampanyası",
"headerImageDTO": {
"id": 2,
"type": "ASSETS",
"imageText": "go",
"color": null
},
"layoutTemplateDTO": {
"layoutTemplate": "card_03",
"scrollDirection": "horizontal",
"groupOrientation": "horizontal"
},
"cellType": "card_info",
"buttonTypes": [
"alllist"
],
"data": [
{
"id": 13,
"imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTIVl8bx-sqjynOaPbHDvVZkhafZ0wtmZui2Q&s",
"rating": 4.5,
"reviewCount": 120,
"name": "Burger House",
"minimumPrice": 50.0,
"distanceKm": 1.2,
"category": "Burger",
"deliveryTime": 25,
"deliveryType": "restaurantCourier",
"campaignList": [
"%20 Discount",
"Free Drink"
]
}
]
},
{
"id": 4,
"title": "Restaurants",
"headerImageDTO": null,
"layoutTemplateDTO": {
"layoutTemplate": "featured_01",
"scrollDirection": "vertical",
"groupOrientation": "vertical"
},
"cellType": "featured_01_info",
"buttonTypes": [
"View",
"Filter"
],
"data": [
{
"id": 12,
"imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTIVl8bx-sqjynOaPbHDvVZkhafZ0wtmZui2Q&s",
"rating": 4.5,
"reviewCount": 120,
"name": "Burger House",
"minimumPrice": 50.0,
"distanceKm": 1.2,
"category": "Burger",
"deliveryTime": 25,
"deliveryType": "restaurantCourier",
"campaignList": [
"%20 Discount",
"Free Drink",
"1+1 Pizza"
]
}
]
}
]public struct UIModel: Decodable,Sendable {
public let id: Int
public let title: String
public var headerImageDTO : HeaderImage?
public var layoutTemplateDTO: LayoutTemplateInfo
public var cellType: CellType
public let buttonTypes: [TitleForSectionButtonType]
public let data: UISectionData
enum CodingKeys: String, CodingKey {
case id, title, headerImageDTO,layoutTemplateDTO, cellType, buttonTypes, data
}
public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
self.id = try c.decode(Int.self, forKey: .id)
self.title = try c.decode(String.self, forKey: .title)
self.layoutTemplateDTO = try c.decode(LayoutTemplateInfo.self, forKey: .layoutTemplateDTO)
self.headerImageDTO = try c.decodeIfPresent(HeaderImage.self, forKey: .headerImageDTO)
self.cellType = try c.decode(CellType.self, forKey: .cellType)
// buttonTypes → convert
let rawBtns = try c.decode([String].self, forKey: .buttonTypes)
self.buttonTypes = rawBtns.map { TitleForSectionButtonType.convert(rawValue: $0) }
// datas → decode by cellType
self.data = try UIModel.decodeDatas(container: c, type: self.cellType)
}
}// MARK: - Cell Type
// Represents the type of collection view cells
public enum CellType: String, Decodable, Sendable {
// Card that contains only an image
case cardImage = "card_image"
// Card that contains an image and a title
case cardImageTitle = "card_image_title"
// Card that displays restaurant information
case cardInfo = "card_info"
// Featured restaurant info (version 01)
case featured01Info = "featured_01_info"
// Featured restaurant info (version 02)
case featured02Info = "featured_02_info"
// Undefined or empty state
case none
}
// MARK: - Cell Data
// Type-safe representation of data passed to a cell
public enum CellData: Sendable {
// Image-only model
case cardImage(OnlyImageModel)
// Image and title model
case cardImageTitle(ImageAndTitleModel)
// Restaurant info model (standard card)
case cardInfo(RestaurantInfoModel)
// Featured restaurant info (version 01)
case featured01Info(RestaurantInfoModel)
// Featured restaurant info (version 02)
case featured02Info(RestaurantInfoModel)
// No data state
case none
// MARK: - Factory Method
// Converts RestaurantInfoModel into the correct CellData based on CellType
public static func restaurant(_ data: RestaurantInfoModel,type: CellType) -> CellData {
switch type {
case .cardInfo:
return .cardInfo(data)
case .featured01Info:
return .featured01Info(data)
case .featured02Info:
return .featured02Info(data)
default:
// Unsupported or incompatible types
return .none
}
}
} Home.Page.mov |
All.Lists.mov |
View.Change.mp4 |
Reflesh.mp4 |