diff --git a/Tracker.xcodeproj/project.pbxproj b/Tracker.xcodeproj/project.pbxproj index 2491201..b6c0159 100644 --- a/Tracker.xcodeproj/project.pbxproj +++ b/Tracker.xcodeproj/project.pbxproj @@ -44,6 +44,10 @@ 847720D92B6183260063EE66 /* ColorsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847720D82B6183260063EE66 /* ColorsCollectionViewCell.swift */; }; 847720DE2B6184720063EE66 /* TrackerRecordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847720DD2B6184720063EE66 /* TrackerRecordStore.swift */; }; 847720E22B6187A40063EE66 /* TrackerModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 847720E02B6187A40063EE66 /* TrackerModel.xcdatamodeld */; }; + 847720E82B66BA300063EE66 /* OnboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847720E72B66BA300063EE66 /* OnboardViewController.swift */; }; + 847720EA2B66BA520063EE66 /* OnboardContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847720E92B66BA520063EE66 /* OnboardContentViewController.swift */; }; + 847720EC2B66BAC20063EE66 /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847720EB2B66BAC20063EE66 /* ObservableValue.swift */; }; + 847720EE2B66F6480063EE66 /* CategoryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847720ED2B66F6480063EE66 /* CategoryViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -86,6 +90,10 @@ 847720D82B6183260063EE66 /* ColorsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsCollectionViewCell.swift; sourceTree = ""; }; 847720DD2B6184720063EE66 /* TrackerRecordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerRecordStore.swift; sourceTree = ""; }; 847720E12B6187A40063EE66 /* TrackerModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = TrackerModel.xcdatamodel; sourceTree = ""; }; + 847720E72B66BA300063EE66 /* OnboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardViewController.swift; sourceTree = ""; }; + 847720E92B66BA520063EE66 /* OnboardContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardContentViewController.swift; sourceTree = ""; }; + 847720EB2B66BAC20063EE66 /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = ""; }; + 847720ED2B66F6480063EE66 /* CategoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -119,9 +127,10 @@ isa = PBXGroup; children = ( 847720832B6043770063EE66 /* Info.plist */, - 847720C62B6046C00063EE66 /* Helpers */, 847720CD2B617EB30063EE66 /* Resources */, 847720BB2B60466A0063EE66 /* Services */, + 847720E32B66B9D60063EE66 /* Onboard */, + 847720C62B6046C00063EE66 /* Helpers */, 847720DC2B6184600063EE66 /* Domain */, 847720DF2B6187820063EE66 /* CoreData */, 847720D32B617F3E0063EE66 /* StatisticsView */, @@ -175,6 +184,7 @@ 847720C42B6046B80063EE66 /* TextField.swift */, 847720CE2B617F010063EE66 /* UIColorMarshalling.swift */, 847720D02B617F0B0063EE66 /* StoreError.swift */, + 847720EB2B66BAC20063EE66 /* ObservableValue.swift */, ); path = Helpers; sourceTree = ""; @@ -241,6 +251,7 @@ isa = PBXGroup; children = ( 847720AB2B6045F50063EE66 /* CategoryViewController.swift */, + 847720ED2B66F6480063EE66 /* CategoryViewModel.swift */, 8477209F2B6045C10063EE66 /* CreatingCategoryViewController.swift */, ); path = Category; @@ -264,6 +275,15 @@ path = CoreData; sourceTree = ""; }; + 847720E32B66B9D60063EE66 /* Onboard */ = { + isa = PBXGroup; + children = ( + 847720E72B66BA300063EE66 /* OnboardViewController.swift */, + 847720E92B66BA520063EE66 /* OnboardContentViewController.swift */, + ); + path = Onboard; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -335,6 +355,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 847720EA2B66BA520063EE66 /* OnboardContentViewController.swift in Sources */, 847720B52B60462B0063EE66 /* CreatingTrackersModel.swift in Sources */, 847720B32B6046230063EE66 /* TrackerCategory.swift in Sources */, 847720AE2B6045FB0063EE66 /* CreatingIrregularEventViewController.swift in Sources */, @@ -344,12 +365,14 @@ 847720D12B617F0B0063EE66 /* StoreError.swift in Sources */, 847720A42B6045D10063EE66 /* TabBarController.swift in Sources */, 847720CF2B617F010063EE66 /* UIColorMarshalling.swift in Sources */, + 847720EE2B66F6480063EE66 /* CategoryViewModel.swift in Sources */, 847720C32B6046AE0063EE66 /* Date.swift in Sources */, 847720BA2B6046630063EE66 /* DataStorege.swift in Sources */, 847720AA2B6045EE0063EE66 /* CreatingTableCell.swift in Sources */, 847720982B60459C0063EE66 /* SupplementaryView.swift in Sources */, 847720CA2B617E070063EE66 /* TrackerStore.swift in Sources */, 847720A62B6045DF0063EE66 /* CreatingHabitViewController.swift in Sources */, + 847720E82B66BA300063EE66 /* OnboardViewController.swift in Sources */, 8477209E2B6045B50063EE66 /* ScheduleViewController.swift in Sources */, 847720A02B6045C10063EE66 /* CreatingCategoryViewController.swift in Sources */, 847720A82B6045E70063EE66 /* CreatingNewTrackerViewController.swift in Sources */, @@ -366,6 +389,7 @@ 847720B12B60461C0063EE66 /* TrackerRecord.swift in Sources */, 847720AC2B6045F50063EE66 /* CategoryViewController.swift in Sources */, 847720782B6043750063EE66 /* SceneDelegate.swift in Sources */, + 847720EC2B66BAC20063EE66 /* ObservableValue.swift in Sources */, 847720A22B6045C80063EE66 /* TrackersViewController.swift in Sources */, 8477209A2B6045A40063EE66 /* TrackerCell.swift in Sources */, 847720962B6045950063EE66 /* StatisticsViewController.swift in Sources */, diff --git a/Tracker/CreatingNewTracker/Category/CategoryViewController.swift b/Tracker/CreatingNewTracker/Category/CategoryViewController.swift index 2d2b1ac..1880cf9 100644 --- a/Tracker/CreatingNewTracker/Category/CategoryViewController.swift +++ b/Tracker/CreatingNewTracker/Category/CategoryViewController.swift @@ -9,22 +9,27 @@ import UIKit // MARK: - CategoryViewDelegate -protocol CategoryViewDelegate: AnyObject { +protocol CategoryViewModelDelegate: AnyObject { func updateData(nameCategory: String) } // MARK: - CategoryViewController final class CategoryViewController: UIViewController { - weak var delegateHabbit: CreatingHabitViewControllerDelegate? - weak var delegateIrregular: CreatingIrregularEventViewControllerDelegate? - private let dataStorege = DataStorege.shared - private var category = [String]() - private let trackerCategoryStore = TrackerCategoryStore() + var viewModel: CategoryViewModel? // MARK: - UiElements - private var tableView: UITableView = .init() + private var tableView: UITableView = { + let tableView = UITableView() + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CategoryCell") + tableView.layer.masksToBounds = true + tableView.layer.cornerRadius = 16 + tableView.backgroundColor = .none + tableView.separatorInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) + tableView.translatesAutoresizingMaskIntoConstraints = false + return tableView + }() private lazy var habitLabel: UILabel = { let label = UILabel() @@ -38,6 +43,7 @@ final class CategoryViewController: UIViewController { private lazy var mainStarImageStub: UIImageView = { let image = UIImageView(image: UIImage(named: "starIcon")) image.clipsToBounds = true + image.isHidden = true image.contentMode = .scaleAspectFill image.translatesAutoresizingMaskIntoConstraints = false return image @@ -48,6 +54,7 @@ final class CategoryViewController: UIViewController { label.text = "Привычки и события можно\nобъединить по смыслу" label.numberOfLines = 2 label.textColor = .blackDay + label.isHidden = true label.textAlignment = .center label.font = .systemFont(ofSize: 12, weight: .medium) label.translatesAutoresizingMaskIntoConstraints = false @@ -67,6 +74,13 @@ final class CategoryViewController: UIViewController { return button }() + // MARK: - Initialisation + + func initialize(viewModel: CategoryViewModel) { + self.viewModel = viewModel + bind() + } + // MARK: - Lifecycle override func viewDidLoad() { @@ -74,6 +88,20 @@ final class CategoryViewController: UIViewController { configViews() configConstraints() checkForAvailableCategories() + try? viewModel?.fetchCategory() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + guard let categories = viewModel?.categories, + categories.isEmpty else { return } + } + + // MARK: Binding + private func bind() { + viewModel?.$categories.bind(action: { [weak self] _ in + self?.checkForAvailableCategories() + }) } // MARK: - Actions @@ -89,31 +117,28 @@ final class CategoryViewController: UIViewController { // MARK: - Private methods private func checkForAvailableCategories() { - try? fetchCategory() tableView.reloadData() + guard let category = viewModel?.categories else { return } if !category.isEmpty { - configTableView() + placeholderDisplaySwitch(isHiden: true) configThereAreCategories() } else { - configForCreateCategory() + placeholderDisplaySwitch(isHiden: false) } } - private func configTableView() { - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CategoryCell") - tableView.delegate = self - tableView.dataSource = self - //tableView.separatorColor = .backgroundDay - tableView.layer.cornerRadius = 16 - tableView.backgroundColor = .none - tableView.layer.masksToBounds = true - tableView.separatorInset = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20) - tableView.translatesAutoresizingMaskIntoConstraints = false + private func placeholderDisplaySwitch(isHiden: Bool) { + mainStarImageStub.isHidden = isHiden + descriptionPlaceholderStub.isHidden = isHiden } private func configViews() { + tableView.delegate = self + tableView.dataSource = self view.backgroundColor = .whiteDay view.addSubview(habitLabel) + view.addSubview(mainStarImageStub) + view.addSubview(descriptionPlaceholderStub) view.addSubview(creatingHabitButton) } @@ -121,6 +146,12 @@ final class CategoryViewController: UIViewController { NSLayoutConstraint.activate([ habitLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), habitLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 27), + mainStarImageStub.widthAnchor.constraint(equalToConstant: 80), + mainStarImageStub.heightAnchor.constraint(equalToConstant: 80), + mainStarImageStub.centerXAnchor.constraint(equalTo: view.centerXAnchor), + mainStarImageStub.centerYAnchor.constraint(equalTo: view.centerYAnchor), + descriptionPlaceholderStub.centerXAnchor.constraint(equalTo: view.centerXAnchor), + descriptionPlaceholderStub.topAnchor.constraint(equalTo: mainStarImageStub.bottomAnchor, constant: 8), creatingHabitButton.heightAnchor.constraint(equalToConstant: 60), creatingHabitButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), creatingHabitButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), @@ -137,66 +168,20 @@ final class CategoryViewController: UIViewController { tableView.bottomAnchor.constraint(equalTo: creatingHabitButton.topAnchor, constant: -38) ]) } - - private func configForCreateCategory() { - view.addSubview(mainStarImageStub) - view.addSubview(descriptionPlaceholderStub) - NSLayoutConstraint.activate([ - mainStarImageStub.widthAnchor.constraint(equalToConstant: 80), - mainStarImageStub.heightAnchor.constraint(equalToConstant: 80), - mainStarImageStub.centerXAnchor.constraint(equalTo: view.centerXAnchor), - mainStarImageStub.centerYAnchor.constraint(equalTo: view.centerYAnchor), - descriptionPlaceholderStub.centerXAnchor.constraint(equalTo: view.centerXAnchor), - descriptionPlaceholderStub.topAnchor.constraint(equalTo: mainStarImageStub.bottomAnchor, constant: 8) - ]) - } - - private func roundingForCellsInATable(cellIndex: Int, numberOfLines: Int) -> CACornerMask { - switch (cellIndex, numberOfLines) { - case (0, 1): - return [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] - case (0, _): - return [.layerMinXMinYCorner, .layerMaxXMinYCorner] - case (_, _) where cellIndex == numberOfLines - 1: - return [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] - default: - return [] - } - } - - private func separatorInsetForCell(index: Int) -> UIEdgeInsets { - let quantityCategory = category.count - 1 - if index == quantityCategory { - return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .greatestFiniteMagnitude) - } else { - return UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) - } - } -} - -// MARK: - CategoryViewDelegate - -extension CategoryViewController: CategoryViewDelegate { - func updateData(nameCategory: String) { - try? createCategory(nameOfCategory: nameCategory) - checkForAvailableCategories() - tableView.reloadData() - } } // MARK: - UITableViewDelegate extension CategoryViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let editingIndexPath = dataStorege.loadIndexPathForCheckmark() { + if let editingIndexPath = viewModel?.loadIndexPathForCheckmark() { let previousSelectedCell = tableView.cellForRow(at: editingIndexPath) previousSelectedCell?.accessoryType = .none } let cell = tableView.cellForRow(at: indexPath) cell?.accessoryType = .checkmark - dataStorege.saveIndexPathForCheckmark(indexPath) - delegateIrregular?.updateSubitle(nameSubitle: category[indexPath.row]) - delegateHabbit?.updateSubitle(nameSubitle: category[indexPath.row]) + viewModel?.selectedCategoryForCheckmark(indexPath) + viewModel?.addingCategoryToCreate(indexPath) tableView.deselectRow(at: indexPath, animated: true) dismiss(animated: true) } @@ -208,7 +193,7 @@ extension CategoryViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { let deleteAction = UIAction(title: "Удалить", attributes: .destructive) { [weak self] _ in guard let self = self else { return } - try? self.removeCategory(atIndex: indexPath.row) + try? self.viewModel?.removeCategory(atIndex: indexPath.row) self.checkForAvailableCategories() } let deleteMenu = UIMenu(title: "", children: [deleteAction]) @@ -222,59 +207,56 @@ extension CategoryViewController: UITableViewDelegate { extension CategoryViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return category.count + guard let count = viewModel?.categoriesCount() else { return 0 } + return count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CategoryCell", for: indexPath) - let categoryName = category[indexPath.row] - cell.textLabel?.text = categoryName + guard let count = viewModel?.categoriesCount() else { return UITableViewCell() } + cell.textLabel?.text = viewModel?.categories[indexPath.row].title cell.textLabel?.textColor = .blackDay cell.backgroundColor = .backgroundDay cell.layer.masksToBounds = true - cell.layer.cornerRadius = 10 - cell.separatorInset = separatorInsetForCell(index: indexPath.row) - cell.layer.maskedCorners = roundingForCellsInATable(cellIndex: indexPath.row, numberOfLines: category.count) - cell.accessoryType = indexPath == dataStorege.loadIndexPathForCheckmark() ? .checkmark : .none + cell.layer.cornerRadius = 16 + cell.separatorInset = separatorInsetForCell(index: indexPath.row, numberOfLines: count) + cell.layer.maskedCorners = roundingForCellsInATable(cellIndex: indexPath.row, numberOfLines: count) + cell.accessoryType = indexPath == viewModel?.loadIndexPathForCheckmark() ? .checkmark : .none return cell } } -// MARK: - CategoryStore +// MARK: - ConfigForCell extension CategoryViewController { - private func fetchCategory() throws { - do { - let categories = try trackerCategoryStore.fetchAllCategories() - category = categories.compactMap { $0.titleCategory } - } catch { - throw StoreError.failedReading - } - } - - private func createCategory(nameOfCategory: String) throws { - do { - let newCategory = TrackerCategory(title: nameOfCategory, trackers: []) - try trackerCategoryStore.createCategory(newCategory) - } catch { - throw StoreError.failedToWrite + func roundingForCellsInATable(cellIndex: Int, numberOfLines: Int) -> CACornerMask { + switch (cellIndex, numberOfLines) { + case (0, 1): + return [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] + case (0, _): + return [.layerMinXMinYCorner, .layerMaxXMinYCorner] + case (_, _) where cellIndex == numberOfLines - 1: + return [.layerMinXMaxYCorner, .layerMaxXMaxYCorner] + default: + return [] } } - private func removeCategory(atIndex index: Int) throws { - let nameOfCategory = category[index] - do { - try trackerCategoryStore.deleteCategory(with: nameOfCategory) - } catch { - throw StoreError.failedActoionDelete + func separatorInsetForCell(index: Int, numberOfLines: Int) -> UIEdgeInsets { + let quantityCategory = numberOfLines - 1 + if index == quantityCategory { + return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .greatestFiniteMagnitude) + } else { + return UIEdgeInsets(top: 0, left: 16, bottom: 0, right: 16) } } } -// MARK: - TrackerStoreDelegate +// MARK: - CreateCategoryViewDelegate -extension CategoryViewController: TrackerCategoryStoreDelegate { - func didUpdateData(in store: TrackerCategoryStore) { - tableView.reloadData() +extension CategoryViewController: CategoryViewModelDelegate { + func updateData(nameCategory: String) { + try? viewModel?.createCategory(nameOfCategory: nameCategory) + try? viewModel?.fetchCategory() } } diff --git a/Tracker/CreatingNewTracker/Category/CategoryViewModel.swift b/Tracker/CreatingNewTracker/Category/CategoryViewModel.swift new file mode 100644 index 0000000..f9cfbca --- /dev/null +++ b/Tracker/CreatingNewTracker/Category/CategoryViewModel.swift @@ -0,0 +1,79 @@ +// +// CategoryViewModel.swift +// Tracker +// +// Created by admin on 28.01.2024. +// + +import Foundation + +// MARK: - CategoryViewModel + +final class CategoryViewModel { + @ObservableValue private(set) var categories: [TrackerCategory] = [] + + weak var delegateHabbit: CreatingHabitViewControllerDelegate? + weak var delegateIrregular: CreatingIrregularEventViewControllerDelegate? + private let dataSorege = DataStorege.shared + private let trackerCategoryStore = TrackerCategoryStore() + + func categoriesCount() -> Int { + return categories.count + } + + func addingCategoryToCreate(_ indexPath: IndexPath){ + let nameCategory = categories[indexPath.row].title + delegateIrregular?.updateSubitle(nameSubitle: nameCategory) + delegateHabbit?.updateSubitle(nameSubitle: nameCategory) + } + + func selectedCategoryForCheckmark(_ indexPath: IndexPath) { + dataSorege.saveIndexPathForCheckmark(indexPath) + } + + func loadIndexPathForCheckmark() -> IndexPath? { + return dataSorege.loadIndexPathForCheckmark() + } +} + +// MARK: - CategoryStoreCoreDate + +extension CategoryViewModel { + func fetchCategory() throws { + do { + let coreDataCategories = try trackerCategoryStore.fetchAllCategories() + categories = try coreDataCategories.compactMap { coreDataCategory in + return try trackerCategoryStore.decodingCategory(from: coreDataCategory) + } + } catch { + throw StoreError.failedReading + } + } + + func createCategory(nameOfCategory: String) throws { + do { + let newCategory = TrackerCategory(title: nameOfCategory, trackers: []) + try trackerCategoryStore.createCategory(newCategory) + } catch { + throw StoreError.failedToWrite + } + } + + func removeCategory(atIndex index: Int) throws { + let nameOfCategory = categories[index].title + do { + try trackerCategoryStore.deleteCategory(with: nameOfCategory) + try fetchCategory() + } catch { + throw StoreError.failedActoionDelete + } + } +} + +// MARK: - TrackerStoreDelegate + +extension CategoryViewModel: TrackerCategoryStoreDelegate { + func didUpdateData(in store: TrackerCategoryStore) { + try? fetchCategory() + } +} diff --git a/Tracker/CreatingNewTracker/Category/CreatingCategoryViewController.swift b/Tracker/CreatingNewTracker/Category/CreatingCategoryViewController.swift index c0c5197..dd34334 100644 --- a/Tracker/CreatingNewTracker/Category/CreatingCategoryViewController.swift +++ b/Tracker/CreatingNewTracker/Category/CreatingCategoryViewController.swift @@ -8,11 +8,11 @@ import UIKit final class CreatingCategoryViewController: UIViewController { - weak var delegate: CategoryViewDelegate? + weak var delegate: CategoryViewModelDelegate? private let categoryViewController = CategoryViewController() private let characterLimitInField = 38 - //MARK: - UiElements + // MARK: - UiElements private lazy var habitLabel: UILabel = { let label = UILabel() diff --git a/Tracker/CreatingNewTracker/CreatingHabitView/CreatingHabitViewController.swift b/Tracker/CreatingNewTracker/CreatingHabitView/CreatingHabitViewController.swift index cf51af3..1085259 100644 --- a/Tracker/CreatingNewTracker/CreatingHabitView/CreatingHabitViewController.swift +++ b/Tracker/CreatingNewTracker/CreatingHabitView/CreatingHabitViewController.swift @@ -323,7 +323,9 @@ extension CreatingHabitViewController: UITableViewDelegate { switch indexPath.row { case 0: let categoryViewController = CategoryViewController() - categoryViewController.delegateHabbit = self + let categoryViewModel = CategoryViewModel() + categoryViewController.initialize(viewModel: categoryViewModel) + categoryViewModel.delegateHabbit = self let navigationController = UINavigationController(rootViewController: categoryViewController) present(navigationController, animated: true) case 1: diff --git a/Tracker/CreatingNewTracker/CreatingHabitView/CreatingIrregularEventViewController.swift b/Tracker/CreatingNewTracker/CreatingHabitView/CreatingIrregularEventViewController.swift index 2b834f0..2948b19 100644 --- a/Tracker/CreatingNewTracker/CreatingHabitView/CreatingIrregularEventViewController.swift +++ b/Tracker/CreatingNewTracker/CreatingHabitView/CreatingIrregularEventViewController.swift @@ -290,7 +290,9 @@ extension CreatingIrregularEventViewController: UITableViewDelegate { switch indexPath.row { case 0: let categoryViewController = CategoryViewController() - categoryViewController.delegateIrregular = self + let categoryViewModel = CategoryViewModel() + categoryViewController.initialize(viewModel: categoryViewModel) + categoryViewModel.delegateIrregular = self let navigationController = UINavigationController(rootViewController: categoryViewController) present(navigationController, animated: true) default: diff --git a/Tracker/Helpers/ObservableValue.swift b/Tracker/Helpers/ObservableValue.swift new file mode 100644 index 0000000..6dd4f37 --- /dev/null +++ b/Tracker/Helpers/ObservableValue.swift @@ -0,0 +1,38 @@ +// +// ObservableValue.swift +// Tracker +// +// Created by admin on 28.01.2024. +// + +import Foundation + +@propertyWrapper +final class ObservableValue { + typealias TypeValue = T + + private var onChange: ((TypeValue) -> Void)? + + var wrappedValue: TypeValue { + didSet { + onChange?(wrappedValue) + } + } + + var projectedValue: ObservableValue { + return self + } + + // MARK: - Initialisation + + init(wrappedValue: TypeValue) { + onChange = nil + self.wrappedValue = wrappedValue + } + + // MARK: - Methods + + func bind(action: @escaping (TypeValue) -> Void) { + self.onChange = action + } +} diff --git a/Tracker/Onboard/OnboardContentViewController.swift b/Tracker/Onboard/OnboardContentViewController.swift new file mode 100644 index 0000000..4e62817 --- /dev/null +++ b/Tracker/Onboard/OnboardContentViewController.swift @@ -0,0 +1,91 @@ +// +// OnboardContentViewController.swift +// Tracker +// +// Created by admin on 28.01.2024. +// + +import UIKit + +// MARK: - UIViewController + +final class OnboardContentViewController: UIViewController { + weak var delegate: ContentViewControllerDelegate? + var descriptionText: String? + var backgroundImage: UIImage? + + // MARK: - UiElements + + private lazy var label: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.font = UIFont.boldSystemFont(ofSize: 32) + label.tintColor = .blackDay + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private lazy var button: UIButton = { + let button = UIButton() + button.setTitle("Вот это технологии!", for: .normal) + button.layer.cornerRadius = 16 + button.backgroundColor = .blackDay + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside) + return button + }() + + private lazy var backgroundImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleToFill + imageView.clipsToBounds = true + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + configViews() + configConstraints() + } + + // MARK: - Actions + + @objc func didTapButton() { + delegate?.didTapButton() + } + + // MARK: - Private methods + + private func configViews() { + label.text = descriptionText + backgroundImageView.image = backgroundImage + view.addSubview(backgroundImageView) + view.addSubview(label) + view.addSubview(button) + } + + private func configConstraints() { + NSLayoutConstraint.activate([ + backgroundImageView.topAnchor.constraint(equalTo: view.topAnchor), + backgroundImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + backgroundImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + backgroundImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + label.heightAnchor.constraint(equalToConstant: 76), + label.widthAnchor.constraint(equalToConstant: 343), + label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + label.centerYAnchor.constraint(equalTo: view.centerYAnchor), + label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -304), + button.heightAnchor.constraint(equalToConstant: 60), + button.widthAnchor.constraint(equalToConstant: 335), + button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -84), + button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + button.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 160) + ]) + } +} diff --git a/Tracker/Onboard/OnboardViewController.swift b/Tracker/Onboard/OnboardViewController.swift new file mode 100644 index 0000000..d2d73be --- /dev/null +++ b/Tracker/Onboard/OnboardViewController.swift @@ -0,0 +1,156 @@ +// +// OnboardViewController.swift +// Tracker +// +// Created by admin on 28.01.2024. +// + +import UIKit + +protocol ContentViewControllerDelegate: AnyObject { + func didTapButton() +} + +// MARK: - OnboardViewController + +final class OnboardViewController: UIPageViewController { + private let dataStorage = DataStorege.shared + private lazy var pageControl: UIPageControl = { + let pageControl = UIPageControl() + pageControl.currentPageIndicatorTintColor = .blackDay + pageControl.pageIndicatorTintColor = .backgroundDay + pageControl.currentPage = 0 + pageControl.addTarget(OnboardViewController.self, action: #selector(pageControlTapped), for: .valueChanged) + pageControl.translatesAutoresizingMaskIntoConstraints = false + return pageControl + }() + + lazy var pages: [UIViewController] = [ + { + let controller = OnboardContentViewController() + controller.backgroundImage = UIImage(named: "blueImage") + controller.descriptionText = "Отслеживайте только то, что хотите" + controller.delegate = self + return controller + }(), + { + let controller = OnboardContentViewController() + controller.backgroundImage = UIImage(named: "redImage") + controller.descriptionText = "Даже если это не литры воды и йога" + controller.delegate = self + return controller + }() + ] + + // MARK: - Initialisation + + init() { + super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + configViews() + configConstraints() + } + + // MARK: - Actions + + @objc func pageControlTapped(_ sender: UIPageControl) { + let tappedPageIndex = sender.currentPage + if tappedPageIndex >= 0 && tappedPageIndex < pages.count { + let targetPage = pages[tappedPageIndex] + guard let currentViewController = viewControllers?.first else { + return + } + if let currentIndex = pages.firstIndex(of: currentViewController) { + let direction: UIPageViewController.NavigationDirection = tappedPageIndex > currentIndex ? .forward : .reverse + setViewControllers([targetPage], direction: direction, animated: true, completion: nil) + } + } + } + + // MARK: - Private methods + + private func configViews() { + if let first = pages.first { + setViewControllers([first], direction: .forward, animated: true, completion: nil) + } + delegate = self + dataSource = self + pageControl.numberOfPages = pages.count + view.addSubview(pageControl) + } + + private func configConstraints() { + NSLayoutConstraint.activate([ + pageControl.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -134), + pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + } +} + +// MARK: - UIPageViewControllerDelegate + +extension OnboardViewController: UIPageViewControllerDelegate { + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + if let currentViewController = pageViewController.viewControllers?.first, + let currentIndex = pages.firstIndex(of: currentViewController) { + pageControl.currentPage = currentIndex + } + } +} + +// MARK: - UIPageViewControllerDataSource + +extension OnboardViewController: UIPageViewControllerDataSource { + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let viewControllerIndex = pages.firstIndex(of: viewController) else { + return nil + } + let previousIndex = viewControllerIndex - 1 + guard previousIndex >= 0 else { + return pages.last + } + return pages[previousIndex] + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let viewControllerIndex = pages.firstIndex(of: viewController) else { + return nil + } + let nextIndex = viewControllerIndex + 1 + guard nextIndex < pages.count else { + return pages.first + } + return pages[nextIndex] + } +} + +// MARK: - ContentViewControllerDelegate + +extension OnboardViewController: ContentViewControllerDelegate { + func didTapButton() { + guard let currentViewController = viewControllers?.first else { return } + if let currentIndex = pages.firstIndex(of: currentViewController) { + let nextIndex = currentIndex + 1 + if nextIndex < pages.count { + let nextViewController = pages[nextIndex] + self.setViewControllers([nextViewController], direction: .forward, animated: true, completion: nil) + pageControl.currentPage = nextIndex + } else { + guard let window = UIApplication.shared.windows.first else { + fatalError("Invalid Configuration") + } + window.rootViewController = TabBarController() + dataStorage.firstLaunchApplication = true + } + } + } +} diff --git a/Tracker/Onboard/OnboardViewController.xcdatamodeld/OnboardViewController.xcdatamodel/contents b/Tracker/Onboard/OnboardViewController.xcdatamodeld/OnboardViewController.xcdatamodel/contents new file mode 100644 index 0000000..1c88aaa --- /dev/null +++ b/Tracker/Onboard/OnboardViewController.xcdatamodeld/OnboardViewController.xcdatamodel/contents @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/Contents.json b/Tracker/Resources/Assets.xcassets/imageBackground/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Tracker/Resources/Assets.xcassets/imageBackground/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@1x.png b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@1x.png new file mode 100644 index 0000000..1e696bf Binary files /dev/null and b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@1x.png differ diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@2x.png b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@2x.png new file mode 100644 index 0000000..cfe2fdf Binary files /dev/null and b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@2x.png differ diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@3x.png b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@3x.png new file mode 100644 index 0000000..df80233 Binary files /dev/null and b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/1@3x.png differ diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/Contents.json b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/Contents.json new file mode 100644 index 0000000..faeff0b --- /dev/null +++ b/Tracker/Resources/Assets.xcassets/imageBackground/blueImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "1@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@1x.png b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@1x.png new file mode 100644 index 0000000..7756d38 Binary files /dev/null and b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@1x.png differ diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@2x.png b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@2x.png new file mode 100644 index 0000000..e03f661 Binary files /dev/null and b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@2x.png differ diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@3x.png b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@3x.png new file mode 100644 index 0000000..16d295d Binary files /dev/null and b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/2@3x.png differ diff --git a/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/Contents.json b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/Contents.json new file mode 100644 index 0000000..5c80030 --- /dev/null +++ b/Tracker/Resources/Assets.xcassets/imageBackground/redImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "2@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "2@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "2@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tracker/Services/DataStorege.swift b/Tracker/Services/DataStorege.swift index ea03434..c0403a0 100644 --- a/Tracker/Services/DataStorege.swift +++ b/Tracker/Services/DataStorege.swift @@ -14,6 +14,16 @@ final class DataStorege { private let indexPathForCheckmark = "IndexPathForCheckmark" private let daysInAWeek = "DaysInAWeek" private let trackerKey = "TrackerKey" + private let firstLaunchKey = "firstLaunchApplication" + + var firstLaunchApplication: Bool { + get { + return defaults.value(forKey: firstLaunchKey) as? Bool ?? false + } + set { + defaults.set(newValue, forKey: firstLaunchKey) + } + } } // MARK: - CategoryViewController diff --git a/Tracker/Services/SceneDelegate.swift b/Tracker/Services/SceneDelegate.swift index fea6069..9e13f17 100644 --- a/Tracker/Services/SceneDelegate.swift +++ b/Tracker/Services/SceneDelegate.swift @@ -9,13 +9,14 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { + let dataStorage = DataStorege.shared var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) - window.rootViewController = TabBarController() + window.rootViewController = dataStorage.firstLaunchApplication ? (TabBarController()) : (OnboardViewController()) window.makeKeyAndVisible() self.window = window }