diff --git a/ArrayTests/ArrayTests.swift b/ArrayTests/ArrayTests.swift new file mode 100644 index 0000000000..91a4b92abb --- /dev/null +++ b/ArrayTests/ArrayTests.swift @@ -0,0 +1,30 @@ +// +// ArrayTests.swift +// ArrayTests +// +// Created by Марат Хасанов on 14.08.2023. +// + +import XCTest +import Foundation + +@testable import MovieQuiz + +class ArrayTests: XCTestCase { + func testGetValueInRange() throws { + let array = [1, 1, 2, 3, 5] + + let value = array[safe: 2] + + XCTAssertNotNil(value) + XCTAssertEqual(value, 2) + } + func testGetValueOutOfRange() throws { + + let array = [1, 1, 2, 3, 5] + + let value = array[safe: 20] + + XCTAssertNil(value) + } +} diff --git a/ArrayTests/MovieQuizPresenterTests.swift b/ArrayTests/MovieQuizPresenterTests.swift new file mode 100644 index 0000000000..77fbcd6f1e --- /dev/null +++ b/ArrayTests/MovieQuizPresenterTests.swift @@ -0,0 +1,31 @@ +// +// MovieQuizViewControllerMock.swift +// MovieQuizViewControllerMock +// +// Created by Марат Хасанов on 14.08.2023. +// + +import XCTest +@testable import MovieQuiz + +final class MovieQuizViewControllerMock: MovieQuizViewControllerProtocol { + func show(quiz step: QuizStepViewModel) {} + func show(quiz result: QuizResultsViewModel) {} + func highlightImageBorder(isCorrectAnswer: Bool) {} + func showLoadingIndicator() {} + func hideLoadingIndicator() {} + func showNetworkError(message: String) {} +} + +final class MovieQuizPresenterTests: XCTestCase { + func testPresenterConvertModel() throws { + let viewControllerMock = MovieQuizViewControllerMock() + let sut = MovieQuizPresenter(viewController: viewControllerMock) + let emptyData = Data() + let question = QuizQuestion(image: emptyData, text: "Question Text", correctAnswer: true) + let viewModel = sut.convert(model: question) + XCTAssertNotNil(viewModel.image) + XCTAssertEqual(viewModel.question, "Question Text") + XCTAssertEqual(viewModel.questionNumber, "1/10") + } +} diff --git a/ArrayTests/MoviesLoaderTests.swift b/ArrayTests/MoviesLoaderTests.swift new file mode 100644 index 0000000000..84d930a4f3 --- /dev/null +++ b/ArrayTests/MoviesLoaderTests.swift @@ -0,0 +1,50 @@ +// +// MoviesLoaderTests.swift +// MoviesLoaderTests +// +// Created by Марат Хасанов on 14.08.2023. +// + +import XCTest + +@testable import MovieQuiz + +class MoviesLoaderTests: XCTestCase { + func testSuccessLoading() throws { + + let stubNetworkClient = StubNetworkClient(emulateError: false) + let loader = MoviesLoader(networkClient: stubNetworkClient) + + let expectation = expectation(description: "Loading expectation") + + loader.loadMovies { result in + switch result { + case .success(let movies): + XCTAssertEqual(movies.items.count, 2) + expectation.fulfill() + case .failure(_): + XCTFail("Unexpected failure") + } + } + waitForExpectations(timeout: 1) + } + + func testFailureLoading() throws { + let stubNetworkClient = StubNetworkClient(emulateError: true) + let loader = MoviesLoader(networkClient: stubNetworkClient) + + let expectation = expectation(description: "Loading expectation") + + loader.loadMovies { result in + switch result { + case .failure(let error): + XCTAssertNotNil(error) + expectation.fulfill() + case .success(_): + XCTFail("Unexpected failure") + } + } + waitForExpectations(timeout: 1) + } +} + diff --git a/MovieQuiz.xcodeproj/project.pbxproj b/MovieQuiz.xcodeproj/project.pbxproj index 45ad649fa4..8f6e77d8ac 100644 --- a/MovieQuiz.xcodeproj/project.pbxproj +++ b/MovieQuiz.xcodeproj/project.pbxproj @@ -16,8 +16,48 @@ AD5EE5DE284D7887003966EF /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD5EE5DD284D7887003966EF /* UIColor+Extensions.swift */; }; AD77F5742857F8810062FB14 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD77F5732857F8810062FB14 /* Date+Extensions.swift */; }; AD7AFA552836189F00399704 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7AFA542836189F00399704 /* Array+Extensions.swift */; }; + F45010BB2A67DE4200105895 /* QuizQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010BA2A67DE4200105895 /* QuizQuestion.swift */; }; + F45010BD2A67DE5100105895 /* QuizStepViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010BC2A67DE5100105895 /* QuizStepViewModel.swift */; }; + F45010BF2A67DE5900105895 /* AlertModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010BE2A67DE5900105895 /* AlertModel.swift */; }; + F45010C12A67DE6700105895 /* GameRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010C02A67DE6700105895 /* GameRecord.swift */; }; + F45010C42A67DE8500105895 /* QuestionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010C32A67DE8500105895 /* QuestionFactory.swift */; }; + F45010C62A67DE9800105895 /* QuestionFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010C52A67DE9800105895 /* QuestionFactoryProtocol.swift */; }; + F45010C82A67DEA000105895 /* QuestionFactoryDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010C72A67DEA000105895 /* QuestionFactoryDelegate.swift */; }; + F45010CA2A67DEA900105895 /* AlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010C92A67DEA900105895 /* AlertPresenter.swift */; }; + F45010CC2A67DEB600105895 /* AlertProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010CB2A67DEB600105895 /* AlertProtocol.swift */; }; + F45010D02A67DECC00105895 /* StatisticService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010CF2A67DECC00105895 /* StatisticService.swift */; }; + F45010D42A67DF1100105895 /* QuizResultsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F45010D32A67DF1100105895 /* QuizResultsViewModel.swift */; }; + F471CA812A54357F00C2E967 /* YS Display-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F471CA7E2A54310300C2E967 /* YS Display-Medium.ttf */; }; + F471CA822A54358300C2E967 /* YS Display-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = F471CA7D2A54310300C2E967 /* YS Display-Bold.ttf */; }; + F476112E2A7983AD006C930D /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F476112D2A7983AD006C930D /* NetworkClient.swift */; }; + F47611302A7991C3006C930D /* MostPopularMovies.swift in Sources */ = {isa = PBXBuildFile; fileRef = F476112F2A7991C3006C930D /* MostPopularMovies.swift */; }; + F47611322A7A24E3006C930D /* MoviesLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47611312A7A24E3006C930D /* MoviesLoader.swift */; }; + F4B050CF2A8A050A00CD72CC /* MovieQuizPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B050CE2A8A050A00CD72CC /* MovieQuizPresenter.swift */; }; + F4B050D12A8A4AF500CD72CC /* MovieQuizViewControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B050D02A8A4AF500CD72CC /* MovieQuizViewControllerProtocol.swift */; }; + F4B050F52A8A9E9B00CD72CC /* ArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B050F42A8A9E9B00CD72CC /* ArrayTests.swift */; }; + F4B051082A8A9FF400CD72CC /* MoviesLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B051012A8A9FF100CD72CC /* MoviesLoaderTests.swift */; }; + F4B051162A8AA2E300CD72CC /* MovieQuizUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B051152A8AA2E300CD72CC /* MovieQuizUITests.swift */; }; + F4B051302A8F866900CD72CC /* StubNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B0512F2A8F866900CD72CC /* StubNetworkClient.swift */; }; + F4B051312A8F881F00CD72CC /* MovieQuizPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B050D82A8A4B9000CD72CC /* MovieQuizPresenterTests.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + F4B050F62A8A9E9B00CD72CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AD1ABAA62831907B00B3E448 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AD1ABAAD2831907B00B3E448; + remoteInfo = MovieQuiz; + }; + F4B051192A8AA2E300CD72CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = AD1ABAA62831907B00B3E448 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AD1ABAAD2831907B00B3E448; + remoteInfo = MovieQuiz; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ AD1ABAAE2831907B00B3E448 /* MovieQuiz.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MovieQuiz.app; sourceTree = BUILT_PRODUCTS_DIR; }; AD1ABAB12831907B00B3E448 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -30,6 +70,32 @@ AD5EE5DD284D7887003966EF /* UIColor+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; AD77F5732857F8810062FB14 /* Date+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; AD7AFA542836189F00399704 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; + F45010BA2A67DE4200105895 /* QuizQuestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizQuestion.swift; sourceTree = ""; }; + F45010BC2A67DE5100105895 /* QuizStepViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizStepViewModel.swift; sourceTree = ""; }; + F45010BE2A67DE5900105895 /* AlertModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertModel.swift; sourceTree = ""; }; + F45010C02A67DE6700105895 /* GameRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameRecord.swift; sourceTree = ""; }; + F45010C32A67DE8500105895 /* QuestionFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionFactory.swift; sourceTree = ""; }; + F45010C52A67DE9800105895 /* QuestionFactoryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionFactoryProtocol.swift; sourceTree = ""; }; + F45010C72A67DEA000105895 /* QuestionFactoryDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuestionFactoryDelegate.swift; sourceTree = ""; }; + F45010C92A67DEA900105895 /* AlertPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresenter.swift; sourceTree = ""; }; + F45010CB2A67DEB600105895 /* AlertProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertProtocol.swift; sourceTree = ""; }; + F45010CF2A67DECC00105895 /* StatisticService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticService.swift; sourceTree = ""; }; + F45010D32A67DF1100105895 /* QuizResultsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizResultsViewModel.swift; sourceTree = ""; }; + F471CA7D2A54310300C2E967 /* YS Display-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "YS Display-Bold.ttf"; sourceTree = ""; }; + F471CA7E2A54310300C2E967 /* YS Display-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "YS Display-Medium.ttf"; sourceTree = ""; }; + F476112D2A7983AD006C930D /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; + F476112F2A7991C3006C930D /* MostPopularMovies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MostPopularMovies.swift; sourceTree = ""; }; + F47611312A7A24E3006C930D /* MoviesLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLoader.swift; sourceTree = ""; }; + F4B050CE2A8A050A00CD72CC /* MovieQuizPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieQuizPresenter.swift; sourceTree = ""; }; + F4B050D02A8A4AF500CD72CC /* MovieQuizViewControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieQuizViewControllerProtocol.swift; sourceTree = ""; }; + F4B050D82A8A4B9000CD72CC /* MovieQuizPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieQuizPresenterTests.swift; sourceTree = ""; }; + F4B050F22A8A9E9B00CD72CC /* ArrayTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ArrayTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F4B050F42A8A9E9B00CD72CC /* ArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayTests.swift; sourceTree = ""; }; + F4B051012A8A9FF100CD72CC /* MoviesLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoviesLoaderTests.swift; sourceTree = ""; }; + F4B051132A8AA2E300CD72CC /* MovieQuizUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MovieQuizUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F4B051152A8AA2E300CD72CC /* MovieQuizUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieQuizUITests.swift; sourceTree = ""; }; + F4B051242A8AA50900CD72CC /* MovieQuizViewControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieQuizViewControllerMock.swift; sourceTree = ""; }; + F4B0512F2A8F866900CD72CC /* StubNetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StubNetworkClient.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -40,6 +106,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F4B050EF2A8A9E9B00CD72CC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4B051102A8AA2E300CD72CC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -47,6 +127,7 @@ isa = PBXGroup; children = ( AD1ABAB52831907B00B3E448 /* MovieQuizViewController.swift */, + F4B050CE2A8A050A00CD72CC /* MovieQuizPresenter.swift */, AD1ABAB72831907B00B3E448 /* Main.storyboard */, ); path = Presentation; @@ -55,6 +136,7 @@ 8F4738332848DE46005DF65E /* Resources */ = { isa = PBXGroup; children = ( + F471CA7C2A54310300C2E967 /* MovieQuizFonts */, AD1ABABC2831907F00B3E448 /* LaunchScreen.storyboard */, AD1ABABA2831907F00B3E448 /* Assets.xcassets */, AD1ABABF2831907F00B3E448 /* Info.plist */, @@ -66,6 +148,9 @@ isa = PBXGroup; children = ( AD1ABAB02831907B00B3E448 /* MovieQuiz */, + F4B050F32A8A9E9B00CD72CC /* ArrayTests */, + F4B051142A8AA2E300CD72CC /* MovieQuizUITests */, + F4B051232A8AA50900CD72CC /* MovieQuizViewControllerMock */, AD1ABAAF2831907B00B3E448 /* Products */, ); sourceTree = ""; @@ -74,6 +159,8 @@ isa = PBXGroup; children = ( AD1ABAAE2831907B00B3E448 /* MovieQuiz.app */, + F4B050F22A8A9E9B00CD72CC /* ArrayTests.xctest */, + F4B051132A8AA2E300CD72CC /* MovieQuizUITests.xctest */, ); name = Products; sourceTree = ""; @@ -81,6 +168,8 @@ AD1ABAB02831907B00B3E448 /* MovieQuiz */ = { isa = PBXGroup; children = ( + F45010B92A67DE2F00105895 /* Models */, + F45010C22A67DE7000105895 /* Services */, 8F4738322848DE2A005DF65E /* Presentation */, ADF0CF75283FDAA10075F54D /* Helpers */, 8F4738332848DE46005DF65E /* Resources */, @@ -100,6 +189,71 @@ path = Helpers; sourceTree = ""; }; + F45010B92A67DE2F00105895 /* Models */ = { + isa = PBXGroup; + children = ( + F45010BA2A67DE4200105895 /* QuizQuestion.swift */, + F45010BC2A67DE5100105895 /* QuizStepViewModel.swift */, + F45010D32A67DF1100105895 /* QuizResultsViewModel.swift */, + F45010BE2A67DE5900105895 /* AlertModel.swift */, + F45010C02A67DE6700105895 /* GameRecord.swift */, + F476112F2A7991C3006C930D /* MostPopularMovies.swift */, + ); + path = Models; + sourceTree = ""; + }; + F45010C22A67DE7000105895 /* Services */ = { + isa = PBXGroup; + children = ( + F45010C32A67DE8500105895 /* QuestionFactory.swift */, + F45010C52A67DE9800105895 /* QuestionFactoryProtocol.swift */, + F45010C72A67DEA000105895 /* QuestionFactoryDelegate.swift */, + F45010C92A67DEA900105895 /* AlertPresenter.swift */, + F45010CB2A67DEB600105895 /* AlertProtocol.swift */, + F45010CF2A67DECC00105895 /* StatisticService.swift */, + F476112D2A7983AD006C930D /* NetworkClient.swift */, + F47611312A7A24E3006C930D /* MoviesLoader.swift */, + F4B0512F2A8F866900CD72CC /* StubNetworkClient.swift */, + F4B050D02A8A4AF500CD72CC /* MovieQuizViewControllerProtocol.swift */, + ); + path = Services; + sourceTree = ""; + }; + F471CA7C2A54310300C2E967 /* MovieQuizFonts */ = { + isa = PBXGroup; + children = ( + F471CA7D2A54310300C2E967 /* YS Display-Bold.ttf */, + F471CA7E2A54310300C2E967 /* YS Display-Medium.ttf */, + ); + path = MovieQuizFonts; + sourceTree = ""; + }; + F4B050F32A8A9E9B00CD72CC /* ArrayTests */ = { + isa = PBXGroup; + children = ( + F4B050F42A8A9E9B00CD72CC /* ArrayTests.swift */, + F4B051012A8A9FF100CD72CC /* MoviesLoaderTests.swift */, + F4B050D82A8A4B9000CD72CC /* MovieQuizPresenterTests.swift */, + ); + path = ArrayTests; + sourceTree = ""; + }; + F4B051142A8AA2E300CD72CC /* MovieQuizUITests */ = { + isa = PBXGroup; + children = ( + F4B051152A8AA2E300CD72CC /* MovieQuizUITests.swift */, + ); + path = MovieQuizUITests; + sourceTree = ""; + }; + F4B051232A8AA50900CD72CC /* MovieQuizViewControllerMock */ = { + isa = PBXGroup; + children = ( + F4B051242A8AA50900CD72CC /* MovieQuizViewControllerMock.swift */, + ); + path = MovieQuizViewControllerMock; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -120,6 +274,42 @@ productReference = AD1ABAAE2831907B00B3E448 /* MovieQuiz.app */; productType = "com.apple.product-type.application"; }; + F4B050F12A8A9E9B00CD72CC /* ArrayTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F4B050F82A8A9E9B00CD72CC /* Build configuration list for PBXNativeTarget "ArrayTests" */; + buildPhases = ( + F4B050EE2A8A9E9B00CD72CC /* Sources */, + F4B050EF2A8A9E9B00CD72CC /* Frameworks */, + F4B050F02A8A9E9B00CD72CC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F4B050F72A8A9E9B00CD72CC /* PBXTargetDependency */, + ); + name = ArrayTests; + productName = ArrayTests; + productReference = F4B050F22A8A9E9B00CD72CC /* ArrayTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F4B051122A8AA2E300CD72CC /* MovieQuizUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F4B0511B2A8AA2E300CD72CC /* Build configuration list for PBXNativeTarget "MovieQuizUITests" */; + buildPhases = ( + F4B0510F2A8AA2E300CD72CC /* Sources */, + F4B051102A8AA2E300CD72CC /* Frameworks */, + F4B051112A8AA2E300CD72CC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F4B0511A2A8AA2E300CD72CC /* PBXTargetDependency */, + ); + name = MovieQuizUITests; + productName = MovieQuizUITests; + productReference = F4B051132A8AA2E300CD72CC /* MovieQuizUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -127,13 +317,21 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 1330; ORGANIZATIONNAME = ""; TargetAttributes = { AD1ABAAD2831907B00B3E448 = { CreatedOnToolsVersion = 13.3.1; }; + F4B050F12A8A9E9B00CD72CC = { + CreatedOnToolsVersion = 14.3.1; + TestTargetID = AD1ABAAD2831907B00B3E448; + }; + F4B051122A8AA2E300CD72CC = { + CreatedOnToolsVersion = 14.3.1; + TestTargetID = AD1ABAAD2831907B00B3E448; + }; }; }; buildConfigurationList = AD1ABAA92831907B00B3E448 /* Build configuration list for PBXProject "MovieQuiz" */; @@ -150,6 +348,8 @@ projectRoot = ""; targets = ( AD1ABAAD2831907B00B3E448 /* MovieQuiz */, + F4B050F12A8A9E9B00CD72CC /* ArrayTests */, + F4B051122A8AA2E300CD72CC /* MovieQuizUITests */, ); }; /* End PBXProject section */ @@ -161,7 +361,23 @@ files = ( AD1ABABE2831907F00B3E448 /* LaunchScreen.storyboard in Resources */, AD1ABABB2831907F00B3E448 /* Assets.xcassets in Resources */, + F471CA822A54358300C2E967 /* YS Display-Bold.ttf in Resources */, AD1ABAB92831907B00B3E448 /* Main.storyboard in Resources */, + F471CA812A54357F00C2E967 /* YS Display-Medium.ttf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4B050F02A8A9E9B00CD72CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4B051112A8AA2E300CD72CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; @@ -172,17 +388,65 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F45010C12A67DE6700105895 /* GameRecord.swift in Sources */, + F45010CA2A67DEA900105895 /* AlertPresenter.swift in Sources */, + F4B050D12A8A4AF500CD72CC /* MovieQuizViewControllerProtocol.swift in Sources */, AD5EE5DE284D7887003966EF /* UIColor+Extensions.swift in Sources */, AD7AFA552836189F00399704 /* Array+Extensions.swift in Sources */, + F45010CC2A67DEB600105895 /* AlertProtocol.swift in Sources */, + F45010BB2A67DE4200105895 /* QuizQuestion.swift in Sources */, + F45010C82A67DEA000105895 /* QuestionFactoryDelegate.swift in Sources */, + F4B051302A8F866900CD72CC /* StubNetworkClient.swift in Sources */, + F45010BD2A67DE5100105895 /* QuizStepViewModel.swift in Sources */, AD1ABAB62831907B00B3E448 /* MovieQuizViewController.swift in Sources */, + F45010C62A67DE9800105895 /* QuestionFactoryProtocol.swift in Sources */, + F476112E2A7983AD006C930D /* NetworkClient.swift in Sources */, + F4B050CF2A8A050A00CD72CC /* MovieQuizPresenter.swift in Sources */, AD77F5742857F8810062FB14 /* Date+Extensions.swift in Sources */, + F45010C42A67DE8500105895 /* QuestionFactory.swift in Sources */, AD1ABAB22831907B00B3E448 /* AppDelegate.swift in Sources */, + F45010BF2A67DE5900105895 /* AlertModel.swift in Sources */, + F47611322A7A24E3006C930D /* MoviesLoader.swift in Sources */, AD1ABAB42831907B00B3E448 /* SceneDelegate.swift in Sources */, + F47611302A7991C3006C930D /* MostPopularMovies.swift in Sources */, + F45010D02A67DECC00105895 /* StatisticService.swift in Sources */, + F45010D42A67DF1100105895 /* QuizResultsViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4B050EE2A8A9E9B00CD72CC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4B050F52A8A9E9B00CD72CC /* ArrayTests.swift in Sources */, + F4B051312A8F881F00CD72CC /* MovieQuizPresenterTests.swift in Sources */, + F4B051082A8A9FF400CD72CC /* MoviesLoaderTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F4B0510F2A8AA2E300CD72CC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F4B051162A8AA2E300CD72CC /* MovieQuizUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + F4B050F72A8A9E9B00CD72CC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AD1ABAAD2831907B00B3E448 /* MovieQuiz */; + targetProxy = F4B050F62A8A9E9B00CD72CC /* PBXContainerItemProxy */; + }; + F4B0511A2A8AA2E300CD72CC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AD1ABAAD2831907B00B3E448 /* MovieQuiz */; + targetProxy = F4B051192A8AA2E300CD72CC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ AD1ABAB72831907B00B3E448 /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -323,7 +587,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = N29P9VDV4Y; + DEVELOPMENT_TEAM = 66QMN95M53; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MovieQuiz/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -353,7 +617,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = N29P9VDV4Y; + DEVELOPMENT_TEAM = 66QMN95M53; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MovieQuiz/Resources/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -377,6 +641,84 @@ }; name = Release; }; + F4B050F92A8A9E9B00CD72CC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 66QMN95M53; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ru.yandex.practicum.ArrayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MovieQuiz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MovieQuiz"; + }; + name = Debug; + }; + F4B050FA2A8A9E9B00CD72CC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 66QMN95M53; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ru.yandex.practicum.ArrayTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MovieQuiz.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MovieQuiz"; + }; + name = Release; + }; + F4B0511C2A8AA2E300CD72CC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 66QMN95M53; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ru.yandex.practicum.MovieQuizUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MovieQuiz; + }; + name = Debug; + }; + F4B0511D2A8AA2E300CD72CC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 66QMN95M53; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = ru.yandex.practicum.MovieQuizUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MovieQuiz; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -398,6 +740,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F4B050F82A8A9E9B00CD72CC /* Build configuration list for PBXNativeTarget "ArrayTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4B050F92A8A9E9B00CD72CC /* Debug */, + F4B050FA2A8A9E9B00CD72CC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F4B0511B2A8AA2E300CD72CC /* Build configuration list for PBXNativeTarget "MovieQuizUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F4B0511C2A8AA2E300CD72CC /* Debug */, + F4B0511D2A8AA2E300CD72CC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = AD1ABAA62831907B00B3E448 /* Project object */; diff --git a/MovieQuiz/Helpers/UIColor+Extensions.swift b/MovieQuiz/Helpers/UIColor+Extensions.swift index 1ba50d2c81..c994c8c342 100644 --- a/MovieQuiz/Helpers/UIColor+Extensions.swift +++ b/MovieQuiz/Helpers/UIColor+Extensions.swift @@ -1,3 +1,10 @@ import UIKit -extension UIColor { } +extension UIColor { + static var ypGreen: UIColor { UIColor(named: "YP Green") ?? UIColor.green } + static var ypRed: UIColor { UIColor(named: "YP Red") ?? UIColor.red } + static var ypBlack: UIColor { UIColor(named: "YP Black") ?? UIColor.black } + static var ypBackground: UIColor { UIColor(named: "YP Background") ?? UIColor.darkGray } + static var ypGray: UIColor { UIColor(named: "YP Gray") ?? UIColor.gray } + static var ypWhite: UIColor { UIColor(named: "YP White") ?? UIColor.white } +} diff --git a/MovieQuiz/Models/AlertModel.swift b/MovieQuiz/Models/AlertModel.swift new file mode 100644 index 0000000000..984630f71b --- /dev/null +++ b/MovieQuiz/Models/AlertModel.swift @@ -0,0 +1,16 @@ +// +// AlertModel.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + +struct AlertModel { + let title: String + let text: String + let buttonText: String + let alertId: String + let completion: () -> Void +} diff --git a/MovieQuiz/Models/GameRecord.swift b/MovieQuiz/Models/GameRecord.swift new file mode 100644 index 0000000000..2ac2f50f59 --- /dev/null +++ b/MovieQuiz/Models/GameRecord.swift @@ -0,0 +1,22 @@ +// +// GameRecord.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + +struct GameRecord: Codable, Comparable { + + //количество правильных ответов + let correct: Int + //количество вопросов квиза + let total: Int + let date: Date + + // метод для сравнения счета на основе правильных ответов + static func < (lhs: GameRecord, rhs: GameRecord) -> Bool { + lhs.correct < rhs.correct + } +} diff --git a/MovieQuiz/Models/MostPopularMovies.swift b/MovieQuiz/Models/MostPopularMovies.swift new file mode 100644 index 0000000000..85eb96ee66 --- /dev/null +++ b/MovieQuiz/Models/MostPopularMovies.swift @@ -0,0 +1,36 @@ +// +// MostPopularMovies.swift +// MovieQuiz +// +// Created by Марат Хасанов on 01.08.2023. +// + +import Foundation + +struct MostPopularMovies: Codable { + let errorMessage: String + let items: [MostPopularMovie] +} + +struct MostPopularMovie: Codable { + let title: String + let rating: String + let imageURL: URL + + var resizedImage: URL { + let urlString = imageURL.absoluteString + + let imageURLString = urlString.components(separatedBy: "._")[0] + "._V0_UX600.jpg" + + guard let newURL = URL(string: imageURLString) else { + return imageURL + } + return newURL + } + + private enum CodingKeys: String, CodingKey { + case title = "fullTitle" + case rating = "imDbRating" + case imageURL = "image" + } +} diff --git a/MovieQuiz/Models/QuizQuestion.swift b/MovieQuiz/Models/QuizQuestion.swift new file mode 100644 index 0000000000..8fef858b72 --- /dev/null +++ b/MovieQuiz/Models/QuizQuestion.swift @@ -0,0 +1,15 @@ +// +// QuizQuestion.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + +//Mock-данные +struct QuizQuestion { + let image: Data + let text: String + let correctAnswer: Bool +} diff --git a/MovieQuiz/Models/QuizResultsViewModel.swift b/MovieQuiz/Models/QuizResultsViewModel.swift new file mode 100644 index 0000000000..61589628e2 --- /dev/null +++ b/MovieQuiz/Models/QuizResultsViewModel.swift @@ -0,0 +1,15 @@ +// +// QuizResultsViewModel.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + +//Результат квиза +struct QuizResultsViewModel { + let title: String + let text: String + let buttonText: String +} diff --git a/MovieQuiz/Models/QuizStepViewModel.swift b/MovieQuiz/Models/QuizStepViewModel.swift new file mode 100644 index 0000000000..71cca720dc --- /dev/null +++ b/MovieQuiz/Models/QuizStepViewModel.swift @@ -0,0 +1,16 @@ +// +// QuizStepViewModel.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation +import UIKit + +//Состояние "Вопрос показан +struct QuizStepViewModel { + let image: UIImage + let question: String + let questionNumber: String +} diff --git a/MovieQuiz/Presentation/Base.lproj/Main.storyboard b/MovieQuiz/Presentation/Base.lproj/Main.storyboard index cf28943f42..5efb6b9d8a 100644 --- a/MovieQuiz/Presentation/Base.lproj/Main.storyboard +++ b/MovieQuiz/Presentation/Base.lproj/Main.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -15,13 +16,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MovieQuiz/Presentation/MovieQuizPresenter.swift b/MovieQuiz/Presentation/MovieQuizPresenter.swift new file mode 100644 index 0000000000..ce628d24dd --- /dev/null +++ b/MovieQuiz/Presentation/MovieQuizPresenter.swift @@ -0,0 +1,131 @@ +// +// MovieQuizPresenter.swift +// MovieQuiz +// +// Created by Марат Хасанов on 14.08.2023. +// + +import UIKit + +final class MovieQuizPresenter: QuestionFactoryDelegate { + func didFailToLoadData(with error: Error) { + + } + + + private var currentQuestionIndex = 0 + private let questionsAmount: Int = 10 + private var correctAnswers: Int = 0 + private var currentQuestion: QuizQuestion? + + private weak var viewController: MovieQuizViewControllerProtocol? + private var questionFactory: QuestionFactoryProtocol? + private let statisticService: StatisticService = StatisticServiceImplementation() + + init(viewController: MovieQuizViewControllerProtocol) { + self.viewController = viewController + + questionFactory = QuestionFactory(moviesLoader: MoviesLoader(), delegate: self) + questionFactory?.loadData() + viewController.showLoadingIndicator() + } + + func didLoadDataFromServer() { + viewController?.hideLoadingIndicator() + questionFactory?.requestNextQuestion() + } + + func didFaildToLoadData(with error: Error) { + viewController?.showNetworkError(message: error.localizedDescription) + } + + func convert(model: QuizQuestion) -> QuizStepViewModel { + QuizStepViewModel(image: UIImage(data: model.image) ?? UIImage(), + question: model.text, + questionNumber: "\(currentQuestionIndex + 1)/\(questionsAmount)") + } + + func isLastQuestuion() -> Bool { + currentQuestionIndex == questionsAmount - 1 + } + + func resetQuestonIndex() { + currentQuestionIndex = 0 + } + + func switchToNextQuestion() { + currentQuestionIndex += 1 + } + + func yesButtonPressed() { + didAnswer(isYes: true) + } + func noButtonPressed() { + didAnswer(isYes: false) + } + + private func didAnswer(isYes: Bool){ + guard let currentQuestion = currentQuestion else { return } + let userAnswer = isYes + proceedWithAnswer(isCorrect: userAnswer == currentQuestion.correctAnswer) + } + + func didAnswer(isCorrectAnswer: Bool) { + if isCorrectAnswer { + correctAnswers += 1 + } + } + + func didReceiveNextQuestion(question: QuizQuestion?) { + guard let question = question else { return } + currentQuestion = question + let viewModel = convert(model: question) + DispatchQueue.main.async { [weak self] in + self?.viewController?.show(quiz: viewModel) + } + } + + func proceedToNextQuestionOrResult() { + if isLastQuestuion() { + + let text = "Вы ответили на \(correctAnswers) из 10, попробуйте еще раз!" + + let viewModel = QuizResultsViewModel(title: "Этот раунд окончен!", + text: text, + buttonText: "Сыграть ещё раз?") + viewController?.show(quiz: viewModel) + }else{ + switchToNextQuestion() + questionFactory?.requestNextQuestion() + } + } + + func restartQuiz() { + resetQuestonIndex() + correctAnswers = 0 + questionFactory?.requestNextQuestion() + } + + func makeResultMessage() -> String { + statisticService.store(correct: correctAnswers, total: questionsAmount) + let message = """ + Ваш результат: \(correctAnswers)/\(questionsAmount) + Количество сыгранных квизов: \(statisticService.gamesCount) + Рекорд: \(statisticService.bestGame.correct)/\(statisticService.bestGame.total)(\(statisticService.bestGame.date.dateTimeString)) + Среедняя точность: \(String(format: "%.2f", statisticService.totalAccuracy))% + """ + return message + } + + func proceedWithAnswer(isCorrect: Bool) { + + didAnswer(isCorrectAnswer: isCorrect) + + viewController?.highlightImageBorder(isCorrectAnswer: isCorrect) + + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + guard let self = self else {return} + self.proceedToNextQuestionOrResult() + } + } +} diff --git a/MovieQuiz/Presentation/MovieQuizViewController.swift b/MovieQuiz/Presentation/MovieQuizViewController.swift index 7aa88b8a27..df549962c8 100644 --- a/MovieQuiz/Presentation/MovieQuizViewController.swift +++ b/MovieQuiz/Presentation/MovieQuizViewController.swift @@ -1,72 +1,236 @@ import UIKit -final class MovieQuizViewController: UIViewController { +final class MovieQuizViewController: UIViewController, MovieQuizViewControllerProtocol { + // MARK: - Lifecycle + + //Типы на экране + struct ViewModel { + let image: UIImage + let questions: String + let questionNumber: String + } + + private var alertPresenter = AlertPresenter() + var statisticService: StatisticService? + var currentQuestion: QuizQuestion? + private var presenter: MovieQuizPresenter! + override func viewDidLoad() { super.viewDidLoad() + + presenter = MovieQuizPresenter(viewController: self) + + //presenter.viewController = self + + //alertPresenter = AlertPresenter(delegate: self) + + statisticService = StatisticServiceImplementation() + + showLoadingIndicator() + + screenSettings() + } + + @IBAction func noButtonClicked(_ sender: UIButton) { + presenter.noButtonPressed() + //setUnavailableButtons() + } + @IBAction func yesButtonClicked(_ sender: UIButton) { + //setUnavailableButtons() + presenter.yesButtonPressed() + + } + + @IBOutlet private weak var imageViev: UIImageView! + @IBOutlet private weak var textLabel: UILabel! + @IBOutlet private weak var indexLabel: UILabel! + @IBOutlet private weak var questionLabel: UILabel! + @IBOutlet private weak var noButton: UIButton! + @IBOutlet private weak var yesButton: UIButton! + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + + //не скрыт + func showLoadingIndicator() { + activityIndicator.isHidden = false + activityIndicator.startAnimating() + } + //скрыт + func hideLoadingIndicator() { + activityIndicator.isHidden = true + } + + func showNetworkError(message: String) { + hideLoadingIndicator() + + let alert = UIAlertController(title: "Ошибка", message: message, preferredStyle: .alert) + + let action = UIAlertAction(title: "Попробовать еще раз", style: .default) { [weak self] _ in + guard let self = self else { return } + self.presenter.restartQuiz() + } + alert.addAction(action) + } + + private func buttonsIsDisabled(){ + noButton.isEnabled = false + yesButton.isEnabled = false + } + + private func buttonsIsEnabled(){ + noButton.isEnabled = true + yesButton.isEnabled = true + } + + func showAlert(model: AlertModel) { + let alert = UIAlertController( + title: model.text, + message: model.text, + preferredStyle: .alert) + let action = UIAlertAction(title: model.buttonText, style: .default) { _ in + model.completion() + } + alert.addAction(action) + alert.view.accessibilityIdentifier = "Game results" + present(alert, animated: true) + } + //Приватный метод вывода на экран вопроса, который принимает на вход вью модель вопроса + func show(quiz result: QuizResultsViewModel) { + let message = presenter.makeResultMessage() + + let alert = UIAlertController( + title: result.title, + message: result.text, + preferredStyle: .alert) + + let action = UIAlertAction( + title: result.buttonText, + style: .default){ [weak self] _ in + guard let self = self else { return } + self.presenter.restartQuiz() + } + alert.addAction(action) + alert.view.accessibilityIdentifier = "Game results" + self.present(alert, animated: true, completion: nil) } -} - -/* - Mock-данные - - - Картинка: The Godfather - Настоящий рейтинг: 9,2 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: ДА - - - Картинка: The Dark Knight - Настоящий рейтинг: 9 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: ДА - - - Картинка: Kill Bill - Настоящий рейтинг: 8,1 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: ДА - - - Картинка: The Avengers - Настоящий рейтинг: 8 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: ДА - - - Картинка: Deadpool - Настоящий рейтинг: 8 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: ДА - - - Картинка: The Green Knight - Настоящий рейтинг: 6,6 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: ДА - - - Картинка: Old - Настоящий рейтинг: 5,8 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: НЕТ - - - Картинка: The Ice Age Adventures of Buck Wild - Настоящий рейтинг: 4,3 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: НЕТ - - - Картинка: Tesla - Настоящий рейтинг: 5,1 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: НЕТ - - Картинка: Vivarium - Настоящий рейтинг: 5,8 - Вопрос: Рейтинг этого фильма больше чем 6? - Ответ: НЕТ - */ + func show(quiz step: QuizStepViewModel) { + imageViev.layer.borderColor = UIColor.clear.cgColor + imageViev.image = step.image + textLabel.text = step.question + indexLabel.text = step.questionNumber + screenSettings() + } + + func highlightImageBorder(isCorrectAnswer: Bool) { + imageViev.layer.masksToBounds = true + imageViev.layer.borderWidth = 8 + imageViev.layer.borderColor = isCorrectAnswer ? UIColor.ypGreen.cgColor : UIColor.ypRed.cgColor + } + + private func setUnavailableButtons() { + noButton.isUserInteractionEnabled = false + yesButton.isUserInteractionEnabled = false + } + + private func setAvailableButtons() { + noButton.isUserInteractionEnabled = true + yesButton.isUserInteractionEnabled = true + } + + private func screenSettings() { + questionTitleLabelStyle() + counterLabelStyle() + imageViewStyle() + imageViewBorderStyle() + textLabelStyle() + yesButtonStyle() + noButtonStyle() + } + + private func questionTitleLabelStyle() { + questionLabel.font = UIFont(name: "YSDisplay-Medium", size: 20) + questionLabel.textColor = .ypWhite + } + + private func counterLabelStyle() { + indexLabel.font = UIFont(name: "YSDisplay-Medium", size: 20) + indexLabel.textColor = .ypWhite + } + + private func imageViewStyle() { + imageViev.layer.cornerRadius = 20 + imageViev.contentMode = .scaleAspectFill + imageViev.backgroundColor = .ypWhite + } + + private func textLabelStyle() { + textLabel.textColor = .ypWhite + textLabel.font = UIFont(name: "YSDisplay-Bold", size: 23) + textLabel.numberOfLines = 2 + textLabel.textAlignment = .center + } + + private func yesButtonStyle() { + yesButton.setTitle("Да", for: .normal) + yesButton.titleLabel?.font = UIFont(name: "YSDisplay-Medium", size: 20) + yesButton.setTitleColor(.ypBlack, for: .normal) + yesButton.layer.cornerRadius = 15 + yesButton.backgroundColor = .ypWhite + } + + private func noButtonStyle() { + noButton.setTitle("Нет", for: .normal) + noButton.titleLabel?.font = UIFont(name: "YSDisplay-Medium", size: 20) + noButton.setTitleColor(.ypBlack, for: .normal) + noButton.layer.cornerRadius = 15 + noButton.backgroundColor = .ypWhite + } + + private func imageViewBorderStyle() { + imageViev.layer.masksToBounds = true + imageViev.layer.borderWidth = 8 + imageViev.layer.borderColor = UIColor.clear.cgColor + imageViev.layer.cornerRadius = 20 + } +} +// func showResult() { +// //statisticService?.store(correct: correctAnswers, total: presenter.questionsAmount) +// statisticService?.updateGameStatisticService(correct: presenter.correctAnswers, amount: presenter.questionsAmount) +// let gameRecord = GameRecord(correct: presenter.correctAnswers, total: presenter.questionsAmount, date: Date()) +// +// if let bestGame = statisticService?.bestGame, +// gameRecord > bestGame { +// statisticService?.store(correct: presenter.correctAnswers, total: presenter.questionsAmount) +// } +// +// let alertModel = AlertModel( +// text: "Этот раунд окончен", +// message: makeMessage(), +// buttonText: "Сыграть еще раз", +// completion: { [weak self] in +// guard let self = self else { return } +// +// self.presenter.resetQuestionIndex() +// self.presenter.correctAnswers = 0 +// ///1111/self.questionFactory?.requestNextQuestion() +// self.presenter.restartGame() +// }) +// alertPresenter?.showAlert(model: alertModel) +// } +// +// private func makeMessage() -> String { //MARK: +// guard let gamesCount = statisticService?.gamesCount, +// let recordCount = statisticService?.bestGame.correct, +// let recordTotal = statisticService?.bestGame.total, +// let recordTime = statisticService?.bestGame.date.dateTimeString, +// let average = statisticService?.totalAccuracy else { +// return "Ошибка при формировании сообщения" +// } +// +// let message = "Ваш результат: \(presenter.correctAnswers)\\\(presenter.questionsAmount)\n" +// .appending("Количество сыгранных квизов: \(gamesCount)\n") +// .appending("Рекорд: \(recordCount)/\(recordTotal) (\(recordTime))\n") +// .appending("Средняя точность \(String(format: "%.2f", average))%") +// return message +// } diff --git a/MovieQuiz/Resources/Assets.xcassets/Colors/YP Background.colorset/Contents.json b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Background.colorset/Contents.json new file mode 100644 index 0000000000..ea40f9c935 --- /dev/null +++ b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0x22", + "green" : "0x1B", + "red" : "0x1A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.600", + "blue" : "0x22", + "green" : "0x1B", + "red" : "0x1A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieQuiz/Resources/Assets.xcassets/Colors/YP Black.colorset/Contents.json b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Black.colorset/Contents.json new file mode 100644 index 0000000000..b15ea4c334 --- /dev/null +++ b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Black.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x22", + "green" : "0x1B", + "red" : "0x1A" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x22", + "green" : "0x1B", + "red" : "0x1A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieQuiz/Resources/Assets.xcassets/Colors/YP Gray.colorset/Contents.json b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Gray.colorset/Contents.json new file mode 100644 index 0000000000..0e90c63f6c --- /dev/null +++ b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Gray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0xE8", + "red" : "0xE6" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xEB", + "green" : "0xE8", + "red" : "0xE6" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieQuiz/Resources/Assets.xcassets/Colors/YP Green.colorset/Contents.json b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Green.colorset/Contents.json new file mode 100644 index 0000000000..183ce985ce --- /dev/null +++ b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Green.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x8E", + "green" : "0xC2", + "red" : "0x60" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x8E", + "green" : "0xC2", + "red" : "0x60" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieQuiz/Resources/Assets.xcassets/Colors/YP Red.colorset/Contents.json b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Red.colorset/Contents.json new file mode 100644 index 0000000000..11976cadab --- /dev/null +++ b/MovieQuiz/Resources/Assets.xcassets/Colors/YP Red.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x6C", + "green" : "0x6B", + "red" : "0xF5" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x6C", + "green" : "0x6B", + "red" : "0xF5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieQuiz/Resources/Assets.xcassets/Colors/YP White.colorset/Contents.json b/MovieQuiz/Resources/Assets.xcassets/Colors/YP White.colorset/Contents.json new file mode 100644 index 0000000000..2536dc2d13 --- /dev/null +++ b/MovieQuiz/Resources/Assets.xcassets/Colors/YP White.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieQuiz/Resources/Assets.xcassets/Images/Default.imageset/Contents.json b/MovieQuiz/Resources/Assets.xcassets/Images/Default.imageset/Contents.json new file mode 100644 index 0000000000..a8cd806d15 --- /dev/null +++ b/MovieQuiz/Resources/Assets.xcassets/Images/Default.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MovieQuiz/Resources/Assets.xcassets/Images/Default.imageset/Default.png b/MovieQuiz/Resources/Assets.xcassets/Images/Default.imageset/Default.png new file mode 100644 index 0000000000..1d9a073e95 Binary files /dev/null and b/MovieQuiz/Resources/Assets.xcassets/Images/Default.imageset/Default.png differ diff --git a/MovieQuiz/Resources/Base.lproj/LaunchScreen.storyboard b/MovieQuiz/Resources/Base.lproj/LaunchScreen.storyboard index d1e0da06b2..3e17ea3152 100644 --- a/MovieQuiz/Resources/Base.lproj/LaunchScreen.storyboard +++ b/MovieQuiz/Resources/Base.lproj/LaunchScreen.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -15,8 +16,20 @@ + + + + + + + + + + + + @@ -24,4 +37,10 @@ + + + + + + diff --git a/MovieQuiz/Resources/Info.plist b/MovieQuiz/Resources/Info.plist index dd3c9afdae..389decae6e 100644 --- a/MovieQuiz/Resources/Info.plist +++ b/MovieQuiz/Resources/Info.plist @@ -2,8 +2,18 @@ + UIAppFonts + + YS Display-Bold.ttf + YS Display-Medium.ttf + UIApplicationSceneManifest + UIAppFonts + + YS Display-Bold + YS Display-Medium + UIApplicationSupportsMultipleScenes UISceneConfigurations diff --git a/MovieQuiz/Resources/MovieQuizFonts/YS Display-Bold.ttf b/MovieQuiz/Resources/MovieQuizFonts/YS Display-Bold.ttf new file mode 100644 index 0000000000..f9b3f03cce Binary files /dev/null and b/MovieQuiz/Resources/MovieQuizFonts/YS Display-Bold.ttf differ diff --git a/MovieQuiz/Resources/MovieQuizFonts/YS Display-Medium.ttf b/MovieQuiz/Resources/MovieQuizFonts/YS Display-Medium.ttf new file mode 100644 index 0000000000..cc63032e21 Binary files /dev/null and b/MovieQuiz/Resources/MovieQuizFonts/YS Display-Medium.ttf differ diff --git a/MovieQuiz/Services/AlertPresenter.swift b/MovieQuiz/Services/AlertPresenter.swift new file mode 100644 index 0000000000..544ad50e41 --- /dev/null +++ b/MovieQuiz/Services/AlertPresenter.swift @@ -0,0 +1,27 @@ +// +// AlertPresenter.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation +import UIKit + +final class AlertPresenter: AlertProtocol { + + func requestAlert(in vc: UIViewController, alertModel: AlertModel) { + let alert = UIAlertController(title: alertModel.title, + message: alertModel.text, + preferredStyle: .alert) + alert.view.accessibilityIdentifier = alertModel.alertId + let action = UIAlertAction(title: alertModel.buttonText, style: .default) { _ in + alertModel.completion() + } + alert.addAction(action) + alert.view.accessibilityIdentifier = "Game results" + + vc.present(alert, animated: true) + } +} + diff --git a/MovieQuiz/Services/AlertProtocol.swift b/MovieQuiz/Services/AlertProtocol.swift new file mode 100644 index 0000000000..46b4297e03 --- /dev/null +++ b/MovieQuiz/Services/AlertProtocol.swift @@ -0,0 +1,13 @@ +// +// AlertPresenterProtocol.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation +import UIKit + +protocol AlertProtocol { + func requestAlert(in vc: UIViewController, alertModel: AlertModel) +} diff --git a/MovieQuiz/Services/MovieQuizViewControllerProtocol.swift b/MovieQuiz/Services/MovieQuizViewControllerProtocol.swift new file mode 100644 index 0000000000..d59a7f7b9a --- /dev/null +++ b/MovieQuiz/Services/MovieQuizViewControllerProtocol.swift @@ -0,0 +1,17 @@ +// +// MovieQuizViewControllerProtocol.swift +// MovieQuiz +// +// Created by Марат Хасанов on 14.08.2023. +// + +import Foundation + +protocol MovieQuizViewControllerProtocol: AnyObject { + func show(quiz step: QuizStepViewModel) + func show(quiz result: QuizResultsViewModel) + func highlightImageBorder(isCorrectAnswer: Bool) + func showLoadingIndicator() + func hideLoadingIndicator() + func showNetworkError(message: String) +} diff --git a/MovieQuiz/Services/MoviesLoader.swift b/MovieQuiz/Services/MoviesLoader.swift new file mode 100644 index 0000000000..70231cad5a --- /dev/null +++ b/MovieQuiz/Services/MoviesLoader.swift @@ -0,0 +1,44 @@ +// +// MoviesLoader.swift +// MovieQuiz +// +// Created by Марат Хасанов on 02.08.2023. +// + +import Foundation + +protocol MoviesLoading { + func loadMovies(handler: @escaping (Result) -> Void) +} +struct MoviesLoader: MoviesLoading { + + private let networkClient: NetworkRouting + init(networkClient: NetworkRouting = NetworkClient()) { + self.networkClient = networkClient + } + + private var mostPopularMoviesUrl: URL { + // Если мы не смогли преобразовать строку в URL, то приложение упадёт с ошибкой + guard let url = URL(string: "https://imdb-api.com/en/API/Top250Movies/k_zcuw1ytf") else { + preconditionFailure("Unable to construct mostPopularMoviesUrl") + } + return url + } + func loadMovies(handler: @escaping (Result) -> Void) { + networkClient.fetch(url: mostPopularMoviesUrl) { result in + switch result { + case .success(let data): + do { + let mostPopularMovies = try JSONDecoder().decode(MostPopularMovies.self, from: data) + handler(.success(mostPopularMovies)) + } catch { + handler(.failure(error)) + } + case .failure(let error): + handler(.failure(error)) + } + } + } +} + + diff --git a/MovieQuiz/Services/NetworkClient.swift b/MovieQuiz/Services/NetworkClient.swift new file mode 100644 index 0000000000..18b03c6ff9 --- /dev/null +++ b/MovieQuiz/Services/NetworkClient.swift @@ -0,0 +1,41 @@ +// +// NetworkClient.swift +// MovieQuiz +// +// Created by Марат Хасанов on 01.08.2023. +// + +import Foundation + +protocol NetworkRouting { + func fetch(url: URL, handler: @escaping (Result) -> Void) +} + +struct NetworkClient: NetworkRouting { + + private enum NetworkError: Error { + case codeError + } + + func fetch(url: URL, handler: @escaping (Result) -> Void) { + let request = URLRequest(url: url) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + handler(.failure(error)) + return + } + + if let response = response as? HTTPURLResponse, + response.statusCode < 200 || response.statusCode >= 300 { + handler(.failure(NetworkError.codeError)) + return + } + + guard let data = data else { return } + handler(.success(data)) + } + + task.resume() + } +} diff --git a/MovieQuiz/Services/QuestionFactory.swift b/MovieQuiz/Services/QuestionFactory.swift new file mode 100644 index 0000000000..b65ed80e42 --- /dev/null +++ b/MovieQuiz/Services/QuestionFactory.swift @@ -0,0 +1,70 @@ +// +// QuestionFactory.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + + +final class QuestionFactory: QuestionFactoryProtocol { + + private weak var delegate: QuestionFactoryDelegate? + + private let moviesLoader: MoviesLoading + + private var movies: [MostPopularMovie] = [] + + init(moviesLoader: MoviesLoading, delegate: QuestionFactoryDelegate?) { + self.delegate = delegate + self.moviesLoader = moviesLoader + } + + func requestNextQuestion() { + DispatchQueue.global().async { [weak self] in + guard let self = self else {return} + let index = (0.. Float(safeQuestionRating) + let question = QuizQuestion(image: imageData, + text: text, + correctAnswer: correctAnswer) + + DispatchQueue.main.async { [weak self] in + guard let self = self else {return} + self.delegate?.didReceiveNextQuestion(question: question) + } + } + } + func loadData() { + moviesLoader.loadMovies { [weak self] result in + DispatchQueue.main.async { + guard let self = self else { return } + + switch result { + case .success(let mostPopularMovies): + self.movies = mostPopularMovies.items + self.delegate?.didLoadDataFromServer() + case .failure(let error): + self.delegate?.didFailToLoadData(with: error) + + } + } + } + } +} diff --git a/MovieQuiz/Services/QuestionFactoryDelegate.swift b/MovieQuiz/Services/QuestionFactoryDelegate.swift new file mode 100644 index 0000000000..a6b29b458e --- /dev/null +++ b/MovieQuiz/Services/QuestionFactoryDelegate.swift @@ -0,0 +1,14 @@ +// +// QuestionFactoryDelegate.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + +protocol QuestionFactoryDelegate: AnyObject { + func didReceiveNextQuestion(question: QuizQuestion?) + func didLoadDataFromServer() + func didFailToLoadData(with error: Error) +} diff --git a/MovieQuiz/Services/QuestionFactoryProtocol.swift b/MovieQuiz/Services/QuestionFactoryProtocol.swift new file mode 100644 index 0000000000..91941281be --- /dev/null +++ b/MovieQuiz/Services/QuestionFactoryProtocol.swift @@ -0,0 +1,14 @@ +// +// QuestionFactoryProtocol.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + +protocol QuestionFactoryProtocol: QuestionFactory { + + func requestNextQuestion() + func loadData() +} diff --git a/MovieQuiz/Services/StatisticService.swift b/MovieQuiz/Services/StatisticService.swift new file mode 100644 index 0000000000..239db0518e --- /dev/null +++ b/MovieQuiz/Services/StatisticService.swift @@ -0,0 +1,87 @@ +// +// StatisticService.swift +// MovieQuiz +// +// Created by Марат Хасанов on 19.07.2023. +// + +import Foundation + +protocol StatisticService { + var totalAccuracy: Double { get } + var gamesCount: Int { get } + var bestGame: GameRecord { get } + + func store(correct count: Int, total amount: Int) +} + +final class StatisticServiceImplementation: StatisticService { + + private enum Keys: String { + case correct, total, bestGame, gameCount + } + + private let userDefaults = UserDefaults.standard + + var total: Int { + get { + userDefaults.integer(forKey: Keys.total.rawValue) + } + set { + userDefaults.set(newValue, forKey: Keys.total.rawValue) + } + } + + var correct: Int { + get { + userDefaults.integer(forKey: Keys.correct.rawValue) + } + set { + userDefaults.set(newValue, forKey: Keys.correct.rawValue) + } + } + + var gamesCount: Int { + get { + return userDefaults.integer(forKey: Keys.gameCount.rawValue) + } + set { + userDefaults.set(newValue, forKey: Keys.gameCount.rawValue) + } + } + + var totalAccuracy: Double { + get { + return total > 0 ? Double(correct) / Double(total) * 100 : 0 + } + } + + var bestGame: GameRecord { + get { + guard let data = userDefaults.data(forKey: Keys.bestGame.rawValue), + let record = try? JSONDecoder().decode(GameRecord.self, from: data) else { + return .init(correct: 0, total: 0, date: Date()) + } + return record + } + + set { + guard let data = try? JSONEncoder().encode(newValue) else { + print("невозможно сохранить результат") + return + } + userDefaults.set(data, forKey: Keys.bestGame.rawValue) + } + } + + func store(correct count: Int, total amount: Int) { + let currentGame = GameRecord(correct: count, total: amount, date: Date()) + if currentGame > bestGame { + bestGame = currentGame + } + correct += count + total += amount + gamesCount += 1 + } + +} diff --git a/MovieQuiz/Services/StubNetworkClient.swift b/MovieQuiz/Services/StubNetworkClient.swift new file mode 100644 index 0000000000..c463a67b8e --- /dev/null +++ b/MovieQuiz/Services/StubNetworkClient.swift @@ -0,0 +1,58 @@ +// +// StubNetworkClient.swift +// MovieQuiz +// +// Created by Марат Хасанов on 18.08.2023. +// + +import Foundation + +struct StubNetworkClient: NetworkRouting { + enum TestError: Error { + case test + } + + let emulateError: Bool + + func fetch(url: URL, handler: @escaping (Result) -> Void) { + if emulateError { + handler(.failure(TestError.test)) + } else { + handler(.success(expectedResponse)) + } + } + + private var expectedResponse: Data { + """ + { + "errorMessage" : "", + "items" : [ + { + "crew" : "Dan Trachtenberg (dir.), Amber Midthunder, Dakota Beavers", + "fullTitle" : "Prey (2022)", + "id" : "tt11866324", + "imDbRating" : "7.2", + "imDbRatingCount" : "93332", + "image" : "https://m.media-amazon.com/images/M/MV5BMDBlMDYxMDktOTUxMS00MjcxLWE2YjQtNjNhMjNmN2Y3ZDA1XkEyXkFqcGdeQXVyMTM1MTE1NDMx._V1_Ratio0.6716_AL_.jpg", + "rank" : "1", + "rankUpDown" : "+23", + "title" : "Prey", + "year" : "2022" + }, + { + "crew" : "Anthony Russo (dir.), Ryan Gosling, Chris Evans", + "fullTitle" : "The Gray Man (2022)", + "id" : "tt1649418", + "imDbRating" : "6.5", + "imDbRatingCount" : "132890", + "image" : "https://m.media-amazon.com/images/M/MV5BOWY4MmFiY2QtMzE1YS00NTg1LWIwOTQtYTI4ZGUzNWIxNTVmXkEyXkFqcGdeQXVyODk4OTc3MTY@._V1_Ratio0.6716_AL_.jpg", + "rank" : "2", + "rankUpDown" : "-1", + "title" : "The Gray Man", + "year" : "2022" + } + ] + } + """.data(using: .utf8) ?? Data() + } +} diff --git a/MovieQuizUITests/MovieQuizUITests.swift b/MovieQuizUITests/MovieQuizUITests.swift new file mode 100644 index 0000000000..d7bc923c6f --- /dev/null +++ b/MovieQuizUITests/MovieQuizUITests.swift @@ -0,0 +1,110 @@ +// +// MovieQuizUITests.swift +// MovieQuizUITests +// +// Created by Марат Хасанов on 14.08.2023. +// + +import XCTest + +final class MovieQuizUITests: XCTestCase { + + var app: XCUIApplication! + + override func setUpWithError() throws { + + app = XCUIApplication() + app.launch() + continueAfterFailure = false + } + + override func tearDownWithError() throws { + app.terminate() + app = nil + } + + func testYesButton() { + sleep(3) + + let firstPoster = app.images["Poster"] + let firstPosterData = firstPoster.screenshot().pngRepresentation + + app.buttons["Yes"].tap() + sleep(3) + + let secondPoster = app.images["Poster"] + let secondPosterData = secondPoster.screenshot().pngRepresentation + + XCTAssertNotEqual(firstPosterData, secondPosterData) + } + + func testNoButton() { + sleep(3) + + let firstPoster = app.images["Poster"] + let firstPosterData = firstPoster.screenshot().pngRepresentation + + app.buttons["No"].tap() + sleep(3) + + let secondPoster = app.images["Poster"] + let secondPosterData = secondPoster.screenshot().pngRepresentation + + let indexLabel = app.staticTexts["Index"] + + XCTAssertNotEqual(firstPosterData, secondPosterData) + XCTAssertEqual(indexLabel.label, "2/10") + } + + func testGameFinishAlert() { + sleep(4) + + for _ in 1...10 { + app.buttons["Yes"].tap() + sleep(3) + } + + let alert = app.alerts["Game results"] + + XCTAssertTrue(alert.exists) + XCTAssertTrue(alert.label == "Этот раунд окончен!") + XCTAssertTrue(alert.buttons.firstMatch.label == "Сыграть ещё раз?") + + } + + func testGameFinish() { + sleep(5) + + for _ in 1...10 { + let button = app.buttons["No"] + button.tap() + sleep(5) + } + + let alert = app.alerts["Game results"] + + XCTAssertTrue(alert.exists) + XCTAssertTrue(alert.label == "Этот раунд окончен!") + XCTAssertTrue(alert.buttons.firstMatch.label == "Сыграть ещё раз?") + } + + func testAlertDismiss() { + sleep(5) + + for _ in 1...10 { + app.buttons["No"].tap() + sleep(3) + } + + let alert = app.alerts["Game results"] + alert.buttons.firstMatch.tap() + + sleep(5) + + let indexLable = app.staticTexts["Index"] + + XCTAssertFalse(alert.exists) + XCTAssertEqual(indexLable.label, "1/10") + } +} + diff --git a/MovieQuizViewControllerMock/MovieQuizViewControllerMock.swift b/MovieQuizViewControllerMock/MovieQuizViewControllerMock.swift new file mode 100644 index 0000000000..2c61d24189 --- /dev/null +++ b/MovieQuizViewControllerMock/MovieQuizViewControllerMock.swift @@ -0,0 +1,39 @@ +// +// MovieQuizViewControllerMock11.swift +// MovieQuizViewControllerMock11 +// +// Created by Марат Хасанов on 14.08.2023. +// + +import XCTest +@testable import MovieQuiz + +final class MovieQuizViewControllerMock: MovieQuizViewControllerProtocol { + func show(quiz step: QuizStepViewModel) { + } + func show(quiz result: QuizResultsViewModel) { + } + func highlightImageBorder(isCorrectAnswer: Bool) { + } + func showLoadingIndicator() { + } + func hideLoadingIndicator() { + } + func showNetworkError(message: String) { + } +} + +//final class MovieQuizPresenterTests: XCTestCase { +// func testPresenterConvertModel() throws { +// let viewControllerMock = MovieQuizViewControllerMock() +// let sut = MovieQuizPresenter(viewController: viewControllerMock) +// +// let emptyData = Data() +// let question = QuizQuestion(image: emptyData, text: "Question Text", correctAnswer: true) +// let viewModel = sut.convert(model: question) +// +// XCTAssertNotNil(viewModel.image) +// XCTAssertEqual(viewModel.question, "Question Text") +// XCTAssertEqual(viewModel.questionNumber, "1/10") +// } +//} diff --git a/README.md b/README.md index 4519e3e369..28f22f06d3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ MovieQuiz - это приложение с квизами о фильмах из [Макет Figma](https://www.figma.com/file/l0IMG3Eys35fUrbvArtwsR/YP-Quiz?node-id=34%3A243) [API IMDb](https://imdb-api.com/api#Top250Movies-header) +k_zcuw1ytf [Шрифты](https://code.s3.yandex.net/Mobile/iOS/Fonts/MovieQuizFonts.zip)