Skip to content

Commit 5ebc9d5

Browse files
committed
feat: Feed Demo 앱 개발
1 parent 33135b9 commit 5ebc9d5

9 files changed

Lines changed: 550 additions & 11 deletions
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//
2+
// FeedDemoMainContainerViewController.swift
3+
// FeedDemo
4+
//
5+
// Created by 김동준 on 7/22/26.
6+
//
7+
8+
import DesignSystem
9+
import UIKit
10+
11+
final class FeedDemoMainContainerViewController: UIViewController {
12+
private let feedViewController: UIViewController
13+
private let navigationBar = MainNavigationBar()
14+
private let contentContainerView = UIView()
15+
private let tabBarPlaceholderView = UIView()
16+
private let tabBarDividerView = UIView()
17+
private let tabBarStackView = UIStackView()
18+
private var tabBarHeightConstraint: NSLayoutConstraint?
19+
20+
init(feedViewController: UIViewController) {
21+
self.feedViewController = feedViewController
22+
super.init(nibName: nil, bundle: nil)
23+
}
24+
25+
@available(*, unavailable)
26+
required init?(coder: NSCoder) {
27+
fatalError("init(coder:) has not been implemented")
28+
}
29+
30+
override func viewDidLoad() {
31+
super.viewDidLoad()
32+
33+
setupUI()
34+
setupLayout()
35+
setupChild()
36+
}
37+
38+
override func viewSafeAreaInsetsDidChange() {
39+
super.viewSafeAreaInsetsDidChange()
40+
41+
tabBarHeightConstraint?.constant = 48 + view.safeAreaInsets.bottom
42+
}
43+
}
44+
45+
private extension FeedDemoMainContainerViewController {
46+
func setupUI() {
47+
view.backgroundColor = .systemWhite
48+
contentContainerView.backgroundColor = .systemWhite
49+
50+
tabBarPlaceholderView.backgroundColor = .systemWhite
51+
tabBarDividerView.backgroundColor = .gray2
52+
53+
tabBarStackView.axis = .horizontal
54+
tabBarStackView.alignment = .fill
55+
tabBarStackView.distribution = .fillEqually
56+
57+
["피드", "챌린지", "마이"].enumerated().forEach { index, title in
58+
tabBarStackView.addArrangedSubview(
59+
makeTabItem(title: title, isSelected: index == 0)
60+
)
61+
}
62+
}
63+
64+
func setupLayout() {
65+
[navigationBar, contentContainerView, tabBarPlaceholderView].forEach {
66+
$0.translatesAutoresizingMaskIntoConstraints = false
67+
view.addSubview($0)
68+
}
69+
70+
[tabBarDividerView, tabBarStackView].forEach {
71+
$0.translatesAutoresizingMaskIntoConstraints = false
72+
tabBarPlaceholderView.addSubview($0)
73+
}
74+
75+
let tabBarHeightConstraint = tabBarPlaceholderView.heightAnchor.constraint(
76+
equalToConstant: 48 + view.safeAreaInsets.bottom
77+
)
78+
self.tabBarHeightConstraint = tabBarHeightConstraint
79+
80+
NSLayoutConstraint.activate([
81+
navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
82+
navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
83+
navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
84+
85+
contentContainerView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor),
86+
contentContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
87+
contentContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
88+
contentContainerView.bottomAnchor.constraint(equalTo: tabBarPlaceholderView.topAnchor),
89+
90+
tabBarPlaceholderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
91+
tabBarPlaceholderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
92+
tabBarPlaceholderView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
93+
tabBarHeightConstraint,
94+
95+
tabBarDividerView.topAnchor.constraint(equalTo: tabBarPlaceholderView.topAnchor),
96+
tabBarDividerView.leadingAnchor.constraint(equalTo: tabBarPlaceholderView.leadingAnchor),
97+
tabBarDividerView.trailingAnchor.constraint(equalTo: tabBarPlaceholderView.trailingAnchor),
98+
tabBarDividerView.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale),
99+
100+
tabBarStackView.topAnchor.constraint(equalTo: tabBarDividerView.bottomAnchor),
101+
tabBarStackView.leadingAnchor.constraint(equalTo: tabBarPlaceholderView.leadingAnchor),
102+
tabBarStackView.trailingAnchor.constraint(equalTo: tabBarPlaceholderView.trailingAnchor),
103+
tabBarStackView.heightAnchor.constraint(equalToConstant: 48)
104+
])
105+
}
106+
107+
func setupChild() {
108+
addChild(feedViewController)
109+
feedViewController.view.translatesAutoresizingMaskIntoConstraints = false
110+
contentContainerView.addSubview(feedViewController.view)
111+
112+
NSLayoutConstraint.activate([
113+
feedViewController.view.topAnchor.constraint(equalTo: contentContainerView.topAnchor),
114+
feedViewController.view.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor),
115+
feedViewController.view.trailingAnchor.constraint(equalTo: contentContainerView.trailingAnchor),
116+
feedViewController.view.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor)
117+
])
118+
119+
feedViewController.didMove(toParent: self)
120+
}
121+
122+
func makeTabItem(title: String, isSelected: Bool) -> UIView {
123+
let label = UILabel()
124+
label.text = title
125+
label.textAlignment = .center
126+
label.font = .systemFont(ofSize: 12, weight: isSelected ? .semibold : .regular)
127+
label.textColor = isSelected ? .gray10 : .gray5
128+
return label
129+
}
130+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// FeedDemoCoordinator.swift
3+
// FeedDemo
4+
//
5+
// Created by 김동준 on 7/22/26.
6+
//
7+
8+
import FeedInterface
9+
import UIKit
10+
11+
@MainActor
12+
final class FeedDemoCoordinator: ObservableObject {
13+
private let dependencyContainer: FeedDemoDependencyContainer
14+
private lazy var feedBuildable = dependencyContainer.makeFeedBuildable()
15+
private let navigationController = UINavigationController()
16+
private weak var feedInputHandler: FeedInputHandler?
17+
18+
init(dependencyContainer: FeedDemoDependencyContainer = FeedDemoDependencyContainer()) {
19+
self.dependencyContainer = dependencyContainer
20+
}
21+
22+
func makeRootViewController() -> UINavigationController {
23+
let feedViewController = feedBuildable.makeFeedViewController(router: self)
24+
feedInputHandler = feedViewController as? FeedInputHandler
25+
let mainContainerViewController = FeedDemoMainContainerViewController(
26+
feedViewController: feedViewController
27+
)
28+
29+
navigationController.setViewControllers([mainContainerViewController], animated: false)
30+
navigationController.navigationBar.isHidden = true
31+
return navigationController
32+
}
33+
}
34+
35+
extension FeedDemoCoordinator: FeedRouter {
36+
func route(from route: FeedRoute) {
37+
switch route {
38+
case .addGroup:
39+
let alert = UIAlertController(
40+
title: "FeedDemo",
41+
message: "그룹 추가는 Demo에서 mock 처리됩니다.",
42+
preferredStyle: .alert
43+
)
44+
alert.addAction(UIAlertAction(title: "확인", style: .default))
45+
navigationController.present(alert, animated: true)
46+
case let .routeToRecord(recordType):
47+
let viewController = feedBuildable.makeFeedRecordViewController(
48+
router: self,
49+
recordType: recordType,
50+
outputHandler: self
51+
)
52+
viewController.hidesBottomBarWhenPushed = true
53+
navigationController.pushViewController(viewController, animated: true)
54+
}
55+
}
56+
}
57+
58+
extension FeedDemoCoordinator: FeedRecordRouter {
59+
func route(from route: FeedRecordRoute) {
60+
switch route {
61+
case .back:
62+
navigationController.popViewController(animated: true)
63+
}
64+
}
65+
}
66+
67+
extension FeedDemoCoordinator: FeedRecordOutputHandler {
68+
func handle(output: FeedRecordOutput) {
69+
switch output {
70+
case .recordCreated:
71+
navigationController.popViewController(animated: true)
72+
73+
guard let coordinator = navigationController.transitionCoordinator else {
74+
feedInputHandler?.handle(input: .recordCreated)
75+
return
76+
}
77+
78+
coordinator.animate(alongsideTransition: nil) { [weak self] _ in
79+
self?.feedInputHandler?.handle(input: .recordCreated)
80+
}
81+
}
82+
}
83+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//
2+
// FeedDemoDependencyContainer.swift
3+
// FeedDemo
4+
//
5+
// Created by 김동준 on 7/22/26.
6+
//
7+
8+
import CoreAuthInterface
9+
import CoreCamera
10+
import CoreCameraInterface
11+
import Feed
12+
import FeedInterface
13+
import FeedTesting
14+
import ModyGroupInterface
15+
16+
final class FeedDemoDependencyContainer {
17+
private let feedRepository: FeedRepositoryProtocol
18+
private let authUseCase: AuthUseCaseProtocol
19+
private let groupUseCase: GroupUseCaseProtocol
20+
private let cameraCaptureBuilder: CameraCaptureBuildable
21+
22+
init(
23+
feedRepository: FeedRepositoryProtocol = FeedMockRepository(),
24+
authUseCase: AuthUseCaseProtocol = FeedDemoAuthUseCase(),
25+
groupUseCase: GroupUseCaseProtocol = FeedDemoGroupUseCase(),
26+
cameraCaptureBuilder: CameraCaptureBuildable = CameraCaptureBuilder()
27+
) {
28+
self.feedRepository = feedRepository
29+
self.authUseCase = authUseCase
30+
self.groupUseCase = groupUseCase
31+
self.cameraCaptureBuilder = cameraCaptureBuilder
32+
}
33+
34+
func makeFeedBuildable() -> FeedBuildable {
35+
let feedUseCase = FeedUseCase(feedRepository: feedRepository)
36+
37+
return FeedBuilder(
38+
makeFeedReactor: { [authUseCase, groupUseCase] router in
39+
FeedReactor(
40+
authUseCase: authUseCase,
41+
groupUseCase: groupUseCase,
42+
feedUseCase: feedUseCase,
43+
router: router
44+
)
45+
},
46+
makeFeedRecordReactor: { router, recordType, outputHandler in
47+
FeedRecordReactor(
48+
router: router,
49+
recordType: recordType,
50+
feedUseCase: feedUseCase,
51+
outputHandler: outputHandler
52+
)
53+
},
54+
cameraCaptureBuilder: cameraCaptureBuilder
55+
)
56+
}
57+
}

Projects/Feature/Feed/Demo/Sources/DefaultDemoCode.swift

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// FeedDemoApp.swift
3+
// FeedDemo
4+
//
5+
// Created by 김동준 on 7/22/26.
6+
//
7+
8+
import SwiftUI
9+
10+
@main
11+
struct FeedDemoApp: App {
12+
@StateObject private var coordinator = FeedDemoCoordinator()
13+
14+
var body: some Scene {
15+
WindowGroup {
16+
FeedDemoRootView(coordinator: coordinator)
17+
.ignoresSafeArea()
18+
}
19+
}
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// FeedDemoRootView.swift
3+
// FeedDemo
4+
//
5+
// Created by 김동준 on 7/22/26.
6+
//
7+
8+
import SwiftUI
9+
import UIKit
10+
11+
struct FeedDemoRootView: UIViewControllerRepresentable {
12+
let coordinator: FeedDemoCoordinator
13+
14+
func makeUIViewController(context: Context) -> UINavigationController {
15+
coordinator.makeRootViewController()
16+
}
17+
18+
func updateUIViewController(
19+
_ uiViewController: UINavigationController,
20+
context: Context
21+
) {}
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// FeedDemoAuthUseCase.swift
3+
// FeedDemo
4+
//
5+
// Created by 김동준 on 7/22/26.
6+
//
7+
8+
import CommonDomain
9+
import CoreAuthInterface
10+
11+
struct FeedDemoAuthUseCase: AuthUseCaseProtocol {
12+
func signIn(
13+
loginType: SocialLoginType,
14+
accessToken: String
15+
) async throws -> AuthSession {
16+
throw CancellationError()
17+
}
18+
19+
func getUserInfo(needUpdateKeyChain: Bool) async throws -> UserInfo {
20+
try await Task.sleep(nanoseconds: 2_000_000_000)
21+
22+
return UserInfo(
23+
memberId: 4537058591744,
24+
nickname: "다함께빡빡빡",
25+
profileImageUrl: "",
26+
daysTogether: 1,
27+
personalInfoCompleted: true,
28+
groupOnboardingCompleted: true,
29+
mainAccessible: true
30+
)
31+
}
32+
33+
func logout() async throws {}
34+
35+
func deleteAccount() async throws {}
36+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// FeedDemoGroupUseCase.swift
3+
// FeedDemo
4+
//
5+
// Created by 김동준 on 7/22/26.
6+
//
7+
8+
import CommonDomain
9+
import ModyGroupInterface
10+
11+
struct FeedDemoGroupUseCase: GroupUseCaseProtocol {
12+
func createGroup(name: String) async throws -> String {
13+
"DEMO"
14+
}
15+
16+
func joinGroup(code: String) async throws {}
17+
18+
func getGroups() async throws -> [GroupModel] {
19+
try await Task.sleep(nanoseconds: 2_000_000_000)
20+
21+
return [
22+
GroupModel(
23+
groupId: 4537058591744,
24+
name: "Feed Demo",
25+
code: "DEMO",
26+
memberCount: 1
27+
)
28+
]
29+
}
30+
31+
func exitGroup(groupId: Int) async throws {}
32+
}

0 commit comments

Comments
 (0)