Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions BookKitty/BookKitty/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,43 @@ import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: - Properties
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SceneDelegate에서 앱 플로우가 시작되었던 것으로 기억하고 있습니다만, 이 코드가 AppDelegate에 작성되어 있는 경우가 궁금합니다~!


var window: UIWindow?
var appCoordinator: AppCoordinator?

// MARK: - Functions

func application(
_: UIApplication,
didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Override point for customization after application launch.
UIFont.registerFonts() // 라이브러리에 사용된 폰트를 프로젝트 내부에서 사용 가능하도록 등록.
// 폰트 등록 및 Firebase 설정
UIFont.registerFonts()
FirebaseApp.configure()

// 윈도우 및 네비게이션 컨트롤러 설정
window = UIWindow(frame: UIScreen.main.bounds)
let navigationController = UINavigationController()

// 온보딩 완료 여부 확인
let isOnboardingCompleted = UserDefaults.standard.bool(forKey: "isOnboardingCompleted")

if !isOnboardingCompleted {
// 온보딩 화면 표시
let onboardingViewController = OnboardingViewController()
onboardingViewController.onFinish = { [weak self] in
self?.startMainFlow()
}
navigationController.viewControllers = [onboardingViewController]
} else {
// 메인 화면 표시
startMainFlow()
}

window?.rootViewController = navigationController
window?.makeKeyAndVisible()

return true
}

Expand All @@ -28,11 +58,18 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
configurationForConnecting connectingSceneSession: UISceneSession,
options _: UIScene.ConnectionOptions
) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
UISceneConfiguration(
name: "Default Configuration",
sessionRole: connectingSceneSession.role
)
}

private func startMainFlow() {
guard let navigationController = window?.rootViewController as? UINavigationController
else {
return
}
appCoordinator = AppCoordinator(navigationController)
appCoordinator?.start()
}
}
49 changes: 39 additions & 10 deletions BookKitty/BookKitty/App/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,48 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let navigationController = UINavigationController()

window = UIWindow(windowScene: windowScene)

// 개발 중 온보딩 화면을 항상 표시하려면 아래 줄을 활성화
UserDefaults.standard.removeObject(forKey: "isOnboardingCompleted")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 지워야 할 듯 합니다!

UserDefaults.standard.set(false, forKey: "isOnboardingCompleted")

// 온보딩 완료 여부 확인
let isOnboardingCompleted = UserDefaults.standard.bool(forKey: "isOnboardingCompleted")

if !isOnboardingCompleted {
// 온보딩 화면 표시
let onboardingViewController = OnboardingViewController()
onboardingViewController.onFinish = { [weak self] in
UserDefaults.standard.set(true, forKey: "isOnboardingCompleted")
self?.startMainFlow()
}
navigationController.viewControllers = [onboardingViewController]
} else {
// 메인 화면 표시
startMainFlow(with: navigationController)
}

window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}

appCoordinator = AppCoordinator(navigationController)
appCoordinator?.start()
private func startMainFlow(with navigationController: UINavigationController? = nil) {
let navController = navigationController ??
(window?.rootViewController as? UINavigationController)
guard let nav = navController else {
return
}

// 최상위 계층(AppDelegate 또는 SceneDelegate)에서 모든 의존성을 생성 및 주입
// 추후 DI Container를 활용, 의존성 생성 책임을 분리
// let persistence = BookCoreDataManager()
// let repository = DefaultBookRepository(bookPersistence: persistence)
// let service = AddBookService(bookRepository: repository)
// let viewModel = GuideViewModel(addBookService: service, bookRepository:
// repository)
// GuideViewController(viewModel: viewModel)
appCoordinator = AppCoordinator(nav)
appCoordinator?.start()
}

