diff --git a/Agenda.xcodeproj/project.pbxproj b/Agenda.xcodeproj/project.pbxproj index dbf06af..9348274 100644 --- a/Agenda.xcodeproj/project.pbxproj +++ b/Agenda.xcodeproj/project.pbxproj @@ -16,12 +16,13 @@ F0027EF2284BE57C00D73820 /* OnboardingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0027EEC284BE57C00D73820 /* OnboardingContainer.swift */; }; F00AB2242805902500C57F9D /* OnboardingTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00AB2232805902400C57F9D /* OnboardingTableView.swift */; }; F010C62C27F75D410068E591 /* GoalTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F010C62B27F75D410068E591 /* GoalTableViewCell.swift */; }; + F014D940285B259200AB60A3 /* SummaryPresenterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F014D93F285B259200AB60A3 /* SummaryPresenterSpy.swift */; }; F016F77C27F7A4D6008D184A /* GoalData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F016F77B27F7A4D6008D184A /* GoalData.swift */; }; - F016F77E27F7B1EC008D184A /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F016F77D27F7B1EC008D184A /* UIAlertController.swift */; }; + F01C5810285C84A10094BCE3 /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01C580F285C84A10094BCE3 /* OnboardingViewModel.swift */; }; + F01C5812285C89D60094BCE3 /* AddGoalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01C5811285C89D60094BCE3 /* AddGoalUITests.swift */; }; F01FDD2D280309040011F1F3 /* OnboardingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01FDD2C280309040011F1F3 /* OnboardingTableViewCell.swift */; }; - F01FDD2F28030A2F0011F1F3 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = F01FDD2E28030A2F0011F1F3 /* UserDefaults.swift */; }; F02572A127FDE750000A9A06 /* Summary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02572A027FDE750000A9A06 /* Summary.swift */; }; - F030A20327F8DD51004B375A /* AlertError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F030A20227F8DD51004B375A /* AlertError.swift */; }; + F037986C285527CB00749971 /* CoreDataManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F037986B285527CB00749971 /* CoreDataManagerTests.swift */; }; F037C52227F5ED1F001D2CBC /* UITextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = F037C52127F5ED1F001D2CBC /* UITextField.swift */; }; F03916F4284D1F5300E36ED9 /* MonthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03916F3284D1F5300E36ED9 /* MonthViewModel.swift */; }; F03FE95C2849544700F265A2 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F03FE95B2849544700F265A2 /* AppCoordinator.swift */; }; @@ -60,6 +61,13 @@ F07513A5284A9B8200BD887D /* SummaryProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = F075139F284A9B8200BD887D /* SummaryProtocols.swift */; }; F07513A6284A9B8200BD887D /* SummaryInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07513A0284A9B8200BD887D /* SummaryInteractor.swift */; }; F07513A7284A9B8200BD887D /* SummaryContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07513A1284A9B8200BD887D /* SummaryContainer.swift */; }; + F07FC1C72858DFFF0028C446 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07FC1C62858DFFF0028C446 /* Icons.swift */; }; + F07FC1C92858E9D20028C446 /* CoreDataManagerSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07FC1C82858E9D20028C446 /* CoreDataManagerSpy.swift */; }; + F090EAB328531DE400510ED5 /* UserDefaultsContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F090EAB228531DE400510ED5 /* UserDefaultsContainer.swift */; }; + F090EAB528533F7B00510ED5 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F090EAB428533F7B00510ED5 /* UserSettings.swift */; }; + F0A831BE28560FC5006F47CF /* AgendaContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A831BD28560FC5006F47CF /* AgendaContainerTests.swift */; }; + F0AD059D285A65DA000612BE /* GoalDetailsPresenterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AD059C285A65DA000612BE /* GoalDetailsPresenterSpy.swift */; }; + F0AD059F285A7058000612BE /* HistoryPresenterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0AD059E285A7058000612BE /* HistoryPresenterSpy.swift */; }; F0BB551B27EF4D5400FA7E99 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0BB551A27EF4D5400FA7E99 /* AppDelegate.swift */; }; F0BB551D27EF4D5400FA7E99 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0BB551C27EF4D5400FA7E99 /* SceneDelegate.swift */; }; F0BB552427EF4D5700FA7E99 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F0BB552327EF4D5700FA7E99 /* Assets.xcassets */; }; @@ -67,18 +75,57 @@ F0BB553827EF4F1E00FA7E99 /* CoreDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0BB553727EF4F1E00FA7E99 /* CoreDataManager.swift */; }; F0BB554327EF511800FA7E99 /* AgendaTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0BB554227EF511800FA7E99 /* AgendaTableViewCell.swift */; }; F0BB554C27EF589200FA7E99 /* Agenda.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F0BB554A27EF589100FA7E99 /* Agenda.xcdatamodeld */; }; + F0C40294285DFDEF000F76F0 /* GoalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C40293285DFDEF000F76F0 /* GoalViewController.swift */; }; + F0C44D00285E462800E745A5 /* GoalDetailsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C44CFF285E462800E745A5 /* GoalDetailsUITests.swift */; }; + F0C44D02285E464E00E745A5 /* HistoryUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C44D01285E464E00E745A5 /* HistoryUITests.swift */; }; + F0C44D04285E465900E745A5 /* SummaryUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C44D03285E465900E745A5 /* SummaryUITests.swift */; }; F0C8DA2227F4605300ED47C5 /* Goal+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C8DA1E27F4605200ED47C5 /* Goal+CoreDataClass.swift */; }; F0C8DA2327F4605300ED47C5 /* Goal+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C8DA1F27F4605200ED47C5 /* Goal+CoreDataProperties.swift */; }; F0C8DA2427F4605300ED47C5 /* Month+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C8DA2027F4605200ED47C5 /* Month+CoreDataClass.swift */; }; F0C8DA2527F4605300ED47C5 /* Month+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C8DA2127F4605200ED47C5 /* Month+CoreDataProperties.swift */; }; F0D83A9027F065EA0029CFD1 /* HistoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0D83A8F27F065EA0029CFD1 /* HistoryTableViewCell.swift */; }; + F0DB531B285861C30074FFBA /* AddGoalInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB531A285861C20074FFBA /* AddGoalInteractorTests.swift */; }; + F0DB531D285861D50074FFBA /* AddGoalContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB531C285861D50074FFBA /* AddGoalContainerTests.swift */; }; + F0DB531F285861EF0074FFBA /* GoalDetailsInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB531E285861EF0074FFBA /* GoalDetailsInteractorTests.swift */; }; + F0DB5321285861FA0074FFBA /* GoalDetailsContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB5320285861FA0074FFBA /* GoalDetailsContainerTests.swift */; }; + F0DB53232858620F0074FFBA /* HistoryInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB53222858620F0074FFBA /* HistoryInteractorTests.swift */; }; + F0DB53252858621B0074FFBA /* HistoryContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB53242858621B0074FFBA /* HistoryContainerTests.swift */; }; + F0DB53272858622B0074FFBA /* SummaryInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB53262858622B0074FFBA /* SummaryInteractorTests.swift */; }; + F0DB53292858624F0074FFBA /* SummaryContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB53282858624F0074FFBA /* SummaryContainerTests.swift */; }; + F0DB532B285862650074FFBA /* OnboardingInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB532A285862650074FFBA /* OnboardingInteractorTests.swift */; }; + F0DB532D2858626F0074FFBA /* OnboardingContainerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DB532C2858626F0074FFBA /* OnboardingContainerTests.swift */; }; + F0E36EF62858F8170000C9F3 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E36EF52858F8160000C9F3 /* UIAlertController.swift */; }; + F0E8174F2855D61700DD4EE1 /* AgendaPresenterSpy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E8174E2855D61700DD4EE1 /* AgendaPresenterSpy.swift */; }; + F0E817522855D66600DD4EE1 /* CoreDataManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E817512855D66600DD4EE1 /* CoreDataManagerMock.swift */; }; + F0E817542855D86100DD4EE1 /* CoreDataManagerStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0E817532855D86100DD4EE1 /* CoreDataManagerStub.swift */; }; F0EA8B2C27FAD2220048327A /* SPIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = F0EA8B2B27FAD2220048327A /* SPIndicator */; }; F0EA8B2E27FAD2B10048327A /* SeparatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EA8B2D27FAD2B10048327A /* SeparatorView.swift */; }; + F0EC6494285640A400DC0042 /* AgendaRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0EC6493285640A400DC0042 /* AgendaRouterTests.swift */; }; F0F45FE72801C2F000BA244D /* Labels.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F45FE62801C2F000BA244D /* Labels.swift */; }; F0F45FEB2801C8DB00BA244D /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F0F45FED2801C8DB00BA244D /* Localizable.strings */; }; F0F45FF02801C90C00BA244D /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0F45FEF2801C90C00BA244D /* String.swift */; }; + F0FFD18C28549D80001EE4A6 /* AgendaInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0FFD18B28549D80001EE4A6 /* AgendaInteractorTests.swift */; }; + F0FFD19628549DC6001EE4A6 /* AgendaUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0FFD19528549DC6001EE4A6 /* AgendaUITestsLaunchTests.swift */; }; + F0FFD1A428549FD5001EE4A6 /* AgendaUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0FFD1A328549FD5001EE4A6 /* AgendaUITests.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + F0055BED2854748F00430622 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F0BB550F27EF4D5400FA7E99 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F0BB551627EF4D5400FA7E99; + remoteInfo = Agenda; + }; + F0FFD19728549DC6001EE4A6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F0BB550F27EF4D5400FA7E99 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F0BB551627EF4D5400FA7E99; + remoteInfo = Agenda; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ F0027EE4284BE01200D73820 /* BaseRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseRouter.swift; sourceTree = ""; }; F0027EE7284BE57C00D73820 /* OnboardingPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPresenter.swift; sourceTree = ""; }; @@ -87,14 +134,16 @@ F0027EEA284BE57C00D73820 /* OnboardingProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProtocols.swift; sourceTree = ""; }; F0027EEB284BE57C00D73820 /* OnboardingInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingInteractor.swift; sourceTree = ""; }; F0027EEC284BE57C00D73820 /* OnboardingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingContainer.swift; sourceTree = ""; }; + F0055BE92854748F00430622 /* AgendaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AgendaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F00AB2232805902400C57F9D /* OnboardingTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTableView.swift; sourceTree = ""; }; F010C62B27F75D410068E591 /* GoalTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalTableViewCell.swift; sourceTree = ""; }; + F014D93F285B259200AB60A3 /* SummaryPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryPresenterSpy.swift; sourceTree = ""; }; F016F77B27F7A4D6008D184A /* GoalData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalData.swift; sourceTree = ""; }; - F016F77D27F7B1EC008D184A /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; + F01C580F285C84A10094BCE3 /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; + F01C5811285C89D60094BCE3 /* AddGoalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGoalUITests.swift; sourceTree = ""; }; F01FDD2C280309040011F1F3 /* OnboardingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTableViewCell.swift; sourceTree = ""; }; - F01FDD2E28030A2F0011F1F3 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; F02572A027FDE750000A9A06 /* Summary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Summary.swift; sourceTree = ""; }; - F030A20227F8DD51004B375A /* AlertError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertError.swift; sourceTree = ""; }; + F037986B285527CB00749971 /* CoreDataManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManagerTests.swift; sourceTree = ""; }; F037C52127F5ED1F001D2CBC /* UITextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITextField.swift; sourceTree = ""; }; F03916F3284D1F5300E36ED9 /* MonthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonthViewModel.swift; sourceTree = ""; }; F03FE95B2849544700F265A2 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; @@ -133,6 +182,13 @@ F075139F284A9B8200BD887D /* SummaryProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryProtocols.swift; sourceTree = ""; }; F07513A0284A9B8200BD887D /* SummaryInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryInteractor.swift; sourceTree = ""; }; F07513A1284A9B8200BD887D /* SummaryContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryContainer.swift; sourceTree = ""; }; + F07FC1C62858DFFF0028C446 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = ""; }; + F07FC1C82858E9D20028C446 /* CoreDataManagerSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManagerSpy.swift; sourceTree = ""; }; + F090EAB228531DE400510ED5 /* UserDefaultsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsContainer.swift; sourceTree = ""; }; + F090EAB428533F7B00510ED5 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = ""; }; + F0A831BD28560FC5006F47CF /* AgendaContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaContainerTests.swift; sourceTree = ""; }; + F0AD059C285A65DA000612BE /* GoalDetailsPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDetailsPresenterSpy.swift; sourceTree = ""; }; + F0AD059E285A7058000612BE /* HistoryPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryPresenterSpy.swift; sourceTree = ""; }; F0BB551727EF4D5400FA7E99 /* Agenda.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Agenda.app; sourceTree = BUILT_PRODUCTS_DIR; }; F0BB551A27EF4D5400FA7E99 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F0BB551C27EF4D5400FA7E99 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -143,20 +199,50 @@ F0BB553727EF4F1E00FA7E99 /* CoreDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManager.swift; sourceTree = ""; }; F0BB554227EF511800FA7E99 /* AgendaTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaTableViewCell.swift; sourceTree = ""; }; F0BB554B27EF589200FA7E99 /* Agenda.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Agenda.xcdatamodel; sourceTree = ""; }; + F0C40293285DFDEF000F76F0 /* GoalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalViewController.swift; sourceTree = ""; }; + F0C44CFF285E462800E745A5 /* GoalDetailsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDetailsUITests.swift; sourceTree = ""; }; + F0C44D01285E464E00E745A5 /* HistoryUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryUITests.swift; sourceTree = ""; }; + F0C44D03285E465900E745A5 /* SummaryUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryUITests.swift; sourceTree = ""; }; F0C8DA1E27F4605200ED47C5 /* Goal+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Goal+CoreDataClass.swift"; sourceTree = ""; }; F0C8DA1F27F4605200ED47C5 /* Goal+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Goal+CoreDataProperties.swift"; sourceTree = ""; }; F0C8DA2027F4605200ED47C5 /* Month+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Month+CoreDataClass.swift"; sourceTree = ""; }; F0C8DA2127F4605200ED47C5 /* Month+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Month+CoreDataProperties.swift"; sourceTree = ""; }; F0D83A8F27F065EA0029CFD1 /* HistoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTableViewCell.swift; sourceTree = ""; }; + F0DB531A285861C20074FFBA /* AddGoalInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGoalInteractorTests.swift; sourceTree = ""; }; + F0DB531C285861D50074FFBA /* AddGoalContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGoalContainerTests.swift; sourceTree = ""; }; + F0DB531E285861EF0074FFBA /* GoalDetailsInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDetailsInteractorTests.swift; sourceTree = ""; }; + F0DB5320285861FA0074FFBA /* GoalDetailsContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoalDetailsContainerTests.swift; sourceTree = ""; }; + F0DB53222858620F0074FFBA /* HistoryInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryInteractorTests.swift; sourceTree = ""; }; + F0DB53242858621B0074FFBA /* HistoryContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContainerTests.swift; sourceTree = ""; }; + F0DB53262858622B0074FFBA /* SummaryInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryInteractorTests.swift; sourceTree = ""; }; + F0DB53282858624F0074FFBA /* SummaryContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryContainerTests.swift; sourceTree = ""; }; + F0DB532A285862650074FFBA /* OnboardingInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingInteractorTests.swift; sourceTree = ""; }; + F0DB532C2858626F0074FFBA /* OnboardingContainerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingContainerTests.swift; sourceTree = ""; }; + F0E36EF52858F8160000C9F3 /* UIAlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = ""; }; + F0E8174E2855D61700DD4EE1 /* AgendaPresenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaPresenterSpy.swift; sourceTree = ""; }; + F0E817512855D66600DD4EE1 /* CoreDataManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManagerMock.swift; sourceTree = ""; }; + F0E817532855D86100DD4EE1 /* CoreDataManagerStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataManagerStub.swift; sourceTree = ""; }; F0EA8B2D27FAD2B10048327A /* SeparatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorView.swift; sourceTree = ""; }; + F0EC6493285640A400DC0042 /* AgendaRouterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaRouterTests.swift; sourceTree = ""; }; F0F45FE62801C2F000BA244D /* Labels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Labels.swift; sourceTree = ""; }; F0F45FE82801C84C00BA244D /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; F0F45FEC2801C8DB00BA244D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; F0F45FEE2801C8E000BA244D /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; F0F45FEF2801C90C00BA244D /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + F0FFD18B28549D80001EE4A6 /* AgendaInteractorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaInteractorTests.swift; sourceTree = ""; }; + F0FFD19128549DC6001EE4A6 /* AgendaUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AgendaUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F0FFD19528549DC6001EE4A6 /* AgendaUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaUITestsLaunchTests.swift; sourceTree = ""; }; + F0FFD1A328549FD5001EE4A6 /* AgendaUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgendaUITests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + F0055BE62854748F00430622 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F0BB551427EF4D5400FA7E99 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -165,6 +251,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F0FFD18E28549DC6001EE4A6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -177,6 +270,7 @@ F0027EEB284BE57C00D73820 /* OnboardingInteractor.swift */, F0027EEC284BE57C00D73820 /* OnboardingContainer.swift */, F0027EF3284BE58800D73820 /* Views */, + F01C580F285C84A10094BCE3 /* OnboardingViewModel.swift */, ); path = Onboarding; sourceTree = ""; @@ -191,13 +285,35 @@ path = Views; sourceTree = ""; }; + F0055BEA2854748F00430622 /* AgendaTests */ = { + isa = PBXGroup; + children = ( + F0FFD18528549D1C001EE4A6 /* AgendaModuleTests */, + F0FFD18628549D2A001EE4A6 /* AddGoalModuleTests */, + F0FFD18728549D34001EE4A6 /* GoalDetailsModuleTests */, + F0FFD18828549D3C001EE4A6 /* HistoryModuleTests */, + F0FFD18928549D42001EE4A6 /* SummaryModuleTests */, + F0FFD18A28549D49001EE4A6 /* OnboardingModuleTests */, + F037986A285527AE00749971 /* ServicesTests */, + F0E817502855D64900DD4EE1 /* Mocks+Stubs+Spies */, + ); + path = AgendaTests; + sourceTree = ""; + }; + F037986A285527AE00749971 /* ServicesTests */ = { + isa = PBXGroup; + children = ( + F037986B285527CB00749971 /* CoreDataManagerTests.swift */, + ); + path = ServicesTests; + sourceTree = ""; + }; F037C51F27F5ECF8001D2CBC /* Extensions */ = { isa = PBXGroup; children = ( F037C52027F5ED00001D2CBC /* UIKit */, F053020B27FA264300C62FAC /* Date.swift */, F0F45FEF2801C90C00BA244D /* String.swift */, - F01FDD2E28030A2F0011F1F3 /* UserDefaults.swift */, ); path = Extensions; sourceTree = ""; @@ -205,8 +321,8 @@ F037C52027F5ED00001D2CBC /* UIKit */ = { isa = PBXGroup; children = ( + F0E36EF52858F8160000C9F3 /* UIAlertController.swift */, F037C52127F5ED1F001D2CBC /* UITextField.swift */, - F016F77D27F7B1EC008D184A /* UIAlertController.swift */, F053020727FA161D00C62FAC /* UIViewController.swift */, ); path = UIKit; @@ -246,7 +362,7 @@ F0751393284A925000BD887D /* HistoryProtocols.swift */, F0751394284A925000BD887D /* HistoryInteractor.swift */, F0751395284A925000BD887D /* HistoryContainer.swift */, - F07513A8284A9D0400BD887D /* View */, + F07513A8284A9D0400BD887D /* Views */, F03916F3284D1F5300E36ED9 /* MonthViewModel.swift */, ); path = History; @@ -311,13 +427,13 @@ path = Modules; sourceTree = ""; }; - F07513A8284A9D0400BD887D /* View */ = { + F07513A8284A9D0400BD887D /* Views */ = { isa = PBXGroup; children = ( F0751392284A925000BD887D /* HistoryViewController.swift */, F0D83A8F27F065EA0029CFD1 /* HistoryTableViewCell.swift */, ); - path = View; + path = Views; sourceTree = ""; }; F07513A9284A9D1800BD887D /* Views */ = { @@ -334,6 +450,8 @@ children = ( F0BB552E27EF4D8700FA7E99 /* README.md */, F0BB551927EF4D5400FA7E99 /* Agenda */, + F0055BEA2854748F00430622 /* AgendaTests */, + F0FFD19228549DC6001EE4A6 /* AgendaUITests */, F0BB551827EF4D5400FA7E99 /* Products */, ); sourceTree = ""; @@ -342,6 +460,8 @@ isa = PBXGroup; children = ( F0BB551727EF4D5400FA7E99 /* Agenda.app */, + F0055BE92854748F00430622 /* AgendaTests.xctest */, + F0FFD19128549DC6001EE4A6 /* AgendaUITests.xctest */, ); name = Products; sourceTree = ""; @@ -365,6 +485,7 @@ children = ( F0BB552327EF4D5700FA7E99 /* Assets.xcassets */, F0BB554A27EF589100FA7E99 /* Agenda.xcdatamodeld */, + F07FC1C62858DFFF0028C446 /* Icons.swift */, F0F45FE62801C2F000BA244D /* Labels.swift */, F0F45FED2801C8DB00BA244D /* Localizable.strings */, F0BB552827EF4D5700FA7E99 /* Info.plist */, @@ -386,8 +507,9 @@ F0BB553427EF4EFE00FA7E99 /* Services */ = { isa = PBXGroup; children = ( - F0BB553727EF4F1E00FA7E99 /* CoreDataManager.swift */, F0027EE4284BE01200D73820 /* BaseRouter.swift */, + F0BB553727EF4F1E00FA7E99 /* CoreDataManager.swift */, + F090EAB228531DE400510ED5 /* UserDefaultsContainer.swift */, ); path = Services; sourceTree = ""; @@ -397,6 +519,7 @@ children = ( F0BB555527EF5AD800FA7E99 /* CoreData */, F016F77B27F7A4D6008D184A /* GoalData.swift */, + F090EAB428533F7B00510ED5 /* UserSettings.swift */, ); path = Models; sourceTree = ""; @@ -404,9 +527,9 @@ F0BB554027EF510500FA7E99 /* Views */ = { isa = PBXGroup; children = ( - F030A20227F8DD51004B375A /* AlertError.swift */, F0EA8B2D27FAD2B10048327A /* SeparatorView.swift */, F010C62B27F75D410068E591 /* GoalTableViewCell.swift */, + F0C40293285DFDEF000F76F0 /* GoalViewController.swift */, ); path = Views; sourceTree = ""; @@ -422,9 +545,141 @@ path = CoreData; sourceTree = ""; }; + F0DB532F285866870074FFBA /* Mocks+Stubs+Spies */ = { + isa = PBXGroup; + children = ( + F0AD059C285A65DA000612BE /* GoalDetailsPresenterSpy.swift */, + ); + path = "Mocks+Stubs+Spies"; + sourceTree = ""; + }; + F0DB53302858668D0074FFBA /* Mocks+Stubs+Spies */ = { + isa = PBXGroup; + children = ( + F0AD059E285A7058000612BE /* HistoryPresenterSpy.swift */, + ); + path = "Mocks+Stubs+Spies"; + sourceTree = ""; + }; + F0DB53312858668F0074FFBA /* Mocks+Stubs+Spies */ = { + isa = PBXGroup; + children = ( + F014D93F285B259200AB60A3 /* SummaryPresenterSpy.swift */, + ); + path = "Mocks+Stubs+Spies"; + sourceTree = ""; + }; + F0E817482855CE5B00DD4EE1 /* Mocks+Stubs+Spies */ = { + isa = PBXGroup; + children = ( + F0E8174E2855D61700DD4EE1 /* AgendaPresenterSpy.swift */, + ); + path = "Mocks+Stubs+Spies"; + sourceTree = ""; + }; + F0E817502855D64900DD4EE1 /* Mocks+Stubs+Spies */ = { + isa = PBXGroup; + children = ( + F0E817512855D66600DD4EE1 /* CoreDataManagerMock.swift */, + F0E817532855D86100DD4EE1 /* CoreDataManagerStub.swift */, + F07FC1C82858E9D20028C446 /* CoreDataManagerSpy.swift */, + ); + path = "Mocks+Stubs+Spies"; + sourceTree = ""; + }; + F0FFD18528549D1C001EE4A6 /* AgendaModuleTests */ = { + isa = PBXGroup; + children = ( + F0E817482855CE5B00DD4EE1 /* Mocks+Stubs+Spies */, + F0FFD18B28549D80001EE4A6 /* AgendaInteractorTests.swift */, + F0A831BD28560FC5006F47CF /* AgendaContainerTests.swift */, + F0EC6493285640A400DC0042 /* AgendaRouterTests.swift */, + ); + path = AgendaModuleTests; + sourceTree = ""; + }; + F0FFD18628549D2A001EE4A6 /* AddGoalModuleTests */ = { + isa = PBXGroup; + children = ( + F0DB531A285861C20074FFBA /* AddGoalInteractorTests.swift */, + F0DB531C285861D50074FFBA /* AddGoalContainerTests.swift */, + ); + path = AddGoalModuleTests; + sourceTree = ""; + }; + F0FFD18728549D34001EE4A6 /* GoalDetailsModuleTests */ = { + isa = PBXGroup; + children = ( + F0DB532F285866870074FFBA /* Mocks+Stubs+Spies */, + F0DB531E285861EF0074FFBA /* GoalDetailsInteractorTests.swift */, + F0DB5320285861FA0074FFBA /* GoalDetailsContainerTests.swift */, + ); + path = GoalDetailsModuleTests; + sourceTree = ""; + }; + F0FFD18828549D3C001EE4A6 /* HistoryModuleTests */ = { + isa = PBXGroup; + children = ( + F0DB53302858668D0074FFBA /* Mocks+Stubs+Spies */, + F0DB53222858620F0074FFBA /* HistoryInteractorTests.swift */, + F0DB53242858621B0074FFBA /* HistoryContainerTests.swift */, + ); + path = HistoryModuleTests; + sourceTree = ""; + }; + F0FFD18928549D42001EE4A6 /* SummaryModuleTests */ = { + isa = PBXGroup; + children = ( + F0DB53312858668F0074FFBA /* Mocks+Stubs+Spies */, + F0DB53262858622B0074FFBA /* SummaryInteractorTests.swift */, + F0DB53282858624F0074FFBA /* SummaryContainerTests.swift */, + ); + path = SummaryModuleTests; + sourceTree = ""; + }; + F0FFD18A28549D49001EE4A6 /* OnboardingModuleTests */ = { + isa = PBXGroup; + children = ( + F0DB532A285862650074FFBA /* OnboardingInteractorTests.swift */, + F0DB532C2858626F0074FFBA /* OnboardingContainerTests.swift */, + ); + path = OnboardingModuleTests; + sourceTree = ""; + }; + F0FFD19228549DC6001EE4A6 /* AgendaUITests */ = { + isa = PBXGroup; + children = ( + F0FFD1A328549FD5001EE4A6 /* AgendaUITests.swift */, + F01C5811285C89D60094BCE3 /* AddGoalUITests.swift */, + F0C44CFF285E462800E745A5 /* GoalDetailsUITests.swift */, + F0C44D01285E464E00E745A5 /* HistoryUITests.swift */, + F0C44D03285E465900E745A5 /* SummaryUITests.swift */, + F0FFD19528549DC6001EE4A6 /* AgendaUITestsLaunchTests.swift */, + ); + path = AgendaUITests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + F0055BE82854748F00430622 /* AgendaTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F0055BEF2854748F00430622 /* Build configuration list for PBXNativeTarget "AgendaTests" */; + buildPhases = ( + F0055BE52854748F00430622 /* Sources */, + F0055BE62854748F00430622 /* Frameworks */, + F0055BE72854748F00430622 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F0055BEE2854748F00430622 /* PBXTargetDependency */, + ); + name = AgendaTests; + productName = AgendaTests; + productReference = F0055BE92854748F00430622 /* AgendaTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; F0BB551627EF4D5400FA7E99 /* Agenda */ = { isa = PBXNativeTarget; buildConfigurationList = F0BB552B27EF4D5700FA7E99 /* Build configuration list for PBXNativeTarget "Agenda" */; @@ -445,6 +700,24 @@ productReference = F0BB551727EF4D5400FA7E99 /* Agenda.app */; productType = "com.apple.product-type.application"; }; + F0FFD19028549DC6001EE4A6 /* AgendaUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F0FFD19928549DC6001EE4A6 /* Build configuration list for PBXNativeTarget "AgendaUITests" */; + buildPhases = ( + F0FFD18D28549DC6001EE4A6 /* Sources */, + F0FFD18E28549DC6001EE4A6 /* Frameworks */, + F0FFD18F28549DC6001EE4A6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F0FFD19828549DC6001EE4A6 /* PBXTargetDependency */, + ); + name = AgendaUITests; + productName = AgendaUITests; + productReference = F0FFD19128549DC6001EE4A6 /* AgendaUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -452,12 +725,20 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1320; + LastSwiftUpdateCheck = 1340; LastUpgradeCheck = 1320; TargetAttributes = { + F0055BE82854748F00430622 = { + CreatedOnToolsVersion = 13.4.1; + TestTargetID = F0BB551627EF4D5400FA7E99; + }; F0BB551627EF4D5400FA7E99 = { CreatedOnToolsVersion = 13.2.1; }; + F0FFD19028549DC6001EE4A6 = { + CreatedOnToolsVersion = 13.4.1; + TestTargetID = F0BB551627EF4D5400FA7E99; + }; }; }; buildConfigurationList = F0BB551227EF4D5400FA7E99 /* Build configuration list for PBXProject "Agenda" */; @@ -478,11 +759,20 @@ projectRoot = ""; targets = ( F0BB551627EF4D5400FA7E99 /* Agenda */, + F0055BE82854748F00430622 /* AgendaTests */, + F0FFD19028549DC6001EE4A6 /* AgendaUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + F0055BE72854748F00430622 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F0BB551527EF4D5400FA7E99 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -493,13 +783,49 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F0FFD18F28549DC6001EE4A6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + F0055BE52854748F00430622 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F0DB532D2858626F0074FFBA /* OnboardingContainerTests.swift in Sources */, + F0AD059D285A65DA000612BE /* GoalDetailsPresenterSpy.swift in Sources */, + F0DB53252858621B0074FFBA /* HistoryContainerTests.swift in Sources */, + F0DB531F285861EF0074FFBA /* GoalDetailsInteractorTests.swift in Sources */, + F0DB5321285861FA0074FFBA /* GoalDetailsContainerTests.swift in Sources */, + F0DB531D285861D50074FFBA /* AddGoalContainerTests.swift in Sources */, + F0DB53272858622B0074FFBA /* SummaryInteractorTests.swift in Sources */, + F0E817542855D86100DD4EE1 /* CoreDataManagerStub.swift in Sources */, + F0A831BE28560FC5006F47CF /* AgendaContainerTests.swift in Sources */, + F014D940285B259200AB60A3 /* SummaryPresenterSpy.swift in Sources */, + F07FC1C92858E9D20028C446 /* CoreDataManagerSpy.swift in Sources */, + F0AD059F285A7058000612BE /* HistoryPresenterSpy.swift in Sources */, + F0E817522855D66600DD4EE1 /* CoreDataManagerMock.swift in Sources */, + F0DB531B285861C30074FFBA /* AddGoalInteractorTests.swift in Sources */, + F037986C285527CB00749971 /* CoreDataManagerTests.swift in Sources */, + F0DB53292858624F0074FFBA /* SummaryContainerTests.swift in Sources */, + F0FFD18C28549D80001EE4A6 /* AgendaInteractorTests.swift in Sources */, + F0DB532B285862650074FFBA /* OnboardingInteractorTests.swift in Sources */, + F0EC6494285640A400DC0042 /* AgendaRouterTests.swift in Sources */, + F0DB53232858620F0074FFBA /* HistoryInteractorTests.swift in Sources */, + F0E8174F2855D61700DD4EE1 /* AgendaPresenterSpy.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F0BB551327EF4D5400FA7E99 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F0C40294285DFDEF000F76F0 /* GoalViewController.swift in Sources */, F0027EF0284BE57C00D73820 /* OnboardingProtocols.swift in Sources */, F0EA8B2E27FAD2B10048327A /* SeparatorView.swift in Sources */, F03FE967284955E300F265A2 /* AddGoalInteractor.swift in Sources */, @@ -521,10 +847,11 @@ F03FE9772849707000F265A2 /* GoalViewModel.swift in Sources */, F0C8DA2227F4605300ED47C5 /* Goal+CoreDataClass.swift in Sources */, F065E7EB2847ECBF00FAEFE9 /* AgendaPresenter.swift in Sources */, - F016F77E27F7B1EC008D184A /* UIAlertController.swift in Sources */, + F07FC1C72858DFFF0028C446 /* Icons.swift in Sources */, F065E7ED2847ECBF00FAEFE9 /* AgendaViewController.swift in Sources */, F02572A127FDE750000A9A06 /* Summary.swift in Sources */, F065E7EC2847ECBF00FAEFE9 /* AgendaRouter.swift in Sources */, + F090EAB328531DE400510ED5 /* UserDefaultsContainer.swift in Sources */, F0027EF2284BE57C00D73820 /* OnboardingContainer.swift in Sources */, F07513A5284A9B8200BD887D /* SummaryProtocols.swift in Sources */, F03FE95C2849544700F265A2 /* AppCoordinator.swift in Sources */, @@ -532,6 +859,7 @@ F0BB551D27EF4D5400FA7E99 /* SceneDelegate.swift in Sources */, F03FE973284955ED00F265A2 /* GoalDetailsInteractor.swift in Sources */, F0027EEE284BE57C00D73820 /* OnboardingRouter.swift in Sources */, + F090EAB528533F7B00510ED5 /* UserSettings.swift in Sources */, F0027EEF284BE57C00D73820 /* OnboardingViewController.swift in Sources */, F03FE970284955ED00F265A2 /* GoalDetailsRouter.swift in Sources */, F053020C27FA264300C62FAC /* Date.swift in Sources */, @@ -540,12 +868,12 @@ F016F77C27F7A4D6008D184A /* GoalData.swift in Sources */, F065E7EE2847ECBF00FAEFE9 /* AgendaProtocols.swift in Sources */, F03FE96F284955ED00F265A2 /* GoalDetailsPresenter.swift in Sources */, - F030A20327F8DD51004B375A /* AlertError.swift in Sources */, F0F45FF02801C90C00BA244D /* String.swift in Sources */, + F01C5810285C84A10094BCE3 /* OnboardingViewModel.swift in Sources */, + F0E36EF62858F8170000C9F3 /* UIAlertController.swift in Sources */, F0027EED284BE57C00D73820 /* OnboardingPresenter.swift in Sources */, F03FE965284955E300F265A2 /* AddGoalViewController.swift in Sources */, F03FE963284955E300F265A2 /* AddGoalPresenter.swift in Sources */, - F01FDD2F28030A2F0011F1F3 /* UserDefaults.swift in Sources */, F0C8DA2527F4605300ED47C5 /* Month+CoreDataProperties.swift in Sources */, F03FE968284955E300F265A2 /* AddGoalContainer.swift in Sources */, F075139B284A925000BD887D /* HistoryContainer.swift in Sources */, @@ -569,8 +897,34 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F0FFD18D28549DC6001EE4A6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F0FFD19628549DC6001EE4A6 /* AgendaUITestsLaunchTests.swift in Sources */, + F0C44D00285E462800E745A5 /* GoalDetailsUITests.swift in Sources */, + F0C44D02285E464E00E745A5 /* HistoryUITests.swift in Sources */, + F0FFD1A428549FD5001EE4A6 /* AgendaUITests.swift in Sources */, + F0C44D04285E465900E745A5 /* SummaryUITests.swift in Sources */, + F01C5812285C89D60094BCE3 /* AddGoalUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + F0055BEE2854748F00430622 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F0BB551627EF4D5400FA7E99 /* Agenda */; + targetProxy = F0055BED2854748F00430622 /* PBXContainerItemProxy */; + }; + F0FFD19828549DC6001EE4A6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F0BB551627EF4D5400FA7E99 /* Agenda */; + targetProxy = F0FFD19728549DC6001EE4A6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ F0BB552527EF4D5700FA7E99 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; @@ -593,6 +947,44 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + F0055BF02854748F00430622 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H8V3Y2JKY7; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.egbad.AgendaTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Agenda.app/Agenda"; + }; + name = Debug; + }; + F0055BF12854748F00430622 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H8V3Y2JKY7; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.egbad.AgendaTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Agenda.app/Agenda"; + }; + name = Release; + }; F0BB552927EF4D5700FA7E99 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -765,9 +1157,54 @@ }; name = Release; }; + F0FFD19A28549DC6001EE4A6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H8V3Y2JKY7; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.egbad.AgendaUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Agenda; + }; + name = Debug; + }; + F0FFD19B28549DC6001EE4A6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = H8V3Y2JKY7; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.egbad.AgendaUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Agenda; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + F0055BEF2854748F00430622 /* Build configuration list for PBXNativeTarget "AgendaTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F0055BF02854748F00430622 /* Debug */, + F0055BF12854748F00430622 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F0BB551227EF4D5400FA7E99 /* Build configuration list for PBXProject "Agenda" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -786,6 +1223,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F0FFD19928549DC6001EE4A6 /* Build configuration list for PBXNativeTarget "AgendaUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F0FFD19A28549DC6001EE4A6 /* Debug */, + F0FFD19B28549DC6001EE4A6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/Agenda.xcodeproj/project.xcworkspace/xcuserdata/egbad.xcuserdatad/UserInterfaceState.xcuserstate b/Agenda.xcodeproj/project.xcworkspace/xcuserdata/egbad.xcuserdatad/UserInterfaceState.xcuserstate index 2671267..8122639 100644 Binary files a/Agenda.xcodeproj/project.xcworkspace/xcuserdata/egbad.xcuserdatad/UserInterfaceState.xcuserstate and b/Agenda.xcodeproj/project.xcworkspace/xcuserdata/egbad.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Agenda.xcodeproj/xcshareddata/xcschemes/Agenda.xcscheme b/Agenda.xcodeproj/xcshareddata/xcschemes/Agenda.xcscheme new file mode 100644 index 0000000..abebfd0 --- /dev/null +++ b/Agenda.xcodeproj/xcshareddata/xcschemes/Agenda.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Agenda.xcodeproj/xcuserdata/egbad.xcuserdatad/xcschemes/xcschememanagement.plist b/Agenda.xcodeproj/xcuserdata/egbad.xcuserdatad/xcschemes/xcschememanagement.plist index 3e6b802..4ae47b9 100644 --- a/Agenda.xcodeproj/xcuserdata/egbad.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Agenda.xcodeproj/xcuserdata/egbad.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,23 @@ 0 + SuppressBuildableAutocreation + + F0055BE82854748F00430622 + + primary + + + F0BB551627EF4D5400FA7E99 + + primary + + + F0FFD19028549DC6001EE4A6 + + primary + + + diff --git a/Agenda/Application/AppCoordinator.swift b/Agenda/Application/AppCoordinator.swift index e4cde0d..6375826 100644 --- a/Agenda/Application/AppCoordinator.swift +++ b/Agenda/Application/AppCoordinator.swift @@ -35,7 +35,7 @@ private extension AppCoordinator { let context = AgendaContext(moduleOutput: nil, moduleDependency: coreDataManager) let container = AgendaContainer.assemble(with: context) - let agendaViewController = createNavController(viewController: container.viewController, itemName: Labels.goals, itemImage: "calendar") + let agendaViewController = createNavController(viewController: container.viewController, itemName: Labels.goals, itemImage: Icons.calendar) viewControllers.append(agendaViewController) subscribeToCoreDataManager(vc: container.viewController) } @@ -44,7 +44,7 @@ private extension AppCoordinator { let context = HistoryContext(moduleOutput: nil, moduleDependency: coreDataManager) let container = HistoryContainer.assemble(with: context) - let historyViewController = createNavController(viewController: container.viewController, itemName: Labels.History.title, itemImage: "clock.fill") + let historyViewController = createNavController(viewController: container.viewController, itemName: Labels.History.title, itemImage: Icons.history) viewControllers.append(historyViewController) subscribeToCoreDataManager(vc: container.viewController) } @@ -52,15 +52,15 @@ private extension AppCoordinator { func setupSummary() { let context = SummaryContext(moduleOutput: nil, moduleDependency: coreDataManager) let container = SummaryContainer.assemble(with: context) - let summaryViewController = createNavController(viewController: container.viewController, itemName: Labels.Summary.title, itemImage: "square.text.square.fill") + let summaryViewController = createNavController(viewController: container.viewController, itemName: Labels.Summary.title, itemImage: Icons.summary) viewControllers.append(summaryViewController) subscribeToCoreDataManager(vc: container.viewController) } - func createNavController(viewController: UIViewController, itemName: String, itemImage: String) -> UINavigationController { + func createNavController(viewController: UIViewController, itemName: String, itemImage: UIImage) -> UINavigationController { let navController = UINavigationController(rootViewController: viewController) - navController.tabBarItem = UITabBarItem(title: itemName, image: UIImage(named: itemImage), tag: 0) + navController.tabBarItem = UITabBarItem(title: itemName, image: itemImage, tag: 0) navController.navigationBar.prefersLargeTitles = true return navController } diff --git a/Agenda/Extensions/UIKit/UIAlertController.swift b/Agenda/Extensions/UIKit/UIAlertController.swift index 6162d0a..ce8182b 100644 --- a/Agenda/Extensions/UIKit/UIAlertController.swift +++ b/Agenda/Extensions/UIKit/UIAlertController.swift @@ -7,8 +7,7 @@ import UIKit -// MARK: this code is needed so that the console no longer displays an error about the allegedly negative width - +/// this code is needed so that the console no longer displays an error about the allegedly negative width on iOS 13 (14?) extension UIAlertController { func negativeWidthConstraint() { for subView in self.view.subviews { diff --git a/Agenda/Extensions/UIKit/UIButton.swift b/Agenda/Extensions/UIKit/UIButton.swift deleted file mode 100644 index 20ea449..0000000 --- a/Agenda/Extensions/UIKit/UIButton.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// UIButton.swift -// Agenda -// -// Created by Егор Бадмаев on 28.12.2021. -// - -import UIKit - -/// Кнопки из бывшего Stepper'a. Кастомного, на весь экран. 10.01.21 - ветка `stepper-v1` -/// Рано или поздно планирую всё же вернуть всё как было, поэтому не удаляю этот файл -/// 9-й тикет в проект в Notion'e - -extension UIButton { - convenience init(type: ButtonType, imageSystemName: String) { - self.init(type: type) - self.setImage(UIImage(systemName: imageSystemName), for: .normal) - self.backgroundColor = UIColor(red: 238/255, green: 238/255, blue: 238/255, alpha: 1) - self.tintColor = .black - self.layer.cornerRadius = 8 - } -} diff --git a/Agenda/Extensions/UIKit/UIViewController.swift b/Agenda/Extensions/UIKit/UIViewController.swift index 17327ce..bb8f9db 100644 --- a/Agenda/Extensions/UIKit/UIViewController.swift +++ b/Agenda/Extensions/UIKit/UIViewController.swift @@ -8,6 +8,30 @@ import UIKit extension UIViewController { + + func alertForError(title: String, message: String) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + let ok = UIAlertAction(title: "OK", style: .default, handler: nil) + alert.addAction(ok) + + present(alert, animated: true, completion: nil) + } + + func alertForDeletion(title: String, message: String, completion: @escaping () -> ()) { + let alert = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) + let yes = UIAlertAction(title: Labels.yes, style: .destructive, handler: { _ in + completion() + }) + let no = UIAlertAction(title: Labels.cancel, style: .default) + + alert.addAction(yes) + alert.addAction(no) + + /// for definition try to open declaration of this functions in Extensions/UIKit/UIAlertController.swift + alert.negativeWidthConstraint() + present(alert, animated: true) + } + func hideKeyboardWhenTappedAround() { let tap = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) tap.cancelsTouchesInView = false @@ -18,20 +42,4 @@ extension UIViewController { @objc private func dismissKeyboard() { view.endEditing(true) } - - func resize(_ cell: GoalTableViewCell, in tableView: UITableView, with textView: UITextView) { - let size = textView.bounds.size - let newSize = tableView.sizeThatFits(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude)) - - if size.height != newSize.height { - UIView.setAnimationsEnabled(false) - tableView.beginUpdates() - tableView.endUpdates() - UIView.setAnimationsEnabled(true) - // Scoll up your textview if required - if let thisIndexPath = tableView.indexPath(for: cell) { - tableView.scrollToRow(at: thisIndexPath, at: .bottom, animated: false) - } - } - } } diff --git a/Agenda/Extensions/UserDefaults.swift b/Agenda/Extensions/UserDefaults.swift deleted file mode 100644 index 12dfd6d..0000000 --- a/Agenda/Extensions/UserDefaults.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// UserDefaults.swift -// Agenda -// -// Created by Егор Бадмаев on 10.04.2022. -// - -import Foundation - -extension UserDefaults { - - private enum UserDefaultsKeys: String { - case hasOnboarded - } - - var hasOnboarded: Bool { - get { - bool(forKey: UserDefaultsKeys.hasOnboarded.rawValue) - } - set { - setValue(newValue, forKey: UserDefaultsKeys.hasOnboarded.rawValue) - } - } -} diff --git a/Agenda/Models/UserSettings.swift b/Agenda/Models/UserSettings.swift new file mode 100644 index 0000000..1f91096 --- /dev/null +++ b/Agenda/Models/UserSettings.swift @@ -0,0 +1,25 @@ +// +// UserSettings.swift +// Agenda +// +// Created by Егор Бадмаев on 10.06.2022. +// + +fileprivate enum SettingsKey: CodingKey { + case hasOnboarded + case summaries +} + +struct UserSettings { + private let storage = UserDefaultsContainer(keyedBy: SettingsKey.self) + + var hasOnboarded: Bool? { + get { storage[.hasOnboarded] } + set { storage[.hasOnboarded] = newValue } + } + + var summaries: [Int]? { + get { storage[.summaries] } + set { storage[.summaries] = newValue} + } +} diff --git a/Agenda/Modules/AddGoal/AddGoalInteractor.swift b/Agenda/Modules/AddGoal/AddGoalInteractor.swift index 39b73a7..9ae676d 100644 --- a/Agenda/Modules/AddGoal/AddGoalInteractor.swift +++ b/Agenda/Modules/AddGoal/AddGoalInteractor.swift @@ -21,7 +21,10 @@ final class AddGoalInteractor { extension AddGoalInteractor: AddGoalInteractorInput { func createGoal(goalData: GoalData) { - coreDataManager.createGoal(data: goalData, in: month) output?.goalDidCreate() + + DispatchQueue.global(qos: .userInitiated).async { [unowned self] in + coreDataManager.createGoal(data: goalData, in: month) + } } } diff --git a/Agenda/Modules/AddGoal/AddGoalViewController.swift b/Agenda/Modules/AddGoal/AddGoalViewController.swift index 3f965f0..ddab92d 100644 --- a/Agenda/Modules/AddGoal/AddGoalViewController.swift +++ b/Agenda/Modules/AddGoal/AddGoalViewController.swift @@ -7,30 +7,27 @@ import UIKit -final class AddGoalViewController: UIViewController { +final class AddGoalViewController: GoalViewController { private let output: AddGoalViewOutput - public var goalData = GoalData() { + public override var goalData: GoalData { didSet { checkBarButtonEnabled() } } - private lazy var doneBarButton: UIBarButtonItem = { + private lazy var closeButtonItem: UIBarButtonItem = { + let barButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(closeThisVC)) + barButton.accessibilityIdentifier = "cancelButtonItem" + return barButton + }() + private lazy var doneButtonItem: UIBarButtonItem = { let barButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped)) barButton.isEnabled = false + barButton.accessibilityIdentifier = "doneButtonItem" return barButton }() - private lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .insetGrouped) - tableView.allowsSelection = false - tableView.dataSource = self - tableView.register(GoalTableViewCell.self, forCellReuseIdentifier: GoalTableViewCell.identifier) - tableView.showsVerticalScrollIndicator = false - tableView.translatesAutoresizingMaskIntoConstraints = false - return tableView - }() init(output: AddGoalViewOutput) { self.output = output @@ -45,57 +42,15 @@ final class AddGoalViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + navigationItem.leftBarButtonItem = closeButtonItem + navigationItem.rightBarButtonItem = doneButtonItem title = Labels.Agenda.newGoal - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(closeThisVC)) - navigationItem.rightBarButtonItem = doneBarButton - - view.backgroundColor = .systemBackground - setupViewAndConstraints() - - // This methods is declared in Extensions/UIKit/UIViewController.swift - // It allows to hide keyboard when user taps in any place - hideKeyboardWhenTappedAround() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - registerForKeyboardNotifications() - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - - unregisterForKeyboardNotifications() } } extension AddGoalViewController: AddGoalViewInput { } -// MARK: - UITableView -extension AddGoalViewController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - 2 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - 2 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: GoalTableViewCell.identifier, for: indexPath) as? GoalTableViewCell - else { return GoalTableViewCell() } - cell.delegate = self - cell.configure(indexPath: indexPath) - return cell - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - UITableView.automaticDimension - } -} - // MARK: - Helper methods private extension AddGoalViewController { @objc func doneButtonTapped() { @@ -108,47 +63,9 @@ private extension AddGoalViewController { func checkBarButtonEnabled() { if !goalData.title.isEmpty, !goalData.current.isEmpty, !goalData.aim.isEmpty { - doneBarButton.isEnabled = true + doneButtonItem.isEnabled = true } else { - doneBarButton.isEnabled = false - } - } - - func registerForKeyboardNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - func unregisterForKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @objc func keyboardWillShow(notification: NSNotification) { - if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0) + doneButtonItem.isEnabled = false } } - - @objc func keyboardWillHide(notification: NSNotification) { - tableView.contentInset = .zero - } - - func setupViewAndConstraints() { - view.addSubview(tableView) - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ]) - } -} - -// MARK: - GoalTableViewCellDelegate -extension AddGoalViewController: GoalTableViewCellDelegate { - func updateHeightOfRow(_ cell: GoalTableViewCell, _ textView: UITextView) { - resize(cell, in: tableView, with: textView) // Update height of UITextView based on text's number of lines - } } diff --git a/Agenda/Modules/Agenda/AgendaContainer.swift b/Agenda/Modules/Agenda/AgendaContainer.swift index 28dd217..90dd9ba 100644 --- a/Agenda/Modules/Agenda/AgendaContainer.swift +++ b/Agenda/Modules/Agenda/AgendaContainer.swift @@ -16,7 +16,7 @@ final class AgendaContainer { let router = AgendaRouter() let interactor = AgendaInteractor(coreDataManager: context.moduleDependency) let presenter = AgendaPresenter(router: router, interactor: interactor) - let viewController = AgendaViewController(output: presenter, isAgenda: context.month == nil ? true : false) + let viewController = AgendaViewController(output: presenter, isAgenda: context.month == nil) presenter.view = viewController presenter.moduleOutput = context.moduleOutput @@ -45,5 +45,5 @@ struct AgendaContext { weak var moduleOutput: AgendaModuleOutput? let moduleDependency: ModuleDependency - var month: Month? = nil + var month: Month? } diff --git a/Agenda/Modules/Agenda/AgendaInteractor.swift b/Agenda/Modules/Agenda/AgendaInteractor.swift index f3f8b4f..bfcd20c 100644 --- a/Agenda/Modules/Agenda/AgendaInteractor.swift +++ b/Agenda/Modules/Agenda/AgendaInteractor.swift @@ -11,7 +11,8 @@ final class AgendaInteractor { weak var output: AgendaInteractorOutput? private let coreDataManager: CoreDataManagerProtocol - public var month: Month! // current month + /// current (Agenda) or selected (MonthDetails) month containing goals that will be displayed in this module + public var month: Month! init(coreDataManager: CoreDataManagerProtocol) { self.coreDataManager = coreDataManager @@ -20,17 +21,17 @@ final class AgendaInteractor { extension AgendaInteractor: AgendaInteractorInput { func fetchMonthGoals() { - if month == nil { + if month == nil { // if month was not provided (e.g. in Agenda module) self.month = coreDataManager.fetchCurrentMonth() } - guard let goals = self.month.goals?.array as? [Goal] else { + guard let goals = month.goals?.array as? [Goal] else { output?.dataDidNotFetch() return } - output?.monthDidFetch(goals: goals, date: month.date.formatTo("MMMMy")) + output?.monthDidFetch(viewModels: makeViewModels(goals), monthInfo: getMonthInfo(), date: month.date.formatTo("MMMMy")) } - func getGoalAt(_ indexPath: IndexPath) { + func getGoal(at indexPath: IndexPath) { guard let goal = month.goals?.object(at: indexPath.row) as? Goal else { output?.dataDidNotFetch() return @@ -39,13 +40,24 @@ extension AgendaInteractor: AgendaInteractorInput { } func replaceGoal(from a: Int, to b: Int) { - guard let goal = month.goals?.object(at: a) as? Goal else { return } - coreDataManager.replaceGoal(goal, in: month, from: a, to: b) + guard let goal = month.goals?.object(at: a) as? Goal else { + output?.dataDidNotFetch() + return + } + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let strongSelf = self else { return } + strongSelf.coreDataManager.replaceGoal(goal, in: strongSelf.month, from: a, to: b) + } } func deleteItem(at indexPath: IndexPath) { - guard let goal = month.goals?.object(at: indexPath.row) as? Goal else { return } - coreDataManager.deleteGoal(goal: goal) + guard let goal = month.goals?.object(at: indexPath.row) as? Goal else { + output?.dataDidNotFetch() + return + } + DispatchQueue.global(qos: .utility).async { [weak coreDataManager] in + coreDataManager?.deleteGoal(goal: goal) + } } func provideDataForAdding() { @@ -53,8 +65,31 @@ extension AgendaInteractor: AgendaInteractorInput { } func checkForOnboarding() { - if !UserDefaults.standard.hasOnboarded { + let settings = UserSettings() + if let hasOnboarded = settings.hasOnboarded, !hasOnboarded { output?.showOnboarding() } } } + +// MARK: - Helper methods +private extension AgendaInteractor { + func getMonthInfo() -> DateViewModel { + let date = Date() + let dateFormatter = DateFormatter() + dateFormatter.setLocalizedDateFormatFromTemplate("MMMM d") + + let calendar = Calendar.current + let days = calendar.range(of: .day, in: .month, for: date)!.count // all days in current month + + return DateViewModel(dayAndMonth: dateFormatter.string(from: date), + year: calendar.dateComponents([.year], from: date).year ?? 0, + progress: Float(calendar.dateComponents([.day], from: date).day!) / Float(days)) + } + + func makeViewModels(_ goals: [Goal]) -> [GoalViewModel] { + return goals.map { goal in + GoalViewModel(name: goal.name, current: Int(goal.current), aim: Int(goal.aim)) + } + } +} diff --git a/Agenda/Modules/Agenda/AgendaPresenter.swift b/Agenda/Modules/Agenda/AgendaPresenter.swift index 383c961..108a29a 100644 --- a/Agenda/Modules/Agenda/AgendaPresenter.swift +++ b/Agenda/Modules/Agenda/AgendaPresenter.swift @@ -36,11 +36,11 @@ extension AgendaPresenter: AgendaViewOutput { interactor.checkForOnboarding() } - func didSelectRowAt(_ indexPath: IndexPath) { - interactor.getGoalAt(indexPath) + func didSelectRow(at indexPath: IndexPath) { + interactor.getGoal(at: indexPath) } - func moveRowAt(from sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + func moveRow(from sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { interactor.replaceGoal(from: sourceIndexPath.row, to: destinationIndexPath.row) } @@ -50,12 +50,12 @@ extension AgendaPresenter: AgendaViewOutput { } extension AgendaPresenter: AgendaInteractorOutput { - func monthDidFetch(goals: [Goal], date: String) { - view?.setMonthData(viewModels: makeViewModels(goals), monthInfo: getMonthInfo(), title: date) + func monthDidFetch(viewModels: [GoalViewModel], monthInfo: DateViewModel, date: String) { + view?.setMonthData(viewModels: viewModels, monthInfo: monthInfo, title: date) } func dataDidNotFetch() { - view?.showAlert(title: Labels.oopsError, message: Labels.Summary.fetchErrorDescription) + view?.showAlert(title: Labels.oopsError, message: Labels.Agenda.unknownErrorDescription) } func showAddGoalModuleWith(month: Month, moduleDependency: CoreDataManagerProtocol) { @@ -70,25 +70,3 @@ extension AgendaPresenter: AgendaInteractorOutput { router.showOnboarding() } } - -// MARK: - Helper methods -private extension AgendaPresenter { - func getMonthInfo() -> DateViewModel { - let date = Date() - let dateFormatter = DateFormatter() - dateFormatter.setLocalizedDateFormatFromTemplate("MMMM d") - - let calendar = Calendar.current - let days = calendar.range(of: .day, in: .month, for: date)!.count // all days in current month - - return DateViewModel(dayAndMonth: dateFormatter.string(from: date), - year: calendar.dateComponents([.year], from: date).year ?? 0, - progress: Float(calendar.dateComponents([.day], from: date).day!) / Float(days)) - } - - func makeViewModels(_ goals: [Goal]) -> [GoalViewModel] { - return goals.map { goal in - GoalViewModel(name: goal.name, current: Int(goal.current), aim: Int(goal.aim)) - } - } -} diff --git a/Agenda/Modules/Agenda/AgendaProtocols.swift b/Agenda/Modules/Agenda/AgendaProtocols.swift index 5b1cf3b..91ab3f8 100644 --- a/Agenda/Modules/Agenda/AgendaProtocols.swift +++ b/Agenda/Modules/Agenda/AgendaProtocols.swift @@ -26,14 +26,14 @@ protocol AgendaViewOutput: AnyObject { func addNewGoal() func checkForOnboarding() - func didSelectRowAt(_ indexPath: IndexPath) - func moveRowAt(from sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) + func didSelectRow(at indexPath: IndexPath) + func moveRow(from sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) func deleteItem(at indexPath: IndexPath) } protocol AgendaInteractorInput: AnyObject { func fetchMonthGoals() - func getGoalAt(_ indexPath: IndexPath) + func getGoal(at indexPath: IndexPath) func replaceGoal(from a: Int, to b: Int) func deleteItem(at indexPath: IndexPath) @@ -42,7 +42,7 @@ protocol AgendaInteractorInput: AnyObject { } protocol AgendaInteractorOutput: AnyObject { - func monthDidFetch(goals: [Goal], date: String) + func monthDidFetch(viewModels: [GoalViewModel], monthInfo: DateViewModel, date: String) func dataDidNotFetch() func showAddGoalModuleWith(month: Month, moduleDependency: CoreDataManagerProtocol) diff --git a/Agenda/Modules/Agenda/ViewModels/DateViewModel.swift b/Agenda/Modules/Agenda/ViewModels/DateViewModel.swift index 54b84c9..ec87a87 100644 --- a/Agenda/Modules/Agenda/ViewModels/DateViewModel.swift +++ b/Agenda/Modules/Agenda/ViewModels/DateViewModel.swift @@ -10,12 +10,6 @@ struct DateViewModel { // to display month info (date and progress bar in Agenda var year: String var progress: Float - init() { - self.dayAndMonth = "" - self.year = "" - self.progress = 0.0 - } - init(dayAndMonth: String, year: Int, progress: Float) { self.dayAndMonth = dayAndMonth self.year = ", \(year)" diff --git a/Agenda/Modules/Agenda/Views/AgendaTableViewCell.swift b/Agenda/Modules/Agenda/Views/AgendaTableViewCell.swift index 34c7362..7480090 100644 --- a/Agenda/Modules/Agenda/Views/AgendaTableViewCell.swift +++ b/Agenda/Modules/Agenda/Views/AgendaTableViewCell.swift @@ -15,6 +15,7 @@ final class AgendaTableViewCell: UITableViewCell { let label = UILabel() label.font = UIFont.systemFont(ofSize: 18) label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "titleLabel" return label }() private lazy var goalProgressView: UIProgressView = { @@ -22,20 +23,23 @@ final class AgendaTableViewCell: UITableViewCell { // progressView.progressTintColor = UIColor(red: 69/255, green: 208/255, blue: 100/255, alpha: 1) progressView.progressTintColor = .systemGreen progressView.translatesAutoresizingMaskIntoConstraints = false + progressView.accessibilityIdentifier = "progressView" return progressView }() private lazy var goalCurrentLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 16) + label.accessibilityIdentifier = "currentLabel" return label }() - private lazy var goalEndLabel: UILabel = { + private lazy var goalAimLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 16) + label.accessibilityIdentifier = "aimLabel" return label }() private lazy var labelsStackView: UIStackView = { - let stackView = UIStackView(arrangedSubviews: [goalCurrentLabel, goalEndLabel]) + let stackView = UIStackView(arrangedSubviews: [goalCurrentLabel, goalAimLabel]) stackView.axis = .horizontal stackView.distribution = .equalSpacing stackView.translatesAutoresizingMaskIntoConstraints = false @@ -53,6 +57,8 @@ final class AgendaTableViewCell: UITableViewCell { } private func setupView() { + accessibilityIdentifier = "AgendaTableViewCell" + contentView.addSubview(goalTextLabel) contentView.addSubview(goalProgressView) contentView.addSubview(labelsStackView) @@ -76,7 +82,7 @@ final class AgendaTableViewCell: UITableViewCell { public func configure(goal: GoalViewModel) { goalTextLabel.text = goal.name goalCurrentLabel.text = goal.current - goalEndLabel.text = goal.aim + goalAimLabel.text = goal.aim goalProgressView.progress = goal.progress } } diff --git a/Agenda/Modules/Agenda/Views/AgendaViewController.swift b/Agenda/Modules/Agenda/Views/AgendaViewController.swift index 9a6e99c..9e97c16 100644 --- a/Agenda/Modules/Agenda/Views/AgendaViewController.swift +++ b/Agenda/Modules/Agenda/Views/AgendaViewController.swift @@ -8,27 +8,36 @@ import UIKit final class AgendaViewController: UIViewController { - + private let output: AgendaViewOutput private var viewModels = [GoalViewModel]() - public let isAgenda: Bool // depending on this property, month data is displayed - + /// Depending on this property, month data is being displayed + public let isAgenda: Bool + + private lazy var addButtonItem: UIBarButtonItem = { + let barButtomItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewGoal)) + barButtomItem.accessibilityIdentifier = "addBarButton" + return barButtomItem + }() private let dayAndMonth: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 24, weight: .bold) label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "dayAndMonthLabel" return label }() private let yearLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 24) label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "yearLabel" return label }() private let monthProgressView: UIProgressView = { let progress = UIProgressView() progress.translatesAutoresizingMaskIntoConstraints = false + progress.accessibilityIdentifier = "monthProgressView" return progress }() private let separatorView = SeparatorView() @@ -41,6 +50,7 @@ final class AgendaViewController: UIViewController { tableView.register(AgendaTableViewCell.self, forCellReuseIdentifier: AgendaTableViewCell.identifier) tableView.showsVerticalScrollIndicator = false tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.accessibilityIdentifier = "tableView" return tableView }() @@ -58,7 +68,6 @@ final class AgendaViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemBackground setupView() setConstraints() } @@ -66,7 +75,12 @@ final class AgendaViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - output.fetchData() // in month details mode there is no possibility to update view models, so fetching data is now here + /** + There is no possibility to update view models in month details, when you change goal details. + So, the only solution we have is to update every time `viewWillAppear` method is being called. + This will not affect negatively on the app's perfomance, because in Agenda this method is called only one time and month details is used by user rarely. + */ + output.fetchData() } override func viewDidAppear(_ animated: Bool) { @@ -105,7 +119,7 @@ private extension AgendaViewController { func setupView() { if isAgenda { - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewGoal)) + navigationItem.rightBarButtonItem = addButtonItem navigationItem.leftBarButtonItem = editButtonItem view.addSubview(monthProgressView) @@ -116,6 +130,7 @@ private extension AgendaViewController { } view.addSubview(separatorView) view.addSubview(tableView) + view.backgroundColor = .systemBackground } func setConstraints() { @@ -173,7 +188,7 @@ extension AgendaViewController: UITableViewDelegate, UITableViewDataSource { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - output.didSelectRowAt(indexPath) + output.didSelectRow(at: indexPath) } override func setEditing(_ editing: Bool, animated: Bool) { @@ -182,26 +197,18 @@ extension AgendaViewController: UITableViewDelegate, UITableViewDataSource { } func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { - output.moveRowAt(from: sourceIndexPath, to: destinationIndexPath) + output.moveRow(from: sourceIndexPath, to: destinationIndexPath) } func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { - let alert = UIAlertController(title: Labels.Agenda.deleteGoalTitle, message: Labels.Agenda.deleteGoalDescription, preferredStyle: .actionSheet) - let yes = UIAlertAction(title: Labels.yes, style: .destructive, handler: { [weak self] _ in + alertForDeletion(title: Labels.Agenda.deleteGoalTitle, message: Labels.Agenda.deleteGoalDescription) { [weak self] in guard let strongSelf = self else { return } strongSelf.output.deleteItem(at: indexPath) strongSelf.viewModels.remove(at: indexPath.row) tableView.deleteRows(at: [indexPath], with: .automatic) - }) - let no = UIAlertAction(title: Labels.cancel, style: .default) - - alert.addAction(yes) - alert.addAction(no) - - alert.negativeWidthConstraint() // for definition try to open declaration of this functions in Extensions/UIKit/UIAlertController.swift - present(alert, animated: true) + } } } } diff --git a/Agenda/Modules/GoalDetails/GoalDetailsInteractor.swift b/Agenda/Modules/GoalDetails/GoalDetailsInteractor.swift index 81102ef..0ba1d3f 100644 --- a/Agenda/Modules/GoalDetails/GoalDetailsInteractor.swift +++ b/Agenda/Modules/GoalDetails/GoalDetailsInteractor.swift @@ -25,16 +25,20 @@ extension GoalDetailsInteractor: GoalDetailsInteractorInput { } func rewriteGoal(with data: GoalData) { - coreDataManager.rewriteGoal(with: data, in: goal) output?.goalDidRewrite() + + DispatchQueue.global(qos: .userInitiated).async { [unowned self] in + coreDataManager.rewriteGoal(with: data, in: goal) + } } - func checkBarButtonEnabled(goalData: GoalData) -> Bool { - if !goalData.title.isEmpty, !goalData.current.isEmpty, !goalData.aim.isEmpty { - if goalData.title != goal.name || goalData.current != String(goal.current) || goalData.aim != String(goal.aim) || goalData.notes != goal.notes { - return true - } + func checkBarButtonEnabled(goalData: GoalData) { + if !goalData.title.isEmpty, !goalData.current.isEmpty, !goalData.aim.isEmpty, + (goalData.title != goal.name || goalData.current != String(goal.current) || + goalData.aim != String(goal.aim) || goalData.notes != goal.notes) { + output?.barButtonDidCheck(with: true) + } else { + output?.barButtonDidCheck(with: false) } - return false } } diff --git a/Agenda/Modules/GoalDetails/GoalDetailsPresenter.swift b/Agenda/Modules/GoalDetails/GoalDetailsPresenter.swift index 6836f99..cf29ad5 100644 --- a/Agenda/Modules/GoalDetails/GoalDetailsPresenter.swift +++ b/Agenda/Modules/GoalDetails/GoalDetailsPresenter.swift @@ -32,7 +32,7 @@ extension GoalDetailsPresenter: GoalDetailsViewOutput { interactor.rewriteGoal(with: data) } - func checkBarButtonEnabled(goalData: GoalData) -> Bool { + func checkBarButtonEnabled(goalData: GoalData) { interactor.checkBarButtonEnabled(goalData: goalData) } } @@ -45,4 +45,8 @@ extension GoalDetailsPresenter: GoalDetailsInteractorOutput { func goalDidLoad(goalData: GoalData) { view?.setViewModel(goalData: goalData) } + + func barButtonDidCheck(with flag: Bool) { + view?.updateBarButton(with: flag) + } } diff --git a/Agenda/Modules/GoalDetails/GoalDetailsProtocols.swift b/Agenda/Modules/GoalDetails/GoalDetailsProtocols.swift index 6fe91d0..bd89ac5 100644 --- a/Agenda/Modules/GoalDetails/GoalDetailsProtocols.swift +++ b/Agenda/Modules/GoalDetails/GoalDetailsProtocols.swift @@ -17,6 +17,7 @@ protocol GoalDetailsModuleOutput: AnyObject { protocol GoalDetailsViewInput: AnyObject { func setViewModel(goalData: GoalData) + func updateBarButton(with flag: Bool) func presentSuccess() } @@ -24,18 +25,19 @@ protocol GoalDetailsViewOutput: AnyObject { func viewDidLoad() func saveButtonTapped(data: GoalData) - func checkBarButtonEnabled(goalData: GoalData) -> Bool + func checkBarButtonEnabled(goalData: GoalData) } protocol GoalDetailsInteractorInput: AnyObject { func provideData() func rewriteGoal(with data: GoalData) - func checkBarButtonEnabled(goalData: GoalData) -> Bool + func checkBarButtonEnabled(goalData: GoalData) } protocol GoalDetailsInteractorOutput: AnyObject { func goalDidLoad(goalData: GoalData) func goalDidRewrite() + func barButtonDidCheck(with flag: Bool) } protocol GoalDetailsRouterInput: AnyObject { diff --git a/Agenda/Modules/GoalDetails/GoalDetailsViewController.swift b/Agenda/Modules/GoalDetails/GoalDetailsViewController.swift index fb483e6..582abc2 100644 --- a/Agenda/Modules/GoalDetails/GoalDetailsViewController.swift +++ b/Agenda/Modules/GoalDetails/GoalDetailsViewController.swift @@ -8,19 +8,20 @@ import UIKit import SPIndicator -final class GoalDetailsViewController: UIViewController { +final class GoalDetailsViewController: GoalViewController { private let output: GoalDetailsViewOutput - public var goalData = GoalData() { + public override var goalData: GoalData { didSet { - saveBarButton.isEnabled = output.checkBarButtonEnabled(goalData: goalData) + output.checkBarButtonEnabled(goalData: goalData) } } - private let saveBarButton: UIBarButtonItem = { + private lazy var saveBarButton: UIBarButtonItem = { let barButton = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveButtonTapped)) barButton.isEnabled = false + barButton.accessibilityIdentifier = "saveBarButton" return barButton }() private let indicatorView: SPIndicatorView = { @@ -30,16 +31,6 @@ final class GoalDetailsViewController: UIViewController { return indicatorView }() - private lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .insetGrouped) - tableView.allowsSelection = false - tableView.dataSource = self - tableView.showsVerticalScrollIndicator = false - tableView.register(GoalTableViewCell.self, forCellReuseIdentifier: GoalTableViewCell.identifier) - tableView.translatesAutoresizingMaskIntoConstraints = false - return tableView - }() - init(output: GoalDetailsViewOutput) { self.output = output @@ -56,52 +47,36 @@ final class GoalDetailsViewController: UIViewController { navigationItem.largeTitleDisplayMode = .never title = Labels.Agenda.details navigationItem.rightBarButtonItem = saveBarButton - view.backgroundColor = .systemGroupedBackground - // This methods is declared in Extensions/UIKit/UIViewController.swift - // It allows to hide keyboard when user taps in any place - hideKeyboardWhenTappedAround() - - setupViewAndConstraints() output.viewDidLoad() } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - registerForKeyboardNotifications() - } - override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - unregisterForKeyboardNotifications() indicatorView.dismiss() } } +// MARK: - ViewInput extension GoalDetailsViewController: GoalDetailsViewInput { func setViewModel(goalData: GoalData) { self.goalData = goalData tableView.reloadData() } + func updateBarButton(with flag: Bool) { + saveBarButton.isEnabled = flag + } + func presentSuccess() { indicatorView.present(haptic: .success) } } // MARK: - UITableView -extension GoalDetailsViewController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - 2 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - 2 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +extension GoalDetailsViewController { + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: GoalTableViewCell.identifier, for: indexPath) as? GoalTableViewCell else { return GoalTableViewCell() } cell.goal = goalData @@ -109,56 +84,13 @@ extension GoalDetailsViewController: UITableViewDataSource { cell.configure(indexPath: indexPath) return cell } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - UITableView.automaticDimension - } } // MARK: - Helper methods private extension GoalDetailsViewController { - @objc func saveButtonTapped() { view.endEditing(true) saveBarButton.isEnabled = false output.saveButtonTapped(data: goalData) } - - func setupViewAndConstraints() { - view.addSubview(tableView) - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ]) - } - - func registerForKeyboardNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - func unregisterForKeyboardNotifications() { - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @objc func keyboardWillShow(notification: NSNotification) { - if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0) - } - } - - @objc func keyboardWillHide(notification: NSNotification) { - tableView.contentInset = .zero - } -} - -// MARK: - GoalTableViewCellDelegate -extension GoalDetailsViewController: GoalTableViewCellDelegate { - func updateHeightOfRow(_ cell: GoalTableViewCell, _ textView: UITextView) { - resize(cell, in: tableView, with: textView) // Update height of UITextView based on text's number of lines - } } diff --git a/Agenda/Modules/History/HistoryInteractor.swift b/Agenda/Modules/History/HistoryInteractor.swift index 8654553..3a0e3c5 100644 --- a/Agenda/Modules/History/HistoryInteractor.swift +++ b/Agenda/Modules/History/HistoryInteractor.swift @@ -25,16 +25,26 @@ extension HistoryInteractor: HistoryInteractorInput { return } self.months = months - output?.dataDidFetch(months: months) + output?.dataDidFetch(viewModels: makeViewModels(months)) } - func didSelectRowAt(_ indexPath: IndexPath) { + func openDetailsByMonth(at indexPath: IndexPath) { let month = months[indexPath.row] output?.showMonthDetailsModule(month: month, moduleDependency: coreDataManager) } - func deleteMonthAt(_ indexPath: IndexPath) { - coreDataManager.deleteMonth(month: months[indexPath.row]) - months.remove(at: indexPath.row) + func deleteMonth(at indexPath: IndexPath) { + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let strongSelf = self else { return } + strongSelf.coreDataManager.deleteMonth(month: strongSelf.months[indexPath.row]) + strongSelf.months.remove(at: indexPath.row) + } + } +} + +// MARK: - Helper methods +private extension HistoryInteractor { + func makeViewModels(_ months: [Month]) -> [MonthViewModel] { + return months.map { MonthViewModel(month: $0) } } } diff --git a/Agenda/Modules/History/HistoryPresenter.swift b/Agenda/Modules/History/HistoryPresenter.swift index a452c0d..e03f81c 100644 --- a/Agenda/Modules/History/HistoryPresenter.swift +++ b/Agenda/Modules/History/HistoryPresenter.swift @@ -28,18 +28,18 @@ extension HistoryPresenter: HistoryViewOutput { interactor.performFetch() } - func didSelectRowAt(_ indexPath: IndexPath) { - interactor.didSelectRowAt(indexPath) + func didSelectRow(at indexPath: IndexPath) { + interactor.openDetailsByMonth(at: indexPath) } - func deleteItemAt(_ indexPath: IndexPath) { - interactor.deleteMonthAt(indexPath) + func deleteItem(at indexPath: IndexPath) { + interactor.deleteMonth(at: indexPath) } } extension HistoryPresenter: HistoryInteractorOutput { - func dataDidFetch(months: [Month]) { - view?.setData(viewModels: makeViewModels(months)) + func dataDidFetch(viewModels: [MonthViewModel]) { + view?.setData(viewModels: viewModels) } func dataDidNotFetch() { @@ -50,10 +50,3 @@ extension HistoryPresenter: HistoryInteractorOutput { router.showMonthDetailsModule(month: month, moduleDependency: moduleDependency) } } - -// MARK: - Helper methods -private extension HistoryPresenter { - func makeViewModels(_ months: [Month]) -> [MonthViewModel] { - return months.map { MonthViewModel(month: $0) } - } -} diff --git a/Agenda/Modules/History/HistoryProtocols.swift b/Agenda/Modules/History/HistoryProtocols.swift index 18a3741..0581740 100644 --- a/Agenda/Modules/History/HistoryProtocols.swift +++ b/Agenda/Modules/History/HistoryProtocols.swift @@ -23,18 +23,18 @@ protocol HistoryViewInput: AnyObject { protocol HistoryViewOutput: AnyObject { func fetchData() - func didSelectRowAt(_ indexPath: IndexPath) - func deleteItemAt(_ indexPath: IndexPath) + func didSelectRow(at indexPath: IndexPath) + func deleteItem(at indexPath: IndexPath) } protocol HistoryInteractorInput: AnyObject { func performFetch() - func didSelectRowAt(_ indexPath: IndexPath) - func deleteMonthAt(_ indexPath: IndexPath) + func openDetailsByMonth(at indexPath: IndexPath) + func deleteMonth(at indexPath: IndexPath) } protocol HistoryInteractorOutput: AnyObject { - func dataDidFetch(months: [Month]) + func dataDidFetch(viewModels: [MonthViewModel]) func dataDidNotFetch() func showMonthDetailsModule(month: Month, moduleDependency: CoreDataManagerProtocol) diff --git a/Agenda/Modules/History/View/HistoryTableViewCell.swift b/Agenda/Modules/History/Views/HistoryTableViewCell.swift similarity index 100% rename from Agenda/Modules/History/View/HistoryTableViewCell.swift rename to Agenda/Modules/History/Views/HistoryTableViewCell.swift diff --git a/Agenda/Modules/History/View/HistoryViewController.swift b/Agenda/Modules/History/Views/HistoryViewController.swift similarity index 82% rename from Agenda/Modules/History/View/HistoryViewController.swift rename to Agenda/Modules/History/Views/HistoryViewController.swift index 9c27f6b..2cd5410 100644 --- a/Agenda/Modules/History/View/HistoryViewController.swift +++ b/Agenda/Modules/History/Views/HistoryViewController.swift @@ -26,17 +26,12 @@ final class HistoryViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() - navigationItem.rightBarButtonItem = editButtonItem - title = Labels.History.title - view.backgroundColor = .systemBackground - - tableView.register(HistoryTableViewCell.self, forCellReuseIdentifier: HistoryTableViewCell.identifier) - tableView.showsVerticalScrollIndicator = false - + setupView() output.fetchData() } } +// MARK: - ViewInput extension HistoryViewController: HistoryViewInput { func showAlert(title: String, message: String) { alertForError(title: title, message: message) @@ -81,7 +76,7 @@ extension HistoryViewController { if indexPath == [0, 0] { // current month tabBarController?.selectedIndex = 0 } else { - output.didSelectRowAt(indexPath) + output.didSelectRow(at: indexPath) } } @@ -100,25 +95,29 @@ extension HistoryViewController { override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { - let alert = UIAlertController(title: Labels.History.deleteMonthTitle, message: Labels.History.deleteMonthDescription, preferredStyle: .actionSheet) - let yes = UIAlertAction(title: Labels.yes, style: .destructive, handler: { [weak self] _ in + alertForDeletion(title: Labels.History.deleteMonthTitle, message: Labels.History.deleteMonthDescription) { [weak self] in guard let strongSelf = self else { return } - strongSelf.output.deleteItemAt(indexPath) + strongSelf.output.deleteItem(at: indexPath) strongSelf.viewModels.remove(at: indexPath.row) tableView.deleteRows(at: [indexPath], with: .automatic) - }) - let no = UIAlertAction(title: Labels.cancel, style: .default) - - alert.addAction(yes) - alert.addAction(no) - - alert.negativeWidthConstraint() // for definition try to open declaration of this functions in Extensions/UIKit/UIAlertController.swift - present(alert, animated: true) + } } } } +// MARK: - Helper methods +private extension HistoryViewController { + func setupView() { + navigationItem.rightBarButtonItem = editButtonItem + title = Labels.History.title + view.backgroundColor = .systemBackground + + tableView.register(HistoryTableViewCell.self, forCellReuseIdentifier: HistoryTableViewCell.identifier) + tableView.showsVerticalScrollIndicator = false + } +} + // MARK: - CoreDataManagerDelegate extension HistoryViewController: CoreDataManagerDelegate { func updateViewModel() { diff --git a/Agenda/Modules/Onboarding/OnboardingInteractor.swift b/Agenda/Modules/Onboarding/OnboardingInteractor.swift index 6b06c70..8ab6581 100644 --- a/Agenda/Modules/Onboarding/OnboardingInteractor.swift +++ b/Agenda/Modules/Onboarding/OnboardingInteractor.swift @@ -13,7 +13,11 @@ final class OnboardingInteractor { extension OnboardingInteractor: OnboardingInteractorInput { func setHasOnboarded() { - UserDefaults.standard.hasOnboarded = true + var settings = UserSettings() + settings.hasOnboarded = true output?.hasOnboardedDidSet() + + /// Set default what kind of summary data will be displayed to the user + settings.summaries = [SummaryKind.percentOfSetGoals.rawValue, SummaryKind.completedGoals.rawValue, SummaryKind.uncompletedGoals.rawValue, SummaryKind.allGoals.rawValue] } } diff --git a/Agenda/Modules/Onboarding/OnboardingViewModel.swift b/Agenda/Modules/Onboarding/OnboardingViewModel.swift new file mode 100644 index 0000000..2997988 --- /dev/null +++ b/Agenda/Modules/Onboarding/OnboardingViewModel.swift @@ -0,0 +1,14 @@ +// +// OnboardingViewModel.swift +// Agenda +// +// Created by Егор Бадмаев on 17.06.2022. +// + +import UIKit + +struct OnboardingViewModel { + let title: String + let description: String + let image: UIImage +} diff --git a/Agenda/Modules/Onboarding/Views/OnboardingTableView.swift b/Agenda/Modules/Onboarding/Views/OnboardingTableView.swift index 6bbee17..a9908de 100644 --- a/Agenda/Modules/Onboarding/Views/OnboardingTableView.swift +++ b/Agenda/Modules/Onboarding/Views/OnboardingTableView.swift @@ -24,9 +24,10 @@ final class OnboardingTableView: UITableView { fatalError("init(coder:) has not been implemented") } - // Since each row of the table view has its own height, which cannot be obtained, - // it is necessary to somehow set the height for the table view, because without this it simply does not display. - // This code allows to set table view contentSize correctly, so there's no need for using heightAnchor + /** + Since each row of the table view has its own height, which cannot be obtained, it is necessary to somehow set the height for the table view, because without this it simply does not display. + This code allows to set table view `contentSize` correctly, so there's no need for using `heightAnchor` + */ override var contentSize: CGSize { didSet { invalidateIntrinsicContentSize() diff --git a/Agenda/Modules/Onboarding/Views/OnboardingTableViewCell.swift b/Agenda/Modules/Onboarding/Views/OnboardingTableViewCell.swift index 315d2df..53cdc10 100644 --- a/Agenda/Modules/Onboarding/Views/OnboardingTableViewCell.swift +++ b/Agenda/Modules/Onboarding/Views/OnboardingTableViewCell.swift @@ -11,33 +11,36 @@ final class OnboardingTableViewCell: UITableViewCell { static let identifier = "onboardingCell" - lazy var iconImageView: UIImageView = { + private lazy var iconImageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit imageView.tintColor = .systemRed imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.accessibilityIdentifier = "onboardingIconImageView" return imageView }() - private lazy var labelsStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [titleLabel, descriptionLabel]) stackView.axis = .vertical stackView.spacing = 2 stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.accessibilityIdentifier = "onboardingLabelsStackView" return stackView }() - lazy var titleLabel: UILabel = { + private lazy var titleLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 label.font = UIFont.systemFont(ofSize: 16, weight: .bold) label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "onboardingTitleLabel" return label }() - lazy var descriptionLabel: UILabel = { + private lazy var descriptionLabel: UILabel = { let label = UILabel() label.numberOfLines = 0 label.font = UIFont.systemFont(ofSize: 16) label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "onboardingDescriptionLabel" return label }() @@ -52,7 +55,8 @@ final class OnboardingTableViewCell: UITableViewCell { } private func setupViewAndConstraints() { - contentView.backgroundColor = .clear + backgroundColor = .clear + accessibilityIdentifier = "OnboardingTableViewCell" contentView.addSubview(iconImageView) contentView.addSubview(labelsStackView) @@ -69,4 +73,10 @@ final class OnboardingTableViewCell: UITableViewCell { labelsStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10) ]) } + + public func configure(with viewModel: OnboardingViewModel) { + titleLabel.text = viewModel.title + descriptionLabel.text = viewModel.description + iconImageView.image = viewModel.image + } } diff --git a/Agenda/Modules/Onboarding/Views/OnboardingViewController.swift b/Agenda/Modules/Onboarding/Views/OnboardingViewController.swift index c63da2a..9ac5f4f 100644 --- a/Agenda/Modules/Onboarding/Views/OnboardingViewController.swift +++ b/Agenda/Modules/Onboarding/Views/OnboardingViewController.swift @@ -10,20 +10,23 @@ import UIKit final class OnboardingViewController: UIViewController { private let output: OnboardingViewOutput - - private let titlesArray = [Labels.Onboarding.title1, Labels.Onboarding.title2, Labels.Onboarding.title3] - private let descriptionsArray = [Labels.Onboarding.description1, Labels.Onboarding.description2, Labels.Onboarding.description3] - private let imagePathsArray = ["lightbulb", "chart.bar.doc.horizontal", "note.text.badge.plus"] + private let viewModels = [ + OnboardingViewModel(title: Labels.Onboarding.title1, description: Labels.Onboarding.description1, image: Icons.lightbulb), + OnboardingViewModel(title: Labels.Onboarding.title2, description: Labels.Onboarding.description2, image: Icons.chartBarDoc), + OnboardingViewModel(title: Labels.Onboarding.title3, description: Labels.Onboarding.description3, image: Icons.notesTextBadgePlus) + ] private let scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.showsVerticalScrollIndicator = false scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.accessibilityIdentifier = "onboardingScrollView" return scrollView }() private let contentView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false + view.accessibilityIdentifier = "onboardingContentView" return view }() @@ -34,11 +37,13 @@ final class OnboardingViewController: UIViewController { label.numberOfLines = 0 label.font = UIFont.systemFont(ofSize: 36, weight: .heavy) label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "welcomeLabel" return label }() private lazy var tableView: OnboardingTableView = { let tableView = OnboardingTableView(frame: .zero, style: .insetGrouped) tableView.dataSource = self + tableView.accessibilityIdentifier = "onboardingTableView" return tableView }() @@ -46,6 +51,7 @@ final class OnboardingViewController: UIViewController { let view = UIView() view.backgroundColor = .systemBackground view.translatesAutoresizingMaskIntoConstraints = false + view.accessibilityIdentifier = "onboardingButtonView" return view }() private lazy var continueButton: UIButton = { @@ -55,8 +61,9 @@ final class OnboardingViewController: UIViewController { button.layer.zPosition = 1 button.addTarget(self, action: #selector(continueButtonTapped), for: .touchUpInside) button.backgroundColor = .systemRed - button.translatesAutoresizingMaskIntoConstraints = false button.layer.cornerRadius = 10 + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "onboardingButton" return button }() @@ -73,9 +80,6 @@ final class OnboardingViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemBackground - isModalInPresentation = true - setupView() setConstraints() } @@ -92,18 +96,20 @@ private extension OnboardingViewController { } func setupView() { + isModalInPresentation = true + view.backgroundColor = .systemBackground view.addSubview(scrollView) - scrollView.addSubview(contentView) + scrollView.addSubview(contentView) contentView.addSubview(welcomeLabel) - let labelText = welcomeLabel.text ?? "" - let labelAttributedText = NSMutableAttributedString(string: labelText) - let index = labelText.lastIndex(of: "A") ?? labelText.startIndex - labelAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemRed, range: NSRange(location: (labelText.distance(from: labelText.startIndex, to: index)), length: 6)) - welcomeLabel.attributedText = labelAttributedText - contentView.addSubview(tableView) + let labelAttributedText = NSMutableAttributedString(string: welcomeLabel.text ?? "") + let agendaAttributedText = NSMutableAttributedString(string: "Agenda") + agendaAttributedText.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemRed, range: NSRange(location: 0, length: 6)) + labelAttributedText.append(agendaAttributedText) + welcomeLabel.attributedText = labelAttributedText + view.addSubview(backgroundButtonView) backgroundButtonView.addSubview(continueButton) } @@ -148,7 +154,7 @@ private extension OnboardingViewController { extension OnboardingViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { - 3 + viewModels.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -158,10 +164,7 @@ extension OnboardingViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: OnboardingTableViewCell.identifier, for: indexPath) as? OnboardingTableViewCell else { return OnboardingTableViewCell() } - cell.backgroundColor = .clear - cell.iconImageView.image = UIImage(named: imagePathsArray[indexPath.section]) - cell.titleLabel.text = titlesArray[indexPath.section] - cell.descriptionLabel.text = descriptionsArray[indexPath.section] + cell.configure(with: viewModels[indexPath.section]) return cell } diff --git a/Agenda/Modules/Summary/Summary.swift b/Agenda/Modules/Summary/Summary.swift index 34e67bb..e64d9a4 100644 --- a/Agenda/Modules/Summary/Summary.swift +++ b/Agenda/Modules/Summary/Summary.swift @@ -8,9 +8,10 @@ import UIKit struct Summary { - let iconImagePath: String + let icon: UIImage let title: String let tintColor: UIColor - let number: Double let measure: String + var number: Double = 0.0 + let kind: SummaryKind } diff --git a/Agenda/Modules/Summary/SummaryInteractor.swift b/Agenda/Modules/Summary/SummaryInteractor.swift index 8588d18..eeb6fb6 100644 --- a/Agenda/Modules/Summary/SummaryInteractor.swift +++ b/Agenda/Modules/Summary/SummaryInteractor.swift @@ -7,11 +7,25 @@ import Foundation +enum SummaryKind: Int { + case percentOfSetGoals, completedGoals, uncompletedGoals, allGoals +} + final class SummaryInteractor { weak var output: SummaryInteractorOutput? + private let settings = UserSettings() public let coreDataManager: CoreDataManagerProtocol + /// This array describes what kind of data will be displayed in cells. User selects the data he needs and then we add/remove these `SummaryCell` enum's cases + public lazy var cells: [SummaryKind] = settings.summaries?.compactMap { SummaryKind(rawValue: $0) } ?? [.percentOfSetGoals, .completedGoals, .uncompletedGoals, .allGoals] + public var summaries: [Summary] = [ + Summary(icon: Icons.grid, title: Labels.Summary.percentOfSetGoals, tintColor: .systemTeal, measure: "% \(Labels.Summary.ofSetGoals)", kind: .percentOfSetGoals), + Summary(icon: Icons.checkmark, title: Labels.Summary.completedGoals, tintColor: .systemGreen, measure: Labels.Summary.goalsDeclension, kind: .completedGoals), + Summary(icon: Icons.xmark, title: Labels.Summary.uncompletedGoals, tintColor: .systemRed, measure: Labels.Summary.goalsDeclension, kind: .uncompletedGoals), + Summary(icon: Icons.sum, title: Labels.Summary.allGoals, tintColor: .systemOrange, measure: Labels.Summary.goalsDeclension, kind: .allGoals) + ] + init(coreDataManager: CoreDataManagerProtocol) { self.coreDataManager = coreDataManager } @@ -23,6 +37,44 @@ extension SummaryInteractor: SummaryInteractorInput { output?.dataDidNotFetch() return } - output?.dataDidFetch(months: months) + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let strongSelf = self else { return } + strongSelf.countGoals(months: months) + + let summaries = strongSelf.summaries.filter { summary in + strongSelf.cells.contains(summary.kind) + } + strongSelf.output?.dataDidFetch(data: summaries) + } + } +} + +// MARK: - Helper methods +private extension SummaryInteractor { + func countGoals(months: [Month]) { + var completedGoalsCounter = 0.0 + var uncompletedGoalsCounter = 0.0 + var allGoalsCounter = 0.0 + var percentage = 0.0 + + for month in months { + guard let goals = month.goals?.array as? [Goal] else { return } + for goal in goals { + if goal.current >= goal.aim { + completedGoalsCounter += 1 + } else { + uncompletedGoalsCounter += 1 + } + allGoalsCounter += 1 + } + + if allGoalsCounter > 0 { + percentage = round(100 * Double(completedGoalsCounter) / Double(allGoalsCounter)) + } + } + summaries[SummaryKind.percentOfSetGoals.rawValue].number = percentage + summaries[SummaryKind.completedGoals.rawValue].number = completedGoalsCounter + summaries[SummaryKind.uncompletedGoals.rawValue].number = uncompletedGoalsCounter + summaries[SummaryKind.allGoals.rawValue].number = allGoalsCounter } } diff --git a/Agenda/Modules/Summary/SummaryPresenter.swift b/Agenda/Modules/Summary/SummaryPresenter.swift index 21fcfc7..cdc39df 100644 --- a/Agenda/Modules/Summary/SummaryPresenter.swift +++ b/Agenda/Modules/Summary/SummaryPresenter.swift @@ -30,40 +30,13 @@ extension SummaryPresenter: SummaryViewOutput { } extension SummaryPresenter: SummaryInteractorOutput { - func dataDidFetch(months: [Month]) { - view?.setData(numbers: countGoals(months: months)) + func dataDidFetch(data: [Summary]) { + DispatchQueue.main.async { [weak self] in + self?.view?.setData(summaries: data) + } } func dataDidNotFetch() { view?.showAlert(title: Labels.oopsError, message: Labels.History.fetchErrorDescription) } } - -// MARK: - Helper methods -private extension SummaryPresenter { - func countGoals(months: [Month]) -> [Double] { - var completedGoalsCounter = 0.0 - var uncompletedGoalsCounter = 0.0 - var allGoalsCounter = 0.0 - var percentage = 0.0 - - for month in months { - guard let goals = month.goals?.array as? [Goal] else { - return [] - } - for goal in goals { - if goal.current >= goal.aim { - completedGoalsCounter += 1 - } else { - uncompletedGoalsCounter += 1 - } - allGoalsCounter += 1 - } - - if allGoalsCounter > 0 { - percentage = round(100 * Double(completedGoalsCounter) / Double(allGoalsCounter)) - } - } - return [percentage, completedGoalsCounter, uncompletedGoalsCounter, allGoalsCounter] - } -} diff --git a/Agenda/Modules/Summary/SummaryProtocols.swift b/Agenda/Modules/Summary/SummaryProtocols.swift index ee9a6e4..880dc0a 100644 --- a/Agenda/Modules/Summary/SummaryProtocols.swift +++ b/Agenda/Modules/Summary/SummaryProtocols.swift @@ -16,7 +16,7 @@ protocol SummaryModuleOutput: AnyObject { protocol SummaryViewInput: AnyObject { func showAlert(title: String, message: String) - func setData(numbers: [Double]) + func setData(summaries: [Summary]) } protocol SummaryViewOutput: AnyObject { @@ -28,7 +28,7 @@ protocol SummaryInteractorInput: AnyObject { } protocol SummaryInteractorOutput: AnyObject { - func dataDidFetch(months: [Month]) + func dataDidFetch(data: [Summary]) func dataDidNotFetch() } diff --git a/Agenda/Modules/Summary/Views/SummaryTableViewCell.swift b/Agenda/Modules/Summary/Views/SummaryTableViewCell.swift index 15423b5..ec2da7c 100644 --- a/Agenda/Modules/Summary/Views/SummaryTableViewCell.swift +++ b/Agenda/Modules/Summary/Views/SummaryTableViewCell.swift @@ -76,7 +76,9 @@ final class SummaryTableViewCell: UITableViewCell { } public func configure(data: Summary) { - iconImageView.image = UIImage(systemName: data.iconImagePath, withConfiguration: UIImage.SymbolConfiguration(weight: .semibold))?.withTintColor(data.tintColor, renderingMode: .alwaysOriginal) + iconImageView.image = data.icon + .withTintColor(data.tintColor, renderingMode: .alwaysOriginal) + .withConfiguration(UIImage.SymbolConfiguration(weight: .semibold)) titleLabel.text = data.title titleLabel.textColor = data.tintColor numberLabel.text = NSNumber(value: data.number).stringValue // to display "1" instead of "1.0" diff --git a/Agenda/Modules/Summary/Views/SummaryViewController.swift b/Agenda/Modules/Summary/Views/SummaryViewController.swift index d6f771a..27e1e61 100644 --- a/Agenda/Modules/Summary/Views/SummaryViewController.swift +++ b/Agenda/Modules/Summary/Views/SummaryViewController.swift @@ -9,13 +9,8 @@ import UIKit final class SummaryViewController: UIViewController { - private let imagePaths = ["number", "checkmark", "xmark", "sum"] - private let titleLabelsText = [Labels.Summary.percentOfSetGoals, Labels.Summary.completedGoals, Labels.Summary.uncompletedGoals, Labels.Summary.allGoals] - private let tintColors: [UIColor] = [.systemTeal, .systemGreen, .systemRed, .systemOrange] - private let measureLabelsText = ["% \(Labels.Summary.ofSetGoals)", Labels.Summary.goalsDeclension, Labels.Summary.goalsDeclension, Labels.Summary.goalsDeclension] - private var numbers = [0.0, 0.0, 0.0, 0.0] // to display in cells in Summary VC - private let output: SummaryViewOutput + private var summaries: [Summary] = [] private lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .insetGrouped) @@ -25,6 +20,7 @@ final class SummaryViewController: UIViewController { tableView.register(SummaryTableViewCell.self, forCellReuseIdentifier: SummaryTableViewCell.identifier) tableView.showsVerticalScrollIndicator = false tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.accessibilityIdentifier = "summaryTableView" return tableView }() @@ -41,18 +37,14 @@ final class SummaryViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - tabBarController?.tabBar.backgroundColor = .systemBackground - view.backgroundColor = .systemGroupedBackground - title = Labels.Summary.title - - setupViewAndConstraints() output.fetchData() + setupViewAndConstraints() } } extension SummaryViewController: SummaryViewInput { - func setData(numbers: [Double]) { - self.numbers = numbers + func setData(summaries: [Summary]) { + self.summaries = summaries tableView.reloadData() } @@ -61,8 +53,13 @@ extension SummaryViewController: SummaryViewInput { } } +// MARK: - Helper methods private extension SummaryViewController { func setupViewAndConstraints() { + tabBarController?.tabBar.backgroundColor = .systemBackground + title = Labels.Summary.title + + view.backgroundColor = .systemGroupedBackground view.addSubview(tableView) NSLayoutConstraint.activate([ @@ -78,7 +75,7 @@ private extension SummaryViewController { extension SummaryViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { - numbers.count + summaries.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -88,8 +85,7 @@ extension SummaryViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: SummaryTableViewCell.identifier, for: indexPath) as? SummaryTableViewCell else { return SummaryTableViewCell() } - let summary = Summary(iconImagePath: imagePaths[indexPath.section], title: titleLabelsText[indexPath.section], tintColor: tintColors[indexPath.section], number: numbers[indexPath.section], measure: measureLabelsText[indexPath.section]) - cell.configure(data: summary) + cell.configure(data: summaries[indexPath.section]) return cell } diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/Contents.json b/Agenda/Resources/Assets.xcassets/Summary Icons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/checkmark.symbolset/Contents.json b/Agenda/Resources/Assets.xcassets/Summary Icons/checkmark.symbolset/Contents.json new file mode 100644 index 0000000..1106c05 --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/checkmark.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "checkmark.svg", + "idiom" : "universal" + } + ] +} diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/checkmark.symbolset/checkmark.svg b/Agenda/Resources/Assets.xcassets/Summary Icons/checkmark.symbolset/checkmark.svg new file mode 100644 index 0000000..49809a2 --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/checkmark.symbolset/checkmark.svg @@ -0,0 +1,161 @@ + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from checkmark + Typeset at 100 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/number.symbolset/Contents.json b/Agenda/Resources/Assets.xcassets/Summary Icons/number.symbolset/Contents.json new file mode 100644 index 0000000..0b0385b --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/number.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "number.svg", + "idiom" : "universal" + } + ] +} diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/number.symbolset/number.svg b/Agenda/Resources/Assets.xcassets/Summary Icons/number.symbolset/number.svg new file mode 100644 index 0000000..87ff74a --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/number.symbolset/number.svg @@ -0,0 +1,161 @@ + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from number + Typeset at 100 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/sum.symbolset/Contents.json b/Agenda/Resources/Assets.xcassets/Summary Icons/sum.symbolset/Contents.json new file mode 100644 index 0000000..520f601 --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/sum.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "sum.svg", + "idiom" : "universal" + } + ] +} diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/sum.symbolset/sum.svg b/Agenda/Resources/Assets.xcassets/Summary Icons/sum.symbolset/sum.svg new file mode 100644 index 0000000..16d4361 --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/sum.symbolset/sum.svg @@ -0,0 +1,161 @@ + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from sum + Typeset at 100 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/xmark.symbolset/Contents.json b/Agenda/Resources/Assets.xcassets/Summary Icons/xmark.symbolset/Contents.json new file mode 100644 index 0000000..683a0fd --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/xmark.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "symbols" : [ + { + "filename" : "xmark.svg", + "idiom" : "universal" + } + ] +} diff --git a/Agenda/Resources/Assets.xcassets/Summary Icons/xmark.symbolset/xmark.svg b/Agenda/Resources/Assets.xcassets/Summary Icons/xmark.symbolset/xmark.svg new file mode 100644 index 0000000..e49c921 --- /dev/null +++ b/Agenda/Resources/Assets.xcassets/Summary Icons/xmark.symbolset/xmark.svg @@ -0,0 +1,161 @@ + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.2.0 + Requires Xcode 12 or greater + Generated from xmark + Typeset at 100 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Agenda/Resources/Icons.swift b/Agenda/Resources/Icons.swift new file mode 100644 index 0000000..5c22859 --- /dev/null +++ b/Agenda/Resources/Icons.swift @@ -0,0 +1,32 @@ +// +// Icons.swift +// Agenda +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import UIKit + +enum Icons { + // Tab bar + static let calendar = Icon("calendar") + static let history = Icon("clock.fill") + static let summary = Icon("square.text.square.fill") + + // Summary + static let grid = Icon("number") + static let checkmark = Icon("checkmark") + static let xmark = Icon("xmark") + static let sum = Icon("sum") + + // Onboarding + static let lightbulb = Icon("lightbulb") + static let chartBarDoc = Icon("chart.bar.doc.horizontal") + static let notesTextBadgePlus = Icon("note.text.badge.plus") +} + +extension Icons { + static func Icon(_ name: String, renderingMode: UIImage.RenderingMode = .alwaysTemplate) -> UIImage { + return UIImage(named: name)!.withRenderingMode(renderingMode) + } +} diff --git a/Agenda/Resources/Labels.swift b/Agenda/Resources/Labels.swift index 6fd6634..31adfda 100644 --- a/Agenda/Resources/Labels.swift +++ b/Agenda/Resources/Labels.swift @@ -35,6 +35,7 @@ enum Labels { static let saved = "saved".localized static let deleteGoalTitle = "deleteGoalTitle".localized static let deleteGoalDescription = "deleteGoalDescription".localized + static let unknownErrorDescription = "unknownErrorDescription".localized } enum History { diff --git a/Agenda/Resources/en.lproj/Localizable.strings b/Agenda/Resources/en.lproj/Localizable.strings index 2ce82a0..6478b69 100644 --- a/Agenda/Resources/en.lproj/Localizable.strings +++ b/Agenda/Resources/en.lproj/Localizable.strings @@ -17,7 +17,7 @@ history = "History"; summary = "Summary"; // Onboarding -welcomeLabel = "Welcome to the Agenda"; +welcomeLabel = "Welcome to the "; continueButtonLabel = "Continue"; title1 = "Planning in a new way"; title2 = "All in plain sight"; @@ -28,10 +28,10 @@ description2 = "Easily track your progress throughout the month with the progres description3 = "Add notes to your goals to concretize them and not forget anything"; // Agenda -titleLabel = "Title"; +titleLabel = "What's to achieve?"; currentLabel = "Current"; aimLabel = "Aim"; -notes = "Notes"; +notes = "Add more details"; newGoal = "New Goal"; details = "Details"; saved = "Saved successfully"; diff --git a/Agenda/Resources/ru.lproj/Localizable.strings b/Agenda/Resources/ru.lproj/Localizable.strings index ccfba6b..b2bcbc5 100644 --- a/Agenda/Resources/ru.lproj/Localizable.strings +++ b/Agenda/Resources/ru.lproj/Localizable.strings @@ -17,7 +17,7 @@ history = "История"; summary = "Обзор"; // Onboarding -welcomeLabel = "Добро пожаловать в Agenda"; +welcomeLabel = "Добро пожаловать в "; continueButtonLabel = "Продолжить"; title1 = "Планирование по-новому"; title2 = "Всё на виду"; @@ -28,15 +28,16 @@ description2 = "Легко отслеживайте свой прогресс в description3 = "Добавляйте заметки к своим целям, чтобы конкретизировать их и ничего не забыть"; // Agenda -titleLabel = "Заголовок"; +titleLabel = "Чего нужно достичь?"; currentLabel = "Текущее значение"; aimLabel = "Цель"; -notes = "Заметки"; +notes = "Добавьте деталей"; newGoal = "Новая цель"; details = "Подробнее"; saved = "Успешно сохранено"; deleteGoalTitle = "Удалить цель"; deleteGoalDescription = "Вы уверены, что хотите удалить эту цель? Отменить это действие будет невозможно"; +unknownErrorDescription = "Возникла непредвиденная ошибка. Пожалуйста, перезапустите приложение"; // History fetchErrorDescription = "Возникла непредвиденная ошибка в результате загрузки вашей истории. Пожалуйста, перезапустите приложение"; diff --git a/Agenda/Services/BaseRouter.swift b/Agenda/Services/BaseRouter.swift index 19005c4..35b255e 100644 --- a/Agenda/Services/BaseRouter.swift +++ b/Agenda/Services/BaseRouter.swift @@ -8,10 +8,8 @@ import UIKit class BaseRouter { - // замыкание, которое возвращает опциональный UINavigationController var navigationControllerProvider: (() -> UINavigationController?)? - - // чтобы не вызывать один и тот же метод, сделаем вот так: + var navigationController: UINavigationController? { navigationControllerProvider?() } diff --git a/Agenda/Services/CoreDataManager.swift b/Agenda/Services/CoreDataManager.swift index 43ea44b..1ca5f46 100644 --- a/Agenda/Services/CoreDataManager.swift +++ b/Agenda/Services/CoreDataManager.swift @@ -7,7 +7,7 @@ import CoreData -protocol CoreDataManagerProtocol { +protocol CoreDataManagerProtocol: AnyObject { func fetchCurrentMonth() -> Month func fetchMonths() -> [Month]? @@ -57,7 +57,7 @@ final class CoreDataManager: NSObject, CoreDataManagerProtocol { let months: [Month]? = try? managedObjectContext.fetch(fetchRequest) if let months = months, !months.isEmpty { // filled with smth? Ok then, display **current** month - return months.first! // not empty check allows us to use force-unwrap + return months.first! } else { // empty? Ok, create new month let month = Month(context: managedObjectContext) @@ -94,7 +94,6 @@ final class CoreDataManager: NSObject, CoreDataManagerProtocol { goal.aim = Int64(data.aim) ?? 0 goal.notes = data.notes - managedObjectContext.refreshAllObjects() // in order to make NSFetchedResultsControllerDelegate work saveContext() updateViewModels() } @@ -106,19 +105,18 @@ final class CoreDataManager: NSObject, CoreDataManagerProtocol { } func deleteMonth(month: Month) { - guard let goals = month.goals?.array as? [Goal] else { return } - goals.forEach { goal in - managedObjectContext.delete(goal) + if let goals = month.goals?.array as? [Goal] { + goals.forEach { managedObjectContext.delete($0) } } managedObjectContext.delete(month) saveContext() - updateViewModels(in: [2]) + updateViewModels(in: [.summary]) } func deleteGoal(goal: Goal) { managedObjectContext.delete(goal) saveContext() - updateViewModels(in: [1, 2]) + updateViewModels(in: [.history, .summary]) } func saveContext() { @@ -131,10 +129,24 @@ final class CoreDataManager: NSObject, CoreDataManagerProtocol { } } } +} + +private extension CoreDataManager { + /** + Updating view controllers after making changes in CoreData. + `enum ViewControllers` implement tab bar's viewcontrollers. + */ + enum ViewControllers: Int { + case agenda + case history + case summary + } - private func updateViewModels(in viewControllersIndicies: [Int] = [0, 1, 2]) { - for index in viewControllersIndicies { - viewControllers[index].updateViewModel() + func updateViewModels(in viewControllers: [ViewControllers] = [.agenda, .history, .summary]) { + DispatchQueue.main.async { [weak self] in + for viewController in viewControllers { + self?.viewControllers[viewController.rawValue].updateViewModel() + } } } } diff --git a/Agenda/Services/UserDefaultsContainer.swift b/Agenda/Services/UserDefaultsContainer.swift new file mode 100644 index 0000000..afb2e83 --- /dev/null +++ b/Agenda/Services/UserDefaultsContainer.swift @@ -0,0 +1,46 @@ +// +// UserDefaultsContainer.swift +// Agenda +// +// Created by Егор Бадмаев on 10.06.2022. +// + +import Foundation + +final class UserDefaultsContainer { + private let userDefaults: UserDefaults + + init(keyedBy: K.Type, userDefaults: UserDefaults = .standard) { + self.userDefaults = userDefaults + } + + public subscript(key: K) -> T? { + get { getValue(forKey: key.stringValue) as? T } + set { set(value: newValue, forKey: key.stringValue) } + } + + public subscript(key: K) -> T? where T: RawRepresentable { + get { + if let rawValue = getValue(forKey: key.stringValue) as? T.RawValue { + return T(rawValue: rawValue) + } else { + return nil + } + } + set { + set(value: newValue?.rawValue, forKey: key.stringValue) + } + } + + private func set(value: Any?, forKey key: String) { + if let value = value { + userDefaults.set(value, forKey: key) + } else { + userDefaults.removeObject(forKey: key) + } + } + + private func getValue(forKey key: String) -> Any? { + return userDefaults.object(forKey: key) + } +} diff --git a/Agenda/Views/AlertError.swift b/Agenda/Views/AlertError.swift deleted file mode 100644 index 1d20b0e..0000000 --- a/Agenda/Views/AlertError.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// AlertError.swift -// Agenda -// -// Created by Егор Бадмаев on 02.04.2022. -// - -import UIKit - -extension UIViewController { - - func alertForError(title: String, message: String) { - - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - let ok = UIAlertAction(title: "OK", style: .default, handler: nil) - alert.addAction(ok) - - present(alert, animated: true, completion: nil) - } -} - diff --git a/Agenda/Views/GoalTableViewCell.swift b/Agenda/Views/GoalTableViewCell.swift index e7cffd6..f6420f0 100644 --- a/Agenda/Views/GoalTableViewCell.swift +++ b/Agenda/Views/GoalTableViewCell.swift @@ -41,6 +41,7 @@ final class GoalTableViewCell: UITableViewCell { textView.textColor = .placeholderText textView.translatesAutoresizingMaskIntoConstraints = false textView.resignFirstResponder() + textView.accessibilityIdentifier = "notesTextView" return textView }() @@ -56,8 +57,14 @@ final class GoalTableViewCell: UITableViewCell { } private func setupView() { + accessibilityIdentifier = "GoalTableViewCell" + titleTextField.accessibilityIdentifier = "titleTextField" + currentTextField.accessibilityIdentifier = "currentTextField" + aimTextField.accessibilityIdentifier = "aimTextField" + contentView.addSubview(label) contentView.addSubview(titleTextField) + titleTextField.clearButtonMode = .always titleTextField.addTarget(self, action: #selector(titleTextFieldChange), for: .allEditingEvents) contentView.addSubview(notesTextView) diff --git a/Agenda/Views/GoalViewController.swift b/Agenda/Views/GoalViewController.swift new file mode 100644 index 0000000..4fbc3cf --- /dev/null +++ b/Agenda/Views/GoalViewController.swift @@ -0,0 +1,124 @@ +// +// GoalViewController.swift +// Agenda +// +// Created by Егор Бадмаев on 18.06.2022. +// + +import UIKit + +class GoalViewController: UIViewController { + + public var goalData = GoalData() + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .insetGrouped) + tableView.allowsSelection = false + tableView.dataSource = self + tableView.register(GoalTableViewCell.self, forCellReuseIdentifier: GoalTableViewCell.identifier) + tableView.showsVerticalScrollIndicator = false + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.accessibilityIdentifier = "GoalTableView" + return tableView + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + + setupViewAndConstraints() + + /// This method allows to hide keyboard when user taps in any place. It is declared in `Extensions/UIKit/UIViewController.swift` + hideKeyboardWhenTappedAround() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + registerForKeyboardNotifications() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + unregisterForKeyboardNotifications() + } +} + +// MARK: - TableView +extension GoalViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 2 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: GoalTableViewCell.identifier, for: indexPath) as? GoalTableViewCell + else { return GoalTableViewCell() } + cell.delegate = self + cell.configure(indexPath: indexPath) + return cell + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + UITableView.automaticDimension + } +} + +// MARK: - Helper methods +private extension GoalViewController { + func registerForKeyboardNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + } + + func unregisterForKeyboardNotifications() { + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) + } + + @objc func keyboardWillShow(notification: NSNotification) { + if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { + tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0) + } + } + + @objc func keyboardWillHide(notification: NSNotification) { + tableView.contentInset = .zero + } + + func setupViewAndConstraints() { + view.backgroundColor = .systemBackground + view.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } +} + +// MARK: - GoalTableViewCellDelegate +extension GoalViewController: GoalTableViewCellDelegate { + /// Update height of `UITextView` based on text's number of lines + func updateHeightOfRow(_ cell: GoalTableViewCell, _ textView: UITextView) { + let size = textView.bounds.size + let newSize = tableView.sizeThatFits(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude)) + + if size.height != newSize.height { + UIView.setAnimationsEnabled(false) + tableView.beginUpdates() + tableView.endUpdates() + UIView.setAnimationsEnabled(true) + // Scroll up to the textview + if let thisIndexPath = tableView.indexPath(for: cell) { + tableView.scrollToRow(at: thisIndexPath, at: .bottom, animated: false) + } + } + } +} diff --git a/AgendaTests/AddGoalModuleTests/AddGoalContainerTests.swift b/AgendaTests/AddGoalModuleTests/AddGoalContainerTests.swift new file mode 100644 index 0000000..d3ea2f6 --- /dev/null +++ b/AgendaTests/AddGoalModuleTests/AddGoalContainerTests.swift @@ -0,0 +1,70 @@ +// +// AddGoalContainerTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class AddGoalModuleOutputMock: AddGoalModuleOutput { + func addGoalModuleDidFinish() { + } +} + +class AddGoalContainerTests: XCTestCase { + + var coreDataManager: CoreDataManagerStub! + var month: Month! + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerStub(containerName: "Agenda") + month = coreDataManager.fetchCurrentMonth() + } + + override func tearDownWithError() throws { + coreDataManager = nil + month = nil + } + + /** + In the next 2 tests we check different cases of assembling `AddGoalContainer`: with and without provided `moduleOutput` + */ + func testAssemblingWithFullContext() throws { + let moduleOutput = AddGoalModuleOutputMock() + let context = AddGoalContext(moduleOutput: moduleOutput, moduleDependency: coreDataManager, month: month) + let container = AddGoalContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? AddGoalViewController, + let presenter = container.input as? AddGoalPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertIdentical(moduleOutput, presenter.moduleOutput, "All injected dependencies should be identical") + } + + func testAssemblingWithoutModuleOutput() throws { + let context = AddGoalContext(moduleDependency: coreDataManager, month: month) + let container = AddGoalContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? AddGoalViewController, + let presenter = container.input as? AddGoalPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertNil(presenter.moduleOutput, "Module output was not provided and should be nil") + } +} diff --git a/AgendaTests/AddGoalModuleTests/AddGoalInteractorTests.swift b/AgendaTests/AddGoalModuleTests/AddGoalInteractorTests.swift new file mode 100644 index 0000000..426bf38 --- /dev/null +++ b/AgendaTests/AddGoalModuleTests/AddGoalInteractorTests.swift @@ -0,0 +1,56 @@ +// +// AddGoalInteractorTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class AddGoalPresenterSpy: AddGoalInteractorOutput { + var goalDidCreateBool = false + + func goalDidCreate() { + goalDidCreateBool = true + } +} + +class AddGoalInteractorTests: XCTestCase { + + var interactor: AddGoalInteractor! + var presenter: AddGoalPresenterSpy! + var coreDataManager: CoreDataManagerSpy! + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerSpy(containerName: "Agenda") + presenter = AddGoalPresenterSpy() + interactor = AddGoalInteractor(coreDataManager: coreDataManager) + interactor.output = presenter + } + + override func tearDownWithError() throws { + interactor = nil + presenter = nil + coreDataManager = nil + } + + /** + Because of usage asynchronous creating goal (because it has no affect on view (except slight hanging), so we may do this in the background for better perfomance). + That is why we need to use `expectation` in our tests. The `fulFill()` method of the expectation is called inside `CoreDataManagerSpy` and is injected right down in the test. + This method requires data to create goal and `Month` instance to create goal in that month. + */ + func testCreatingGoal() throws { + let expectation = self.expectation(description: "Creating goal expectation") + coreDataManager.expectation = expectation + let goalData = GoalData(title: "Sample", current: "\(20)", aim: "\(100)") + let month = coreDataManager.fetchCurrentMonth() + interactor.month = month + + interactor.createGoal(goalData: goalData) + + waitForExpectations(timeout: 1) + XCTAssertTrue(presenter.goalDidCreateBool) + XCTAssertTrue(coreDataManager.goalDidCreate) + } +} diff --git a/AgendaTests/AgendaModuleTests/AgendaContainerTests.swift b/AgendaTests/AgendaModuleTests/AgendaContainerTests.swift new file mode 100644 index 0000000..5cb971c --- /dev/null +++ b/AgendaTests/AgendaModuleTests/AgendaContainerTests.swift @@ -0,0 +1,96 @@ +// +// AgendaContainerTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 12.06.2022. +// + +import XCTest +@testable import Agenda + +class AgendaModuleOutputMock: AgendaModuleOutput { + func monthDetailsModuleDidFinish() {} +} + +class AgendaContainerTests: XCTestCase { + + var container: AgendaContainer! + var context: AgendaContext! + + override func setUpWithError() throws { + context = AgendaContext(moduleDependency: CoreDataManagerMock()) + container = AgendaContainer.assemble(with: context) + } + + override func tearDownWithError() throws { + context = nil + container = nil + } + + /** + In the next tests we will check that the module consists of the correct parts and all dependencies are filled in. + The tests will differ by creating different contexts + */ + func testAssemblingWithFullContext() throws { + let moduleOutput = AgendaModuleOutputMock() + let coreDataManager = CoreDataManagerStub(containerName: "Agenda") + let month = coreDataManager.fetchCurrentMonth() + + context = AgendaContext(moduleOutput: moduleOutput, moduleDependency: coreDataManager, month: month) + container = AgendaContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.router) + XCTAssertNotNil(container.viewController) + + guard let viewController = container.viewController as? AgendaViewController, + let presenter = container.input as? AgendaPresenter, + let _ = container.router as? AgendaRouter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertFalse(viewController.isAgenda, "Since month was provided, this VC is no longer 'Agenda' but 'MonthDetails'") + XCTAssertIdentical(moduleOutput, presenter.moduleOutput, "All injected dependencies should be identical") + } + + func testAssemblingWithoutMonth() throws { + let moduleOutput = AgendaModuleOutputMock() + + context = AgendaContext(moduleOutput: moduleOutput, moduleDependency: CoreDataManagerMock()) + container = AgendaContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? AgendaViewController, + let presenter = container.input as? AgendaPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertTrue(viewController.isAgenda, "Since month was not provided, this VC is 'Agenda'") + XCTAssertIdentical(presenter.view, viewController) + XCTAssertIdentical(moduleOutput, presenter.moduleOutput, "All injected dependencies should be identical") + } + + func testAssemblingWithoutModuleOutputAndMonth() throws { + context = AgendaContext(moduleDependency: CoreDataManagerMock()) + container = AgendaContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? AgendaViewController, + let presenter = container.input as? AgendaPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertTrue(viewController.isAgenda, "Since month was not provided, this VC is 'Agenda'") + XCTAssertIdentical(presenter.view, viewController) + XCTAssertNil(presenter.moduleOutput, "Module output was not provided and should be nil") + } +} diff --git a/AgendaTests/AgendaModuleTests/AgendaInteractorTests.swift b/AgendaTests/AgendaModuleTests/AgendaInteractorTests.swift new file mode 100644 index 0000000..fa39b29 --- /dev/null +++ b/AgendaTests/AgendaModuleTests/AgendaInteractorTests.swift @@ -0,0 +1,138 @@ +// +// AgendaInteractorTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 11.06.2022. +// + +import XCTest +@testable import Agenda + +class AgendaInteractorTests: XCTestCase { + + var interactor: AgendaInteractor! + var presenter: AgendaPresenterSpy! + + let coreDataManager = CoreDataManagerSpy(containerName: "Agenda") + + override func setUpWithError() throws { + presenter = AgendaPresenterSpy() + interactor = AgendaInteractor(coreDataManager: coreDataManager) + interactor.output = presenter + } + + override func tearDownWithError() throws { + interactor = nil + presenter = nil + } + + /** + The next 2 tests check for fetching month: the first one without month provided, the next one - with. + We use stub on Core Data manager, recreate interactor with new Core Data manager injected, and then use method under testing + */ + func testFetchMonthGoalsWithMonthNil() throws { + interactor.fetchMonthGoals() + + XCTAssertFalse(presenter.dataDidNotFetchBool) + XCTAssertNotNil(interactor.month, "Month should not be nil") + XCTAssertNotNil(presenter.viewModels, "View models should not be nil") + XCTAssertNotNil(presenter.monthInfo, "Month info should not be nil") + XCTAssertNotNil(presenter.date, "Date should not be nil") + } + + func testFetchMonthGoalsWithProvidedMonth() throws { + let month = coreDataManager.fetchCurrentMonth() // creates new month + interactor.month = month + + interactor.fetchMonthGoals() + + XCTAssertFalse(presenter.dataDidNotFetchBool) + XCTAssertNotNil(interactor.month, "Month should not be nil") + XCTAssertNotNil(presenter.viewModels, "View models should not be nil") + XCTAssertNotNil(presenter.monthInfo, "Month info should not be nil") + XCTAssertNotNil(presenter.date, "Date should not be nil") + } + + /** + The next 2 tests describe providing data from presenter to router, when it's time to create new module with some data provided by interactor + Firstly, we create stub Core Data manager, recreate interactor with injecting stub and month. + Then we need to create sample goals to our month, so there will be possibility to get something. + In the end, we check that provided goal is not nil and moduleDependency was provided clearly + */ + func testGettingGoalAtIndexPath() throws { + let month = coreDataManager.fetchCurrentMonth() // creates new month + interactor.month = month + coreDataManager.createGoal(data: GoalData(title: "Sample", current: "\(50)", aim: "\(100)"), in: month) + + interactor.getGoal(at: IndexPath(row: 0, section: 0)) + + XCTAssertFalse(presenter.dataDidNotFetchBool) + XCTAssertTrue(presenter.goalDidProvide, "Goal was not provided by interactor") + XCTAssertIdentical(coreDataManager, presenter.dependencyProvided) + } + + func testDataProvidingForAddingGoalModule() throws { + let month = coreDataManager.fetchCurrentMonth() // creates new month + interactor.month = month + + interactor.provideDataForAdding() + + XCTAssertFalse(presenter.dataDidNotFetchBool) + XCTAssertNotNil(presenter.monthProvided, "Month was not provided by interactor") + XCTAssertIdentical(coreDataManager, presenter.dependencyProvided) + } + + /** + We simulate opening onboarding, and then check: if user hasn't onboarded, onboarding will be shown, otherwise - on the contrary. + So, the `settings.hasOnboarded` and `presenter.onboardingDidShow` will never be equal. + If user **has onboarded**, the onboarding **will not be shown** and vice versa. + */ + func testCheckForOnboarding() throws { + interactor.checkForOnboarding() + + let settings = UserSettings() + XCTAssertNotEqual(settings.hasOnboarded, presenter.onboardingDidShow) + } + + /** + We test providing all the data to the Core Data manager. + Because of usage asynchronous replacing goal (because it has no affect on view (except slight hanging), so we may do this in the background for better perfomance). + That is why we need to use `expectation` in our tests. The `fulFill()` method of the expectation is called inside `CoreDataManagerSpy` and is injected right down in the test. + We need to create sample goal, because the `replaceGoal(from:, to:)` method of Interactor provide `Goal` instance at `from` index to the replacing method of Core Data Manager. + */ + func testReplacingGoal() throws { + let expectation = self.expectation(description: "Replacing Goal Expectation") + let month = coreDataManager.fetchCurrentMonth() // creates new month + interactor.month = month + coreDataManager.createGoal(data: GoalData(title: "Sample", current: "\(50)", aim: "\(100)"), in: month) + coreDataManager.expectation = expectation + + interactor.replaceGoal(from: 0, to: 2) + + XCTAssertFalse(presenter.dataDidNotFetchBool) + waitForExpectations(timeout: 3, handler: nil) + XCTAssertTrue(coreDataManager.goalDidReplace) + XCTAssertNotNil(coreDataManager.month) + XCTAssertNotNil(coreDataManager.goal) + XCTAssertNotNil(coreDataManager.fromTo) + } + + /** + Same as in the previous test, we use asynchronous deleting goal (because it has no affect on view (except slight hanging), so we may do this in the background for better perfomance). + That is why we need to use `expectation` in our tests. The `fulFill()` method of the expectation is called inside `CoreDataManagerSpy` and is injected right down in the test. + We assert true, that the goal was deleted by Core Data Manager. + We need to create sample goal, because `deleteItem(at:)` method of Interactor requires `IndexPath` of the goal to be deleted to get its' instance and provides it to the Core Data Manager + */ + func testDeletingGoal() throws { + let expectation = self.expectation(description: "Deleting Goal Expectation") + let month = coreDataManager.fetchCurrentMonth() // creates new month + interactor.month = month + coreDataManager.createGoal(data: GoalData(title: "Sample", current: "\(50)", aim: "\(100)"), in: month) + coreDataManager.expectation = expectation + + interactor.deleteItem(at: IndexPath(row: 0, section: 0)) + + waitForExpectations(timeout: 3, handler: nil) + XCTAssertTrue(coreDataManager.goalDidDelete) + } +} diff --git a/AgendaTests/AgendaModuleTests/AgendaRouterTests.swift b/AgendaTests/AgendaModuleTests/AgendaRouterTests.swift new file mode 100644 index 0000000..3c4a680 --- /dev/null +++ b/AgendaTests/AgendaModuleTests/AgendaRouterTests.swift @@ -0,0 +1,102 @@ +// +// AgendaRouterTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 12.06.2022. +// + +import XCTest +@testable import Agenda + +class AgendaRouterTests: XCTestCase { + + var router: AgendaRouter! + var viewController: UIViewController! + + override func setUpWithError() throws { + router = AgendaRouter() + viewController = UIViewController() + + router.navigationControllerProvider = { [weak viewController] in + viewController?.navigationController + } + } + + override func tearDownWithError() throws { + router = nil + viewController = nil + } + + /** + Let's check that navigation controller is not equal to nil + And that some view controller is presented + */ + func testOpeningAddGoalModule() throws { + let coreDataManager = CoreDataManagerStub(containerName: "Agenda") + let month = coreDataManager.fetchCurrentMonth() + router.showAddGoalModule(in: month, moduleDependency: coreDataManager) + + XCTAssertNotNil(router.navigationController, "Router's navigation controller should not be nil") + XCTAssertNotIdentical(router.navigationController, router.navigationController?.presentedViewController) + } + + func testOpeningGoalDetailsModule() throws { + let coreDataManager = CoreDataManagerStub(containerName: "Agenda") + let month = coreDataManager.fetchCurrentMonth() + coreDataManager.createGoal(data: GoalData(title: "Sample goal", current: "\(75)", aim: "\(100)", notes: ""), in: month) + + if let goal = month.goals?.object(at: 0) as? Goal { + router.showDetailsModule(by: goal, moduleDependency: coreDataManager) + } else { + XCTFail("Goal should not be nil") + } + + XCTAssertNotNil(router.navigationController, "Router's navigation controller should not be nil") + XCTAssertNotIdentical(router.navigationController, router.navigationController?.presentedViewController) + } + + func testOpeningOnboardingModule() throws { + router.showOnboarding() + + XCTAssertNotNil(router.navigationController, "Router's navigation controller should not be nil") + XCTAssertNotIdentical(router.navigationController, router.navigationController?.presentedViewController) + } + + /** + Let's check that navigation controller is not equal to nil + And that there is no view controller in presented + */ + func testDismissingAddGoalModule() throws { + let coreDataManager = CoreDataManagerStub(containerName: "Agenda") + let month = coreDataManager.fetchCurrentMonth() + + router.showAddGoalModule(in: month, moduleDependency: coreDataManager) + router.addGoalModuleDidFinish() + + XCTAssertNotNil(router.navigationController, "Router's navigation controller should not be nil") + XCTAssertNil(router.navigationController?.presentedViewController, "There should be no presented view controller in router's navigation controller") + } + + func testDismissingGoalDetailsModule() throws { + let coreDataManager = CoreDataManagerStub(containerName: "Agenda") + let month = coreDataManager.fetchCurrentMonth() + coreDataManager.createGoal(data: GoalData(title: "Sample goal", current: "\(75)", aim: "\(100)", notes: ""), in: month) + + if let goal = month.goals?.object(at: 0) as? Goal { + router.showDetailsModule(by: goal, moduleDependency: coreDataManager) + } else { + XCTFail("Goal should not be nil") + } + + XCTAssertNotNil(router.navigationController, "Router's navigation controller should not be nil") + XCTAssertNil(router.navigationController?.presentedViewController, "There should be no presented view controller in router's navigation controller") + } + + func testDismissingOnboardingModule() throws { + router.showOnboarding() + router.onboardingModuleDidFinish() + + XCTAssertNotNil(router.navigationController, "Router's navigation controller should not be nil") + XCTAssertNil(router.navigationController?.presentedViewController, "There should be no presented view controller in router's navigation controller") + } +} diff --git a/AgendaTests/AgendaModuleTests/Mocks+Stubs+Spies/AgendaPresenterSpy.swift b/AgendaTests/AgendaModuleTests/Mocks+Stubs+Spies/AgendaPresenterSpy.swift new file mode 100644 index 0000000..ce4d7da --- /dev/null +++ b/AgendaTests/AgendaModuleTests/Mocks+Stubs+Spies/AgendaPresenterSpy.swift @@ -0,0 +1,44 @@ +// +// AgendaPresenterSpy.swift +// AgendaTests +// +// Created by Егор Бадмаев on 12.06.2022. +// + +@testable import Agenda + +class AgendaPresenterSpy: AgendaInteractorOutput { + var viewModels: [GoalViewModel]? + var monthInfo: DateViewModel? + var date: String? + + var dataDidNotFetchBool = false + var dependencyProvided: CoreDataManagerProtocol? + var monthProvided: Month? + var goalDidProvide = false + var onboardingDidShow = false + + func monthDidFetch(viewModels: [GoalViewModel], monthInfo: DateViewModel, date: String) { + self.viewModels = viewModels + self.monthInfo = monthInfo + self.date = date + } + + func dataDidNotFetch() { + dataDidNotFetchBool = true + } + + func showAddGoalModuleWith(month: Month, moduleDependency: CoreDataManagerProtocol) { + monthProvided = month + dependencyProvided = moduleDependency + } + + func showDetailsModuleWith(goal: Goal, moduleDependency: CoreDataManagerProtocol) { + goalDidProvide = true + dependencyProvided = moduleDependency + } + + func showOnboarding() { + onboardingDidShow = true + } +} diff --git a/AgendaTests/GoalDetailsModuleTests/GoalDetailsContainerTests.swift b/AgendaTests/GoalDetailsModuleTests/GoalDetailsContainerTests.swift new file mode 100644 index 0000000..66549aa --- /dev/null +++ b/AgendaTests/GoalDetailsModuleTests/GoalDetailsContainerTests.swift @@ -0,0 +1,77 @@ +// +// GoalDetailsContainerTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class GoalDetailsModuleOutputMock: GoalDetailsModuleOutput { + func goalDetailsModuleDidFinish() { + } +} + +class GoalDetailsContainerTests: XCTestCase { + + var coreDataManager: CoreDataManagerStub! + var goal: Goal! + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerStub(containerName: "Agenda") + + let month = coreDataManager.fetchCurrentMonth() + coreDataManager.createGoal(data: GoalData(title: "Sample", current: "\(10)", aim: "\(100)"), in: month) + guard let goal = month.goals?.array.first as? Goal else { + XCTFail("Goal should not be nil") + return + } + self.goal = goal + } + + override func tearDownWithError() throws { + coreDataManager = nil + goal = nil + } + + /** + In the next 2 tests we check different cases of assembling `GoalDetailsContainer`: with and without provided `moduleOutput` + */ + func testAssemblingWithFullContext() throws { + let moduleOutput = GoalDetailsModuleOutputMock() + let context = GoalDetailsContext(moduleOutput: moduleOutput, moduleDependency: coreDataManager, goal: goal) + let container = GoalDetailsContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? GoalDetailsViewController, + let presenter = container.input as? GoalDetailsPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertIdentical(moduleOutput, presenter.moduleOutput, "All injected dependencies should be identical") + } + + func testAssemblingWithoutModuleOutput() throws { + let context = GoalDetailsContext(moduleDependency: coreDataManager, goal: goal) + let container = GoalDetailsContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? GoalDetailsViewController, + let presenter = container.input as? GoalDetailsPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertNil(presenter.moduleOutput, "Module output was not provided and should be nil") + } +} diff --git a/AgendaTests/GoalDetailsModuleTests/GoalDetailsInteractorTests.swift b/AgendaTests/GoalDetailsModuleTests/GoalDetailsInteractorTests.swift new file mode 100644 index 0000000..e0b41b0 --- /dev/null +++ b/AgendaTests/GoalDetailsModuleTests/GoalDetailsInteractorTests.swift @@ -0,0 +1,72 @@ +// +// GoalDetailsInteractorTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class GoalDetailsInteractorTests: XCTestCase { + + var interactor: GoalDetailsInteractor! + var presenter: GoalDetailsPresenterSpy! + var coreDataManager: CoreDataManagerSpy! + + let goalData = GoalData(title: "Sample", current: "\(10)", aim: "\(100)") + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerSpy(containerName: "Agenda") + presenter = GoalDetailsPresenterSpy() + interactor = GoalDetailsInteractor(coreDataManager: coreDataManager) + interactor.output = presenter + + let month = coreDataManager.fetchCurrentMonth() + coreDataManager.createGoal(data: goalData, in: month) + guard let goal = month.goals?.array.first as? Goal else { + XCTFail("Goal should not be nil") + return + } + interactor.goal = goal + } + + override func tearDownWithError() throws { + interactor = nil + presenter = nil + coreDataManager = nil + } + + func testProvidingData() throws { + interactor.provideData() + + XCTAssertEqual(presenter.goalData.title, goalData.title) + XCTAssertEqual(presenter.goalData.current, goalData.current) + XCTAssertEqual(presenter.goalData.aim, goalData.aim) + XCTAssertEqual(presenter.goalData.notes, goalData.notes) + } + + func testRewritingGoal() throws { + let expectation = self.expectation(description: "Rewriting goal expectation") + coreDataManager.expectation = expectation + + interactor.rewriteGoal(with: goalData) + + XCTAssertTrue(presenter.goalDidRewriteBool) + waitForExpectations(timeout: 1) + XCTAssertTrue(coreDataManager.goalDidRewrite) + } + + func testCheckingBarButtonEnabledWithoutChanges() throws { + interactor.checkBarButtonEnabled(goalData: goalData) + + XCTAssertFalse(presenter.barButtonFlag) + } + + func testCheckingBarButtonEnabledWithChanges() throws { + let changedGoalData = GoalData(title: "New title", current: "\(75)", aim: "\(100)") + interactor.checkBarButtonEnabled(goalData: changedGoalData) + + XCTAssertTrue(presenter.barButtonFlag) + } +} diff --git a/AgendaTests/GoalDetailsModuleTests/Mocks+Stubs+Spies/GoalDetailsPresenterSpy.swift b/AgendaTests/GoalDetailsModuleTests/Mocks+Stubs+Spies/GoalDetailsPresenterSpy.swift new file mode 100644 index 0000000..ef0a20e --- /dev/null +++ b/AgendaTests/GoalDetailsModuleTests/Mocks+Stubs+Spies/GoalDetailsPresenterSpy.swift @@ -0,0 +1,27 @@ +// +// GoalDetailsPresenterSpy.swift +// AgendaTests +// +// Created by Егор Бадмаев on 15.06.2022. +// + +@testable import Agenda + +class GoalDetailsPresenterSpy: GoalDetailsInteractorOutput { + + var goalData: GoalData! + var goalDidRewriteBool = false + var barButtonFlag: Bool! + + func goalDidLoad(goalData: GoalData) { + self.goalData = goalData + } + + func goalDidRewrite() { + goalDidRewriteBool = true + } + + func barButtonDidCheck(with flag: Bool) { + barButtonFlag = flag + } +} diff --git a/AgendaTests/HistoryModuleTests/HistoryContainerTests.swift b/AgendaTests/HistoryModuleTests/HistoryContainerTests.swift new file mode 100644 index 0000000..13fb6b1 --- /dev/null +++ b/AgendaTests/HistoryModuleTests/HistoryContainerTests.swift @@ -0,0 +1,66 @@ +// +// HistoryContainerTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class HistoryModuleOutputMock: HistoryModuleOutput { +} + +class HistoryContainerTests: XCTestCase { + + var coreDataManager: CoreDataManagerMock! + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerMock() + } + + override func tearDownWithError() throws { + coreDataManager = nil + } + + /** + In the next 2 tests we check different cases of assembling `HistoryContainer`: with and without provided `moduleOutput` + */ + func testAssemblingWithFullContext() throws { + let moduleOutput = HistoryModuleOutputMock() + let context = HistoryContext(moduleOutput: moduleOutput, moduleDependency: coreDataManager) + let container = HistoryContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? HistoryViewController, + let presenter = container.input as? HistoryPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertIdentical(moduleOutput, presenter.moduleOutput, "All injected dependencies should be identical") + } + + func testAssemblingWithoutModuleOutput() throws { + let context = HistoryContext(moduleDependency: coreDataManager) + let container = HistoryContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNil(container.input.moduleOutput, "Module output was not provided and should be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? HistoryViewController, + let presenter = container.input as? HistoryPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertNil(presenter.moduleOutput, "Module output was not provided and should be nil") + } +} diff --git a/AgendaTests/HistoryModuleTests/HistoryInteractorTests.swift b/AgendaTests/HistoryModuleTests/HistoryInteractorTests.swift new file mode 100644 index 0000000..49137f3 --- /dev/null +++ b/AgendaTests/HistoryModuleTests/HistoryInteractorTests.swift @@ -0,0 +1,71 @@ +// +// HistoryInteractorTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class HistoryInteractorTests: XCTestCase { + + var interactor: HistoryInteractor! + var presenter: HistoryPresenterSpy! + var coreDataManager: CoreDataManagerSpy! + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerSpy(containerName: "Agenda") + presenter = HistoryPresenterSpy() + interactor = HistoryInteractor(coreDataManager: coreDataManager) + interactor.output = presenter + } + + override func tearDownWithError() throws { + interactor = nil + presenter = nil + coreDataManager = nil + } + + func testFetchingMonths() throws { + let expectation = self.expectation(description: "Fetching months in HistoryInteractor") + presenter.expectation = expectation + + interactor.performFetch() + + XCTAssertFalse(presenter.dataDidNotFetchBool) + waitForExpectations(timeout: 3) + XCTAssertNotNil(presenter.viewModels, "View models should not be nil") + } + + func testFetchingMonthsWithError() throws { + coreDataManager.failFetchingMonth = true + + interactor.performFetch() + + XCTAssertTrue(presenter.dataDidNotFetchBool) + } + + func testOpeningDetailsByMonth() throws { + let indexPath = IndexPath(row: 0, section: 0) + interactor.performFetch() + + interactor.openDetailsByMonth(at: indexPath) + + XCTAssertNotNil(presenter.month, "Month provided for another module should not be nil") + XCTAssertIdentical(coreDataManager, presenter.moduleDependency) + } + + func testDeletingMonth() throws { + let expectation = self.expectation(description: "Deleting month expectation") + let indexPath = IndexPath(row: 0, section: 0) + interactor.performFetch() + coreDataManager.expectation = expectation + + interactor.deleteMonth(at: indexPath) + + waitForExpectations(timeout: 3) + XCTAssertNotNil(coreDataManager.month) + XCTAssertTrue(coreDataManager.monthDidDelete) + } +} diff --git a/AgendaTests/HistoryModuleTests/Mocks+Stubs+Spies/HistoryPresenterSpy.swift b/AgendaTests/HistoryModuleTests/Mocks+Stubs+Spies/HistoryPresenterSpy.swift new file mode 100644 index 0000000..dec8b30 --- /dev/null +++ b/AgendaTests/HistoryModuleTests/Mocks+Stubs+Spies/HistoryPresenterSpy.swift @@ -0,0 +1,36 @@ +// +// HistoryPresenterSpy.swift +// AgendaTests +// +// Created by Егор Бадмаев on 15.06.2022. +// + +@testable import Agenda +import XCTest + +class HistoryPresenterSpy: HistoryInteractorOutput { + + var viewModels: [MonthViewModel]! + var dataDidNotFetchBool = false + var month: Month! + var moduleDependency: CoreDataManagerProtocol! + + var expectation: XCTestExpectation! + + func dataDidFetch(viewModels: [MonthViewModel]) { + self.viewModels = viewModels + + if let expectation = expectation { + expectation.fulfill() + } + } + + func dataDidNotFetch() { + dataDidNotFetchBool = true + } + + func showMonthDetailsModule(month: Month, moduleDependency: CoreDataManagerProtocol) { + self.month = month + self.moduleDependency = moduleDependency + } +} diff --git a/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerMock.swift b/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerMock.swift new file mode 100644 index 0000000..6b7fa61 --- /dev/null +++ b/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerMock.swift @@ -0,0 +1,23 @@ +// +// CoreDataManagerMock.swift +// AgendaTests +// +// Created by Егор Бадмаев on 12.06.2022. +// + +@testable import Agenda + +class CoreDataManagerMock: CoreDataManagerProtocol { + func fetchCurrentMonth() -> Month { + return Month() + } + func fetchMonths() -> [Month]? { + return nil + } + func createGoal(data: GoalData, in month: Month) {} + func rewriteGoal(with data: GoalData, in goal: Goal) {} + func replaceGoal(_ goal: Goal, in month: Month, from: Int, to: Int) {} + func deleteMonth(month: Month) {} + func deleteGoal(goal: Goal) {} + func saveContext() {} +} diff --git a/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerSpy.swift b/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerSpy.swift new file mode 100644 index 0000000..ab8b7a8 --- /dev/null +++ b/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerSpy.swift @@ -0,0 +1,78 @@ +// +// CoreDataManagerSpy.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import Foundation +import CoreData +@testable import Agenda +import XCTest + +class CoreDataManagerSpy: CoreDataManagerStub { + + var goal: Goal! + var month: Month! + var fromTo: (Int, Int)! + var expectation: XCTestExpectation! + var failFetchingMonth = false + + var goalDidCreate = false + var goalDidRewrite = false + var goalDidReplace = false + var monthDidDelete = false + var goalDidDelete = false + + override func fetchMonths() -> [Month]? { + guard !failFetchingMonth else { + return nil + } + return super.fetchMonths() + } + + override func createGoal(data: GoalData, in month: Month) { + super.createGoal(data: data, in: month) + goalDidCreate = true + + if let expectation = expectation { + expectation.fulfill() + } + } + + override func rewriteGoal(with data: GoalData, in goal: Goal) { + goalDidRewrite = true + + if let expectation = expectation { + expectation.fulfill() + } + } + + override func replaceGoal(_ goal: Goal, in month: Month, from: Int, to: Int) { + self.goal = goal + self.month = month + self.fromTo = (from, to) + goalDidReplace = true + + if let expectation = expectation { + expectation.fulfill() + } + } + + override func deleteMonth(month: Month) { + self.month = month + monthDidDelete = true + + if let expectation = expectation { + expectation.fulfill() + } + } + + override func deleteGoal(goal: Goal) { + goalDidDelete = true + + if let expectation = expectation { + expectation.fulfill() + } + } +} diff --git a/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerStub.swift b/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerStub.swift new file mode 100644 index 0000000..38b4f46 --- /dev/null +++ b/AgendaTests/Mocks+Stubs+Spies/CoreDataManagerStub.swift @@ -0,0 +1,90 @@ +// +// CoreDataManagerStub.swift +// AgendaTests +// +// Created by Егор Бадмаев on 12.06.2022. +// + +import Foundation +import CoreData +@testable import Agenda + +class CoreDataManagerStub: CoreDataManagerProtocol { + + let calendarDate = Calendar.current.dateComponents([.year, .month], from: Date()) + let dateFormatter = DateFormatter() + + let managedObjectContext: NSManagedObjectContext + let persistentContainer: NSPersistentContainer + + lazy var month1: Month = { + let month = Month(context: managedObjectContext) + month.date = dateFormatter.date(from: "01.\(calendarDate.month ?? 0).\(calendarDate.year ?? 0)") ?? Date() + return month + }() + lazy var month2: Month = { + let month = Month(context: managedObjectContext) + month.date = dateFormatter.date(from: "01.\((calendarDate.month ?? 2) - 1).\(calendarDate.year ?? 0)") ?? Date() + return month + }() + + public static let model: NSManagedObjectModel = { + // swiftlint:disable force_unwrapping + let modelURL = Bundle.main.url(forResource: "Agenda", withExtension: "momd")! + return NSManagedObjectModel(contentsOf: modelURL)! + // swiftlint:enable force_unwrapping + }() + + init(containerName: String) { + let persistentStoreDescription = NSPersistentStoreDescription() + persistentStoreDescription.type = NSInMemoryStoreType + + let container = NSPersistentContainer(name: containerName, managedObjectModel: CoreDataManagerStub.model) + container.persistentStoreDescriptions = [persistentStoreDescription] + container.loadPersistentStores { _, error in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + } + persistentContainer = container + managedObjectContext = persistentContainer.newBackgroundContext() + dateFormatter.dateFormat = "dd.MM.yyyy" + } + + // creates sample month + func fetchCurrentMonth() -> Month { + let month = Month(context: managedObjectContext) + month.date = dateFormatter.date(from: "01.\(calendarDate.month ?? 0).\(calendarDate.year ?? 0)") ?? Date() + return month + } + + func fetchMonths() -> [Month]? { + return [month1, month2] + } + + func createGoal(data: GoalData, in month: Month) { + let goal = Goal(context: managedObjectContext) + + goal.name = data.title + goal.current = Int64(data.current) ?? 0 + goal.aim = Int64(data.aim) ?? 0 + goal.notes = data.notes + + month.addToGoals(goal) + } + + func rewriteGoal(with data: GoalData, in goal: Goal) { + } + + func replaceGoal(_ goal: Goal, in month: Month, from: Int, to: Int) { + } + + func deleteMonth(month: Month) { + } + + func deleteGoal(goal: Goal) { + } + + func saveContext() { + } +} diff --git a/AgendaTests/OnboardingModuleTests/OnboardingContainerTests.swift b/AgendaTests/OnboardingModuleTests/OnboardingContainerTests.swift new file mode 100644 index 0000000..3439d74 --- /dev/null +++ b/AgendaTests/OnboardingModuleTests/OnboardingContainerTests.swift @@ -0,0 +1,56 @@ +// +// OnboardingContainerTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class OnboardingModuleOutputMock: OnboardingModuleOutput { + func onboardingModuleDidFinish() { + } +} + +class OnboardingContainerTests: XCTestCase { + /** + In the next 2 tests we check different cases of assembling `OnboardingContainer`: with and without provided `moduleOutput` + */ + func testAssemblingWithFullContext() throws { + let moduleOutput = OnboardingModuleOutputMock() + let context = OnboardingContext(moduleOutput: moduleOutput) + let container = OnboardingContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? OnboardingViewController, + let presenter = container.input as? OnboardingPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertIdentical(moduleOutput, presenter.moduleOutput, "All injected dependencies should be identical") + } + + func testAssemblingWithoutModuleOutput() throws { + let context = OnboardingContext() + let container = OnboardingContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? OnboardingViewController, + let presenter = container.input as? OnboardingPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertNil(presenter.moduleOutput, "Module output was not provided and should be nil") + } +} diff --git a/AgendaTests/OnboardingModuleTests/OnboardingInteractorTests.swift b/AgendaTests/OnboardingModuleTests/OnboardingInteractorTests.swift new file mode 100644 index 0000000..fb66e9e --- /dev/null +++ b/AgendaTests/OnboardingModuleTests/OnboardingInteractorTests.swift @@ -0,0 +1,47 @@ +// +// OnboardingInteractorTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class OnboardingPresenterSpy: OnboardingInteractorOutput { + var hasOnboardedDidSetBool = false + + func hasOnboardedDidSet() { + hasOnboardedDidSetBool = true + } +} + +class OnboardingInteractorTests: XCTestCase { + + var interactor: OnboardingInteractor! + var presenter: OnboardingPresenterSpy! + + override func setUpWithError() throws { + interactor = OnboardingInteractor() + presenter = OnboardingPresenterSpy() + interactor.output = presenter + } + + override func tearDownWithError() throws { + interactor = nil + presenter = nil + } + + func testSettingHasOnboarded() throws { + let settings = UserSettings() + + interactor.setHasOnboarded() + + XCTAssertTrue(presenter.hasOnboardedDidSetBool) + guard let hasOnboarded = settings.hasOnboarded else { + XCTFail("Has onboarded in UserDefaults should not be nil") + return + } + XCTAssertTrue(hasOnboarded) + } +} diff --git a/AgendaTests/ServicesTests/CoreDataManagerTests.swift b/AgendaTests/ServicesTests/CoreDataManagerTests.swift new file mode 100644 index 0000000..d08becf --- /dev/null +++ b/AgendaTests/ServicesTests/CoreDataManagerTests.swift @@ -0,0 +1,117 @@ +// +// CoreDataManagerTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 11.06.2022. +// + +import XCTest +import CoreData +@testable import Agenda + +class CoreDataManagerDelegateMock: CoreDataManagerDelegate { + func updateViewModel() {} +} + +class CoreDataManagerTests: XCTestCase { + + var coreDataManager: CoreDataManager! + var month: Month! + + let calendarDate = Calendar.current.dateComponents([.year, .month], from: Date()) + let dateFormatter = DateFormatter() + + override func setUpWithError() throws { + coreDataManager = CoreDataManager(containerName: "Agenda") + month = Month(context: coreDataManager.managedObjectContext) + dateFormatter.dateFormat = "dd.MM.yyyy" + month.date = dateFormatter.date(from: "01.\(calendarDate.month ?? 0).\((calendarDate.year ?? 1970) - 1)") ?? Date() + + coreDataManager.viewControllers.append(CoreDataManagerDelegateMock()) + } + + override func tearDownWithError() throws { + coreDataManager.managedObjectContext.delete(month) + coreDataManager = nil + month = nil + } + + /** + The next two tests will check for providing months. Both of them should not throw an error or return nil + `fetchCurrentMonth(:)` method must always return month instance + `fetchMonths(:)` method must always return array of months + */ + func testFetchingCurrentMonth() throws { + XCTAssertNoThrow(coreDataManager.fetchCurrentMonth()) + } + + func testFetchingArrayOfMonths() throws { + let months = coreDataManager.fetchMonths() + XCTAssertNotNil(months, "Months were not fetched successfully") + } + + /** + This test checks for creating goal in sample month. Let's go by block + First of all, we need to create sample data of type `GoalData`. Create sample `goalData` and use `createGoal(data:, in:)` method of CoreDataManager + And the last block contains `createdGoal` variable. It is the only created goal, so we can use `first` property + */ + func testCreatingGoal() throws { + let goalData = GoalData(title: "Sample goal", current: String(75), aim: String(100), notes: "") + coreDataManager.createGoal(data: goalData, in: month) + + let createdGoal = month.goals?.array.first as? Goal + XCTAssertNotNil(createdGoal, "Created goal should not be nil") + XCTAssertEqual(createdGoal?.month, month, "Months of the created goal and sample one should be the same") + XCTAssertEqual(createdGoal?.name, goalData.title) + XCTAssertEqual("\(createdGoal?.aim ?? 0)", goalData.aim) + XCTAssertEqual("\(createdGoal?.current ?? 0)", goalData.current) + XCTAssertEqual(createdGoal?.notes, goalData.notes) + } + + /** + Rewrinting goal requires two parameters: new `GoalData` and `Goal`, where old data will be rewritten + We need to create 2 `GoalData` instances with the old and new data respectively + Then we need to create goal, using `startGoalData` and rewrite its' data. It is the only created goal, so we can use `first` property + In the end, we test parameters that differ + */ + func testRewritingGoalsData() throws { + let startGoalData = GoalData(title: "Sample goal", current: String(75), aim: String(100), notes: "") + let newGoalData = GoalData(title: "Sample goal", current: String(100), aim: String(100), notes: "Sample goal notes") + + coreDataManager.createGoal(data: startGoalData, in: month) + let createdGoal = month.goals?.array.first as? Goal + if let goal = createdGoal { + coreDataManager.rewriteGoal(with: newGoalData, in: goal) + } else { + XCTFail("Created goal should not be nil") + } + XCTAssertNotNil(createdGoal, "Created goal should not be nil") + XCTAssertNotEqual("\(createdGoal?.current ?? 0)", startGoalData.current) + XCTAssertNotEqual(createdGoal?.notes, startGoalData.notes) + } + + /** + To check reordering we need to create sample goals. The first one will be moved from the first place to last, so their order will look like this: _"2, 3, 1"_ + We replace and get array of goals of the current month. + Then we test every sample goals' names (to simplify, since the `name` property is enough for us) + */ + func testReorderingGoals() throws { + for i in 1...3 { + let goalData = GoalData(title: "Sample goal \(i)", current: "\(75 + i * 5)", aim: "\(100)", notes: "") + coreDataManager.createGoal(data: goalData, in: month) + } + let firstGoal = month.goals?.array.first as? Goal + + if let goal = firstGoal { + coreDataManager.replaceGoal(goal, in: month, from: 0, to: 2) + } else { + XCTFail("Goal should not be nil") + } + let goals = month.goals?.array as? [Goal] + + XCTAssertNotNil(goals, "Goals should not be nil") + XCTAssertEqual(goals?.first?.name, "Sample goal 2") // Goal #2 is the first + XCTAssertEqual(goals?.last?.name, "Sample goal 1") // Goal #1 is the last + XCTAssertNotEqual(goals?.last?.name, "Sample goal 3") // Goal #3 is not the last one now + } +} diff --git a/AgendaTests/SummaryModuleTests/Mocks+Stubs+Spies/SummaryPresenterSpy.swift b/AgendaTests/SummaryModuleTests/Mocks+Stubs+Spies/SummaryPresenterSpy.swift new file mode 100644 index 0000000..7c0f15c --- /dev/null +++ b/AgendaTests/SummaryModuleTests/Mocks+Stubs+Spies/SummaryPresenterSpy.swift @@ -0,0 +1,29 @@ +// +// SummaryPresenter.swift +// AgendaTests +// +// Created by Егор Бадмаев on 16.06.2022. +// + +import XCTest +@testable import Agenda + +class SummaryPresenterSpy: SummaryInteractorOutput { + + var dataDidNotFetchBool = false + var data: [Summary]! + + var expectation: XCTestExpectation! + + func dataDidFetch(data: [Summary]) { + self.data = data + + if let expectation = expectation { + expectation.fulfill() + } + } + + func dataDidNotFetch() { + dataDidNotFetchBool = true + } +} diff --git a/AgendaTests/SummaryModuleTests/SummaryContainerTests.swift b/AgendaTests/SummaryModuleTests/SummaryContainerTests.swift new file mode 100644 index 0000000..097c2ce --- /dev/null +++ b/AgendaTests/SummaryModuleTests/SummaryContainerTests.swift @@ -0,0 +1,65 @@ +// +// SummaryContainerTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class SummaryModuleOutputMock: SummaryModuleOutput { +} + +class SummaryContainerTests: XCTestCase { + + var coreDataManager: CoreDataManagerMock! + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerMock() + } + + override func tearDownWithError() throws { + coreDataManager = nil + } + + /** + In the next 2 tests we check different cases of assembling `SummaryContainer`: with and without provided `moduleOutput` + */ + func testAssemblingWithFullContext() throws { + let moduleOutput = SummaryModuleOutputMock() + let context = SummaryContext(moduleOutput: moduleOutput, moduleDependency: coreDataManager) + let container = SummaryContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? SummaryViewController, + let presenter = container.input as? SummaryPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertIdentical(moduleOutput, presenter.moduleOutput, "All injected dependencies should be identical") + } + + func testAssemblingWithoutModuleOutput() throws { + let context = SummaryContext(moduleDependency: coreDataManager) + let container = SummaryContainer.assemble(with: context) + + XCTAssertNotNil(container.input, "Module input should not be nil") + XCTAssertNotNil(container.viewController) + XCTAssertNotNil(container.router) + + guard let viewController = container.viewController as? SummaryViewController, + let presenter = container.input as? SummaryPresenter + else { + XCTFail("Container assebled with wrong components") + return + } + XCTAssertIdentical(presenter.view, viewController) + XCTAssertNil(presenter.moduleOutput, "Module output was not provided and should be nil") + } +} diff --git a/AgendaTests/SummaryModuleTests/SummaryInteractorTests.swift b/AgendaTests/SummaryModuleTests/SummaryInteractorTests.swift new file mode 100644 index 0000000..9955fea --- /dev/null +++ b/AgendaTests/SummaryModuleTests/SummaryInteractorTests.swift @@ -0,0 +1,80 @@ +// +// SummaryInteractorTests.swift +// AgendaTests +// +// Created by Егор Бадмаев on 14.06.2022. +// + +import XCTest +@testable import Agenda + +class SummaryInteractorTests: XCTestCase { + + var interactor: SummaryInteractor! + var presenter: SummaryPresenterSpy! + var coreDataManager: CoreDataManagerSpy! + + override func setUpWithError() throws { + coreDataManager = CoreDataManagerSpy(containerName: "Agenda") + presenter = SummaryPresenterSpy() + interactor = SummaryInteractor(coreDataManager: coreDataManager) + interactor.output = presenter + } + + override func tearDownWithError() throws { + interactor = nil + presenter = nil + coreDataManager = nil + } + + func testPerfomingFetchWithError() throws { + coreDataManager.failFetchingMonth = true + interactor.performFetch() + + XCTAssertTrue(presenter.dataDidNotFetchBool) + } + + func testPerfomingFetchWithEmptyData() throws { + let expectation = self.expectation(description: "Fetching months in HistoryInteractor") + presenter.expectation = expectation + + interactor.performFetch() + + XCTAssertFalse(presenter.dataDidNotFetchBool) + waitForExpectations(timeout: 1) + XCTAssertNotNil(presenter.data, "Provided data should not be nil") + + presenter.data.forEach { summary in + if summary.number > 0 { + XCTFail("There should not be any data") + } + } + } + + func testPerfomingFetchWithSomeData() throws { + let expectation = self.expectation(description: "Fetching months in HistoryInteractor") + presenter.expectation = expectation + interactor.summaries = [ + Summary(icon: Icons.grid, title: Labels.Summary.percentOfSetGoals, tintColor: .systemTeal, measure: "% \(Labels.Summary.ofSetGoals)", kind: .percentOfSetGoals), + Summary(icon: Icons.checkmark, title: Labels.Summary.completedGoals, tintColor: .systemGreen, measure: Labels.Summary.goalsDeclension, kind: .completedGoals), + Summary(icon: Icons.xmark, title: Labels.Summary.uncompletedGoals, tintColor: .systemRed, measure: Labels.Summary.goalsDeclension, kind: .uncompletedGoals), + Summary(icon: Icons.sum, title: Labels.Summary.allGoals, tintColor: .systemOrange, measure: Labels.Summary.goalsDeclension, kind: .allGoals) + ] + var settings = UserSettings() + settings.summaries = [SummaryKind.percentOfSetGoals.rawValue, SummaryKind.completedGoals.rawValue, SummaryKind.uncompletedGoals.rawValue, SummaryKind.allGoals.rawValue] + let goalData1 = GoalData(title: "Sample 1", current: "\(75)", aim: "\(100)") + let goalData2 = GoalData(title: "Sample 2", current: "\(100)", aim: "\(100)") + coreDataManager.createGoal(data: goalData1, in: coreDataManager.month1) + coreDataManager.createGoal(data: goalData2, in: coreDataManager.month2) + + interactor.performFetch() + + XCTAssertFalse(presenter.dataDidNotFetchBool) + waitForExpectations(timeout: 1) + XCTAssertNotNil(presenter.data, "Provided data should not be nil") + XCTAssertEqual(presenter.data[0].number, 50) // percentOfSetGoals + XCTAssertEqual(presenter.data[1].number, 1) // completedGoals + XCTAssertEqual(presenter.data[2].number, 1) // uncompletedGoals + XCTAssertEqual(presenter.data[3].number, 2) // allGoals + } +} diff --git a/AgendaUITests/AddGoalUITests.swift b/AgendaUITests/AddGoalUITests.swift new file mode 100644 index 0000000..9ba066b --- /dev/null +++ b/AgendaUITests/AddGoalUITests.swift @@ -0,0 +1,122 @@ +// +// AddGoalUITests.swift +// AgendaUITests +// +// Created by Егор Бадмаев on 17.06.2022. +// + +import XCTest + +/* + checkBarButtonEnabled можно проверить через .isHittable + */ + +class AddGoalUITests: XCTestCase { + + var app: XCUIApplication! + + let device = XCUIDevice.shared + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + /// Opening AddGoal module every time the tests start + app.navigationBars.buttons["addBarButton"].tap() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + app.terminate() + } + + func testAddGoalViewExists() throws { + let cancelBarButton = app.navigationBars.buttons["cancelButtonItem"] + let doneBarButton = app.navigationBars.buttons["doneButtonItem"] + let tableView = app.tables["GoalTableView"] + let titleCell = tableView.cells.element(boundBy: 0) + let notesCell = tableView.cells.element(boundBy: 1) + let currentCell = tableView.cells.element(boundBy: 2) + let aimCell = tableView.cells.element(boundBy: 3) + + XCTAssertTrue(cancelBarButton.exists) + XCTAssertTrue(cancelBarButton.isEnabled) + XCTAssertTrue(doneBarButton.exists) + XCTAssertFalse(doneBarButton.isEnabled) + XCTAssertTrue(tableView.exists) + XCTAssertTrue(titleCell.exists) + XCTAssertTrue(notesCell.exists) + XCTAssertTrue(currentCell.exists) + XCTAssertTrue(aimCell.exists) + } + + /** + Before _Done_ button is enabled, user need to fill 3 required text fields. In the next 3 tests we check for button's enabling. Only last one will have effect + */ + func testAddGoalWithOneOfThreeFields() throws { + let tableView = app.tables["GoalTableView"] + let doneBarButton = app.navigationBars.buttons["doneButtonItem"] + let cell = tableView.cells["GoalTableViewCell"].firstMatch + let titleTextField = cell.textFields["titleTextField"] + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: tableView) + wait(for: [expectation], timeout: 5) + + titleTextField.tap() + titleTextField.typeText("Sample title") + + XCTAssertTrue(tableView.exists) + XCTAssertTrue(cell.exists) + XCTAssertTrue(titleTextField.exists) + XCTAssertFalse(doneBarButton.isEnabled) + } + + func testAddGoalWithTwoOfThreeFields() throws { + let tableView = app.tables["GoalTableView"] + let doneBarButton = app.navigationBars.buttons["doneButtonItem"] + let currentCell = tableView.cells.element(boundBy: 2) + let currentTextField = currentCell.textFields["currentTextField"] + let aimCell = tableView.cells.element(boundBy: 3) + let aimTextField = aimCell.textFields["aimTextField"] + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: tableView) + wait(for: [expectation], timeout: 5) + + currentTextField.tap() + currentTextField.typeText("120") + aimTextField.tap() + aimTextField.typeText("600") + + XCTAssertTrue(tableView.exists) + XCTAssertTrue(currentCell.exists) + XCTAssertTrue(aimCell.exists) + XCTAssertFalse(doneBarButton.isEnabled) + } + + func testAddGoalWithFullThreeFields() throws { + let tableView = app.tables["GoalTableView"] + let doneBarButton = app.navigationBars.buttons["doneButtonItem"] + let titleCell = tableView.cells.element(boundBy: 0) + let currentCell = tableView.cells.element(boundBy: 2) + let aimCell = tableView.cells.element(boundBy: 3) + + let titleTextField = titleCell.textFields["titleTextField"] + let currentTextField = currentCell.textFields["currentTextField"] + let aimTextField = aimCell.textFields["aimTextField"] + + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: tableView) + wait(for: [expectation], timeout: 5) + + titleTextField.tap() + titleTextField.typeText("Sample title") + currentTextField.tap() + currentTextField.typeText("120") + aimTextField.tap() + aimTextField.typeText("600") + + XCTAssertTrue(doneBarButton.isEnabled) + } +} diff --git a/AgendaUITests/AgendaUITests.swift b/AgendaUITests/AgendaUITests.swift new file mode 100644 index 0000000..6d811de --- /dev/null +++ b/AgendaUITests/AgendaUITests.swift @@ -0,0 +1,99 @@ +// +// AgendaViewTest.swift +// AgendaUITests +// +// Created by Егор Бадмаев on 11.06.2022. +// + +import XCTest +@testable import Agenda + +class AgendaUITests: XCTestCase { + + var app: XCUIApplication! + + let device = XCUIDevice.shared + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + app.terminate() + } + /** + In this test we check that everything is correctly displayed on the screen + */ + func testAgendaViewModule() throws { + let barButtons = app.navigationBars.buttons + XCTAssertEqual(barButtons.count, 2) + + let monthProgressView = app.progressIndicators["monthProgressView"] + let dayAndMonthLabel = app.staticTexts["dayAndMonthLabel"] + let yearLabel = app.staticTexts["yearLabel"] + let tableView = app.tables["tableView"] + XCTAssertTrue(monthProgressView.exists) + XCTAssertTrue(dayAndMonthLabel.exists) + XCTAssertTrue(yearLabel.exists) + XCTAssertTrue(tableView.exists) + } + + /** + We test opening GoalDetails module and providing correct (the same) data, as it was in cell with identifier `AgendaTableViewCell`. And then we close this module + */ + func testGoalDetails() throws { + // opening + let tableView = app.tables["tableView"] + let cell = tableView.cells["AgendaTableViewCell"].firstMatch + XCTAssertTrue(cell.exists) + XCTAssertTrue(cell.isHittable) + + // check for correct data providing + let title = cell.staticTexts.element(boundBy: 1).label // text from titleLabel + cell.tap() + let goalTableView = app.tables["GoalTableView"] + let goalCell = goalTableView.cells["GoalTableViewCell"].firstMatch + let titleTextField = goalCell.textFields["titleTextField"] + + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: goalTableView) + wait(for: [expectation], timeout: 5) + XCTAssertEqual(title, titleTextField.value as! String) + XCTAssertTrue(goalTableView.exists) + XCTAssertTrue(goalCell.exists) + // closing + let backButton = app.navigationBars.buttons.firstMatch + backButton.tap() + XCTAssertFalse(goalTableView.exists) + XCTAssertFalse(goalCell.exists) + } + + func testOpeningAndClosingAddGoal() throws { + app.navigationBars.buttons["addBarButton"].tap() + + let tableView = app.tables["GoalTableView"] + let currentCell = tableView.cells.element(boundBy: 2) + let currentTextField = currentCell.textFields["currentTextField"] + let aimCell = tableView.cells.element(boundBy: 3) + let aimTextField = aimCell.textFields["aimTextField"] + + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: tableView) + + wait(for: [expectation], timeout: 5) + XCTAssertTrue(tableView.exists) + XCTAssertTrue(currentCell.exists) + XCTAssertTrue(aimCell.exists) + XCTAssertEqual(currentTextField.placeholderValue, "0") + XCTAssertEqual(aimTextField.placeholderValue, "0") + + let cancel = app.navigationBars.buttons["cancelButtonItem"] + cancel.tap() + XCTAssertFalse(tableView.exists) + } +} diff --git a/AgendaUITests/AgendaUITestsLaunchTests.swift b/AgendaUITests/AgendaUITestsLaunchTests.swift new file mode 100644 index 0000000..21f556a --- /dev/null +++ b/AgendaUITests/AgendaUITestsLaunchTests.swift @@ -0,0 +1,32 @@ +// +// AgendaUITestsLaunchTests.swift +// AgendaUITests +// +// Created by Егор Бадмаев on 11.06.2022. +// + +import XCTest + +class AgendaUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/AgendaUITests/GoalDetailsUITests.swift b/AgendaUITests/GoalDetailsUITests.swift new file mode 100644 index 0000000..83811a4 --- /dev/null +++ b/AgendaUITests/GoalDetailsUITests.swift @@ -0,0 +1,135 @@ +// +// GoalDetailsUITests.swift +// AgendaUITests +// +// Created by Егор Бадмаев on 18.06.2022. +// + +import XCTest + +class GoalDetailsUITests: XCTestCase { + + var app: XCUIApplication! + + let device = XCUIDevice.shared + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + /// Opening GoalDetails module from Agenda module + let agendaTableView = app.tables["tableView"] + let cell = agendaTableView.cells.firstMatch + cell.tap() + let goalsTableView = app.tables["GoalTableView"] + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: goalsTableView) + wait(for: [expectation], timeout: 5) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + app.terminate() + } + + func testGoalDetailsViewExists() throws { + let backButton = app.navigationBars.buttons.firstMatch + let saveBarButton = app.navigationBars.buttons["saveBarButton"] + let tableView = app.tables["GoalTableView"] + let titleCell = tableView.cells.element(boundBy: 0) + let notesCell = tableView.cells.element(boundBy: 1) + let currentCell = tableView.cells.element(boundBy: 2) + let aimCell = tableView.cells.element(boundBy: 3) + + XCTAssertTrue(backButton.exists) + XCTAssertTrue(saveBarButton.exists) + XCTAssertFalse(saveBarButton.isEnabled) + XCTAssertTrue(tableView.exists) + XCTAssertTrue(titleCell.exists) + XCTAssertTrue(notesCell.exists) + XCTAssertTrue(currentCell.exists) + XCTAssertTrue(aimCell.exists) + } + + func testWithSomeChangesInTitle() throws { + let saveBarButton = app.navigationBars.buttons["saveBarButton"] + let tableView = app.tables["GoalTableView"] + let titleCell = tableView.cells.element(boundBy: 0) + let titleTextField = titleCell.textFields["titleTextField"] + + titleTextField.tap() + titleTextField.typeText("Sample title") + + XCTAssertTrue(saveBarButton.isEnabled) + } + + func testWithSomeChangesInNotes() throws { + let saveBarButton = app.navigationBars.buttons["saveBarButton"] + let tableView = app.tables["GoalTableView"] + let notesCell = tableView.cells.element(boundBy: 1) + let notesTextView = notesCell.textViews["notesTextView"] + + notesTextView.tap() + notesTextView.typeText("Sample title") + + XCTAssertTrue(saveBarButton.isEnabled) + } + + func testWithSomeChangesInCurrentTextField() throws { + let saveBarButton = app.navigationBars.buttons["saveBarButton"] + let tableView = app.tables["GoalTableView"] + let currentCell = tableView.cells.element(boundBy: 2) + let currentTextField = currentCell.textFields["currentTextField"] + + currentTextField.tap() + currentTextField.typeText("100") + + XCTAssertTrue(saveBarButton.isEnabled) + } + + func testWithSomeChangesInAimTextField() throws { + let saveBarButton = app.navigationBars.buttons["saveBarButton"] + let tableView = app.tables["GoalTableView"] + let aimCell = tableView.cells.element(boundBy: 3) + let aimTextField = aimCell.textFields["aimTextField"] + + aimTextField.tap() + aimTextField.typeText("100") + + XCTAssertTrue(saveBarButton.isEnabled) + } + + func testMakingSomeChangesAndThenBack() throws { + let saveBarButton = app.navigationBars.buttons["saveBarButton"] + let tableView = app.tables["GoalTableView"] + let titleCell = tableView.cells.element(boundBy: 0) + let titleTextField = titleCell.textFields["titleTextField"] + let oldValue = titleTextField.value as! String + + titleTextField.tap() + app.keys["delete"].tap() + + titleTextField.typeText("\(oldValue.last!)") + XCTAssertFalse(saveBarButton.isEnabled) + } + + func testPresentingIndicatorView() throws { + let saveBarButton = app.navigationBars.buttons["saveBarButton"] + let tableView = app.tables["GoalTableView"] + let aimCell = tableView.cells.element(boundBy: 3) + let aimTextField = aimCell.textFields["aimTextField"] + + aimTextField.tap() + aimTextField.typeText("100") + saveBarButton.tap() + + let indicatorView = app.staticTexts["Saved successfully"] + + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: indicatorView) + wait(for: [expectation], timeout: 5) + } +} diff --git a/AgendaUITests/HistoryUITests.swift b/AgendaUITests/HistoryUITests.swift new file mode 100644 index 0000000..474ff8b --- /dev/null +++ b/AgendaUITests/HistoryUITests.swift @@ -0,0 +1,92 @@ +// +// HistoryUITests.swift +// AgendaUITests +// +// Created by Егор Бадмаев on 18.06.2022. +// + +import XCTest + +class HistoryUITests: XCTestCase { + + var app: XCUIApplication! + + let device = XCUIDevice.shared + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + /// Opening History module in tab bar + app.tabBars.buttons.element(boundBy: 1).tap() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + app.terminate() + } + + func testHistoryViewSetup() throws { + let editButtonItem = app.navigationBars.firstMatch + let tableView = app.tables.firstMatch + let cell = tableView.cells.firstMatch + + XCTAssertTrue(editButtonItem.exists) + XCTAssertTrue(tableView.exists) + XCTAssertTrue(cell.exists) + XCTAssertTrue(cell.isEnabled) + } + + func testOpeningCurrentMonth() throws { + let tableView = app.tables.firstMatch + let cell = tableView.cells.firstMatch + cell.tap() + + let agendaTableView = app.tables["tableView"] + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: agendaTableView) + wait(for: [expectation], timeout: 5) + } + + func testOpeningMonthDetailsModule() throws { + let tableView = app.tables.firstMatch + let cell = tableView.cells.element(boundBy: 1) + cell.tap() + + let monthDetailsTableView = app.tables["tableView"] + let existsPredicate = NSPredicate(format: "exists == true") + let expectation = XCTNSPredicateExpectation(predicate: existsPredicate, object: monthDetailsTableView) + wait(for: [expectation], timeout: 5) + } + + func testOpeningAlertWithDeletingMonth() throws { + let tableView = app.tables.firstMatch + let cell = tableView.cells.element(boundBy: 1) + cell.swipeLeft() + + addUIInterruptionMonitor(withDescription: "Delete month") { alert in + alert.buttons.firstMatch.tap() + return true + } + + let deleteButton = cell.buttons.firstMatch + deleteButton.tap() + + } + + func testOpeningAlertAndDeletingMonth() throws { + let tableView = app.tables.firstMatch + let cell = tableView.cells.element(boundBy: 1) + cell.swipeLeft() + let deleteButton = cell.buttons.firstMatch + deleteButton.tap() + + addUIInterruptionMonitor(withDescription: "Delete month") { alert in + alert.buttons.element(boundBy: 1).tap() + return true + } + } +} diff --git a/AgendaUITests/SummaryUITests.swift b/AgendaUITests/SummaryUITests.swift new file mode 100644 index 0000000..976bad6 --- /dev/null +++ b/AgendaUITests/SummaryUITests.swift @@ -0,0 +1,36 @@ +// +// SummaryUITests.swift +// AgendaUITests +// +// Created by Егор Бадмаев on 18.06.2022. +// + +import XCTest + +class SummaryUITests: XCTestCase { + + var app: XCUIApplication! + + let device = XCUIDevice.shared + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + + app = XCUIApplication() + app.launch() + + /// Opening Summary module in tab bar + app.tabBars.buttons.element(boundBy: 2).tap() + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + app.terminate() + } + + func testSummaryViewSetup() { + let tableView = app.tables["summaryTableView"] + XCTAssertTrue(tableView.exists) + } +} diff --git a/README.md b/README.md index 37e3379..8fbb0f5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Agenda +# Agenda Application that allows you to track your goals for a month # Demo @@ -23,7 +23,8 @@ My first independent pet-project. In this project, I demonstrated the knowledge 5. VIPER 6. Localization 7. SPM -8. Testing (Unit/UI) +8. Testing (Unit/UI) using XCTest +9. Concurrency # Requirements - Minimum version: iOS 13 @@ -33,7 +34,7 @@ My first independent pet-project. In this project, I demonstrated the knowledge - The app looks correct for iPhone SE (1st gen.) and above --- -# Agenda +# Agenda Приложение, позволяющее отслеживать цели на месяц # Демо @@ -57,7 +58,8 @@ My first independent pet-project. In this project, I demonstrated the knowledge 5. VIPER 6. Локализация 7. SPM -8. Тестирование (Unit/UI) +8. Тестирование (Unit/UI) используя XCTest +9. Многопоточность # Требования - Минимальная версия: iOS 13