-
Notifications
You must be signed in to change notification settings - Fork 0
feat : onboarding view #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,19 +26,48 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { | |
| let navigationController = UINavigationController() | ||
|
|
||
| window = UIWindow(windowScene: windowScene) | ||
|
|
||
| // 개발 중 온보딩 화면을 항상 표시하려면 아래 줄을 활성화 | ||
| UserDefaults.standard.removeObject(forKey: "isOnboardingCompleted") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
| @@ -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() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수직 스크롤은 비활성화해야 할 것 같아요!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SceneDelegate에서 앱 플로우가 시작되었던 것으로 기억하고 있습니다만, 이 코드가 AppDelegate에 작성되어 있는 경우가 궁금합니다~!