// 최상위 계층(AppDelegate 또는 SceneDelegate)에서 모든 의존성을 생성 및 주입
// 추후 DI Container를 활용, 의존성 생성 책임을 분리
// let persistence = BookCoreDataManager()
// let repository = DefaultBookRepository(bookPersistence: persistence)
// let service = AddBookService(bookRepository: repository)
// let viewModel = GuideViewModel(addBookService: service, bookRepository:
// repository)
// GuideViewController(viewModel: viewModel)
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ public final class TextExtractionService: TextExtractable {

private func correctKoreanText(_ text: String) -> String {
let replacements: [String: String] = [
"뛌끼": "뜨끼", "햐야": "해야", "따름": "따름",
"뛌끼": "뜨끼", "햐야": "해야", "따릉": "따름",
"(\\b\\w+\\b) \\1": "$1", // 반복 단어 제거
"(?<=[가-힣])\\s+(?=[을를이가])": "", // 조사 띄어쓰기 교정
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"data" : [
{
"filename" : "Tutorial 1.json",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"data" : [
{
"filename" : "Tutorial 2.json",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"data" : [
{
"filename" : "Tutorial 3.json",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"data" : [
{
"filename" : "Tutorial 4.json",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//
// OnboardingViewController.swift
// BookKitty
//
// Created by 반성준 on 2/25/25.
//

import DesignSystem
import Lottie
import UIKit

class OnboardingViewController: UIViewController {
// MARK: - Properties

var onFinish: (() -> Void)?

private let scrollView = UIScrollView()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수직 스크롤은 비활성화해야 할 것 같아요!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

디자인에서도 스크롤을 없도록 애니메이션도 만들었던 거라 아마 필요없을 꺼라 생각합니다.

private let pageControl = UIStackView()
private let nextButton = UIButton(type: .system)
private let viewModel = OnboardingViewModel()
private var pageIndicators: [UIView] = []

// MARK: - Lifecycle

override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
viewModel.loadTutorials()
}

// MARK: - Functions

private func setupUI() {
view.backgroundColor = .white

// ✅ DesignSystem에서 색상 가져오기
let brandSubColor = Colors.brandSub
let brandSub2Color = Colors.brandSub2

// ScrollView 설정
scrollView.isPagingEnabled = true
scrollView.showsHorizontalScrollIndicator = false
scrollView.delegate = self

// PageControl (원형 Indicator)
pageControl.axis = .horizontal
pageControl.alignment = .center
pageControl.distribution = .fillEqually
pageControl.spacing = 10

for i in 0 ..< 4 {
let dot = UIView()
dot.backgroundColor = i == 0 ? brandSubColor : brandSub2Color
dot.layer.cornerRadius = 6 // ✅ 원형 유지 (12 × 12)
dot.clipsToBounds = true
dot.translatesAutoresizingMaskIntoConstraints = false
pageControl.addArrangedSubview(dot)
pageIndicators.append(dot)

NSLayoutConstraint.activate([
dot.widthAnchor.constraint(equalToConstant: 12),
dot.heightAnchor.constraint(equalToConstant: 12),
])
}

// "다음" 버튼 설정
nextButton.setTitle("다음", for: .normal)
nextButton.backgroundColor = brandSubColor
nextButton.setTitleColor(.white, for: .normal)
nextButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
nextButton.layer.cornerRadius = 10
nextButton.translatesAutoresizingMaskIntoConstraints = false
nextButton.addTarget(self, action: #selector(nextButtonTapped), for: .touchUpInside)

// ✅ 계층 구조 추가
view.addSubview(scrollView)
view.addSubview(nextButton)
view.addSubview(pageControl)

// ✅ 버튼과 원형 인디케이터를 최상위로 올리기
view.bringSubviewToFront(nextButton)
view.bringSubviewToFront(pageControl)

scrollView.translatesAutoresizingMaskIntoConstraints = false
pageControl.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),

// ✅ 원형 인디케이터 높이 추가 (보이도록)
pageControl.bottomAnchor.constraint(equalTo: nextButton.topAnchor, constant: -30),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pageControl.widthAnchor.constraint(equalToConstant: 80),
pageControl.heightAnchor.constraint(equalToConstant: 12), // ✅ 높이 추가

nextButton.bottomAnchor.constraint(
equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: -40
), // ✅ 더 아래로 배치
nextButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
nextButton.widthAnchor.constraint(equalToConstant: 354),
nextButton.heightAnchor.constraint(equalToConstant: 48),
])
}

private func bindViewModel() {
viewModel.onTutorialsLoaded = { [weak self] in
self?.createTutorialViews()
}
}

private func createTutorialViews() {
for (index, tutorial) in viewModel.tutorials.enumerated() {
let pageView = UIView()

let animationView = LottieAnimationView()
animationView.animation = LottieAnimation.named(tutorial.fileName)
animationView.loopMode = .loop
animationView.play()

pageView.addSubview(animationView)

animationView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
animationView.centerXAnchor.constraint(equalTo: pageView.centerXAnchor),
animationView.topAnchor.constraint(equalTo: pageView.topAnchor, constant: 80),
animationView.widthAnchor.constraint(
equalTo: pageView.widthAnchor,
multiplier: 1.0
),
animationView.heightAnchor.constraint(equalToConstant: 523),
])

scrollView.addSubview(pageView)
pageView.frame = CGRect(
x: CGFloat(index) * view.frame.width,
y: 0,
width: view.frame.width,
height: view.frame.height
)
}

scrollView.contentSize = CGSize(
width: view.frame.width * CGFloat(viewModel.tutorials.count),
height: view.frame.height
)

updatePageControl()
}

/// ✅ "다음" 버튼을 눌렀을 때 페이지 이동
@objc
private func nextButtonTapped() {
let brandSubColor = Colors.brandSub

let currentPage = Int(scrollView.contentOffset.x / view.frame.width)
let nextPage = currentPage + 1

if nextPage < viewModel.tutorials.count {
let offset = CGPoint(x: CGFloat(nextPage) * view.frame.width, y: 0)
scrollView.setContentOffset(offset, animated: true)
} else {
// ✅ 마지막 페이지에서는 온보딩 완료 처리
UserDefaults.standard.set(true, forKey: "isOnboardingCompleted")
onFinish?()
}
}

private func updatePageControl() {
let brandSubColor = Colors.brandSub
let brandSub2Color = Colors.brandSub2

for (index, dot) in pageIndicators.enumerated() {
dot.backgroundColor = index == Int(scrollView.contentOffset.x / view.frame.width) ?
brandSubColor : brandSub2Color
}
}
}

extension OnboardingViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_: UIScrollView) {
updatePageControl()
}
}
Loading