diff --git a/ios-quiz-challenge/README.md b/ios-quiz-challenge/README.md new file mode 100644 index 0000000000..34f694d858 --- /dev/null +++ b/ios-quiz-challenge/README.md @@ -0,0 +1,112 @@ +# iOS Quiz Challenge + +Aplicativo iOS de quiz desenvolvido em Swift para o iOS Developer Challenge. + +## Requisitos + +- Xcode 26 ou superior +- iOS Simulator com iOS 26 ou superior +- CocoaPods 1.17 ou superior +- Ruby disponível no ambiente local + +## Como Rodar + +1. Instale as dependências: + +```sh +cd ios-quiz-challenge +pod install +``` + +2. Abra o workspace: + +```sh +open ios-quiz-challenge.xcworkspace +``` + +3. Selecione o scheme `ios-quiz-challenge`. + +4. Rode o app em um simulador iOS. + +## Como Testar + +Pelo Xcode, use o scheme: + +- `ios-quiz-challenge-UnitTests` + +## Funcionalidades + +- Cadastro de nickname antes de iniciar o quiz. +- Fluxo de 10 perguntas de múltipla escolha. +- Perguntas carregadas via `GET /question`. +- Respostas validadas via `POST /answer?questionId=$id`. +- Feedback visual de resposta correta/incorreta antes da próxima pergunta. +- Timer de 120 segundos para o quiz. +- Finalização automática quando o timer chega a zero. +- Pontuação final calculada por `acertos * segundosRestantes`. +- Tela de resultado com opção de reiniciar o quiz. +- Ranking com persistência local. + +## Decisões De Produto + +- A persistência mantém o melhor score por nickname, em vez de armazenar todas as tentativas. Essa decisão privilegia uma experiência de ranking mais clara para o usuário. +- Os snapshot tests são unit-style, instanciando views/componentes diretamente com mocks/spies, sem navegação e2e pelo app. + +## Arquitetura + +O app é organizado por feature, foi decidido mante-los no target principal por questão de comodidade, mas poderia ter sido utilizada a mesma estratégia de modularização dos modulos dNetwork e DynaUI, as features mapeadas são: + +- `Entry`: cadastro e carregamento do nickname. +- `Quiz`: fluxo de perguntas, respostas, timer e pontuação. +- `Result`: exibição e salvamento do resultado. +- `Ranking`: listagem dos melhores scores. +- `Shared`: componentes visuais compartilhados. + +Cada feature segue uma separação de responsabilidades inspirada em VIP/Clean Architecture: + +- `Configurator`: montagem da feature e injeção de dependências. +- `Interactor`: regras de negócio e orquestração de use cases. +- `Presenter`: formatação de dados para exibição. +- `Router`: navegação. +- `View/ViewState`: SwiftUI e estado observável. +- `Domain`: use cases e contratos. +- `Service`: comunicação HTTP e DTOs. + +## Dependências + +O projeto usa CocoaPods com pods locais: + +- `DynaUI`: componentes visuais reutilizáveis. +- `dNetwork`: cliente HTTP baseado em `URLSession`. +- `dDependencies`: registro e resolução simples de dependências. + +## Persistência + +Os dados são persistidos com `UserDefaults` por meio de `UserDefaultsPlayerRepository`. + +São armazenados: + +- nickname atual; +- melhores scores por nickname. + +## API + +Host utilizado: + +```text +https://quiz-api-bwi5hjqyaq-uc.a.run.app +``` + +Endpoints: + +- `GET /question` +- `POST /answer?questionId=$id` + +## Observações + +- Para simular a tela de erro, uma forma simples é alterar temporariamente o path `question` para um endpoint inválido em `QuizService.swift`. +- Se `xcodebuild` falhar com erro de `xcode-select`, selecione uma instalação completa do Xcode: + +```sh +sudo xcode-select -s /Applications/Xcode.app/Contents/Developer +``` diff --git a/ios-quiz-challenge/ios-quiz-challenge-UnitTests.xctestplan b/ios-quiz-challenge/ios-quiz-challenge-UnitTests.xctestplan new file mode 100644 index 0000000000..e32e4bc7f9 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge-UnitTests.xctestplan @@ -0,0 +1,30 @@ +{ + "configurations" : [ + { + "id" : "22091426-329E-450B-BC3D-E1B4673828BA", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "performanceAntipatternCheckerEnabled" : true, + "targetForVariableExpansion" : { + "containerPath" : "container:ios-quiz-challenge.xcodeproj", + "identifier" : "6C73BA4E300410E900C231BC", + "name" : "ios-quiz-challenge" + } + }, + "testTargets" : [ + { + "parallelizable" : false, + "target" : { + "containerPath" : "container:ios-quiz-challenge.xcodeproj", + "identifier" : "6C73BA5D300410EB00C231BC", + "name" : "ios-quiz-challengeTests" + } + } + ], + "version" : 1 +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/DynaUI/DynaUI.podspec b/ios-quiz-challenge/ios-quiz-challenge/DynaUI/DynaUI.podspec new file mode 100644 index 0000000000..c015a0a437 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/DynaUI/DynaUI.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |spec| + spec.name = 'DynaUI' + spec.version = '0.1.0' + spec.summary = 'SwiftUI components for the DynaUI Design System.' + spec.description = 'A lightweight Design System built with reusable SwiftUI components.' + + spec.homepage = 'https://github.com/kiyo92/DynaUI' + spec.license = { :type => 'MIT' } + spec.author = 'João Marcus' + + spec.source = { + :git => 'https://github.com/kiyo92/DynaUI.git', + :tag => spec.version.to_s + } + + spec.ios.deployment_target = '26.0' + spec.swift_versions = ['5.9', '6.0'] + + spec.source_files = 'Sources/**/*.swift' + spec.frameworks = 'SwiftUI' +end \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/DynaUI/Sources/Components/DynaOptionButton.swift b/ios-quiz-challenge/ios-quiz-challenge/DynaUI/Sources/Components/DynaOptionButton.swift new file mode 100644 index 0000000000..166acb3b23 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/DynaUI/Sources/Components/DynaOptionButton.swift @@ -0,0 +1,184 @@ +// +// DynaOptionButton.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 12/07/26. +// + +import SwiftUI + +public struct DynaOptionButton: View { + + public enum State: Equatable { + case idle + case selected + case correct + case incorrect + } + + private let title: String + private let state: State + private let isEnabled: Bool + private let action: () -> Void + + public init( + _ title: String, + state: State = .idle, + isEnabled: Bool = true, + action: @escaping () -> Void + ) { + self.title = title + self.state = state + self.isEnabled = isEnabled + self.action = action + } + + public var body: some View { + Button(action: action) { + Text(title) + .font(.system(size: 14, weight: .semibold)) + .multilineTextAlignment(.center) + .foregroundColor(foregroundColor) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity) + .frame(minHeight: 44) + } + .buttonStyle( + DynaOptionButtonStyle( + backgroundColor: backgroundColor, + borderColor: borderColor + ) + ) + .disabled(!isEnabled) + .accessibilityLabel(title) + .accessibilityAddTraits( + state == .selected ? .isSelected : [] + ) + } +} + +private extension DynaOptionButton { + + var backgroundColor: Color { + switch state { + case .idle: + return .white + + case .selected: + return Color( + red: 0.42, + green: 0.21, + blue: 1 + ) + + case .correct: + return Color( + red: 0.16, + green: 0.68, + blue: 0.35 + ) + + case .incorrect: + return Color( + red: 0.91, + green: 0.22, + blue: 0.27 + ) + } + } + + var foregroundColor: Color { + switch state { + case .idle: + return .black + + case .selected, .correct, .incorrect: + return .white + } + } + + var borderColor: Color { + .black + } +} + +private struct DynaOptionButtonStyle: ButtonStyle { + + let backgroundColor: Color + let borderColor: Color + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .background { + ZStack { + RoundedRectangle( + cornerRadius: 17, + style: .continuous + ) + .fill(Color.black) + .offset( + y: configuration.isPressed ? 1 : 4 + ) + + RoundedRectangle( + cornerRadius: 17, + style: .continuous + ) + .fill(backgroundColor) + + RoundedRectangle( + cornerRadius: 17, + style: .continuous + ) + .stroke( + borderColor, + lineWidth: 2 + ) + } + } + .contentShape( + RoundedRectangle( + cornerRadius: 17, + style: .continuous + ) + ) + .offset( + y: configuration.isPressed ? 3 : 0 + ) + .scaleEffect( + configuration.isPressed ? 0.99 : 1 + ) + .padding(.bottom, 4) + .animation( + .easeOut(duration: 0.08), + value: configuration.isPressed + ) + } +} + +struct DynaOptionButton_Previews: PreviewProvider { + + static var previews: some View { + VStack(spacing: 16) { + DynaOptionButton("Husky") {} + + DynaOptionButton( + "Pitbull", + state: .selected + ) {} + + DynaOptionButton( + "Shiba Inu", + state: .correct + ) {} + + DynaOptionButton( + "Golden Retriever", + state: .incorrect + ) {} + } + .padding(24) + .background(Color.gray.opacity(0.15)) + .previewLayout(.sizeThatFits) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/DynaUI/Sources/Components/DynaPanel.swift b/ios-quiz-challenge/ios-quiz-challenge/DynaUI/Sources/Components/DynaPanel.swift new file mode 100644 index 0000000000..64efa02ee8 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/DynaUI/Sources/Components/DynaPanel.swift @@ -0,0 +1,443 @@ +// +// DynaPanel.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 14/07/26. +// + +import SwiftUI + +public enum DynaPanelLayout { + case vertical + case horizontal + + var scrollAxis: Axis.Set { + switch self { + case .vertical: + return .vertical + + case .horizontal: + return .horizontal + } + } +} + +@MainActor +public struct DynaPanelStyle { + public var backgroundColor: Color + public var shadowColor: Color + public var borderColor: Color + + public var borderWidth: CGFloat + public var cornerRadius: CGFloat + + public var shadowOffset: CGSize + + public var contentInsets: EdgeInsets + public var itemSpacing: CGFloat + public var headerContentSpacing: CGFloat + + public init( + backgroundColor: Color, + shadowColor: Color, + borderColor: Color = .black, + borderWidth: CGFloat = 2, + cornerRadius: CGFloat = 21, + shadowOffset: CGSize = .init(width: 8, height: 8), + contentInsets: EdgeInsets = .init( + top: 16, + leading: 8, + bottom: 16, + trailing: 8 + ), + itemSpacing: CGFloat = 8, + headerContentSpacing: CGFloat = 12 + ) { + self.backgroundColor = backgroundColor + self.shadowColor = shadowColor + self.borderColor = borderColor + self.borderWidth = borderWidth + self.cornerRadius = cornerRadius + self.shadowOffset = shadowOffset + self.contentInsets = contentInsets + self.itemSpacing = itemSpacing + self.headerContentSpacing = headerContentSpacing + } + + public static let `default` = DynaPanelStyle( + backgroundColor: Color( + red: 0.42, + green: 0.20, + blue: 1 + ), + shadowColor: Color( + red: 1, + green: 0.23, + blue: 0.52 + ) + ) +} + +public struct DynaPanelMotion { + public var panelInitialEdge: Edge + public var shadowInitialEdge: Edge + public var distance: CGFloat + + public init( + panelInitialEdge: Edge = .leading, + shadowInitialEdge: Edge = .trailing, + distance: CGFloat = 500 + ) { + self.panelInitialEdge = panelInitialEdge + self.shadowInitialEdge = shadowInitialEdge + self.distance = distance + } + + public static func opposingHorizontal( + distance: CGFloat = 500 + ) -> DynaPanelMotion { + DynaPanelMotion( + panelInitialEdge: .leading, + shadowInitialEdge: .trailing, + distance: distance + ) + } +} + +public struct DynaPanel: View { + private let layout: DynaPanelLayout + private let scrollsContent: Bool + + private let style: DynaPanelStyle + private let motion: DynaPanelMotion + + private let isPanelVisible: Bool + private let isShadowVisible: Bool + + private let header: PanelHeader? + private let content: Content + + public init( + title: String? = nil, + icon: String? = nil, + buttonTitle: String? = nil, + layout: DynaPanelLayout = .vertical, + scrollsContent: Bool = false, + style: DynaPanelStyle = .default, + motion: DynaPanelMotion = .opposingHorizontal(), + isPanelVisible: Bool = true, + isShadowVisible: Bool = true, + titleFont: Font = .headline, + titleColor: Color = .primary, + headerSpacing: CGFloat = 8, + buttonAction: (() -> Void)? = nil, + @ViewBuilder content: () -> Content + ) { + self.layout = layout + self.scrollsContent = scrollsContent + self.style = style + self.motion = motion + self.isPanelVisible = isPanelVisible + self.isShadowVisible = isShadowVisible + self.header = title.map { + PanelHeader( + icon: icon, + title: $0, + buttonTitle: buttonTitle, + titleFont: titleFont, + titleColor: titleColor, + spacing: headerSpacing, + buttonAction: buttonAction + ) + } + self.content = content() + } + + public var body: some View { + panelSurface + .offset(panelOffset) + .opacity(isPanelVisible ? 1 : 0) + .background { + panelShadow + } + .padding( + .leading, + max(0, -style.shadowOffset.width) + ) + .padding( + .trailing, + max(0, style.shadowOffset.width) + ) + .padding( + .top, + max(0, -style.shadowOffset.height) + ) + .padding( + .bottom, + max(0, style.shadowOffset.height) + ) + } + + private var panelSurface: some View { + VStack( + alignment: .leading, + spacing: style.headerContentSpacing + ) { + if let header { + PanelHeaderView(header: header) + .frame( + maxWidth: .infinity, + alignment: .leading + ) + } + + arrangedContent + } + .padding(style.contentInsets) + .background { + panelShape + .fill(style.backgroundColor) + } + .overlay { + panelShape + .stroke( + style.borderColor, + lineWidth: style.borderWidth + ) + } + } + + @ViewBuilder + private var arrangedContent: some View { + if scrollsContent { + ScrollView( + layout.scrollAxis, + showsIndicators: false + ) { + stackContent + } + } else { + stackContent + } + } + + @ViewBuilder + private var stackContent: some View { + switch layout { + case .vertical: + VStack(spacing: style.itemSpacing) { + content + } + .frame(maxWidth: .infinity) + + case .horizontal: + HStack(spacing: style.itemSpacing) { + content + } + } + } + + private var panelShadow: some View { + panelShape + .fill(style.shadowColor) + .overlay { + panelShape + .stroke( + style.borderColor, + lineWidth: style.borderWidth + ) + } + .offset(shadowOffset) + .opacity(isShadowVisible ? 1 : 0) + } + + private var panelShape: RoundedRectangle { + RoundedRectangle( + cornerRadius: style.cornerRadius, + style: .continuous + ) + } + + private var panelOffset: CGSize { + guard !isPanelVisible else { + return .zero + } + + return hiddenOffset( + for: motion.panelInitialEdge + ) + } + + private var shadowOffset: CGSize { + let entranceOffset: CGSize + + if isShadowVisible { + entranceOffset = .zero + } else { + entranceOffset = hiddenOffset( + for: motion.shadowInitialEdge + ) + } + + return CGSize( + width: style.shadowOffset.width + + entranceOffset.width, + height: style.shadowOffset.height + + entranceOffset.height + ) + } + + private func hiddenOffset( + for edge: Edge + ) -> CGSize { + switch edge { + case .top: + return CGSize( + width: 0, + height: -motion.distance + ) + + case .leading: + return CGSize( + width: -motion.distance, + height: 0 + ) + + case .bottom: + return CGSize( + width: 0, + height: motion.distance + ) + + case .trailing: + return CGSize( + width: motion.distance, + height: 0 + ) + } + } +} + +private struct PanelHeader { + let icon: String? + let title: String + let buttonTitle: String? + let titleFont: Font + let titleColor: Color + let spacing: CGFloat + let buttonAction: (() -> Void)? +} + +private struct PanelHeaderView: View { + let header: PanelHeader + + var body: some View { + HStack(spacing: header.spacing) { + if let icon = header.icon { + iconView(icon) + } + + Text(header.title) + .font(header.titleFont) + .foregroundStyle(header.titleColor) + + Spacer(minLength: header.spacing) + + if + let buttonTitle = header.buttonTitle, + let buttonAction = header.buttonAction + { + Button(buttonTitle, action: buttonAction) + .foregroundStyle(header.titleColor) + } + } + .frame(maxWidth: .infinity) + .padding(.horizontal, header.icon == nil ? 0 : 4) + } +} + +private extension PanelHeaderView { + func iconView(_ icon: String) -> some View { + Circle() + .foregroundStyle(.black) + .frame(width: 26, height: 26) + .overlay { + Image(systemName: icon) + .font(.system(size: 14)) + .foregroundStyle(.black) + .background { + Circle() + .frame(width: 22, height: 22) + .foregroundStyle(.white) + } + .offset( + CGSize(width: -1, height: -1) + ) + } + } +} + +#Preview { + @Previewable var options: [String] = ["Poodle", "Husky", "Golden Retriever"] + + DynaPanel( + title: "Choose a category", + layout: .horizontal, + scrollsContent: true, + titleColor: .white + ) { + ForEach( + options.enumerated(), + id: \.element.self + ) { index, option in + DynaOptionButton(option) {} + } + } + + DynaPanel( + title: "Power-ups", + icon: "bolt.fill", + buttonTitle: "See all", + layout: .horizontal, + scrollsContent: false, + titleColor: .white, + buttonAction: { + print("See all") + } + ) { + VStack(spacing: 12) { + Text("Kiyo") + .foregroundStyle(.white) + .font(Font.system(size: 48, weight: .bold)) + .frame(maxWidth: .infinity) + + HStack { + ForEach( + options.enumerated(), + id: \.element.self + ) { index, option in + DynaOptionButton(option) {} + } + } + } + .padding(.horizontal, 4) + } + + DynaPanel( + title: "Select an answer", + layout: .vertical, + titleFont: .system( + size: 18, + weight: .bold + ), + titleColor: .white + ) { + ForEach( + options.enumerated(), + id: \.element.self + ) { index, option in + DynaOptionButton(option) {} + } + } + +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Podfile b/ios-quiz-challenge/ios-quiz-challenge/Podfile new file mode 100644 index 0000000000..f0ced5cfac --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Podfile @@ -0,0 +1,23 @@ +platform :ios, '26.0' + +target 'ios-quiz-challenge' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! :linkage => :static + + # Pods for ios-quiz-challenge + + pod 'DynaUI', :path => 'DynaUI' + pod 'dNetwork', :path => 'dNetwork' + pod 'dDependencies', :path => 'dDependencies' + + target 'ios-quiz-challengeTests' do + inherit! :search_paths + # Pods for testing + end + + target 'ios-quiz-challengeUITests' do + inherit! :search_paths + # Pods for testing + end + +end diff --git a/ios-quiz-challenge/ios-quiz-challenge/Podfile.lock b/ios-quiz-challenge/ios-quiz-challenge/Podfile.lock new file mode 100644 index 0000000000..4358213233 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Podfile.lock @@ -0,0 +1,26 @@ +PODS: + - dDependencies (0.1.0) + - dNetwork (0.1.0) + - DynaUI (0.1.0) + +DEPENDENCIES: + - dDependencies (from `dDependencies`) + - dNetwork (from `dNetwork`) + - DynaUI (from `DynaUI`) + +EXTERNAL SOURCES: + dDependencies: + :path: dDependencies + dNetwork: + :path: dNetwork + DynaUI: + :path: DynaUI + +SPEC CHECKSUMS: + dDependencies: 2fed3f9d22f6d13ac31d468b92163fc5b33ab125 + dNetwork: 48b8164ccad2c5e87810c5092cae20a37bf69733 + DynaUI: 4a1a9ecdabc66ae21c9a74e82fa85c327b512427 + +PODFILE CHECKSUM: 5a23873c6a3d4c3e475b4c1dbb1b22287258e820 + +COCOAPODS: 1.17.0 diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/DynaUI.podspec.json b/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/DynaUI.podspec.json new file mode 100644 index 0000000000..bf5af93474 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/DynaUI.podspec.json @@ -0,0 +1,25 @@ +{ + "name": "DynaUI", + "version": "0.1.0", + "summary": "SwiftUI components for the DynaUI Design System.", + "description": "A lightweight Design System built with reusable SwiftUI components.", + "homepage": "https://github.com/kiyo92/DynaUI", + "license": { + "type": "MIT" + }, + "authors": "João Marcus", + "source": { + "git": "https://github.com/kiyo92/DynaUI.git", + "tag": "0.1.0" + }, + "platforms": { + "ios": "26.0" + }, + "swift_versions": [ + "5.9", + "6.0" + ], + "source_files": "Sources/**/*.swift", + "frameworks": "SwiftUI", + "swift_version": "6.0" +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/dDependencies.podspec.json b/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/dDependencies.podspec.json new file mode 100644 index 0000000000..77eb812b4f --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/dDependencies.podspec.json @@ -0,0 +1,25 @@ +{ + "name": "dDependencies", + "module_name": "dDependencies", + "version": "0.1.0", + "summary": "Lightweight dependency injection for modular apps.", + "description": "A small type-based dependency container with property wrapper support.", + "homepage": "https://github.com/kiyo92/dDependencies", + "license": { + "type": "MIT" + }, + "authors": "João Marcus", + "source": { + "git": "https://github.com/kiyo92/dDependencies.git", + "tag": "0.1.0" + }, + "platforms": { + "ios": "26.0" + }, + "swift_versions": [ + "5.9", + "6.0" + ], + "source_files": "Sources/**/*.swift", + "swift_version": "6.0" +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/dNetwork.podspec.json b/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/dNetwork.podspec.json new file mode 100644 index 0000000000..9b723d1b87 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Local Podspecs/dNetwork.podspec.json @@ -0,0 +1,26 @@ +{ + "name": "dNetwork", + "module_name": "dNetwork", + "version": "0.1.0", + "summary": "A lightweight and model-agnostic networking module.", + "description": "A reusable networking module based on URLSession,\nasync/await and generic Decodable responses.", + "homepage": "https://github.com/kiyo92/dNetwork", + "license": { + "type": "MIT" + }, + "authors": "João Marcus", + "source": { + "git": "https://github.com/kiyo92/dNetwork.git", + "tag": "0.1.0" + }, + "platforms": { + "ios": "26.0" + }, + "swift_versions": [ + "5.9", + "6.0" + ], + "source_files": "Sources/**/*.swift", + "frameworks": "Foundation", + "swift_version": "6.0" +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Manifest.lock b/ios-quiz-challenge/ios-quiz-challenge/Pods/Manifest.lock new file mode 100644 index 0000000000..4358213233 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Manifest.lock @@ -0,0 +1,26 @@ +PODS: + - dDependencies (0.1.0) + - dNetwork (0.1.0) + - DynaUI (0.1.0) + +DEPENDENCIES: + - dDependencies (from `dDependencies`) + - dNetwork (from `dNetwork`) + - DynaUI (from `DynaUI`) + +EXTERNAL SOURCES: + dDependencies: + :path: dDependencies + dNetwork: + :path: dNetwork + DynaUI: + :path: DynaUI + +SPEC CHECKSUMS: + dDependencies: 2fed3f9d22f6d13ac31d468b92163fc5b33ab125 + dNetwork: 48b8164ccad2c5e87810c5092cae20a37bf69733 + DynaUI: 4a1a9ecdabc66ae21c9a74e82fa85c327b512427 + +PODFILE CHECKSUM: 5a23873c6a3d4c3e475b4c1dbb1b22287258e820 + +COCOAPODS: 1.17.0 diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/project.pbxproj b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..efb6e56193 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,1438 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 01BD507F3DBED5D6551C5118E62098B8 /* Pods-ios-quiz-challengeUITests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 1567478DE7C25A35B181E71E118C099E /* Pods-ios-quiz-challengeUITests-dummy.m */; }; + 02C023BC49A0B249BBAD8AC9978B80FF /* DynaPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F4006BB8A2871393ED7D7FEA4BB909F /* DynaPanel.swift */; }; + 05E468EB975A7CEED3EF65B8FD2C7C10 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C5158A1517DFD46BAB18B3CDADBB54 /* NetworkClient.swift */; }; + 0C26FA963198133CC9FE89850ED2C4EB /* URLSessionNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE5F206A75802579B51D7580C181AEE3 /* URLSessionNetworkClient.swift */; }; + 17019FE02E7E702D77A71EE6DCCF37F8 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5910856E862FBF7020C03543B97A3125 /* HTTPMethod.swift */; }; + 21AEF8B3B766891D28F93A41AA3FF524 /* Pods-ios-quiz-challenge-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 2B8B17D249FBAE277C0B769DBAD9B715 /* Pods-ios-quiz-challenge-dummy.m */; }; + 259810C5B05000058B8CA542E3EAB1A2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */; }; + 28C08AFF3020C750DA74B45862E562F1 /* dNetwork-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = BE4CBA235AF672F0E8CE95C48BD8A333 /* dNetwork-dummy.m */; }; + 2B632A4D83431F1014514F19AE3ADAC5 /* dDependencies-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7D978F563E143434BE6C10D706922781 /* dDependencies-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2DB3B2126CD9B1CE18A659A37AB4D809 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */; }; + 3556B80C50AC3A0E62CEE9947044E24C /* NetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD309C31EB96A9D98F4DC8DF72CA1ED /* NetworkRequest.swift */; }; + 3627CC8E4ECF4060AFF89093D68D853B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */; }; + 3AD8AF044FEB7B20578AFCCEBB5C2BCA /* Dependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE8F897430164594A7A25DAA4E67C66 /* Dependencies.swift */; }; + 48E264A19F8C18539C7918AAC52DE99C /* dDependencies-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B6C0AE751100A076F1B958117B2A461 /* dDependencies-dummy.m */; }; + 4E07A8DE5201FC14D03D0EF98C3C43EC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */; }; + 500D116BDBD9D2C87650E0BDAB9F2A6D /* DynaUI-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = F6754089AD76D7D0397768F00E287E1D /* DynaUI-dummy.m */; }; + 569C5B53C887B45D6F861F39EDA35CCC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */; }; + 56F514AA58B73BB71699660A5DB75087 /* DynaOptionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB5073339EBB3040EF1E6CDE0143382 /* DynaOptionButton.swift */; }; + 660A474F8FD3C7913F0A8B4ABFD6EEB9 /* Pods-ios-quiz-challengeTests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 03AC0A331E0290B93DEA2C8C3197ECE0 /* Pods-ios-quiz-challengeTests-dummy.m */; }; + 72018F68D432D55F59A96992C592BE69 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */; }; + 7F304C5337979316DA06EE1B1F7A3764 /* NetworkConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61403ED48762CBB534DA390A43EDABC6 /* NetworkConfiguration.swift */; }; + 89F577AC13D45822F49A56AE0EF2EC71 /* Pods-ios-quiz-challenge-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ADC749FEECBB5863CC61DA8BB66420C /* Pods-ios-quiz-challenge-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 99860E48D8D6AFA46028E7B051E5ACC5 /* DynaUI-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = D6BCC8FAC41940241ACA802A4A38EFFB /* DynaUI-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A47335C1E8F36E08C36D668F9BCE87ED /* dNetwork-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = A248F5A68479244962454CD8985B4899 /* dNetwork-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C383E099BA5C9F13997C06405774ABD0 /* Pods-ios-quiz-challengeUITests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = C596977A7DD4343514DE682F9004A224 /* Pods-ios-quiz-challengeUITests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C9CDFE4A2A9E7CEEA4F4F2D74ACFE31C /* Dependency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D31DF0D660DA098CC4CAAFF1A08E04A /* Dependency.swift */; }; + DDA9917969B55EB371A907D930835CF7 /* Pods-ios-quiz-challengeTests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 8F7334C5ED9E9D95DCA770C1DBB08E02 /* Pods-ios-quiz-challengeTests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DF45E85C9691E4E39075C5AEAF3B5E9A /* DependencyContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 611280F4EE23EADC04228F940AEFF5B6 /* DependencyContainer.swift */; }; + EE6A94F9CA4E9C3B46E15FFAC8A1E709 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B26C96CF063EB77FC47B2A9EA61031F3 /* NetworkError.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 01BC76377549D892D2B19017338F9C03 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E68F29D714C2B5C3693226C76D5A84CA; + remoteInfo = dNetwork; + }; + 334594055D8B4C24DF8B7B39CF8984F5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 99228BEC951C60D024142E5D09D021F8; + remoteInfo = "Pods-ios-quiz-challenge"; + }; + 5069EFCF9FB332FA00EF22F4D5F9B708 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9B5CF9E8D1AC04E6A740A5FB3468C11B; + remoteInfo = DynaUI; + }; + 67DA63055360AC5A3DEF60BB6D1C6FA0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6FC696808A11245A1FD7D1D51C880F84; + remoteInfo = dDependencies; + }; + AEDAF30BCC8FCA9D8D3E1FCF19EB2B60 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 99228BEC951C60D024142E5D09D021F8; + remoteInfo = "Pods-ios-quiz-challenge"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 01896A6087A3A00970B090411DD64910 /* dDependencies.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = dDependencies.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 03AC0A331E0290B93DEA2C8C3197ECE0 /* Pods-ios-quiz-challengeTests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-ios-quiz-challengeTests-dummy.m"; sourceTree = ""; }; + 057A361FFB473A3CE95C2F576FAA91E6 /* Pods-ios-quiz-challenge-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-ios-quiz-challenge-acknowledgements.markdown"; sourceTree = ""; }; + 065AD148865B90432B947984CA3A11E2 /* Pods-ios-quiz-challengeUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ios-quiz-challengeUITests.release.xcconfig"; sourceTree = ""; }; + 0B8FD4300EA8C73B34BE8219CF433DEA /* dDependencies-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "dDependencies-Info.plist"; sourceTree = ""; }; + 0F4006BB8A2871393ED7D7FEA4BB909F /* DynaPanel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DynaPanel.swift; path = Sources/Components/DynaPanel.swift; sourceTree = ""; }; + 0F9715B667E2EB376C42FCEB1B67D9DD /* Pods-ios-quiz-challenge-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ios-quiz-challenge-acknowledgements.plist"; sourceTree = ""; }; + 14C6DB07C9ACE6E4C7262B7E75D195AE /* DynaUI-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "DynaUI-Info.plist"; sourceTree = ""; }; + 1567478DE7C25A35B181E71E118C099E /* Pods-ios-quiz-challengeUITests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-ios-quiz-challengeUITests-dummy.m"; sourceTree = ""; }; + 15B5FCF81A09CD3E675EC41281AB5F21 /* Pods-ios-quiz-challenge */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-ios-quiz-challenge"; path = Pods_ios_quiz_challenge.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 274D61F3D307FBDBB47173C8E1414E67 /* dNetwork.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = dNetwork.modulemap; sourceTree = ""; }; + 2814DD23C280F6DB17BBC26DFC92CCDB /* Pods-ios-quiz-challenge-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ios-quiz-challenge-Info.plist"; sourceTree = ""; }; + 29C53BE8FC1EBE0D3D5DA13608BF9930 /* Pods-ios-quiz-challengeTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ios-quiz-challengeTests.debug.xcconfig"; sourceTree = ""; }; + 2B8B17D249FBAE277C0B769DBAD9B715 /* Pods-ios-quiz-challenge-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-ios-quiz-challenge-dummy.m"; sourceTree = ""; }; + 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 2D31DF0D660DA098CC4CAAFF1A08E04A /* Dependency.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Dependency.swift; path = Sources/Dependency.swift; sourceTree = ""; }; + 33670A9E830A201E9D496A57CD77C790 /* Pods-ios-quiz-challengeUITests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-ios-quiz-challengeUITests.modulemap"; sourceTree = ""; }; + 401232E25EEEC8AF819BA53F9BC0DFC7 /* DynaUI-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DynaUI-prefix.pch"; sourceTree = ""; }; + 41059A0E39AA688A50A524864A507202 /* Pods-ios-quiz-challengeUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ios-quiz-challengeUITests.debug.xcconfig"; sourceTree = ""; }; + 43F5338125DCCFB04C54B94162A3294B /* Pods-ios-quiz-challenge.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ios-quiz-challenge.debug.xcconfig"; sourceTree = ""; }; + 4E7909ADB52C3C8129B8B7A2698E71BB /* dDependencies.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = dDependencies.debug.xcconfig; sourceTree = ""; }; + 518FADA19C4F8914D55AE5CEB927A3E7 /* dNetwork.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = dNetwork.release.xcconfig; sourceTree = ""; }; + 5465573A763797D2616C870DA5CD5983 /* Pods-ios-quiz-challengeTests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ios-quiz-challengeTests-acknowledgements.plist"; sourceTree = ""; }; + 5910856E862FBF7020C03543B97A3125 /* HTTPMethod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = Sources/HTTPMethod.swift; sourceTree = ""; }; + 5924C9A05910550B27723839BC2173EA /* Pods-ios-quiz-challenge.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-ios-quiz-challenge.modulemap"; sourceTree = ""; }; + 5CB5073339EBB3040EF1E6CDE0143382 /* DynaOptionButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DynaOptionButton.swift; path = Sources/Components/DynaOptionButton.swift; sourceTree = ""; }; + 5CBB5CB45DECCA80BD2EBD44C8BAD9C8 /* DynaUI.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = DynaUI.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 611280F4EE23EADC04228F940AEFF5B6 /* DependencyContainer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DependencyContainer.swift; path = Sources/DependencyContainer.swift; sourceTree = ""; }; + 61403ED48762CBB534DA390A43EDABC6 /* NetworkConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkConfiguration.swift; path = Sources/NetworkConfiguration.swift; sourceTree = ""; }; + 6382F2EFEE1758B93CD5E3D8DBBE0835 /* dNetwork-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "dNetwork-Info.plist"; sourceTree = ""; }; + 64825E643DF8F8AD185CF98F206FE74F /* dDependencies-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "dDependencies-prefix.pch"; sourceTree = ""; }; + 6F7D54DE63306AB8DBCC97F840A11C31 /* dNetwork.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = dNetwork.debug.xcconfig; sourceTree = ""; }; + 6FE8F897430164594A7A25DAA4E67C66 /* Dependencies.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Dependencies.swift; path = Sources/Dependencies.swift; sourceTree = ""; }; + 715F1AE981A255FAF75AB7338CE17A65 /* DynaUI */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = DynaUI; path = DynaUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7ADC749FEECBB5863CC61DA8BB66420C /* Pods-ios-quiz-challenge-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-ios-quiz-challenge-umbrella.h"; sourceTree = ""; }; + 7D978F563E143434BE6C10D706922781 /* dDependencies-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "dDependencies-umbrella.h"; sourceTree = ""; }; + 7DA3B5AADF55D8F1428C57974EC45E62 /* Pods-ios-quiz-challengeUITests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ios-quiz-challengeUITests-Info.plist"; sourceTree = ""; }; + 8C74F73AFEAF6853F8859E0ECC7BD2E1 /* DynaUI.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DynaUI.debug.xcconfig; sourceTree = ""; }; + 8D042EB8E0D5A9244C8A2DE205723930 /* dNetwork */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = dNetwork; path = dNetwork.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8F7334C5ED9E9D95DCA770C1DBB08E02 /* Pods-ios-quiz-challengeTests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-ios-quiz-challengeTests-umbrella.h"; sourceTree = ""; }; + 90A811C01588AC536FA7EAAA85922FC0 /* Pods-ios-quiz-challengeTests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-ios-quiz-challengeTests-acknowledgements.markdown"; sourceTree = ""; }; + 9B6C0AE751100A076F1B958117B2A461 /* dDependencies-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "dDependencies-dummy.m"; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9F20DEE61EF7B3814FEECF2765D542D0 /* Pods-ios-quiz-challenge.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ios-quiz-challenge.release.xcconfig"; sourceTree = ""; }; + A248F5A68479244962454CD8985B4899 /* dNetwork-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "dNetwork-umbrella.h"; sourceTree = ""; }; + AE5F206A75802579B51D7580C181AEE3 /* URLSessionNetworkClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = URLSessionNetworkClient.swift; path = Sources/URLSessionNetworkClient.swift; sourceTree = ""; }; + AF52E0C208B5968AA317C4C341D97B65 /* dDependencies.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = dDependencies.release.xcconfig; sourceTree = ""; }; + B26C96CF063EB77FC47B2A9EA61031F3 /* NetworkError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkError.swift; path = Sources/NetworkError.swift; sourceTree = ""; }; + B566DD710CC57EDD2977A586906014AB /* dDependencies */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = dDependencies; path = dDependencies.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B6C5158A1517DFD46BAB18B3CDADBB54 /* NetworkClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkClient.swift; path = Sources/NetworkClient.swift; sourceTree = ""; }; + B816286E726DF7974E25FA1B02DC85BD /* DynaUI.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DynaUI.release.xcconfig; sourceTree = ""; }; + BE4CBA235AF672F0E8CE95C48BD8A333 /* dNetwork-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "dNetwork-dummy.m"; sourceTree = ""; }; + C1C513FB1670366291D9634BE7C874F6 /* Pods-ios-quiz-challengeTests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-ios-quiz-challengeTests"; path = Pods_ios_quiz_challengeTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C345CD6DD6C43B71E9914FE5F96D3331 /* Pods-ios-quiz-challengeUITests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-ios-quiz-challengeUITests-acknowledgements.markdown"; sourceTree = ""; }; + C3C17F3CDDEA57DA3D1CD2DBC6E0DF31 /* dNetwork-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "dNetwork-prefix.pch"; sourceTree = ""; }; + C596977A7DD4343514DE682F9004A224 /* Pods-ios-quiz-challengeUITests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-ios-quiz-challengeUITests-umbrella.h"; sourceTree = ""; }; + C61BA413EC7513DD504C28F99297D1AF /* Pods-ios-quiz-challengeUITests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-ios-quiz-challengeUITests"; path = Pods_ios_quiz_challengeUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C8995C11911C5BE295B98A9CD39E8A4B /* Pods-ios-quiz-challengeTests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ios-quiz-challengeTests-Info.plist"; sourceTree = ""; }; + CA39924A6E29B3365C51DBA2E2E19690 /* Pods-ios-quiz-challengeTests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-ios-quiz-challengeTests.modulemap"; sourceTree = ""; }; + CAFCCF19BB9C0B6EF1123EC11D636721 /* Pods-ios-quiz-challengeTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-ios-quiz-challengeTests.release.xcconfig"; sourceTree = ""; }; + D6BCC8FAC41940241ACA802A4A38EFFB /* DynaUI-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DynaUI-umbrella.h"; sourceTree = ""; }; + D9ACFF31DB8A44DB53A5763ED14BB123 /* Pods-ios-quiz-challengeUITests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-ios-quiz-challengeUITests-acknowledgements.plist"; sourceTree = ""; }; + E74F536A950131D9F95EA0C344F9C9BE /* dDependencies.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = dDependencies.modulemap; sourceTree = ""; }; + F4FD4C963DF9A05CB43339CE007B6784 /* dNetwork.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; path = dNetwork.podspec; sourceTree = ""; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + F6754089AD76D7D0397768F00E287E1D /* DynaUI-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "DynaUI-dummy.m"; sourceTree = ""; }; + F6816E9CCACD56ACA62D8C242F99A91A /* DynaUI.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = DynaUI.modulemap; sourceTree = ""; }; + FCD309C31EB96A9D98F4DC8DF72CA1ED /* NetworkRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkRequest.swift; path = Sources/NetworkRequest.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 403722843540BAEE86212263E8F4972A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 569C5B53C887B45D6F861F39EDA35CCC /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4C623B10F729093BB520437DE222F8F4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 72018F68D432D55F59A96992C592BE69 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5D752353F897028BCD835F6BC227320F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4E07A8DE5201FC14D03D0EF98C3C43EC /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BC9ADCBB1D448A66AA08C3B17281D39E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 259810C5B05000058B8CA542E3EAB1A2 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BE18F9D5B9A7E03F4ECD253A0CAD6967 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2DB3B2126CD9B1CE18A659A37AB4D809 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EBC7AB65987D24D970A9882D40443CAC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3627CC8E4ECF4060AFF89093D68D853B /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 043245FCAD8F5218CBC669A5BA0DFDA6 /* dNetwork */ = { + isa = PBXGroup; + children = ( + 5910856E862FBF7020C03543B97A3125 /* HTTPMethod.swift */, + B6C5158A1517DFD46BAB18B3CDADBB54 /* NetworkClient.swift */, + 61403ED48762CBB534DA390A43EDABC6 /* NetworkConfiguration.swift */, + B26C96CF063EB77FC47B2A9EA61031F3 /* NetworkError.swift */, + FCD309C31EB96A9D98F4DC8DF72CA1ED /* NetworkRequest.swift */, + AE5F206A75802579B51D7580C181AEE3 /* URLSessionNetworkClient.swift */, + 7BA6E3963D297F81C68FEE2EC17D905B /* Pod */, + E5F852CB2142143483C085252F5A604E /* Support Files */, + ); + name = dNetwork; + path = ../dNetwork; + sourceTree = ""; + }; + 1AFE1218610D149048322C013788C076 /* Pod */ = { + isa = PBXGroup; + children = ( + 01896A6087A3A00970B090411DD64910 /* dDependencies.podspec */, + ); + name = Pod; + sourceTree = ""; + }; + 5E2F20BECC40DECCCBDDAFBA2F52BFA0 /* Pods-ios-quiz-challengeTests */ = { + isa = PBXGroup; + children = ( + CA39924A6E29B3365C51DBA2E2E19690 /* Pods-ios-quiz-challengeTests.modulemap */, + 90A811C01588AC536FA7EAAA85922FC0 /* Pods-ios-quiz-challengeTests-acknowledgements.markdown */, + 5465573A763797D2616C870DA5CD5983 /* Pods-ios-quiz-challengeTests-acknowledgements.plist */, + 03AC0A331E0290B93DEA2C8C3197ECE0 /* Pods-ios-quiz-challengeTests-dummy.m */, + C8995C11911C5BE295B98A9CD39E8A4B /* Pods-ios-quiz-challengeTests-Info.plist */, + 8F7334C5ED9E9D95DCA770C1DBB08E02 /* Pods-ios-quiz-challengeTests-umbrella.h */, + 29C53BE8FC1EBE0D3D5DA13608BF9930 /* Pods-ios-quiz-challengeTests.debug.xcconfig */, + CAFCCF19BB9C0B6EF1123EC11D636721 /* Pods-ios-quiz-challengeTests.release.xcconfig */, + ); + name = "Pods-ios-quiz-challengeTests"; + path = "Target Support Files/Pods-ios-quiz-challengeTests"; + sourceTree = ""; + }; + 5FBEFF6D55D9A092D1C2A5968772E175 /* Pods-ios-quiz-challenge */ = { + isa = PBXGroup; + children = ( + 5924C9A05910550B27723839BC2173EA /* Pods-ios-quiz-challenge.modulemap */, + 057A361FFB473A3CE95C2F576FAA91E6 /* Pods-ios-quiz-challenge-acknowledgements.markdown */, + 0F9715B667E2EB376C42FCEB1B67D9DD /* Pods-ios-quiz-challenge-acknowledgements.plist */, + 2B8B17D249FBAE277C0B769DBAD9B715 /* Pods-ios-quiz-challenge-dummy.m */, + 2814DD23C280F6DB17BBC26DFC92CCDB /* Pods-ios-quiz-challenge-Info.plist */, + 7ADC749FEECBB5863CC61DA8BB66420C /* Pods-ios-quiz-challenge-umbrella.h */, + 43F5338125DCCFB04C54B94162A3294B /* Pods-ios-quiz-challenge.debug.xcconfig */, + 9F20DEE61EF7B3814FEECF2765D542D0 /* Pods-ios-quiz-challenge.release.xcconfig */, + ); + name = "Pods-ios-quiz-challenge"; + path = "Target Support Files/Pods-ios-quiz-challenge"; + sourceTree = ""; + }; + 7AECBA61365544F357B06DE1C8710425 /* iOS */ = { + isa = PBXGroup; + children = ( + 2CB74A84DF62E0DBFFA4715EF79E08A1 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 7BA6E3963D297F81C68FEE2EC17D905B /* Pod */ = { + isa = PBXGroup; + children = ( + F4FD4C963DF9A05CB43339CE007B6784 /* dNetwork.podspec */, + ); + name = Pod; + sourceTree = ""; + }; + 7F1B568EF02B7AB6C7F749BB651B95EF /* dDependencies */ = { + isa = PBXGroup; + children = ( + 6FE8F897430164594A7A25DAA4E67C66 /* Dependencies.swift */, + 2D31DF0D660DA098CC4CAAFF1A08E04A /* Dependency.swift */, + 611280F4EE23EADC04228F940AEFF5B6 /* DependencyContainer.swift */, + 1AFE1218610D149048322C013788C076 /* Pod */, + 9799CF91D6D8144F2438C90C1F1E44A3 /* Support Files */, + ); + name = dDependencies; + path = ../dDependencies; + sourceTree = ""; + }; + 7F6ACCE39A9CA5FB191F5E05D1C46EF3 /* Products */ = { + isa = PBXGroup; + children = ( + B566DD710CC57EDD2977A586906014AB /* dDependencies */, + 8D042EB8E0D5A9244C8A2DE205723930 /* dNetwork */, + 715F1AE981A255FAF75AB7338CE17A65 /* DynaUI */, + 15B5FCF81A09CD3E675EC41281AB5F21 /* Pods-ios-quiz-challenge */, + C1C513FB1670366291D9634BE7C874F6 /* Pods-ios-quiz-challengeTests */, + C61BA413EC7513DD504C28F99297D1AF /* Pods-ios-quiz-challengeUITests */, + ); + name = Products; + sourceTree = ""; + }; + 9799CF91D6D8144F2438C90C1F1E44A3 /* Support Files */ = { + isa = PBXGroup; + children = ( + E74F536A950131D9F95EA0C344F9C9BE /* dDependencies.modulemap */, + 9B6C0AE751100A076F1B958117B2A461 /* dDependencies-dummy.m */, + 0B8FD4300EA8C73B34BE8219CF433DEA /* dDependencies-Info.plist */, + 64825E643DF8F8AD185CF98F206FE74F /* dDependencies-prefix.pch */, + 7D978F563E143434BE6C10D706922781 /* dDependencies-umbrella.h */, + 4E7909ADB52C3C8129B8B7A2698E71BB /* dDependencies.debug.xcconfig */, + AF52E0C208B5968AA317C4C341D97B65 /* dDependencies.release.xcconfig */, + ); + name = "Support Files"; + path = "../Pods/Target Support Files/dDependencies"; + sourceTree = ""; + }; + A0FC48C2144B6F3D74FC3BB8BF6C2168 /* Pods-ios-quiz-challengeUITests */ = { + isa = PBXGroup; + children = ( + 33670A9E830A201E9D496A57CD77C790 /* Pods-ios-quiz-challengeUITests.modulemap */, + C345CD6DD6C43B71E9914FE5F96D3331 /* Pods-ios-quiz-challengeUITests-acknowledgements.markdown */, + D9ACFF31DB8A44DB53A5763ED14BB123 /* Pods-ios-quiz-challengeUITests-acknowledgements.plist */, + 1567478DE7C25A35B181E71E118C099E /* Pods-ios-quiz-challengeUITests-dummy.m */, + 7DA3B5AADF55D8F1428C57974EC45E62 /* Pods-ios-quiz-challengeUITests-Info.plist */, + C596977A7DD4343514DE682F9004A224 /* Pods-ios-quiz-challengeUITests-umbrella.h */, + 41059A0E39AA688A50A524864A507202 /* Pods-ios-quiz-challengeUITests.debug.xcconfig */, + 065AD148865B90432B947984CA3A11E2 /* Pods-ios-quiz-challengeUITests.release.xcconfig */, + ); + name = "Pods-ios-quiz-challengeUITests"; + path = "Target Support Files/Pods-ios-quiz-challengeUITests"; + sourceTree = ""; + }; + A120F4A24BFD64927F1821D9F2910F12 /* DynaUI */ = { + isa = PBXGroup; + children = ( + 5CB5073339EBB3040EF1E6CDE0143382 /* DynaOptionButton.swift */, + 0F4006BB8A2871393ED7D7FEA4BB909F /* DynaPanel.swift */, + A61C72FEF6F941B3176C25272FF2EB5D /* Pod */, + BD2DA45C049DD1165620BF41869C2DF7 /* Support Files */, + ); + name = DynaUI; + path = ../DynaUI; + sourceTree = ""; + }; + A61C72FEF6F941B3176C25272FF2EB5D /* Pod */ = { + isa = PBXGroup; + children = ( + 5CBB5CB45DECCA80BD2EBD44C8BAD9C8 /* DynaUI.podspec */, + ); + name = Pod; + sourceTree = ""; + }; + BD2DA45C049DD1165620BF41869C2DF7 /* Support Files */ = { + isa = PBXGroup; + children = ( + F6816E9CCACD56ACA62D8C242F99A91A /* DynaUI.modulemap */, + F6754089AD76D7D0397768F00E287E1D /* DynaUI-dummy.m */, + 14C6DB07C9ACE6E4C7262B7E75D195AE /* DynaUI-Info.plist */, + 401232E25EEEC8AF819BA53F9BC0DFC7 /* DynaUI-prefix.pch */, + D6BCC8FAC41940241ACA802A4A38EFFB /* DynaUI-umbrella.h */, + 8C74F73AFEAF6853F8859E0ECC7BD2E1 /* DynaUI.debug.xcconfig */, + B816286E726DF7974E25FA1B02DC85BD /* DynaUI.release.xcconfig */, + ); + name = "Support Files"; + path = "../Pods/Target Support Files/DynaUI"; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + DC9B4C26438A2CFF2816C155FD97EE04 /* Development Pods */, + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */, + 7F6ACCE39A9CA5FB191F5E05D1C46EF3 /* Products */, + FA355C35FA770AE043F0E5FAED1606D5 /* Targets Support Files */, + ); + sourceTree = ""; + }; + D210D550F4EA176C3123ED886F8F87F5 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7AECBA61365544F357B06DE1C8710425 /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + DC9B4C26438A2CFF2816C155FD97EE04 /* Development Pods */ = { + isa = PBXGroup; + children = ( + 7F1B568EF02B7AB6C7F749BB651B95EF /* dDependencies */, + 043245FCAD8F5218CBC669A5BA0DFDA6 /* dNetwork */, + A120F4A24BFD64927F1821D9F2910F12 /* DynaUI */, + ); + name = "Development Pods"; + sourceTree = ""; + }; + E5F852CB2142143483C085252F5A604E /* Support Files */ = { + isa = PBXGroup; + children = ( + 274D61F3D307FBDBB47173C8E1414E67 /* dNetwork.modulemap */, + BE4CBA235AF672F0E8CE95C48BD8A333 /* dNetwork-dummy.m */, + 6382F2EFEE1758B93CD5E3D8DBBE0835 /* dNetwork-Info.plist */, + C3C17F3CDDEA57DA3D1CD2DBC6E0DF31 /* dNetwork-prefix.pch */, + A248F5A68479244962454CD8985B4899 /* dNetwork-umbrella.h */, + 6F7D54DE63306AB8DBCC97F840A11C31 /* dNetwork.debug.xcconfig */, + 518FADA19C4F8914D55AE5CEB927A3E7 /* dNetwork.release.xcconfig */, + ); + name = "Support Files"; + path = "../Pods/Target Support Files/dNetwork"; + sourceTree = ""; + }; + FA355C35FA770AE043F0E5FAED1606D5 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + 5FBEFF6D55D9A092D1C2A5968772E175 /* Pods-ios-quiz-challenge */, + 5E2F20BECC40DECCCBDDAFBA2F52BFA0 /* Pods-ios-quiz-challengeTests */, + A0FC48C2144B6F3D74FC3BB8BF6C2168 /* Pods-ios-quiz-challengeUITests */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 08B3B7C09F3392549F489762676B83CB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C383E099BA5C9F13997C06405774ABD0 /* Pods-ios-quiz-challengeUITests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 339ABAD30BAFA6171550C9FF07994ACA /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 2B632A4D83431F1014514F19AE3ADAC5 /* dDependencies-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B281BB5865A6FB659DA56160123689AB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 99860E48D8D6AFA46028E7B051E5ACC5 /* DynaUI-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D286323411AE97FD6B5F8291C08D08CC /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + A47335C1E8F36E08C36D668F9BCE87ED /* dNetwork-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DB75BEA6F2E1D29A3DDD53CF1B87ECA8 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 89F577AC13D45822F49A56AE0EF2EC71 /* Pods-ios-quiz-challenge-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F63F2C9CA1A1E46EF647676DF7AF5822 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + DDA9917969B55EB371A907D930835CF7 /* Pods-ios-quiz-challengeTests-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 3848E3B1B65A4EB57024F7E96DBE9B72 /* Pods-ios-quiz-challengeUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 21B2288FDF5E16E0F2036490D3AE307D /* Build configuration list for PBXNativeTarget "Pods-ios-quiz-challengeUITests" */; + buildPhases = ( + 08B3B7C09F3392549F489762676B83CB /* Headers */, + 67513F2D5E5E59096AFCFB690855E136 /* Sources */, + 4C623B10F729093BB520437DE222F8F4 /* Frameworks */, + C3C9C3F30943802361BF043902325DA9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D85BBC85F1562D512A20D47E23866DC4 /* PBXTargetDependency */, + ); + name = "Pods-ios-quiz-challengeUITests"; + productName = Pods_ios_quiz_challengeUITests; + productReference = C61BA413EC7513DD504C28F99297D1AF /* Pods-ios-quiz-challengeUITests */; + productType = "com.apple.product-type.framework"; + }; + 6FC696808A11245A1FD7D1D51C880F84 /* dDependencies */ = { + isa = PBXNativeTarget; + buildConfigurationList = 92502C4A9E43CC5B01069B6BA992096A /* Build configuration list for PBXNativeTarget "dDependencies" */; + buildPhases = ( + 339ABAD30BAFA6171550C9FF07994ACA /* Headers */, + 057DF6DE6BB93A2C5E2F459DC24398B5 /* Sources */, + 403722843540BAEE86212263E8F4972A /* Frameworks */, + 16AB71816E8139EC744B11AC9A10702F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = dDependencies; + productName = dDependencies; + productReference = B566DD710CC57EDD2977A586906014AB /* dDependencies */; + productType = "com.apple.product-type.framework"; + }; + 99228BEC951C60D024142E5D09D021F8 /* Pods-ios-quiz-challenge */ = { + isa = PBXNativeTarget; + buildConfigurationList = EA1AF2297942FEAC829580BA19D27286 /* Build configuration list for PBXNativeTarget "Pods-ios-quiz-challenge" */; + buildPhases = ( + DB75BEA6F2E1D29A3DDD53CF1B87ECA8 /* Headers */, + BE163813F061679BC111F2981A0042FB /* Sources */, + BC9ADCBB1D448A66AA08C3B17281D39E /* Frameworks */, + E136732B5C7C10EB0E6AA177DE1DF7B4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 8DB0D72B71384BDE6D547CBF5457AA72 /* PBXTargetDependency */, + 00A5B57CE9E493BF1490DBA1EF0B1575 /* PBXTargetDependency */, + 36219A6955E7EF4E12A21BDE66C26838 /* PBXTargetDependency */, + ); + name = "Pods-ios-quiz-challenge"; + productName = Pods_ios_quiz_challenge; + productReference = 15B5FCF81A09CD3E675EC41281AB5F21 /* Pods-ios-quiz-challenge */; + productType = "com.apple.product-type.framework"; + }; + 9B5CF9E8D1AC04E6A740A5FB3468C11B /* DynaUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = EA2E453BED2D11B48714B4EC6717EF10 /* Build configuration list for PBXNativeTarget "DynaUI" */; + buildPhases = ( + B281BB5865A6FB659DA56160123689AB /* Headers */, + 940F8B4BA945353FB44FB00909F4F47D /* Sources */, + EBC7AB65987D24D970A9882D40443CAC /* Frameworks */, + 99538706C9D107543CD2DDFEBF17AB7D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DynaUI; + productName = DynaUI; + productReference = 715F1AE981A255FAF75AB7338CE17A65 /* DynaUI */; + productType = "com.apple.product-type.framework"; + }; + B7198810CEF5018DED5584F45A0CF103 /* Pods-ios-quiz-challengeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E5308CA50492F7E1FED41AAD87F7323D /* Build configuration list for PBXNativeTarget "Pods-ios-quiz-challengeTests" */; + buildPhases = ( + F63F2C9CA1A1E46EF647676DF7AF5822 /* Headers */, + 3BDE14048E3406F4377DA2B53B21C148 /* Sources */, + 5D752353F897028BCD835F6BC227320F /* Frameworks */, + EF29B705D253E17035D665793769BA9A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7932602EBDA2A56E47159749104510D7 /* PBXTargetDependency */, + ); + name = "Pods-ios-quiz-challengeTests"; + productName = Pods_ios_quiz_challengeTests; + productReference = C1C513FB1670366291D9634BE7C874F6 /* Pods-ios-quiz-challengeTests */; + productType = "com.apple.product-type.framework"; + }; + E68F29D714C2B5C3693226C76D5A84CA /* dNetwork */ = { + isa = PBXNativeTarget; + buildConfigurationList = 17721F89ED944EB6DBBF622C717182D0 /* Build configuration list for PBXNativeTarget "dNetwork" */; + buildPhases = ( + D286323411AE97FD6B5F8291C08D08CC /* Headers */, + 9A77271E966D3A9013A0BC306EF6DBA5 /* Sources */, + BE18F9D5B9A7E03F4ECD253A0CAD6967 /* Frameworks */, + C0FB703F608EEEF2BF3D57A5B689DFE9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = dNetwork; + productName = dNetwork; + productReference = 8D042EB8E0D5A9244C8A2DE205723930 /* dNetwork */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 16.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + minimizedProjectReferenceProxies = 0; + preferredProjectObjectVersion = 100; + productRefGroup = 7F6ACCE39A9CA5FB191F5E05D1C46EF3 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6FC696808A11245A1FD7D1D51C880F84 /* dDependencies */, + E68F29D714C2B5C3693226C76D5A84CA /* dNetwork */, + 9B5CF9E8D1AC04E6A740A5FB3468C11B /* DynaUI */, + 99228BEC951C60D024142E5D09D021F8 /* Pods-ios-quiz-challenge */, + B7198810CEF5018DED5584F45A0CF103 /* Pods-ios-quiz-challengeTests */, + 3848E3B1B65A4EB57024F7E96DBE9B72 /* Pods-ios-quiz-challengeUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 16AB71816E8139EC744B11AC9A10702F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 99538706C9D107543CD2DDFEBF17AB7D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C0FB703F608EEEF2BF3D57A5B689DFE9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + C3C9C3F30943802361BF043902325DA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E136732B5C7C10EB0E6AA177DE1DF7B4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + EF29B705D253E17035D665793769BA9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 057DF6DE6BB93A2C5E2F459DC24398B5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 48E264A19F8C18539C7918AAC52DE99C /* dDependencies-dummy.m in Sources */, + 3AD8AF044FEB7B20578AFCCEBB5C2BCA /* Dependencies.swift in Sources */, + C9CDFE4A2A9E7CEEA4F4F2D74ACFE31C /* Dependency.swift in Sources */, + DF45E85C9691E4E39075C5AEAF3B5E9A /* DependencyContainer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3BDE14048E3406F4377DA2B53B21C148 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 660A474F8FD3C7913F0A8B4ABFD6EEB9 /* Pods-ios-quiz-challengeTests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 67513F2D5E5E59096AFCFB690855E136 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 01BD507F3DBED5D6551C5118E62098B8 /* Pods-ios-quiz-challengeUITests-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 940F8B4BA945353FB44FB00909F4F47D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 56F514AA58B73BB71699660A5DB75087 /* DynaOptionButton.swift in Sources */, + 02C023BC49A0B249BBAD8AC9978B80FF /* DynaPanel.swift in Sources */, + 500D116BDBD9D2C87650E0BDAB9F2A6D /* DynaUI-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9A77271E966D3A9013A0BC306EF6DBA5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 28C08AFF3020C750DA74B45862E562F1 /* dNetwork-dummy.m in Sources */, + 17019FE02E7E702D77A71EE6DCCF37F8 /* HTTPMethod.swift in Sources */, + 05E468EB975A7CEED3EF65B8FD2C7C10 /* NetworkClient.swift in Sources */, + 7F304C5337979316DA06EE1B1F7A3764 /* NetworkConfiguration.swift in Sources */, + EE6A94F9CA4E9C3B46E15FFAC8A1E709 /* NetworkError.swift in Sources */, + 3556B80C50AC3A0E62CEE9947044E24C /* NetworkRequest.swift in Sources */, + 0C26FA963198133CC9FE89850ED2C4EB /* URLSessionNetworkClient.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + BE163813F061679BC111F2981A0042FB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 21AEF8B3B766891D28F93A41AA3FF524 /* Pods-ios-quiz-challenge-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00A5B57CE9E493BF1490DBA1EF0B1575 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = dDependencies; + target = 6FC696808A11245A1FD7D1D51C880F84 /* dDependencies */; + targetProxy = 67DA63055360AC5A3DEF60BB6D1C6FA0 /* PBXContainerItemProxy */; + }; + 36219A6955E7EF4E12A21BDE66C26838 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = dNetwork; + target = E68F29D714C2B5C3693226C76D5A84CA /* dNetwork */; + targetProxy = 01BC76377549D892D2B19017338F9C03 /* PBXContainerItemProxy */; + }; + 7932602EBDA2A56E47159749104510D7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Pods-ios-quiz-challenge"; + target = 99228BEC951C60D024142E5D09D021F8 /* Pods-ios-quiz-challenge */; + targetProxy = AEDAF30BCC8FCA9D8D3E1FCF19EB2B60 /* PBXContainerItemProxy */; + }; + 8DB0D72B71384BDE6D547CBF5457AA72 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = DynaUI; + target = 9B5CF9E8D1AC04E6A740A5FB3468C11B /* DynaUI */; + targetProxy = 5069EFCF9FB332FA00EF22F4D5F9B708 /* PBXContainerItemProxy */; + }; + D85BBC85F1562D512A20D47E23866DC4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Pods-ios-quiz-challenge"; + target = 99228BEC951C60D024142E5D09D021F8 /* Pods-ios-quiz-challenge */; + targetProxy = 334594055D8B4C24DF8B7B39CF8984F5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 046D106B13CD86C56E430C2CD3A84CE3 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AF52E0C208B5968AA317C4C341D97B65 /* dDependencies.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/dDependencies/dDependencies-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/dDependencies/dDependencies-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/dDependencies/dDependencies.modulemap"; + PRODUCT_MODULE_NAME = dDependencies; + PRODUCT_NAME = dDependencies; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 2207FA6F3D2D58374D5DC4B929C58DCB /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B816286E726DF7974E25FA1B02DC85BD /* DynaUI.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/DynaUI/DynaUI-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/DynaUI/DynaUI-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/DynaUI/DynaUI.modulemap"; + PRODUCT_MODULE_NAME = DynaUI; + PRODUCT_NAME = DynaUI; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 2C5DD4A7855AC1081D4EF87D69745652 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9F20DEE61EF7B3814FEECF2765D542D0 /* Pods-ios-quiz-challenge.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 740FDD226A4B6F85B1B564F77ABAD92C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8C74F73AFEAF6853F8859E0ECC7BD2E1 /* DynaUI.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/DynaUI/DynaUI-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/DynaUI/DynaUI-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/DynaUI/DynaUI.modulemap"; + PRODUCT_MODULE_NAME = DynaUI; + PRODUCT_NAME = DynaUI; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8ECF740658CF6DCC3B3271D95B9630C4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 41059A0E39AA688A50A524864A507202 /* Pods-ios-quiz-challengeUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 9248FB869881F9B39BE18C435BA3B865 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + 986ED01D73937DE71A65C9797CA47298 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E7909ADB52C3C8129B8B7A2698E71BB /* dDependencies.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/dDependencies/dDependencies-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/dDependencies/dDependencies-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/dDependencies/dDependencies.modulemap"; + PRODUCT_MODULE_NAME = dDependencies; + PRODUCT_NAME = dDependencies; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 9C29D7ED41353AB5E7B2658298B78238 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6F7D54DE63306AB8DBCC97F840A11C31 /* dNetwork.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/dNetwork/dNetwork-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/dNetwork/dNetwork-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/dNetwork/dNetwork.modulemap"; + PRODUCT_MODULE_NAME = dNetwork; + PRODUCT_NAME = dNetwork; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + A8AB2A436890A6A582214B43036B6FE0 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 065AD148865B90432B947984CA3A11E2 /* Pods-ios-quiz-challengeUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + BC5D817118EF9631F60429B197656AC1 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CAFCCF19BB9C0B6EF1123EC11D636721 /* Pods-ios-quiz-challengeTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + C3C90DC26DCDB41D1CE971B7CE2A81E2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 29C53BE8FC1EBE0D3D5DA13608BF9930 /* Pods-ios-quiz-challengeTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + D867F23C889390E45132D7C4FF5E3E57 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 518FADA19C4F8914D55AE5CEB927A3E7 /* dNetwork.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_PREFIX_HEADER = "Target Support Files/dNetwork/dNetwork-prefix.pch"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = "Target Support Files/dNetwork/dNetwork-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/dNetwork/dNetwork.modulemap"; + PRODUCT_MODULE_NAME = dNetwork; + PRODUCT_NAME = dNetwork; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + DD06B5DC80566EBF37948F17AD60FAEB /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 43F5338125DCCFB04C54B94162A3294B /* Pods-ios-quiz-challenge.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = NO; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + INFOPLIST_FILE = "Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + EB120F3E76B717F0894562179A7F72CC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 17721F89ED944EB6DBBF622C717182D0 /* Build configuration list for PBXNativeTarget "dNetwork" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9C29D7ED41353AB5E7B2658298B78238 /* Debug */, + D867F23C889390E45132D7C4FF5E3E57 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 21B2288FDF5E16E0F2036490D3AE307D /* Build configuration list for PBXNativeTarget "Pods-ios-quiz-challengeUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8ECF740658CF6DCC3B3271D95B9630C4 /* Debug */, + A8AB2A436890A6A582214B43036B6FE0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EB120F3E76B717F0894562179A7F72CC /* Debug */, + 9248FB869881F9B39BE18C435BA3B865 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 92502C4A9E43CC5B01069B6BA992096A /* Build configuration list for PBXNativeTarget "dDependencies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 986ED01D73937DE71A65C9797CA47298 /* Debug */, + 046D106B13CD86C56E430C2CD3A84CE3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E5308CA50492F7E1FED41AAD87F7323D /* Build configuration list for PBXNativeTarget "Pods-ios-quiz-challengeTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C3C90DC26DCDB41D1CE971B7CE2A81E2 /* Debug */, + BC5D817118EF9631F60429B197656AC1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EA1AF2297942FEAC829580BA19D27286 /* Build configuration list for PBXNativeTarget "Pods-ios-quiz-challenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD06B5DC80566EBF37948F17AD60FAEB /* Debug */, + 2C5DD4A7855AC1081D4EF87D69745652 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + EA2E453BED2D11B48714B4EC6717EF10 /* Build configuration list for PBXNativeTarget "DynaUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 740FDD226A4B6F85B1B564F77ABAD92C /* Debug */, + 2207FA6F3D2D58374D5DC4B929C58DCB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/DynaUI.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/DynaUI.xcscheme new file mode 100644 index 0000000000..1be24b39c9 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/DynaUI.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challenge.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challenge.xcscheme new file mode 100644 index 0000000000..f3df54e81d --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challenge.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challengeTests.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challengeTests.xcscheme new file mode 100644 index 0000000000..3961ff40f6 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challengeTests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challengeUITests.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challengeUITests.xcscheme new file mode 100644 index 0000000000..1ab4c8cb0c --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/Pods-ios-quiz-challengeUITests.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/dDependencies.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/dDependencies.xcscheme new file mode 100644 index 0000000000..afb10f9dd8 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/dDependencies.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/dNetwork.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/dNetwork.xcscheme new file mode 100644 index 0000000000..ff043773b4 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/dNetwork.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/xcschememanagement.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..6326614765 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Pods.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,41 @@ + + + + + SchemeUserState + + DynaUI.xcscheme + + isShown + + + Pods-ios-quiz-challenge.xcscheme + + isShown + + + Pods-ios-quiz-challengeTests.xcscheme + + isShown + + + Pods-ios-quiz-challengeUITests.xcscheme + + isShown + + + dDependencies.xcscheme + + isShown + + + dNetwork.xcscheme + + isShown + + + + SuppressBuildableAutocreation + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-Info.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-Info.plist new file mode 100644 index 0000000000..434e06af4d --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.1.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-dummy.m b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-dummy.m new file mode 100644 index 0000000000..d11f8dd3e1 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_DynaUI : NSObject +@end +@implementation PodsDummy_DynaUI +@end diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-prefix.pch b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-prefix.pch new file mode 100644 index 0000000000..beb2a24418 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-umbrella.h b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-umbrella.h new file mode 100644 index 0000000000..aa0e3b225a --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double DynaUIVersionNumber; +FOUNDATION_EXPORT const unsigned char DynaUIVersionString[]; + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.debug.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.debug.xcconfig new file mode 100644 index 0000000000..7a8a4f11d7 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.debug.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DynaUI +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../DynaUI +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.modulemap b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.modulemap new file mode 100644 index 0000000000..f1f9e43d25 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.modulemap @@ -0,0 +1,6 @@ +framework module DynaUI { + umbrella header "DynaUI-umbrella.h" + + export * + module * { export * } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.release.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.release.xcconfig new file mode 100644 index 0000000000..7a8a4f11d7 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/DynaUI/DynaUI.release.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DynaUI +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../DynaUI +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-Info.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-Info.plist new file mode 100644 index 0000000000..19cf209d21 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-acknowledgements.markdown b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-acknowledgements.markdown new file mode 100644 index 0000000000..102af75385 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-acknowledgements.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-acknowledgements.plist new file mode 100644 index 0000000000..7acbad1eab --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-acknowledgements.plist @@ -0,0 +1,29 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-dummy.m b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-dummy.m new file mode 100644 index 0000000000..bb28278976 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_ios_quiz_challenge : NSObject +@end +@implementation PodsDummy_Pods_ios_quiz_challenge +@end diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Debug-input-files.xcfilelist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000000..5e12233e86 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks.sh +${BUILT_PRODUCTS_DIR}/DynaUI/DynaUI.framework \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Debug-output-files.xcfilelist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000000..a190bcc957 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DynaUI.framework \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Release-input-files.xcfilelist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000000..5e12233e86 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks.sh +${BUILT_PRODUCTS_DIR}/DynaUI/DynaUI.framework \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Release-output-files.xcfilelist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000000..a190bcc957 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks-Release-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DynaUI.framework \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks.sh b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks.sh new file mode 100755 index 0000000000..91219a2235 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-frameworks.sh @@ -0,0 +1,186 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink -f "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into the targets temp dir. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" + + local basename + basename="$(basename -s .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" + else + # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=0 +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/DynaUI/DynaUI.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/DynaUI/DynaUI.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-umbrella.h b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-umbrella.h new file mode 100644 index 0000000000..aee023fe55 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_ios_quiz_challengeVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_ios_quiz_challengeVersionString[]; + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.debug.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.debug.xcconfig new file mode 100644 index 0000000000..7ff15f4e87 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.debug.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI/DynaUI.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies/dDependencies.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork/dNetwork.framework/Headers" +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -ObjC -framework "DynaUI" -framework "Foundation" -framework "SwiftUI" -framework "dDependencies" -framework "dNetwork" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "-F${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "-F${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.modulemap b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.modulemap new file mode 100644 index 0000000000..bf24074980 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.modulemap @@ -0,0 +1,6 @@ +framework module Pods_ios_quiz_challenge { + umbrella header "Pods-ios-quiz-challenge-umbrella.h" + + export * + module * { export * } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.release.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.release.xcconfig new file mode 100644 index 0000000000..7ff15f4e87 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.release.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI/DynaUI.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies/dDependencies.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork/dNetwork.framework/Headers" +LIBRARY_SEARCH_PATHS = $(inherited) "${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -ObjC -framework "DynaUI" -framework "Foundation" -framework "SwiftUI" -framework "dDependencies" -framework "dNetwork" +OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "-F${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "-F${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-Info.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-Info.plist new file mode 100644 index 0000000000..19cf209d21 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-acknowledgements.markdown b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-acknowledgements.markdown new file mode 100644 index 0000000000..102af75385 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-acknowledgements.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-acknowledgements.plist new file mode 100644 index 0000000000..7acbad1eab --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-acknowledgements.plist @@ -0,0 +1,29 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-dummy.m b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-dummy.m new file mode 100644 index 0000000000..d950addc24 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_ios_quiz_challengeTests : NSObject +@end +@implementation PodsDummy_Pods_ios_quiz_challengeTests +@end diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-umbrella.h b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-umbrella.h new file mode 100644 index 0000000000..8fefd4d77b --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_ios_quiz_challengeTestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_ios_quiz_challengeTestsVersionString[]; + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.debug.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.debug.xcconfig new file mode 100644 index 0000000000..0d7f2a4c12 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.debug.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI/DynaUI.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies/dDependencies.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork/dNetwork.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "SwiftUI" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.modulemap b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.modulemap new file mode 100644 index 0000000000..6d7aafd198 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_ios_quiz_challengeTests { + umbrella header "Pods-ios-quiz-challengeTests-umbrella.h" + + export * + module * { export * } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.release.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.release.xcconfig new file mode 100644 index 0000000000..0d7f2a4c12 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.release.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI/DynaUI.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies/dDependencies.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork/dNetwork.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "SwiftUI" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-Info.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-Info.plist new file mode 100644 index 0000000000..19cf209d21 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-acknowledgements.markdown b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-acknowledgements.markdown new file mode 100644 index 0000000000..102af75385 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-acknowledgements.markdown @@ -0,0 +1,3 @@ +# Acknowledgements +This application makes use of the following third party libraries: +Generated by CocoaPods - https://cocoapods.org diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-acknowledgements.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-acknowledgements.plist new file mode 100644 index 0000000000..7acbad1eab --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-acknowledgements.plist @@ -0,0 +1,29 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-dummy.m b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-dummy.m new file mode 100644 index 0000000000..14f4f972c9 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_ios_quiz_challengeUITests : NSObject +@end +@implementation PodsDummy_Pods_ios_quiz_challengeUITests +@end diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-umbrella.h b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-umbrella.h new file mode 100644 index 0000000000..e6b1fc6455 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_ios_quiz_challengeUITestsVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_ios_quiz_challengeUITestsVersionString[]; + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.debug.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.debug.xcconfig new file mode 100644 index 0000000000..0d7f2a4c12 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.debug.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI/DynaUI.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies/dDependencies.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork/dNetwork.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "SwiftUI" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.modulemap b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.modulemap new file mode 100644 index 0000000000..79840f0160 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.modulemap @@ -0,0 +1,6 @@ +framework module Pods_ios_quiz_challengeUITests { + umbrella header "Pods-ios-quiz-challengeUITests-umbrella.h" + + export * + module * { export * } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.release.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.release.xcconfig new file mode 100644 index 0000000000..0d7f2a4c12 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.release.xcconfig @@ -0,0 +1,11 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/DynaUI/DynaUI.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dDependencies/dDependencies.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/dNetwork/dNetwork.framework/Headers" +OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "SwiftUI" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-Info.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-Info.plist new file mode 100644 index 0000000000..434e06af4d --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.1.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-dummy.m b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-dummy.m new file mode 100644 index 0000000000..89383f6295 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_dDependencies : NSObject +@end +@implementation PodsDummy_dDependencies +@end diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-prefix.pch b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-prefix.pch new file mode 100644 index 0000000000..beb2a24418 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-umbrella.h b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-umbrella.h new file mode 100644 index 0000000000..1f74ab56a3 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double dDependenciesVersionNumber; +FOUNDATION_EXPORT const unsigned char dDependenciesVersionString[]; + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.debug.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.debug.xcconfig new file mode 100644 index 0000000000..a2183267dc --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.debug.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/dDependencies +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../dDependencies +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.modulemap b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.modulemap new file mode 100644 index 0000000000..c2b44ff707 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.modulemap @@ -0,0 +1,6 @@ +framework module dDependencies { + umbrella header "dDependencies-umbrella.h" + + export * + module * { export * } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.release.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.release.xcconfig new file mode 100644 index 0000000000..a2183267dc --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dDependencies/dDependencies.release.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/dDependencies +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../dDependencies +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-Info.plist b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-Info.plist new file mode 100644 index 0000000000..434e06af4d --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 0.1.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-dummy.m b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-dummy.m new file mode 100644 index 0000000000..2a4ad20a94 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_dNetwork : NSObject +@end +@implementation PodsDummy_dNetwork +@end diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-prefix.pch b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-prefix.pch new file mode 100644 index 0000000000..beb2a24418 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-umbrella.h b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-umbrella.h new file mode 100644 index 0000000000..592c43bdbc --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double dNetworkVersionNumber; +FOUNDATION_EXPORT const unsigned char dNetworkVersionString[]; + diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.debug.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.debug.xcconfig new file mode 100644 index 0000000000..3d09d251c1 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.debug.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/dNetwork +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../dNetwork +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.modulemap b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.modulemap new file mode 100644 index 0000000000..6ef733b2a8 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.modulemap @@ -0,0 +1,6 @@ +framework module dNetwork { + umbrella header "dNetwork-umbrella.h" + + export * + module * { export * } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.release.xcconfig b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.release.xcconfig new file mode 100644 index 0000000000..3d09d251c1 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/Pods/Target Support Files/dNetwork/dNetwork.release.xcconfig @@ -0,0 +1,13 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/dNetwork +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/../dNetwork +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/Dependencies.swift b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/Dependencies.swift new file mode 100644 index 0000000000..253066db7c --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/Dependencies.swift @@ -0,0 +1,41 @@ +// +// Dependencies.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +public enum Dependencies { + + public static func register( + _ dependency: Value, + as type: Value.Type = Value.self + ) { + DependencyContainer.shared.register( + dependency, + as: type + ) + } + + public static func resolve( + _ type: Value.Type = Value.self + ) -> Value { + DependencyContainer.shared.resolve(type) + } + + public static func remove( + _ type: Value.Type + ) { + DependencyContainer.shared.remove(type) + } + + public static func contains( + _ type: Value.Type + ) -> Bool { + DependencyContainer.shared.contains(type) + } + + public static func reset() { + DependencyContainer.shared.removeAll() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/Dependency.swift b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/Dependency.swift new file mode 100644 index 0000000000..d0f6f74462 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/Dependency.swift @@ -0,0 +1,17 @@ +// +// Dependency.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + + +@propertyWrapper +public struct Dependency { + + public init() {} + + public var wrappedValue: Value { + Dependencies.resolve(Value.self) + } +} \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/DependencyContainer.swift b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/DependencyContainer.swift new file mode 100644 index 0000000000..00a286ab5b --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/Sources/DependencyContainer.swift @@ -0,0 +1,93 @@ +// +// DependencyContainer.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +public final class DependencyContainer: @unchecked Sendable { + + public static let shared = DependencyContainer() + + private var dependencies: [ObjectIdentifier: Any] = [:] + private let lock = NSRecursiveLock() + + private init() {} + + public func register( + _ dependency: Value, + as type: Value.Type = Value.self + ) { + lock.lock() + defer { lock.unlock() } + + dependencies[ObjectIdentifier(type)] = dependency + } + + public func resolve( + _ type: Value.Type = Value.self + ) -> Value { + lock.lock() + defer { lock.unlock() } + + let identifier = ObjectIdentifier(type) + + guard let dependency = dependencies[identifier] else { + fatalError( + """ + Dependency<\(String(reflecting: type))> was not registered. + + Register it before resolving: + + Dependencies.register( + dependency, + as: \(String(reflecting: type)).self + ) + """ + ) + } + + guard let typedDependency = dependency as? Value else { + fatalError( + """ + Dependency registered for \(String(reflecting: type)) + has an incompatible runtime type: + \(String(reflecting: Swift.type(of: dependency))) + """ + ) + } + + return typedDependency + } + + public func remove( + _ type: Value.Type + ) { + lock.lock() + defer { lock.unlock() } + + dependencies.removeValue( + forKey: ObjectIdentifier(type) + ) + } + + public func removeAll() { + lock.lock() + defer { lock.unlock() } + + dependencies.removeAll() + } + + public func contains( + _ type: Value.Type + ) -> Bool { + lock.lock() + defer { lock.unlock() } + + return dependencies[ + ObjectIdentifier(type) + ] != nil + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dDependencies/dDependencies.podspec b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/dDependencies.podspec new file mode 100644 index 0000000000..1f55b0b5c6 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dDependencies/dDependencies.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |spec| + spec.name = 'dDependencies' + spec.module_name = 'dDependencies' + spec.version = '0.1.0' + spec.summary = 'Lightweight dependency injection for modular apps.' + spec.description = 'A small type-based dependency container with property wrapper support.' + + spec.homepage = 'https://github.com/kiyo92/dDependencies' + spec.license = { :type => 'MIT' } + spec.author = 'João Marcus' + + spec.source = { + :git => 'https://github.com/kiyo92/dDependencies.git', + :tag => spec.version.to_s + } + + spec.ios.deployment_target = '26.0' + spec.swift_versions = ['5.9', '6.0'] + + spec.source_files = 'Sources/**/*.swift' +end \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/HTTPMethod.swift b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/HTTPMethod.swift new file mode 100644 index 0000000000..7e7a89373b --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/HTTPMethod.swift @@ -0,0 +1,17 @@ +// +// HTTPMethod.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +public enum HTTPMethod: String, Sendable { + case get = "GET" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" + case head = "HEAD" +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkClient.swift b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkClient.swift new file mode 100644 index 0000000000..e7279f9a48 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkClient.swift @@ -0,0 +1,19 @@ +// +// NetworkClient.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +public protocol NetworkClient { + func send( + _ request: NetworkRequest, + as responseType: Response.Type + ) async throws -> Response + + func send( + _ request: NetworkRequest + ) async throws +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkConfiguration.swift b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkConfiguration.swift new file mode 100644 index 0000000000..8286673fbf --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkConfiguration.swift @@ -0,0 +1,29 @@ +// +// NetworkConfiguration.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +public struct NetworkConfiguration: Sendable { + public let baseURL: URL + public let defaultHeaders: [String: String] + public let timeoutInterval: TimeInterval + public let validStatusCodes: Range + + public init( + baseURL: URL, + defaultHeaders: [String: String] = [ + "Accept": "application/json" + ], + timeoutInterval: TimeInterval = 60, + validStatusCodes: Range = 200..<300 + ) { + self.baseURL = baseURL + self.defaultHeaders = defaultHeaders + self.timeoutInterval = timeoutInterval + self.validStatusCodes = validStatusCodes + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkError.swift b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkError.swift new file mode 100644 index 0000000000..ac931c4882 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkError.swift @@ -0,0 +1,43 @@ +// +// NetworkError.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +public enum NetworkError: Error { + case invalidURL + case transportFailure(underlyingError: Error) + case invalidResponse + case unacceptableStatusCode( + statusCode: Int, + data: Data + ) + case decodingFailure( + underlyingError: Error, + data: Data + ) +} + +extension NetworkError: LocalizedError { + public var errorDescription: String? { + switch self { + case .invalidURL: + return "Não foi possível construir a URL da requisição." + + case let .transportFailure(error): + return "A requisição falhou: \(error.localizedDescription)" + + case .invalidResponse: + return "A resposta recebida não é uma resposta HTTP válida." + + case let .unacceptableStatusCode(statusCode, _): + return "O servidor respondeu com o status HTTP \(statusCode)." + + case let .decodingFailure(error, _): + return "Não foi possível decodificar a resposta: \(error.localizedDescription)" + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkRequest.swift b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkRequest.swift new file mode 100644 index 0000000000..7cb42f5306 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/NetworkRequest.swift @@ -0,0 +1,33 @@ +// +// NetworkRequest.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +public struct NetworkRequest: Sendable { + public let path: String + public let method: HTTPMethod + public let queryItems: [URLQueryItem] + public let headers: [String: String] + public let body: Data? + public let timeoutInterval: TimeInterval? + + public init( + path: String, + method: HTTPMethod = .get, + queryItems: [URLQueryItem] = [], + headers: [String: String] = [:], + body: Data? = nil, + timeoutInterval: TimeInterval? = nil + ) { + self.path = path + self.method = method + self.queryItems = queryItems + self.headers = headers + self.body = body + self.timeoutInterval = timeoutInterval + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/URLSessionNetworkClient.swift b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/URLSessionNetworkClient.swift new file mode 100644 index 0000000000..4ccbd0cda2 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/Sources/URLSessionNetworkClient.swift @@ -0,0 +1,170 @@ +// +// URLSessionNetworkClient.swift +// Pods +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +public final class URLSessionNetworkClient: NetworkClient { + public typealias DecoderFactory = () -> JSONDecoder + + private let configuration: NetworkConfiguration + private let session: URLSession + private let decoderFactory: DecoderFactory + + public init( + configuration: NetworkConfiguration, + session: URLSession = .shared, + decoderFactory: @escaping DecoderFactory = { + JSONDecoder() + } + ) { + self.configuration = configuration + self.session = session + self.decoderFactory = decoderFactory + } + + public func send( + _ request: NetworkRequest, + as responseType: Response.Type + ) async throws -> Response { + let data = try await execute(request) + + do { + let decoder = decoderFactory() + + return try decoder.decode( + responseType, + from: data + ) + } catch { + throw NetworkError.decodingFailure( + underlyingError: error, + data: data + ) + } + } + + public func send( + _ request: NetworkRequest + ) async throws { + _ = try await execute(request) + } +} + +private extension URLSessionNetworkClient { + func execute( + _ request: NetworkRequest + ) async throws -> Data { + let urlRequest = try makeURLRequest( + from: request + ) + + let data: Data + let response: URLResponse + + do { + (data, response) = try await session.data( + for: urlRequest + ) + } catch { + throw NetworkError.transportFailure( + underlyingError: error + ) + } + + guard let httpResponse = response as? HTTPURLResponse else { + throw NetworkError.invalidResponse + } + + guard configuration.validStatusCodes.contains( + httpResponse.statusCode + ) else { + throw NetworkError.unacceptableStatusCode( + statusCode: httpResponse.statusCode, + data: data + ) + } + + return data + } + + func makeURLRequest( + from request: NetworkRequest + ) throws -> URLRequest { + let url = try makeURL( + path: request.path, + queryItems: request.queryItems + ) + + var urlRequest = URLRequest( + url: url, + timeoutInterval: request.timeoutInterval + ?? configuration.timeoutInterval + ) + + urlRequest.httpMethod = request.method.rawValue + urlRequest.httpBody = request.body + + applyHeaders( + configuration.defaultHeaders, + to: &urlRequest + ) + + applyHeaders( + request.headers, + to: &urlRequest + ) + + return urlRequest + } + + func makeURL( + path: String, + queryItems: [URLQueryItem] + ) throws -> URL { + let normalizedPath = path.trimmingCharacters( + in: CharacterSet(charactersIn: "/") + ) + + let requestURL: URL + + if normalizedPath.isEmpty { + requestURL = configuration.baseURL + } else { + requestURL = configuration.baseURL + .appendingPathComponent(normalizedPath) + } + + guard var components = URLComponents( + url: requestURL, + resolvingAgainstBaseURL: false + ) else { + throw NetworkError.invalidURL + } + + if !queryItems.isEmpty { + components.queryItems = queryItems + } + + guard let finalURL = components.url else { + throw NetworkError.invalidURL + } + + return finalURL + } + + func applyHeaders( + _ headers: [String: String], + to request: inout URLRequest + ) { + headers.forEach { key, value in + request.setValue( + value, + forHTTPHeaderField: key + ) + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/dNetwork/dNetwork.podspec b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/dNetwork.podspec new file mode 100644 index 0000000000..b91d02af18 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/dNetwork/dNetwork.podspec @@ -0,0 +1,25 @@ +Pod::Spec.new do |spec| + spec.name = 'dNetwork' + spec.module_name = 'dNetwork' + spec.version = '0.1.0' + spec.summary = 'A lightweight and model-agnostic networking module.' + spec.description = <<-DESC + A reusable networking module based on URLSession, + async/await and generic Decodable responses. + DESC + + spec.homepage = 'https://github.com/kiyo92/dNetwork' + spec.license = { :type => 'MIT' } + spec.author = 'João Marcus' + + spec.source = { + :git => 'https://github.com/kiyo92/dNetwork.git', + :tag => spec.version.to_s + } + + spec.ios.deployment_target = '26.0' + spec.swift_versions = ['5.9', '6.0'] + + spec.source_files = 'Sources/**/*.swift' + spec.frameworks = 'Foundation' +end \ No newline at end of file diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.pbxproj b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..933fd5c3f2 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.pbxproj @@ -0,0 +1,702 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 51A3B1D0E60F7E463DEFFAC6 /* Pods_ios_quiz_challenge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 908C85BD4FA36AA70E4DD708 /* Pods_ios_quiz_challenge.framework */; }; + B58A07671F79B30B174B780B /* Pods_ios_quiz_challengeUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 31A3613707190FF7F1371175 /* Pods_ios_quiz_challengeUITests.framework */; }; + FE8CDB01FEA586E37C9A3147 /* Pods_ios_quiz_challengeTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25EC7173F6807BD00EBD6FD8 /* Pods_ios_quiz_challengeTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 6C73BA5F300410EB00C231BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6C73BA47300410E900C231BC /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6C73BA4E300410E900C231BC; + remoteInfo = "ios-quiz-challenge"; + }; + 6C73BA69300410EB00C231BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6C73BA47300410E900C231BC /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6C73BA4E300410E900C231BC; + remoteInfo = "ios-quiz-challenge"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0E1BA7E84284EDFC295D2767 /* Pods-ios-quiz-challenge.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios-quiz-challenge.debug.xcconfig"; path = "Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.debug.xcconfig"; sourceTree = ""; }; + 10AC858A4C4C34F2738B20AD /* Pods-ios-quiz-challengeTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios-quiz-challengeTests.release.xcconfig"; path = "Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.release.xcconfig"; sourceTree = ""; }; + 25EC7173F6807BD00EBD6FD8 /* Pods_ios_quiz_challengeTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ios_quiz_challengeTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 31A3613707190FF7F1371175 /* Pods_ios_quiz_challengeUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ios_quiz_challengeUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 672096844A938AB1A3C930F6 /* Pods-ios-quiz-challengeUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios-quiz-challengeUITests.debug.xcconfig"; path = "Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.debug.xcconfig"; sourceTree = ""; }; + 6C73BA4F300410E900C231BC /* ios-quiz-challenge.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ios-quiz-challenge.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C73BA5E300410EB00C231BC /* ios-quiz-challengeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ios-quiz-challengeTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C73BA68300410EB00C231BC /* ios-quiz-challengeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "ios-quiz-challengeUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 908C85BD4FA36AA70E4DD708 /* Pods_ios_quiz_challenge.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ios_quiz_challenge.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9562AAAADF454FABE616F40F /* Pods-ios-quiz-challenge.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios-quiz-challenge.release.xcconfig"; path = "Target Support Files/Pods-ios-quiz-challenge/Pods-ios-quiz-challenge.release.xcconfig"; sourceTree = ""; }; + B9C4063290BF33973A25D89C /* Pods-ios-quiz-challengeUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios-quiz-challengeUITests.release.xcconfig"; path = "Target Support Files/Pods-ios-quiz-challengeUITests/Pods-ios-quiz-challengeUITests.release.xcconfig"; sourceTree = ""; }; + D589E873A96E31014BFDA873 /* Pods-ios-quiz-challengeTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ios-quiz-challengeTests.debug.xcconfig"; path = "Target Support Files/Pods-ios-quiz-challengeTests/Pods-ios-quiz-challengeTests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 6C73BA51300410E900C231BC /* ios-quiz-challenge */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "ios-quiz-challenge"; + sourceTree = ""; + }; + 6C73BA61300410EB00C231BC /* ios-quiz-challengeTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "ios-quiz-challengeTests"; + sourceTree = ""; + }; + 6C73BA6B300410EB00C231BC /* ios-quiz-challengeUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = "ios-quiz-challengeUITests"; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6C73BA4C300410E900C231BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 51A3B1D0E60F7E463DEFFAC6 /* Pods_ios_quiz_challenge.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6C73BA5B300410EB00C231BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FE8CDB01FEA586E37C9A3147 /* Pods_ios_quiz_challengeTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6C73BA65300410EB00C231BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B58A07671F79B30B174B780B /* Pods_ios_quiz_challengeUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 22865CEF040C5AB64AA44EE0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 908C85BD4FA36AA70E4DD708 /* Pods_ios_quiz_challenge.framework */, + 25EC7173F6807BD00EBD6FD8 /* Pods_ios_quiz_challengeTests.framework */, + 31A3613707190FF7F1371175 /* Pods_ios_quiz_challengeUITests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 6C73BA46300410E900C231BC = { + isa = PBXGroup; + children = ( + 6C73BA51300410E900C231BC /* ios-quiz-challenge */, + 6C73BA61300410EB00C231BC /* ios-quiz-challengeTests */, + 6C73BA6B300410EB00C231BC /* ios-quiz-challengeUITests */, + 6C73BA50300410E900C231BC /* Products */, + 7765D44D4C4961BDC32FDFCF /* Pods */, + 22865CEF040C5AB64AA44EE0 /* Frameworks */, + ); + sourceTree = ""; + }; + 6C73BA50300410E900C231BC /* Products */ = { + isa = PBXGroup; + children = ( + 6C73BA4F300410E900C231BC /* ios-quiz-challenge.app */, + 6C73BA5E300410EB00C231BC /* ios-quiz-challengeTests.xctest */, + 6C73BA68300410EB00C231BC /* ios-quiz-challengeUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7765D44D4C4961BDC32FDFCF /* Pods */ = { + isa = PBXGroup; + children = ( + 0E1BA7E84284EDFC295D2767 /* Pods-ios-quiz-challenge.debug.xcconfig */, + 9562AAAADF454FABE616F40F /* Pods-ios-quiz-challenge.release.xcconfig */, + D589E873A96E31014BFDA873 /* Pods-ios-quiz-challengeTests.debug.xcconfig */, + 10AC858A4C4C34F2738B20AD /* Pods-ios-quiz-challengeTests.release.xcconfig */, + 672096844A938AB1A3C930F6 /* Pods-ios-quiz-challengeUITests.debug.xcconfig */, + B9C4063290BF33973A25D89C /* Pods-ios-quiz-challengeUITests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6C73BA4E300410E900C231BC /* ios-quiz-challenge */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6C73BA72300410EB00C231BC /* Build configuration list for PBXNativeTarget "ios-quiz-challenge" */; + buildPhases = ( + EF4C1D54B29D3018E0BF7057 /* [CP] Check Pods Manifest.lock */, + 6C73BA4B300410E900C231BC /* Sources */, + 6C73BA4C300410E900C231BC /* Frameworks */, + 6C73BA4D300410E900C231BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 6C73BA51300410E900C231BC /* ios-quiz-challenge */, + ); + name = "ios-quiz-challenge"; + productName = "ios-quiz-challenge"; + productReference = 6C73BA4F300410E900C231BC /* ios-quiz-challenge.app */; + productType = "com.apple.product-type.application"; + }; + 6C73BA5D300410EB00C231BC /* ios-quiz-challengeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6C73BA75300410EB00C231BC /* Build configuration list for PBXNativeTarget "ios-quiz-challengeTests" */; + buildPhases = ( + F6BD2AF27102A4C2A43FF235 /* [CP] Check Pods Manifest.lock */, + 6C73BA5A300410EB00C231BC /* Sources */, + 6C73BA5B300410EB00C231BC /* Frameworks */, + 6C73BA5C300410EB00C231BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6C73BA60300410EB00C231BC /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 6C73BA61300410EB00C231BC /* ios-quiz-challengeTests */, + ); + name = "ios-quiz-challengeTests"; + productName = "ios-quiz-challengeTests"; + productReference = 6C73BA5E300410EB00C231BC /* ios-quiz-challengeTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 6C73BA67300410EB00C231BC /* ios-quiz-challengeUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6C73BA78300410EB00C231BC /* Build configuration list for PBXNativeTarget "ios-quiz-challengeUITests" */; + buildPhases = ( + 28313E81EECE61B0C231912B /* [CP] Check Pods Manifest.lock */, + 6C73BA64300410EB00C231BC /* Sources */, + 6C73BA65300410EB00C231BC /* Frameworks */, + 6C73BA66300410EB00C231BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6C73BA6A300410EB00C231BC /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 6C73BA6B300410EB00C231BC /* ios-quiz-challengeUITests */, + ); + name = "ios-quiz-challengeUITests"; + productName = "ios-quiz-challengeUITests"; + productReference = 6C73BA68300410EB00C231BC /* ios-quiz-challengeUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6C73BA47300410E900C231BC /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2610; + LastUpgradeCheck = 2610; + TargetAttributes = { + 6C73BA4E300410E900C231BC = { + CreatedOnToolsVersion = 26.1.1; + }; + 6C73BA5D300410EB00C231BC = { + CreatedOnToolsVersion = 26.1.1; + TestTargetID = 6C73BA4E300410E900C231BC; + }; + 6C73BA67300410EB00C231BC = { + CreatedOnToolsVersion = 26.1.1; + TestTargetID = 6C73BA4E300410E900C231BC; + }; + }; + }; + buildConfigurationList = 6C73BA4A300410E900C231BC /* Build configuration list for PBXProject "ios-quiz-challenge" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6C73BA46300410E900C231BC; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 6C73BA50300410E900C231BC /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6C73BA4E300410E900C231BC /* ios-quiz-challenge */, + 6C73BA5D300410EB00C231BC /* ios-quiz-challengeTests */, + 6C73BA67300410EB00C231BC /* ios-quiz-challengeUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6C73BA4D300410E900C231BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6C73BA5C300410EB00C231BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6C73BA66300410EB00C231BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 28313E81EECE61B0C231912B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ios-quiz-challengeUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EF4C1D54B29D3018E0BF7057 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ios-quiz-challenge-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F6BD2AF27102A4C2A43FF235 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ios-quiz-challengeTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6C73BA4B300410E900C231BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6C73BA5A300410EB00C231BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6C73BA64300410EB00C231BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 6C73BA60300410EB00C231BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6C73BA4E300410E900C231BC /* ios-quiz-challenge */; + targetProxy = 6C73BA5F300410EB00C231BC /* PBXContainerItemProxy */; + }; + 6C73BA6A300410EB00C231BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6C73BA4E300410E900C231BC /* ios-quiz-challenge */; + targetProxy = 6C73BA69300410EB00C231BC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 6C73BA70300410EB00C231BC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CGGNPW2AJS; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 6C73BA71300410EB00C231BC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = CGGNPW2AJS; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6C73BA73300410EB00C231BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0E1BA7E84284EDFC295D2767 /* Pods-ios-quiz-challenge.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CGGNPW2AJS; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.kiyo.ios-quiz-challenge"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6C73BA74300410EB00C231BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9562AAAADF454FABE616F40F /* Pods-ios-quiz-challenge.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CGGNPW2AJS; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.kiyo.ios-quiz-challenge"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 6C73BA76300410EB00C231BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D589E873A96E31014BFDA873 /* Pods-ios-quiz-challengeTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CGGNPW2AJS; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.kiyo.ios-quiz-challengeTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios-quiz-challenge.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ios-quiz-challenge"; + }; + name = Debug; + }; + 6C73BA77300410EB00C231BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 10AC858A4C4C34F2738B20AD /* Pods-ios-quiz-challengeTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CGGNPW2AJS; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.kiyo.ios-quiz-challengeTests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios-quiz-challenge.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ios-quiz-challenge"; + }; + name = Release; + }; + 6C73BA79300410EB00C231BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 672096844A938AB1A3C930F6 /* Pods-ios-quiz-challengeUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CGGNPW2AJS; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.kiyo.ios-quiz-challengeUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "ios-quiz-challenge"; + }; + name = Debug; + }; + 6C73BA7A300410EB00C231BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B9C4063290BF33973A25D89C /* Pods-ios-quiz-challengeUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CGGNPW2AJS; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.kiyo.ios-quiz-challengeUITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "ios-quiz-challenge"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6C73BA4A300410E900C231BC /* Build configuration list for PBXProject "ios-quiz-challenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6C73BA70300410EB00C231BC /* Debug */, + 6C73BA71300410EB00C231BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6C73BA72300410EB00C231BC /* Build configuration list for PBXNativeTarget "ios-quiz-challenge" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6C73BA73300410EB00C231BC /* Debug */, + 6C73BA74300410EB00C231BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6C73BA75300410EB00C231BC /* Build configuration list for PBXNativeTarget "ios-quiz-challengeTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6C73BA76300410EB00C231BC /* Debug */, + 6C73BA77300410EB00C231BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6C73BA78300410EB00C231BC /* Build configuration list for PBXNativeTarget "ios-quiz-challengeUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6C73BA79300410EB00C231BC /* Debug */, + 6C73BA7A300410EB00C231BC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 6C73BA47300410E900C231BC /* Project object */; +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.xcworkspace/xcuserdata/kiyo.xcuserdatad/UserInterfaceState.xcuserstate b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.xcworkspace/xcuserdata/kiyo.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..1c95ac879a Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/project.xcworkspace/xcuserdata/kiyo.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcshareddata/xcschemes/ios-quiz-challenge-UnitTests.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcshareddata/xcschemes/ios-quiz-challenge-UnitTests.xcscheme new file mode 100644 index 0000000000..55c4d27607 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcshareddata/xcschemes/ios-quiz-challenge-UnitTests.xcscheme @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcshareddata/xcschemes/ios-quiz-challenge.xcscheme b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcshareddata/xcschemes/ios-quiz-challenge.xcscheme new file mode 100644 index 0000000000..777267acfc --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcshareddata/xcschemes/ios-quiz-challenge.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/xcschememanagement.plist b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000000..984cae6f26 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcodeproj/xcuserdata/kiyo.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,27 @@ + + + + + SchemeUserState + + ios-quiz-challenge.xcscheme_^#shared#^_ + + orderHint + 6 + + + SuppressBuildableAutocreation + + 6C73BA4E300410E900C231BC + + primary + + + 6C73BA67300410EB00C231BC + + primary + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/contents.xcworkspacedata b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..7da032f4a5 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/xcuserdata/kiyo.xcuserdatad/UserInterfaceState.xcuserstate b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/xcuserdata/kiyo.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000..c4fbb0b772 Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/xcuserdata/kiyo.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/xcuserdata/kiyo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/xcuserdata/kiyo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000000..f2eedb7d83 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge.xcworkspace/xcuserdata/kiyo.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/AccentColor.colorset/Contents.json b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..2305880107 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/Contents.json b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Dependencies/AppDependenciesConfigurator.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Dependencies/AppDependenciesConfigurator.swift new file mode 100644 index 0000000000..14bda76b4b --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Dependencies/AppDependenciesConfigurator.swift @@ -0,0 +1,55 @@ +// +// AppDependenciesConfigurator.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import dDependencies +import dNetwork +import Foundation + +enum AppDependenciesConfigurator { + + static func configure() { + registerNetworkClient() + registerPlayerRepository() + } + + private static func registerNetworkClient() { + guard let baseURL = URL( + string: "https://quiz-api-bwi5hjqyaq-uc.a.run.app/" + ) else { + preconditionFailure("Invalid quiz API base URL.") + } + + let configuration = NetworkConfiguration( + baseURL: baseURL, + defaultHeaders: [ + "Accept": "application/json", + "Content-Type": "application/json" + ], + timeoutInterval: 30 + ) + + let networkClient: NetworkClient = URLSessionNetworkClient(configuration: configuration) { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return decoder + } + + Dependencies.register( + networkClient, + as: NetworkClient.self + ) + } + + private static func registerPlayerRepository() { + let repository: PlayerRepository = UserDefaultsPlayerRepository() + + Dependencies.register( + repository, + as: PlayerRepository.self + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryConfigurator.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryConfigurator.swift new file mode 100644 index 0000000000..9ae9799145 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryConfigurator.swift @@ -0,0 +1,70 @@ +// +// QuizEntryConfigurator.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import dDependencies + +class QuizEntryConfigurator { + + struct Dependencies { + let playerRepository: any PlayerRepository + + init() { + @Dependency + var playerRepository: any PlayerRepository + + self.playerRepository = playerRepository + } + + init( + playerRepository: any PlayerRepository + ) { + self.playerRepository = playerRepository + } + } + + @MainActor + static func make( + dependencies: Dependencies = .init(), + onStartQuiz: @escaping (String) -> Void, + onOpenRanking: @escaping (String) -> Void + ) -> QuizEntryView { + let viewState = QuizEntryViewState() + + let presenter = QuizEntryPresenter( + view: viewState + ) + + let router = QuizEntryRouter( + onStartQuiz: onStartQuiz, + onOpenRanking: onOpenRanking + ) + + let loadCurrentNickname = + LoadCurrentNicknameUseCase( + repository: dependencies.playerRepository + ) + + let saveCurrentNickname = + SaveCurrentNicknameUseCase( + repository: dependencies.playerRepository + ) + + let interactor = QuizEntryInteractor( + useCases: .init( + loadCurrentNickname: loadCurrentNickname, + saveCurrentNickname: saveCurrentNickname + ), + presenter: presenter, + router: router + ) + + return QuizEntryView( + state: viewState, + interactor: interactor + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryInteractor.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryInteractor.swift new file mode 100644 index 0000000000..34eebb7209 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryInteractor.swift @@ -0,0 +1,121 @@ +// +// QuizEntryInteractor.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +@MainActor +protocol QuizEntryInteracting: AnyObject { + func load() async + + func updateNickname( + _ nickname: String + ) + + func startQuiz() async + func openRanking() async +} + +@MainActor +final class QuizEntryInteractor: QuizEntryInteracting { + + struct UseCases { + let loadCurrentNickname: + any LoadCurrentNicknameUseCaseProtocol + + let saveCurrentNickname: + any SaveCurrentNicknameUseCaseProtocol + } + + private let useCases: UseCases + private let presenter: any QuizEntryPresenting + private let router: any QuizEntryRouting + + private var nickname = "" + private var isProcessing = false + + init( + useCases: UseCases, + presenter: any QuizEntryPresenting, + router: any QuizEntryRouting + ) { + self.useCases = useCases + self.presenter = presenter + self.router = router + } + + func load() async { + let savedNickname = await useCases + .loadCurrentNickname + .execute() ?? "" + + nickname = savedNickname + + presenter.present( + nickname: savedNickname + ) + } + + func updateNickname( + _ nickname: String + ) { + self.nickname = nickname + + presenter.clearError() + presenter.present(nickname: nickname) + } + + func startQuiz() async { + guard let nickname = await persistNickname() else { + return + } + + router.routeToQuiz( + nickname: nickname + ) + } + + func openRanking() async { + guard let nickname = await persistNickname() else { + return + } + + router.routeToRanking( + nickname: nickname + ) + } +} + +private extension QuizEntryInteractor { + + func persistNickname() async -> String? { + guard !isProcessing else { + return nil + } + + isProcessing = true + presenter.presentProcessing(true) + + defer { + isProcessing = false + presenter.presentProcessing(false) + } + + do { + let savedNickname = try await useCases + .saveCurrentNickname + .execute(nickname: nickname) + + nickname = savedNickname + presenter.present(nickname: savedNickname) + + return savedNickname + } catch { + presenter.present(error: error) + return nil + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryPresenter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryPresenter.swift new file mode 100644 index 0000000000..0b82b63119 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryPresenter.swift @@ -0,0 +1,61 @@ +// +// QuizEntryPresenter.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +@MainActor +protocol QuizEntryPresenting: AnyObject { + func present(nickname: String) + func presentProcessing(_ isProcessing: Bool) + func present(error: Error) + func clearError() +} + +@MainActor +final class QuizEntryPresenter: QuizEntryPresenting { + + private weak var view: (any QuizEntryDisplaying)? + + init( + view: any QuizEntryDisplaying + ) { + self.view = view + } + + func present( + nickname: String + ) { + let canContinue = !nickname + .trimmingCharacters( + in: .whitespacesAndNewlines + ) + .isEmpty + + view?.display( + nickname: nickname, + canContinue: canContinue + ) + } + + func presentProcessing( + _ isProcessing: Bool + ) { + view?.displayProcessing(isProcessing) + } + + func present( + error: Error + ) { + view?.display( + errorMessage: error.localizedDescription + ) + } + + func clearError() { + view?.display(errorMessage: nil) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryRouter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryRouter.swift new file mode 100644 index 0000000000..0ee0f3bc0f --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/QuizEntryRouter.swift @@ -0,0 +1,46 @@ +// +// QuizEntryRouter.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +@MainActor +protocol QuizEntryRouting: AnyObject { + func routeToQuiz( + nickname: String + ) + + func routeToRanking( + nickname: String + ) +} + +@MainActor +final class QuizEntryRouter: QuizEntryRouting { + + private let onStartQuiz: (String) -> Void + private let onOpenRanking: (String) -> Void + + init( + onStartQuiz: @escaping (String) -> Void, + onOpenRanking: @escaping (String) -> Void + ) { + self.onStartQuiz = onStartQuiz + self.onOpenRanking = onOpenRanking + } + + func routeToQuiz( + nickname: String + ) { + onStartQuiz(nickname) + } + + func routeToRanking( + nickname: String + ) { + onOpenRanking(nickname) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Domain/LoadCurrentNicknameUseCase.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Domain/LoadCurrentNicknameUseCase.swift new file mode 100644 index 0000000000..3f4fb90cec --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Domain/LoadCurrentNicknameUseCase.swift @@ -0,0 +1,27 @@ +// +// LoadCurrentNicknameUseCase.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +protocol LoadCurrentNicknameUseCaseProtocol { + func execute() async -> String? +} + +final class LoadCurrentNicknameUseCase: LoadCurrentNicknameUseCaseProtocol { + + private let repository: PlayerRepository + + init( + repository: PlayerRepository + ) { + self.repository = repository + } + + func execute() async -> String? { + await repository.currentNickname() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Domain/SaveCurrentNicknameUseCase.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Domain/SaveCurrentNicknameUseCase.swift new file mode 100644 index 0000000000..13cc91c843 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Domain/SaveCurrentNicknameUseCase.swift @@ -0,0 +1,54 @@ +// +// SaveCurrentNicknameUseCase.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +enum NicknameValidationError: LocalizedError { + case empty + + var errorDescription: String? { + switch self { + case .empty: + return "Digite um nick para continuar." + } + } +} + +protocol SaveCurrentNicknameUseCaseProtocol { + func execute( + nickname: String + ) async throws -> String +} + +final class SaveCurrentNicknameUseCase: SaveCurrentNicknameUseCaseProtocol { + + private let repository: PlayerRepository + + init( + repository: PlayerRepository + ) { + self.repository = repository + } + + func execute( + nickname: String + ) async throws -> String { + let normalizedNickname = nickname.trimmingCharacters( + in: .whitespacesAndNewlines + ) + + guard !normalizedNickname.isEmpty else { + throw NicknameValidationError.empty + } + + await repository.setCurrentNickname( + normalizedNickname + ) + + return normalizedNickname + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Model/PlayerScore.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Model/PlayerScore.swift new file mode 100644 index 0000000000..7e76dcff1e --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/Model/PlayerScore.swift @@ -0,0 +1,17 @@ +// +// PlayerScore.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +struct PlayerScore: Codable, Equatable, Identifiable, Sendable { + let nickname: String + let score: Int + + var id: String { + nickname.lowercased() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/PlayerRepository.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/PlayerRepository.swift new file mode 100644 index 0000000000..3a63cb15d0 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/PlayerRepository.swift @@ -0,0 +1,20 @@ +// +// PlayerRepository.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +protocol PlayerRepository: Sendable { + func setCurrentNickname(_ nickname: String) async + func currentNickname() async -> String? + + func save( + score: Int, + for nickname: String + ) async throws + + func ranking() async -> [PlayerScore] +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/UserDefaultsPlayerRepository.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/UserDefaultsPlayerRepository.swift new file mode 100644 index 0000000000..78c7d18ee1 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/Repository/UserDefaultsPlayerRepository.swift @@ -0,0 +1,114 @@ +// +// UserDefaultsPlayerRepository.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +actor UserDefaultsPlayerRepository: PlayerRepository { + + private enum Keys { + static let currentNickname = "quiz.currentNickname" + static let scores = "quiz.playerScores" + } + + private let userDefaults: UserDefaults + private let encoder = JSONEncoder() + private let decoder = JSONDecoder() + + init( + userDefaults: UserDefaults = .standard + ) { + self.userDefaults = userDefaults + } + + func setCurrentNickname( + _ nickname: String + ) async { + userDefaults.set( + nickname, + forKey: Keys.currentNickname + ) + } + + func currentNickname() async -> String? { + userDefaults.string( + forKey: Keys.currentNickname + ) + } + + func save( + score: Int, + for nickname: String + ) async throws { + let normalizedNickname = nickname.trimmingCharacters( + in: .whitespacesAndNewlines + ) + + var scores = storedScores() + + if let index = scores.firstIndex( + where: { + $0.nickname.compare( + normalizedNickname, + options: .caseInsensitive + ) == .orderedSame + } + ) { + let currentScore = scores[index].score + + scores[index] = PlayerScore( + nickname: normalizedNickname, + score: max(currentScore, score) + ) + } else { + scores.append( + PlayerScore( + nickname: normalizedNickname, + score: score + ) + ) + } + + let data = try encoder.encode(scores) + + userDefaults.set( + data, + forKey: Keys.scores + ) + } + + func ranking() async -> [PlayerScore] { + storedScores() + .sorted { + if $0.score == $1.score { + return $0.nickname.localizedCaseInsensitiveCompare( + $1.nickname + ) == .orderedAscending + } + + return $0.score > $1.score + } + } +} + +private extension UserDefaultsPlayerRepository { + + func storedScores() -> [PlayerScore] { + guard + let data = userDefaults.data( + forKey: Keys.scores + ), + let scores = try? decoder.decode( + [PlayerScore].self, + from: data + ) + else { + return [] + } + + return scores + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/View/QuizEntryView.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/View/QuizEntryView.swift new file mode 100644 index 0000000000..895aa2a148 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Entry/View/QuizEntryView.swift @@ -0,0 +1,301 @@ +// +// QuizEntryView.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import SwiftUI +import Combine + +@MainActor +protocol QuizEntryDisplaying: AnyObject { + func display(nickname: String, canContinue: Bool) + func displayProcessing(_ isProcessing: Bool) + func display(errorMessage: String?) +} + +@MainActor +final class QuizEntryViewState: ObservableObject, QuizEntryDisplaying { + + @Published private(set) var nickname = "" + @Published private(set) var canContinue = false + @Published private(set) var isProcessing = false + @Published private(set) var errorMessage: String? + + func display(nickname: String, canContinue: Bool) { + self.nickname = nickname + self.canContinue = canContinue + } + + func displayProcessing(_ isProcessing: Bool) { + self.isProcessing = isProcessing + } + + func display(errorMessage: String?) { + self.errorMessage = errorMessage + } +} + +@MainActor +struct QuizEntryView: View { + + @StateObject private var state: QuizEntryViewState + + private let interactor: any QuizEntryInteracting + + init( + state: QuizEntryViewState, + interactor: any QuizEntryInteracting + ) { + _state = StateObject( + wrappedValue: state + ) + + self.interactor = interactor + } + + var body: some View { + ZStack { + Color( + red: 0.42, + green: 0.20, + blue: 1 + ) + .ignoresSafeArea() + + VStack(spacing: 36) { + title + + entryPanel + } + .padding(.horizontal, 24) + } + .task { + await interactor.load() + } + } +} + +private extension QuizEntryView { + + var title: some View { + VStack(spacing: 8) { + Text("Quiz App") + .font( + .system( + size: 42, + weight: .black, + design: .rounded + ) + ) + .foregroundStyle(.white) + + Text("Teste seus conhecimentos") + .font(.subheadline) + .foregroundStyle( + .white.opacity(0.8) + ) + } + } + + var entryPanel: some View { + VStack(spacing: 20) { + VStack( + alignment: .leading, + spacing: 8 + ) { + Text("Seu nick") + .font(.headline) + + TextField( + "Digite seu nick", + text: Binding( + get: { + state.nickname + }, + set: { + interactor.updateNickname($0) + } + ) + ) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .padding(.horizontal, 16) + .frame(height: 52) + .background( + RoundedRectangle( + cornerRadius: 14, + style: .continuous + ) + .fill(Color.white) + ) + .overlay { + RoundedRectangle( + cornerRadius: 14, + style: .continuous + ) + .stroke(Color.black, lineWidth: 2) + } + + if let errorMessage = state.errorMessage { + Text(errorMessage) + .font(.footnote) + .foregroundStyle(.red) + } + } + + Button { + Task { + await interactor.startQuiz() + } + } label: { + buttonLabel( + title: "Iniciar", + systemImage: "play.fill" + ) + } + .buttonStyle( + QuizEntryPrimaryButtonStyle() + ) + .disabled( + !state.canContinue || + state.isProcessing + ) + + Button { + Task { + await interactor.openRanking() + } + } label: { + buttonLabel( + title: "Minha posição no ranking", + systemImage: "trophy.fill" + ) + } + .buttonStyle( + QuizEntrySecondaryButtonStyle() + ) + .disabled( + !state.canContinue || + state.isProcessing + ) + } + .padding(24) + .background { + RoundedRectangle( + cornerRadius: 24, + style: .continuous + ) + .fill(Color.white) + } + .overlay { + RoundedRectangle( + cornerRadius: 24, + style: .continuous + ) + .stroke(Color.black, lineWidth: 2) + } + .background { + RoundedRectangle( + cornerRadius: 24, + style: .continuous + ) + .fill( + Color( + red: 1, + green: 0.23, + blue: 0.52 + ) + ) + .overlay { + RoundedRectangle( + cornerRadius: 24, + style: .continuous + ) + .stroke(Color.black, lineWidth: 2) + } + .offset(x: 8, y: 8) + } + .padding(.trailing, 8) + .padding(.bottom, 8) + } + + func buttonLabel(title: String, systemImage: String) -> some View { + HStack { + Image(systemName: systemImage) + + Text(title) + .fontWeight(.bold) + + Spacer() + + if state.isProcessing { + ProgressView() + .tint(Color.purple) + } + } + .frame(maxWidth: .infinity) + } + + private struct QuizEntryPrimaryButtonStyle: ButtonStyle { + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundStyle(.white) + .padding(.horizontal, 18) + .frame(height: 52) + .background { + RoundedRectangle( + cornerRadius: 16, + style: .continuous + ) + .fill(Color.black) + } + .scaleEffect( + configuration.isPressed ? 0.97 : 1 + ) + .animation( + .spring( + response: 0.25, + dampingFraction: 0.7 + ), + value: configuration.isPressed + ) + } + } + + private struct QuizEntrySecondaryButtonStyle: ButtonStyle { + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundStyle(.black) + .padding(.horizontal, 18) + .frame(height: 52) + .background { + RoundedRectangle( + cornerRadius: 16, + style: .continuous + ) + .fill(Color.white) + } + .overlay { + RoundedRectangle( + cornerRadius: 16, + style: .continuous + ) + .stroke(Color.black, lineWidth: 2) + } + .scaleEffect( + configuration.isPressed ? 0.97 : 1 + ) + .animation( + .spring( + response: 0.25, + dampingFraction: 0.7 + ), + value: configuration.isPressed + ) + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Navigation/AppFlowView.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Navigation/AppFlowView.swift new file mode 100644 index 0000000000..906b440605 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Navigation/AppFlowView.swift @@ -0,0 +1,150 @@ +// +// AppFlowView.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import SwiftUI + +@MainActor +struct AppFlowView: View { + @State private var navigationPath: [AppRoute] = [] + + var body: some View { + NavigationStack(path: $navigationPath) { + entryScene + .navigationDestination( + for: AppRoute.self, + destination: destination + ) + } + } +} + +private extension AppFlowView { + var entryScene: some View { + QuizEntryConfigurator.make( + onStartQuiz: { nickname in + openQuiz(nickname: nickname) + }, + onOpenRanking: { nickname in + openRanking(nickname: nickname) + } + ) + .toolbar(.hidden, for: .navigationBar) + } + + @ViewBuilder + func destination(for route: AppRoute) -> some View { + switch route { + case let .quiz(nickname): + quizScene(nickname: nickname) + + case let .result(nickname, score): + resultScene( + nickname: nickname, + score: score + ) + + case let .ranking(nickname): + rankingScene(nickname: nickname) + } + } + + func quizScene(nickname: String) -> some View { + QuizConfigurator.make( + questionNumber: 1, + totalQuestions: 10, + onClose: { + closeCurrentScene() + }, + onFinish: { score in + showResult( + nickname: nickname, + score: score + ) + } + ) + .toolbar(.hidden, for: .navigationBar) + } + + func resultScene( + nickname: String, + score: Int + ) -> some View { + QuizResultConfigurator.make( + nickname: nickname, + score: score, + totalQuestions: 10, + onRestart: { + restartQuiz(nickname: nickname) + }, + onOpenRanking: { + openRanking(nickname: nickname) + }, + onClose: { + closeFlow() + } + ) + .toolbar(.hidden, for: .navigationBar) + } + + func rankingScene( + nickname: String + ) -> some View { + RankingConfigurator.make( + nickname: nickname, + onClose: { + closeCurrentScene() + } + ) + .toolbar(.hidden, for: .navigationBar) + } + + func openQuiz(nickname: String) { + navigationPath.append( + .quiz(nickname: nickname) + ) + } + + func openRanking(nickname: String) { + navigationPath.append( + .ranking(nickname: nickname) + ) + } + + func showResult( + nickname: String, + score: Int + ) { + let previousPath = navigationPath.dropLast() + + navigationPath = Array(previousPath) + [ + .result( + nickname: nickname, + score: score + ) + ] + } + + func restartQuiz(nickname: String) { + navigationPath = [ + .quiz(nickname: nickname) + ] + } + + func closeCurrentScene() { + guard !navigationPath.isEmpty else { + return + } + + navigationPath = Array( + navigationPath.dropLast() + ) + } + + func closeFlow() { + navigationPath = [] + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Navigation/AppRoute.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Navigation/AppRoute.swift new file mode 100644 index 0000000000..f9966cc411 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Navigation/AppRoute.swift @@ -0,0 +1,14 @@ +// +// AppRoute.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +enum AppRoute: Hashable { + case quiz(nickname: String) + case result(nickname: String, score: Int) + case ranking(nickname: String) +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Domain/AnswerQuizQuestionUseCase.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Domain/AnswerQuizQuestionUseCase.swift new file mode 100644 index 0000000000..b6cbccbfac --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Domain/AnswerQuizQuestionUseCase.swift @@ -0,0 +1,32 @@ +// +// AnswerQuizQuestionUseCase.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +protocol AnswerQuizQuestionUseCaseProtocol { + func execute(questionID: String, answer: String) async throws -> Bool +} + +final class AnswerQuizQuestionUseCase: AnswerQuizQuestionUseCaseProtocol { + + private let service: QuizServiceProtocol + + init( + service: QuizServiceProtocol + ) { + self.service = service + } + + func execute(questionID: String, answer: String) async throws -> Bool { + let response = try await service.answerQuestion( + questionID: questionID, + answer: answer + ) + + return response.result + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Domain/FetchQuizQuestionUseCase.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Domain/FetchQuizQuestionUseCase.swift new file mode 100644 index 0000000000..4eb702555c --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Domain/FetchQuizQuestionUseCase.swift @@ -0,0 +1,35 @@ +// +// FetchQuizQuestionUseCaseProtocol.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +protocol FetchQuizQuestionUseCaseProtocol { + func execute(questionNumber: Int) async throws -> QuizQuestion +} + +final class FetchQuizQuestionUseCase: FetchQuizQuestionUseCaseProtocol { + + private let service: QuizServiceProtocol + + init(service: QuizServiceProtocol) { + self.service = service + } + + func execute(questionNumber: Int) async throws -> QuizQuestion { + let dto = try await service.fetchQuestion() + + return QuizQuestion( + id: dto.id, + number: questionNumber, + title: dto.statement, + imageName: nil, + options: dto.options.map { option in + QuizOption(title: option) + } + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Model/QuizQuestion.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Model/QuizQuestion.swift new file mode 100644 index 0000000000..4a5d44ea3a --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Model/QuizQuestion.swift @@ -0,0 +1,29 @@ +// +// QuizQuestion.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 13/07/26. +// + +import Foundation + +struct QuizOption: Identifiable, Equatable { + let id: UUID + let title: String + + init( + id: UUID = UUID(), + title: String + ) { + self.id = id + self.title = title + } +} + +struct QuizQuestion: Identifiable, Equatable { + let id: String + let number: Int + let title: String + let imageName: String? + let options: [QuizOption] +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizConfigurator.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizConfigurator.swift new file mode 100644 index 0000000000..637a30b2bb --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizConfigurator.swift @@ -0,0 +1,73 @@ +// +// QuizConfigurator.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import dDependencies +import dNetwork + +class QuizConfigurator { + + struct Dependencies { + let networkClient: any NetworkClient + + init() { + @Dependency + var networkClient: any NetworkClient + + self.networkClient = networkClient + } + + init( + networkClient: any NetworkClient + ) { + self.networkClient = networkClient + } + } + + @MainActor + static func make( + dependencies: Dependencies = .init(), + questionNumber: Int = 1, + totalQuestions: Int = 1, + quizDurationSeconds: Int = 120, + onClose: @escaping () -> Void, + onFinish: @escaping (Int) -> Void + ) -> QuizView { + let service = QuizService( + networkClient: dependencies.networkClient + ) + + let fetchQuestionUseCase = FetchQuizQuestionUseCase(service: service) + let answerQuestionUseCase = AnswerQuizQuestionUseCase(service: service) + + let viewState = QuizViewState( + remainingSeconds: quizDurationSeconds + ) + + let presenter = QuizPresenter( + view: viewState + ) + + let router = QuizRouter(onClose: onClose, onFinish: onFinish) + + let interactor = QuizInteractor( + useCases: .init( + fetchQuestion: fetchQuestionUseCase, + answerQuestion: answerQuestionUseCase + ), + presenter: presenter, + router: router, + totalQuestions: totalQuestions, + quizDurationSeconds: quizDurationSeconds + ) + + return QuizView( + state: viewState, + interactor: interactor, + totalQuestions: totalQuestions + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizInteractor.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizInteractor.swift new file mode 100644 index 0000000000..54cc71953d --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizInteractor.swift @@ -0,0 +1,264 @@ +// +// QuizInteracting.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +@MainActor +protocol QuizInteracting: AnyObject { + func loadQuestion() async + func retry() async + func selectAnswer(_ option: QuizOption) async + func close() +} + +@MainActor +final class QuizInteractor: QuizInteracting { + + struct UseCases { + let fetchQuestion: FetchQuizQuestionUseCaseProtocol + let answerQuestion: AnswerQuizQuestionUseCaseProtocol + } + + private let useCases: UseCases + private let presenter: any QuizPresenting + private let router: any QuizRouting + private let totalQuestions: Int + private let answerResultDelayNanoseconds: UInt64 + private let timerTickNanoseconds: UInt64 + + private var currentQuestion: QuizQuestion? + private var currentQuestionNumber = 0 + private var score = 0 + private var remainingSeconds: Int + + private var isLoadingQuestion = false + private var isAnswering = false + private var isFinished = false + private var timerTask: Task? + + init( + useCases: UseCases, + presenter: any QuizPresenting, + router: any QuizRouting, + totalQuestions: Int, + quizDurationSeconds: Int = 120, + timerTickNanoseconds: UInt64 = 1_000_000_000, + answerResultDelayNanoseconds: UInt64 = 1_000_000_000 + ) { + self.useCases = useCases + self.presenter = presenter + self.router = router + self.totalQuestions = totalQuestions + self.remainingSeconds = quizDurationSeconds + self.timerTickNanoseconds = timerTickNanoseconds + self.answerResultDelayNanoseconds = answerResultDelayNanoseconds + } + + deinit { + timerTask?.cancel() + } + + func loadQuestion() async { + guard currentQuestion == nil else { + return + } + + do { + try await fetchNextQuestion(displaysLoading: true) + } catch { + guard !isFinished else { + return + } + + presenter.present(error: error) + } + } + + func retry() async { + do { + try await fetchNextQuestion(displaysLoading: true) + } catch { + guard !isFinished else { + return + } + + presenter.present(error: error) + } + } + + func selectAnswer(_ option: QuizOption) async { + guard + !isAnswering, + !isLoadingQuestion, + !isFinished, + let question = currentQuestion, + question.options.contains( + where: { $0.id == option.id } + ) + else { + return + } + + startTimerIfNeeded() + isAnswering = true + presenter.presentAnswering(optionID: option.id) + + let isCorrect: Bool + + do { + isCorrect = try await useCases + .answerQuestion + .execute(questionID: question.id, answer: option.title) + } catch { + guard !isFinished else { + return + } + + presenter.presentAnswerError(error) + isAnswering = false + pauseTimer() + return + } + + guard !isFinished else { + return + } + + if isCorrect { + score += 1 + presenter.present(score: score) + } + + presenter.presentAnswerResult( + isCorrect: isCorrect + ) + + try? await Task.sleep( + nanoseconds: answerResultDelayNanoseconds + ) + + guard !isFinished else { + return + } + + if currentQuestionNumber >= totalQuestions { + finishQuiz() + return + } + + do { + try await fetchNextQuestion( + displaysLoading: false + ) + isAnswering = false + } catch { + guard !isFinished else { + return + } + + isAnswering = false + presenter.present(error: error) + } + } + + func close() { + isFinished = true + timerTask?.cancel() + router.close() + } +} + +private extension QuizInteractor { + func startTimerIfNeeded() { + guard timerTask == nil else { + return + } + + presenter.present(remainingSeconds: remainingSeconds) + let timerTickNanoseconds = timerTickNanoseconds + + timerTask = Task { [weak self] in + while !Task.isCancelled { + try? await Task.sleep( + nanoseconds: timerTickNanoseconds + ) + + if Task.isCancelled { + return + } + + await self?.tickTimer() + } + } + } + + func pauseTimer() { + timerTask?.cancel() + timerTask = nil + } + + func tickTimer() { + guard !isFinished, remainingSeconds > 0 else { + return + } + + remainingSeconds -= 1 + presenter.present(remainingSeconds: remainingSeconds) + + if remainingSeconds == 0 { + finishQuiz() + } + } + + func finishQuiz() { + guard !isFinished else { + return + } + + isFinished = true + timerTask?.cancel() + router.finishQuiz( + score: score * remainingSeconds + ) + } + + func fetchNextQuestion(displaysLoading: Bool) async throws { + guard !isLoadingQuestion, !isFinished else { + return + } + + isLoadingQuestion = true + pauseTimer() + + defer { + isLoadingQuestion = false + } + + if displaysLoading { + presenter.presentLoading() + } + + let nextQuestionNumber = + currentQuestionNumber + 1 + + let question = try await useCases + .fetchQuestion + .execute(questionNumber: nextQuestionNumber) + + guard !isFinished else { + return + } + + currentQuestionNumber = nextQuestionNumber + currentQuestion = question + + presenter.present( + question: question + ) + startTimerIfNeeded() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizPresenter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizPresenter.swift new file mode 100644 index 0000000000..bcc364ad01 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizPresenter.swift @@ -0,0 +1,92 @@ +// +// QuizPresenting.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +@MainActor +protocol QuizPresenting: AnyObject { + func presentLoading() + func present(question: QuizQuestion) + func present(score: Int) + func present(remainingSeconds: Int) + func presentAnswering(optionID: QuizOption.ID) + func presentAnswerResult(isCorrect: Bool) + func present(error: Error) + func presentAnswerError(_ error: Error) +} + +@MainActor +final class QuizPresenter: QuizPresenting { + + private weak var view: QuizDisplaying? + + init(view: QuizDisplaying) { + self.view = view + } + + func presentLoading() { + view?.displayLoading() + } + + func present(question: QuizQuestion) { + view?.display( + question: format(question) + ) + } + + func present(score: Int) { + view?.display(score: score) + } + + func present(remainingSeconds: Int) { + view?.display(remainingSeconds: remainingSeconds) + } + + func presentAnswering(optionID: QuizOption.ID) { + view?.displayAnswering(optionID: optionID) + } + + func presentAnswerResult(isCorrect: Bool) { + view?.displayAnswerResult(isCorrect: isCorrect) + } + + func present(error: Error) { + view?.display( + errorMessage: error.localizedDescription + ) + } + + func presentAnswerError( _ error: Error) { + view?.displayAnswerError( + message: error.localizedDescription + ) + } +} + +private extension QuizPresenter { + + func format(_ question: QuizQuestion) -> QuizQuestion { + QuizQuestion( + id: question.id, + number: question.number, + title: format(question.title), + imageName: question.imageName, + options: question.options.map { option in + QuizOption( + id: option.id, + title: format(option.title) + ) + } + ) + } + + func format(_ text: String) -> String { + text.trimmingCharacters( + in: .whitespacesAndNewlines + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizRouter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizRouter.swift new file mode 100644 index 0000000000..e73b7c914e --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizRouter.swift @@ -0,0 +1,37 @@ +// +// QuizRouter.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +@MainActor +protocol QuizRouting: AnyObject { + func close() + func finishQuiz(score: Int) +} + +@MainActor +final class QuizRouter: QuizRouting { + + private let onClose: () -> Void + private let onFinish: (Int) -> Void + + init( + onClose: @escaping () -> Void, + onFinish: @escaping (Int) -> Void + ) { + self.onClose = onClose + self.onFinish = onFinish + } + + func close() { + onClose() + } + + func finishQuiz(score: Int) { + onFinish(score) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizViewModel.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizViewModel.swift new file mode 100644 index 0000000000..2977aaad41 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/QuizViewModel.swift @@ -0,0 +1,49 @@ +// +// QuizViewModel.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation +import Combine + +final class QuizViewModel: ObservableObject { + + enum State: Equatable { + case idle + case loading + case loaded(QuizQuestionDTO) + case error(String) + } + + @Published private(set) var state: State = .idle + + private let service: QuizServiceProtocol + + init( + service: QuizServiceProtocol + ) { + self.service = service + } + + func load() async { + guard state != .loading else { + return + } + + state = .loading + + do { + let dto = try await service.fetchQuestion() + state = .loaded(dto) + } catch { + state = .error(error.localizedDescription) + } + } + + func retry() async { + state = .idle + await load() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/DTO/AnswerQuizQuestionDTO.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/DTO/AnswerQuizQuestionDTO.swift new file mode 100644 index 0000000000..231772ba8e --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/DTO/AnswerQuizQuestionDTO.swift @@ -0,0 +1,16 @@ +// +// AnswerQuizQuestionDTO.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +struct AnswerQuizQuestionRequestDTO: Encodable { + let answer: String +} + +struct AnswerQuizQuestionResponseDTO: Decodable, Equatable, Sendable { + let result: Bool +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/DTO/QuizQuestionDTO.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/DTO/QuizQuestionDTO.swift new file mode 100644 index 0000000000..71e947b207 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/DTO/QuizQuestionDTO.swift @@ -0,0 +1,14 @@ +// +// QuizQuestionDTO.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import Foundation + +struct QuizQuestionDTO: Decodable, Equatable, Sendable { + let id: String + let statement: String + let options: [String] +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/QuizService.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/QuizService.swift new file mode 100644 index 0000000000..b981b09b11 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/Service/QuizService.swift @@ -0,0 +1,71 @@ +// +// QuizService.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 15/07/26. +// + +import dDependencies +import dNetwork + +protocol QuizServiceProtocol { + func fetchQuestion() async throws -> QuizQuestionDTO + func answerQuestion( + questionID: String, + answer: String + ) async throws -> AnswerQuizQuestionResponseDTO +} + +final class QuizService: QuizServiceProtocol { + + private let networkClient: NetworkClient + private let encoder: JSONEncoder + + init( + networkClient: NetworkClient, + encoder: JSONEncoder = JSONEncoder() + ) { + self.networkClient = networkClient + self.encoder = encoder + } + + func fetchQuestion() async throws -> QuizQuestionDTO { + let request = NetworkRequest( + path: "question", + method: .get + ) + + return try await networkClient.send( + request, + as: QuizQuestionDTO.self + ) + } + + func answerQuestion( + questionID: String, + answer: String + ) async throws -> AnswerQuizQuestionResponseDTO { + let body = try encoder.encode( + AnswerQuizQuestionRequestDTO( + answer: answer + ) + ) + + let request = NetworkRequest( + path: "answer", + method: .post, + queryItems: [ + URLQueryItem( + name: "questionId", + value: questionID + ) + ], + body: body + ) + + return try await networkClient.send( + request, + as: AnswerQuizQuestionResponseDTO.self + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizContentView.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizContentView.swift new file mode 100644 index 0000000000..5081f0f9f2 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizContentView.swift @@ -0,0 +1,210 @@ +// +// QuizContentView.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 13/07/26. +// + +import SwiftUI + +struct QuizContentView: View { + let question: QuizQuestion + let totalQuestions: Int + + let selectedOptionID: QuizOption.ID? + + let answerResult: Bool? + let isAnswering: Bool + + let onAnswer: (QuizOption) -> Void + + @State private var showProgress = false + @State private var showQuestion = false + @State private var showPinkShadow = false + @State private var showPurpleCard = false + @State private var visibleOptionCount = 0 + + var body: some View { + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + VStack(spacing: .zero) { + Spacer(minLength: 16) + + progressView + .padding(.top, 8) + + Spacer(minLength: 32) + + questionView + .padding(.top, 14) + + Spacer(minLength: 48) + + optionsPanel + .padding(.top, 15) + .padding(.horizontal, 18) + + Spacer(minLength: 115) + } + .frame( + maxWidth: .infinity, + minHeight: geometry.size.height + ) + } + } + .task(id: question.id) { + await runEntranceAnimation() + } + } +} + +// MARK: - Content + +private extension QuizContentView { + var progressView: some View { + Text("\(question.number)/\(totalQuestions)") + .font( + .system( + size: 17, + weight: .medium + ) + ) + .foregroundStyle(.black) + .opacity(showProgress ? 1 : .zero) + .scaleEffect(showProgress ? 1 : 0.9) + } + + var questionView: some View { + Text(question.title) + .font( + .system( + size: 19, + weight: .semibold + ) + ) + .foregroundStyle(.black) + .multilineTextAlignment(.center) + .padding(.horizontal, 24) + .opacity(showQuestion ? 1 : .zero) + .offset( + x: showQuestion ? .zero : 180 + ) + } + + var optionsPanel: some View { + QuizOptionsPanel( + options: question.options, + selectedOptionID: selectedOptionID, + answerResult: answerResult, + isAnswering: isAnswering, + showPinkShadow: showPinkShadow, + showPurpleCard: showPurpleCard, + visibleOptionCount: visibleOptionCount, + onAnswer: onAnswer + ) + } +} + +// MARK: - Entrance animation + +private extension QuizContentView { + @MainActor + func runEntranceAnimation() async { + resetAnimation() + + do { + try await wait(milliseconds: 100) + + withAnimation( + .easeOut(duration: 0.22) + ) { + showProgress = true + } + + try await wait(milliseconds: 100) + + withAnimation( + .spring( + response: 0.52, + dampingFraction: 0.76, + blendDuration: .zero + ) + ) { + showQuestion = true + } + + try await wait(milliseconds: 160) + + withAnimation( + .spring( + response: 0.62, + dampingFraction: 0.78, + blendDuration: .zero + ) + ) { + showPinkShadow = true + } + + try await wait(milliseconds: 70) + + withAnimation( + .spring( + response: 0.62, + dampingFraction: 0.78, + blendDuration: .zero + ) + ) { + showPurpleCard = true + } + + try await wait(milliseconds: 210) + + for index in question.options.indices { + try Task.checkCancellation() + + withAnimation( + .interpolatingSpring( + mass: 0.58, + stiffness: 290, + damping: 15, + initialVelocity: 0.8 + ) + ) { + visibleOptionCount = index + 1 + } + + try await wait(milliseconds: 105) + } + } catch is CancellationError { + assertionFailure( + "Unexpected error" + ) + } catch { + assertionFailure( + "Unexpected quiz animation error: \(error)" + ) + } + } + + @MainActor + func resetAnimation() { + var transaction = Transaction() + transaction.disablesAnimations = true + + withTransaction(transaction) { + showProgress = false + showQuestion = false + showPinkShadow = false + showPurpleCard = false + visibleOptionCount = .zero + } + } + + func wait( + milliseconds: UInt64 + ) async throws { + try await Task.sleep( + nanoseconds: milliseconds * 1_000_000 + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizFixedHeader.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizFixedHeader.swift new file mode 100644 index 0000000000..cbc348850b --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizFixedHeader.swift @@ -0,0 +1,89 @@ +// +// QuizFixedHeader.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 13/07/26. +// + +import SwiftUI + +struct QuizFixedHeader: View { + let score: Int + let remainingSeconds: Int + let onClose: () -> Void + + var body: some View { + VStack(spacing: 14) { + HStack { + Button(action: onClose) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 17, weight: .bold)) + .foregroundColor(.black) + } + .buttonStyle(.plain) + .accessibilityLabel("Fechar quiz") + + Spacer() + + Text("Score : \(score)") + .font(.system(size: 13, weight: .bold)) + .foregroundColor(.black) + } + + QuizTimerBar(remainingSeconds: remainingSeconds) + } + .padding(.horizontal, 20) + .padding(.top, 8) + .padding(.bottom, 12) + .background(Color.white) + } +} + +struct QuizTimerBar: View { + let remainingSeconds: Int + + var body: some View { + ZStack { + RoundedRectangle( + cornerRadius: 15, + style: .continuous + ) + .fill(Color.black) + .offset(y: 4) + + RoundedRectangle( + cornerRadius: 15, + style: .continuous + ) + .fill(Color.white) + .overlay { + RoundedRectangle( + cornerRadius: 15, + style: .continuous + ) + .stroke(Color.black, lineWidth: 2) + } + + RoundedRectangle( + cornerRadius: 11, + style: .continuous + ) + .fill(Color.black) + .padding(4) + + HStack { + Text("\(remainingSeconds) Sec") + .font(.system(size: 13, weight: .bold)) + + Spacer() + + Image(systemName: "timer") + .font(.system(size: 16, weight: .semibold)) + } + .foregroundColor(.white) + .padding(.horizontal, 14) + } + .frame(height: 42) + .padding(.bottom, 4) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizOptionsPanel.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizOptionsPanel.swift new file mode 100644 index 0000000000..63d7afadad --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizOptionsPanel.swift @@ -0,0 +1,126 @@ +// +// QuizOptionsPanel.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 13/07/26. +// + +import SwiftUI +import DynaUI + +struct QuizOptionsPanel: View { + private enum Constants { + static let optionInitialScale: CGFloat = 0.62 + } + + let options: [QuizOption] + + let selectedOptionID: QuizOption.ID? + let answerResult: Bool? + let isAnswering: Bool + + let showPinkShadow: Bool + let showPurpleCard: Bool + let visibleOptionCount: Int + + let onAnswer: (QuizOption) -> Void + + var body: some View { + DynaPanel( + layout: .vertical, + style: .default, + motion: .opposingHorizontal( + distance: 500 + ), + isPanelVisible: showPurpleCard, + isShadowVisible: showPinkShadow, + ) { + ForEach( + Array(options.enumerated()), + id: \.element.id + ) { index, option in + optionButton( + option, + at: index + ) + } + } + } + + private func optionButton( + _ option: QuizOption, + at index: Int + ) -> some View { + let isVisible = index < visibleOptionCount + + return DynaOptionButton( + option.title, + state: buttonState(for: option), + isEnabled: isVisible + && !isAnswering + && answerResult == nil + ) { + onAnswer(option) + } + .opacity(isVisible ? 1 : .zero) + .scaleEffect(isVisible ? 1 : 0.62) + } + + private func buttonState(for option: QuizOption) -> DynaOptionButton.State { + guard selectedOptionID == option.id else { + return .idle + } + + guard let answerResult else { + return .selected + } + + return answerResult + ? .correct + : .incorrect + } + +} + +private extension DynaPanelStyle { + static let quizOptions = DynaPanelStyle( + backgroundColor: QuizPalette.purple, + shadowColor: QuizPalette.pink, + borderColor: .black, + borderWidth: 2, + cornerRadius: 21, + shadowOffset: CGSize( + width: 8, + height: 8 + ), + contentInsets: EdgeInsets( + top: 16, + leading: 8, + bottom: 16, + trailing: 8 + ), + itemSpacing: 6 + ) +} + +#Preview { + VStack { + QuizOptionsPanel( + options: [ + .init(title: "Option 1"), + .init(title: "Option 2"), + .init(title: "Option 3"), + .init(title: "Option 4") + ], + selectedOptionID: nil, + answerResult: true, + isAnswering: false, + showPinkShadow: true, + showPurpleCard: true, + visibleOptionCount: 4 + ) { option in + + } + } + .frame(height: .zero) +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizView.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizView.swift new file mode 100644 index 0000000000..f4e90932b5 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Quiz/View/QuizView.swift @@ -0,0 +1,309 @@ +// +// QuizView.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 12/07/26. +// + +import Combine +import DynaUI +import Foundation +import SwiftUI + +// WIP +enum QuizPalette { + static let purple = Color( + red: 0.42, + green: 0.20, + blue: 1 + ) + + static let pink = Color( + red: 1, + green: 0.23, + blue: 0.52 + ) +} + +@MainActor +protocol QuizDisplaying: AnyObject { + func displayLoading() + func display(question: QuizQuestion) + func display(score: Int) + func display(remainingSeconds: Int) + func displayAnswering(optionID: QuizOption.ID) + func displayAnswerResult(isCorrect: Bool) + func display(errorMessage: String) + func displayAnswerError(message: String) +} + +@MainActor +final class QuizViewState: ObservableObject, QuizDisplaying { + + @Published private(set) var question: QuizQuestion? + @Published private(set) var selectedOptionID: QuizOption.ID? + @Published private(set) var score = 0 + @Published private(set) var remainingSeconds: Int + @Published private(set) var isLoading = false + @Published private(set) var isAnswering = false + @Published private(set) var answerResult: Bool? + @Published private(set) var errorMessage: String? + @Published private(set) var answerErrorMessage: String? + + init(remainingSeconds: Int = 120) { + self.remainingSeconds = remainingSeconds + } + + func displayLoading() { + isLoading = true + errorMessage = nil + question = nil + } + + func display(question: QuizQuestion) { + self.question = question + selectedOptionID = nil + answerResult = nil + answerErrorMessage = nil + isAnswering = false + isLoading = false + } + + func display(score: Int) { + self.score = score + } + + func display(remainingSeconds: Int) { + self.remainingSeconds = remainingSeconds + } + + func displayAnswering(optionID: QuizOption.ID) { + selectedOptionID = optionID + answerResult = nil + answerErrorMessage = nil + isAnswering = true + } + + func displayAnswerResult(isCorrect: Bool) { + answerResult = isCorrect + isAnswering = false + } + + func display(errorMessage: String) { + question = nil + isLoading = false + self.errorMessage = errorMessage + } + + func displayAnswerError(message: String) { + selectedOptionID = nil + answerResult = nil + isAnswering = false + answerErrorMessage = message + } + + func dismissAnswerError() { + answerErrorMessage = nil + } +} + +@MainActor +struct QuizView: View { + + @StateObject + private var state: QuizViewState + + private let interactor: any QuizInteracting + + let totalQuestions: Int + + init( + state: QuizViewState, + interactor: any QuizInteracting, + totalQuestions: Int + ) { + _state = StateObject( + wrappedValue: state + ) + + self.interactor = interactor + self.totalQuestions = totalQuestions + } + + var body: some View { + ZStack { + VStack(spacing: .zero) { + QuizFixedHeader( + score: state.score, + remainingSeconds: state.remainingSeconds, + onClose: { + interactor.close() + } + ) + + content + } + + if let answerErrorMessage = state.answerErrorMessage { + answerErrorOverlay(message: answerErrorMessage) + } + } + .preferredColorScheme(.light) + .task { + await interactor.loadQuestion() + } + } +} + +private extension QuizView { + + @ViewBuilder + var content: some View { + if let question = state.question { + questionContent(question) + } else if let errorMessage = state.errorMessage { + errorContent( + message: errorMessage + ) + } else { + loadingContent + } + } + + func questionContent(_ question: QuizQuestion) -> some View { + QuizContentView( + question: question, + totalQuestions: totalQuestions, + selectedOptionID: state.selectedOptionID, + answerResult: state.answerResult, + isAnswering: state.isAnswering, + onAnswer: handleAnswer + ) + } + + var loadingContent: some View { + ZStack { + Color.white + + ProgressView() + .controlSize(.large) + } + .frame( + maxWidth: .infinity, + maxHeight: .infinity + ) + } + + func errorContent(message: String) -> some View { + ZStack { + Color.white + + DynaPanel( + title: "Algo deu errado", + titleFont: .system(size: 18, weight: .bold), + titleColor: .white + ) { + VStack(spacing: 14) { + RaisedCard { + VStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 34, weight: .black)) + .foregroundStyle(QuizPalette.pink) + + Text("Não foi possível carregar a pergunta") + .font(.system(size: 17, weight: .black, design: .rounded)) + .foregroundStyle(.black) + .multilineTextAlignment(.center) + + Text(message) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.black.opacity(0.58)) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .padding(.horizontal, 12) + } + .padding(.bottom, 4) + + DynaOptionButton("Tentar novamente") { + Task { + await interactor.retry() + } + } + } + .padding(.horizontal, 4) + } + .padding(.horizontal, 18) + .padding(.bottom, 8) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + + func answerErrorOverlay(message: String) -> some View { + ZStack { + Color.black + .opacity(0.34) + .ignoresSafeArea() + + DynaPanel( + title: "Ops!", + titleFont: .system(size: 18, weight: .black, design: .rounded), + titleColor: .white + ) { + VStack(spacing: 14) { + RaisedCard { + VStack(spacing: 12) { + Image(systemName: "wifi.exclamationmark") + .font(.system(size: 36, weight: .black)) + .foregroundStyle(QuizPalette.pink) + + Text("Não foi possível enviar sua resposta") + .font(.system(size: 17, weight: .black, design: .rounded)) + .foregroundStyle(.black) + .multilineTextAlignment(.center) + + Text(message) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.black.opacity(0.58)) + .multilineTextAlignment(.center) + + Text("O tempo está pausado. Escolha uma alternativa para tentar novamente.") + .font(.system(size: 13, weight: .bold)) + .foregroundStyle(QuizPalette.purple) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + .padding(.horizontal, 12) + } + + DynaOptionButton("Escolher novamente") { + withAnimation(.easeOut(duration: 0.18)) { + state.dismissAnswerError() + } + } + } + .padding(.horizontal, 4) + } + .padding(.horizontal, 18) + .padding(.bottom, 8) + } + .transition(.opacity) + } + + func handleAnswer( _ option: QuizOption) { + Task { + await interactor.selectAnswer(option) + } + } +} + +#Preview { + QuizConfigurator.make( + questionNumber: 1, + totalQuestions: 10, + onClose: {}, + onFinish: { _ in } + ) +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/Domain/LoadRankingUseCase.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/Domain/LoadRankingUseCase.swift new file mode 100644 index 0000000000..dea9790f9e --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/Domain/LoadRankingUseCase.swift @@ -0,0 +1,24 @@ +// +// LoadRankingUseCase.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +protocol LoadRankingUseCaseProtocol { + func execute() async -> [PlayerScore] +} + +final class LoadRankingUseCase: LoadRankingUseCaseProtocol { + private let repository: PlayerRepository + + init(repository: PlayerRepository) { + self.repository = repository + } + + func execute() async -> [PlayerScore] { + await repository.ranking() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingConfigurator.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingConfigurator.swift new file mode 100644 index 0000000000..1d3638f9ca --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingConfigurator.swift @@ -0,0 +1,53 @@ +// +// RankingConfigurator.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import dDependencies + +enum RankingConfigurator { + struct Dependencies { + let playerRepository: any PlayerRepository + + init() { + @Dependency + var playerRepository: any PlayerRepository + + self.playerRepository = playerRepository + } + + init(playerRepository: any PlayerRepository) { + self.playerRepository = playerRepository + } + } + + @MainActor + static func make( + dependencies: Dependencies = .init(), + nickname: String, + onClose: @escaping () -> Void + ) -> RankingView { + let viewState = RankingViewState() + + let presenter = RankingPresenter(view: viewState) + let router = RankingRouter(onClose: onClose) + + let loadRanking = LoadRankingUseCase( + repository: dependencies.playerRepository + ) + + let interactor = RankingInteractor( + useCases: .init(loadRanking: loadRanking), + presenter: presenter, + router: router + ) + + return RankingView( + state: viewState, + interactor: interactor, + nickname: nickname + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingInteractor.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingInteractor.swift new file mode 100644 index 0000000000..7446db1873 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingInteractor.swift @@ -0,0 +1,55 @@ +// +// RankingInteractor.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +@MainActor +protocol RankingInteracting: AnyObject { + func load() async + func close() +} + +@MainActor +final class RankingInteractor: RankingInteracting { + struct UseCases { + let loadRanking: any LoadRankingUseCaseProtocol + } + + private let useCases: UseCases + private let presenter: any RankingPresenting + private let router: any RankingRouting + + private var isLoading = false + + init( + useCases: UseCases, + presenter: any RankingPresenting, + router: any RankingRouting + ) { + self.useCases = useCases + self.presenter = presenter + self.router = router + } + + func load() async { + guard !isLoading else { + return + } + + isLoading = true + presenter.presentLoading() + + let scores = await useCases.loadRanking.execute() + + presenter.present(scores: scores) + isLoading = false + } + + func close() { + router.close() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingPresenter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingPresenter.swift new file mode 100644 index 0000000000..8582281228 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingPresenter.swift @@ -0,0 +1,31 @@ +// +// RankingPresenter.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +@MainActor +protocol RankingPresenting: AnyObject { + func presentLoading() + func present(scores: [PlayerScore]) +} + +@MainActor +final class RankingPresenter: RankingPresenting { + private weak var view: RankingDisplaying? + + init(view: RankingDisplaying) { + self.view = view + } + + func presentLoading() { + view?.displayLoading() + } + + func present(scores: [PlayerScore]) { + view?.display(scores: scores) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingRouter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingRouter.swift new file mode 100644 index 0000000000..22a3bedc59 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/RankingRouter.swift @@ -0,0 +1,26 @@ +// +// RankingRouter.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +@MainActor +protocol RankingRouting: AnyObject { + func close() +} + +@MainActor +final class RankingRouter: RankingRouting { + private let onClose: () -> Void + + init(onClose: @escaping () -> Void) { + self.onClose = onClose + } + + func close() { + onClose() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/View/RankingView.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/View/RankingView.swift new file mode 100644 index 0000000000..bf5de4e282 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Ranking/View/RankingView.swift @@ -0,0 +1,201 @@ +// +// RankingView.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import DynaUI +import SwiftUI +import Combine + +@MainActor +protocol RankingDisplaying: AnyObject { + func displayLoading() + func display(scores: [PlayerScore]) +} + +@MainActor +final class RankingViewState: ObservableObject, RankingDisplaying { + @Published private(set) var scores: [PlayerScore] = [] + @Published private(set) var isLoading = false + + func displayLoading() { + isLoading = true + } + + func display(scores: [PlayerScore]) { + self.scores = scores + isLoading = false + } +} + +@MainActor +struct RankingView: View { + @StateObject private var state: RankingViewState + + private let interactor: any RankingInteracting + private let nickname: String + + init( + state: RankingViewState, + interactor: any RankingInteracting, + nickname: String + ) { + _state = StateObject(wrappedValue: state) + self.interactor = interactor + self.nickname = nickname + } + + var body: some View { + ZStack { + Color.white + .ignoresSafeArea() + + VStack(spacing: .zero) { + QuizFlowHeader( + title: "Ranking", + detail: "Jogador : \(nickname)", + onClose: interactor.close + ) + + ScrollView(showsIndicators: false) { + VStack(spacing: 32) { + rankingTitle + + rankingPanel + } + .padding(.horizontal, 18) + .padding(.top, 34) + .padding(.bottom, 32) + } + } + } + .task { + await interactor.load() + } + } +} + +private extension RankingView { + var rankingTitle: some View { + VStack(spacing: 8) { + Text("Ranking") + .font(.system(size: 28, weight: .black, design: .rounded)) + .foregroundStyle(.black) + + Text(positionText) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(.black.opacity(0.58)) + } + .frame(maxWidth: .infinity) + } + + var rankingPanel: some View { + DynaPanel( + title: "Melhores scores", + titleFont: .system(size: 18, weight: .bold), + titleColor: .white + ) { + VStack(spacing: 10) { + content + + DynaOptionButton( + "Voltar", + action: interactor.close + ) + .padding(.top, 6) + } + .padding(.horizontal, 4) + } + } + + @ViewBuilder + var content: some View { + if state.isLoading { + ProgressView() + .tint(.white) + .controlSize(.large) + .frame(maxWidth: .infinity) + .padding(.vertical, 36) + } else if state.scores.isEmpty { + RaisedCard { + Text("Nenhuma pontuação salva ainda.") + .font(.system(size: 14, weight: .bold)) + .foregroundStyle(.black) + .frame(maxWidth: .infinity) + .padding(.vertical, 18) + } + .padding(.bottom, 4) + } else { + ForEach( + Array(state.scores.enumerated()), + id: \.element.id + ) { index, score in + rankingRow( + score, + position: index + 1 + ) + .padding(.bottom, 4) + } + } + } + + func rankingRow( + _ score: PlayerScore, + position: Int + ) -> some View { + let isCurrentPlayer = score.nickname.compare( + nickname, + options: .caseInsensitive + ) == .orderedSame + + return RaisedCard { + HStack(spacing: 12) { + Text("\(position)") + .font(.system(size: 13, weight: .black, design: .rounded)) + .frame(width: 32, height: 32) + .background { + Circle() + .fill(isCurrentPlayer ? QuizPalette.pink : .black) + } + .foregroundStyle(.white) + + VStack(alignment: .leading, spacing: 3) { + Text(score.nickname) + .font(.system(size: 15, weight: .bold)) + .foregroundStyle(.black) + + if isCurrentPlayer { + Text("Você") + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(QuizPalette.purple) + } + } + + Spacer() + + Text("\(score.score) pts") + .font(.system(size: 14, weight: .black, design: .rounded)) + .foregroundStyle(.black) + } + .padding(.horizontal, 12) + .padding(.vertical, 10) + } + } + + var positionText: String { + guard let index = state.scores.firstIndex( + where: { + $0.nickname.compare( + nickname, + options: .caseInsensitive + ) == .orderedSame + } + ) else { + return "Jogador: \(nickname)" + } + + return "Você está na \(index + 1)ª posição" + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/Domain/SaveQuizResultUseCase.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/Domain/SaveQuizResultUseCase.swift new file mode 100644 index 0000000000..dd2dd4f8d6 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/Domain/SaveQuizResultUseCase.swift @@ -0,0 +1,33 @@ +// +// SaveQuizResultUseCase.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +protocol SaveQuizResultUseCaseProtocol { + func execute( + nickname: String, + score: Int + ) async throws +} + +final class SaveQuizResultUseCase: SaveQuizResultUseCaseProtocol { + private let repository: PlayerRepository + + init(repository: PlayerRepository) { + self.repository = repository + } + + func execute( + nickname: String, + score: Int + ) async throws { + try await repository.save( + score: score, + for: nickname + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultConfigurator.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultConfigurator.swift new file mode 100644 index 0000000000..231876a109 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultConfigurator.swift @@ -0,0 +1,66 @@ +// +// QuizResultConfigurator.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import dDependencies + +enum QuizResultConfigurator { + struct Dependencies { + let playerRepository: any PlayerRepository + + init() { + @Dependency + var playerRepository: any PlayerRepository + + self.playerRepository = playerRepository + } + + init(playerRepository: any PlayerRepository) { + self.playerRepository = playerRepository + } + } + + @MainActor + static func make( + dependencies: Dependencies = .init(), + nickname: String, + score: Int, + totalQuestions: Int, + onRestart: @escaping () -> Void, + onOpenRanking: @escaping () -> Void, + onClose: @escaping () -> Void + ) -> QuizResultView { + let viewState = QuizResultViewState() + + let presenter = QuizResultPresenter(view: viewState) + + let router = QuizResultRouter( + onRestart: onRestart, + onOpenRanking: onOpenRanking, + onClose: onClose + ) + + let saveResult = SaveQuizResultUseCase( + repository: dependencies.playerRepository + ) + + let interactor = QuizResultInteractor( + nickname: nickname, + score: score, + useCases: .init(saveResult: saveResult), + presenter: presenter, + router: router + ) + + return QuizResultView( + state: viewState, + interactor: interactor, + nickname: nickname, + score: score, + totalQuestions: totalQuestions + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultInteractor.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultInteractor.swift new file mode 100644 index 0000000000..edbc9c4f70 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultInteractor.swift @@ -0,0 +1,77 @@ +// +// QuizResultInteractor.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +@MainActor +protocol QuizResultInteracting: AnyObject { + func load() async + func restartQuiz() + func openRanking() + func close() +} + +@MainActor +final class QuizResultInteractor: QuizResultInteracting { + struct UseCases { + let saveResult: any SaveQuizResultUseCaseProtocol + } + + private let nickname: String + private let score: Int + private let useCases: UseCases + private let presenter: any QuizResultPresenting + private let router: any QuizResultRouting + + private var didSaveResult = false + + init( + nickname: String, + score: Int, + useCases: UseCases, + presenter: any QuizResultPresenting, + router: any QuizResultRouting + ) { + self.nickname = nickname + self.score = score + self.useCases = useCases + self.presenter = presenter + self.router = router + } + + func load() async { + guard !didSaveResult else { + return + } + + didSaveResult = true + presenter.presentSavingScore() + + do { + try await useCases.saveResult.execute( + nickname: nickname, + score: score + ) + + presenter.presentSavedScore() + } catch { + presenter.presentSaveError(error) + } + } + + func restartQuiz() { + router.restartQuiz() + } + + func openRanking() { + router.openRanking() + } + + func close() { + router.close() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultPresenter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultPresenter.swift new file mode 100644 index 0000000000..5c8cb55a93 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultPresenter.swift @@ -0,0 +1,38 @@ +// +// QuizResultPresenter.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +@MainActor +protocol QuizResultPresenting: AnyObject { + func presentSavingScore() + func presentSavedScore() + func presentSaveError(_ error: Error) +} + +@MainActor +final class QuizResultPresenter: QuizResultPresenting { + private weak var view: QuizResultDisplaying? + + init(view: QuizResultDisplaying) { + self.view = view + } + + func presentSavingScore() { + view?.displaySavingScore() + } + + func presentSavedScore() { + view?.displaySavedScore() + } + + func presentSaveError(_ error: Error) { + view?.displaySaveError( + message: error.localizedDescription + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultRouter.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultRouter.swift new file mode 100644 index 0000000000..1cb1c2b1d3 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/QuizResultRouter.swift @@ -0,0 +1,44 @@ +// +// QuizResultRouter.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation + +@MainActor +protocol QuizResultRouting: AnyObject { + func restartQuiz() + func openRanking() + func close() +} + +@MainActor +final class QuizResultRouter: QuizResultRouting { + private let onRestart: () -> Void + private let onOpenRanking: () -> Void + private let onClose: () -> Void + + init( + onRestart: @escaping () -> Void, + onOpenRanking: @escaping () -> Void, + onClose: @escaping () -> Void + ) { + self.onRestart = onRestart + self.onOpenRanking = onOpenRanking + self.onClose = onClose + } + + func restartQuiz() { + onRestart() + } + + func openRanking() { + onOpenRanking() + } + + func close() { + onClose() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/View/QuizResultView.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/View/QuizResultView.swift new file mode 100644 index 0000000000..47b056dcd5 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Result/View/QuizResultView.swift @@ -0,0 +1,200 @@ +// +// QuizResultView.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import DynaUI +import SwiftUI +import Combine + +@MainActor +protocol QuizResultDisplaying: AnyObject { + func displaySavingScore() + func displaySavedScore() + func displaySaveError(message: String) +} + +@MainActor +final class QuizResultViewState: ObservableObject, QuizResultDisplaying { + @Published private(set) var isSavingScore = false + @Published private(set) var saveErrorMessage: String? + + func displaySavingScore() { + isSavingScore = true + saveErrorMessage = nil + } + + func displaySavedScore() { + isSavingScore = false + saveErrorMessage = nil + } + + func displaySaveError(message: String) { + isSavingScore = false + saveErrorMessage = message + } +} + +@MainActor +struct QuizResultView: View { + @StateObject private var state: QuizResultViewState + + private let interactor: any QuizResultInteracting + private let nickname: String + private let score: Int + private let totalQuestions: Int + + init( + state: QuizResultViewState, + interactor: any QuizResultInteracting, + nickname: String, + score: Int, + totalQuestions: Int + ) { + _state = StateObject(wrappedValue: state) + self.interactor = interactor + self.nickname = nickname + self.score = score + self.totalQuestions = totalQuestions + } + + var body: some View { + ZStack { + Color.white + .ignoresSafeArea() + + VStack(spacing: .zero) { + QuizFlowHeader( + title: "Resultado", + detail: "Score : \(score)", + isCloseEnabled: !state.isSavingScore, + onClose: interactor.close + ) + + ScrollView(showsIndicators: false) { + VStack(spacing: 32) { + resultTitle + + resultPanel + } + .padding(.horizontal, 18) + .padding(.top, 34) + .padding(.bottom, 32) + } + } + } + .task { + await interactor.load() + } + } +} + +private extension QuizResultView { + var resultTitle: some View { + VStack(spacing: 8) { + Text("Quiz finalizado") + .font(.system(size: 28, weight: .black, design: .rounded)) + .foregroundStyle(.black) + + Text(nickname) + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(.black.opacity(0.58)) + } + .frame(maxWidth: .infinity) + } + + var resultPanel: some View { + DynaPanel( + title: "Sua pontuação", + titleFont: .system(size: 18, weight: .bold), + titleColor: .white + ) { + VStack(spacing: 14) { + scoreCard + + saveStatus + + DynaOptionButton( + "Jogar novamente", + isEnabled: !state.isSavingScore, + action: interactor.restartQuiz + ) + + DynaOptionButton( + "Ver ranking", + isEnabled: !state.isSavingScore, + action: interactor.openRanking + ) + + DynaOptionButton( + "Voltar ao início", + isEnabled: !state.isSavingScore, + action: interactor.close + ) + } + .padding(.horizontal, 4) + } + } + + var scoreCard: some View { + RaisedCard { + VStack(spacing: 4) { + Text("\(score) pts") + .font(.system(size: 58, weight: .black, design: .rounded)) + .foregroundStyle(.black) + + Text(resultMessage) + .font(.system(size: 14, weight: .bold)) + .foregroundStyle(.black.opacity(0.58)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 22) + } + .padding(.bottom, 4) + } + + @ViewBuilder + var saveStatus: some View { + if state.isSavingScore { + HStack(spacing: 8) { + ProgressView() + .tint(.white) + + Text("Salvando pontuação...") + .font(.system(size: 13, weight: .semibold)) + } + .foregroundStyle(.white) + .frame(maxWidth: .infinity) + } else if let saveErrorMessage = state.saveErrorMessage { + Text("Não foi possível salvar sua pontuação: \(saveErrorMessage)") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.white) + .multilineTextAlignment(.center) + .padding(.horizontal, 8) + } + } + + var resultMessage: String { + score > 0 ? "Boa tentativa!" : "Continue tentando" + } +} + +#Preview { + QuizResultView( + state: QuizResultViewState(), + interactor: QuizResultInteractorMock(), + nickname: "Kiyo", + score: 0, + totalQuestions: 0 + ) +} + + +class QuizResultInteractorMock: QuizResultInteracting { + func load() async {} + func restartQuiz() {} + func openRanking() {} + func close() {} +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Shared/View/QuizFlowHeader.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Shared/View/QuizFlowHeader.swift new file mode 100644 index 0000000000..896909199f --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Shared/View/QuizFlowHeader.swift @@ -0,0 +1,56 @@ +// +// QuizFlowHeader.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import SwiftUI + +struct QuizFlowHeader: View { + let title: String + let detail: String + let isCloseEnabled: Bool + let onClose: () -> Void + + init( + title: String, + detail: String, + isCloseEnabled: Bool = true, + onClose: @escaping () -> Void + ) { + self.title = title + self.detail = detail + self.isCloseEnabled = isCloseEnabled + self.onClose = onClose + } + + var body: some View { + HStack(spacing: 12) { + Button(action: onClose) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 17, weight: .bold)) + .foregroundColor(.black) + } + .buttonStyle(.plain) + .disabled(!isCloseEnabled) + .accessibilityLabel("Fechar") + + Text(title) + .font(.system(size: 13, weight: .bold)) + .foregroundColor(.black) + + Spacer() + + Text(detail) + .font(.system(size: 13, weight: .bold)) + .foregroundColor(.black) + .lineLimit(1) + .minimumScaleFactor(0.72) + } + .padding(.horizontal, 20) + .padding(.top, 8) + .padding(.bottom, 12) + .background(Color.white) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Shared/View/RaisedCard.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Shared/View/RaisedCard.swift new file mode 100644 index 0000000000..7ab64169d3 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/Shared/View/RaisedCard.swift @@ -0,0 +1,45 @@ +// +// RaisedCard.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import SwiftUI + +struct RaisedCard: View { + @ViewBuilder var content: () -> Content + + var body: some View { + content() + .background { + ZStack { + RoundedRectangle( + cornerRadius: 17, + style: .continuous + ) + .fill(Color.black) + .offset(y: 4) + + RoundedRectangle( + cornerRadius: 17, + style: .continuous + ) + .fill(Color.white) + + RoundedRectangle( + cornerRadius: 17, + style: .continuous + ) + .stroke(Color.black, lineWidth: 2) + } + } + } +} + +#Preview { + RaisedCard { + Text("Kiyo") + .padding() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/ios_quiz_challengeApp.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/ios_quiz_challengeApp.swift new file mode 100644 index 0000000000..30a16ce140 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challenge/ios_quiz_challengeApp.swift @@ -0,0 +1,23 @@ +// +// ios_quiz_challengeApp.swift +// ios-quiz-challenge +// +// Created by João Marcus Dionisio Araujo on 12/07/26. +// + +import SwiftUI +import SwiftData + +@main +struct ios_quiz_challengeApp: App { + + init() { + AppDependenciesConfigurator.configure() + } + + var body: some Scene { + WindowGroup { + AppFlowView() + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/LoadCurrentNicknameUseCaseTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/LoadCurrentNicknameUseCaseTests.swift new file mode 100644 index 0000000000..4624017eb1 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/LoadCurrentNicknameUseCaseTests.swift @@ -0,0 +1,22 @@ +// +// LoadCurrentNicknameUseCaseTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +struct LoadCurrentNicknameUseCaseTests { + + @Test func returnsPersistedNickname() async { + let repository = PlayerRepositorySpy() + await repository.setCurrentNickname("Kiyo") + let sut = await LoadCurrentNicknameUseCase(repository: repository) + + let nickname = await sut.execute() + + #expect(nickname == "Kiyo") + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/SaveCurrentNicknameUseCaseTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/SaveCurrentNicknameUseCaseTests.swift new file mode 100644 index 0000000000..279fa29d16 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/SaveCurrentNicknameUseCaseTests.swift @@ -0,0 +1,37 @@ +// +// SaveCurrentNicknameUseCaseTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +struct SaveCurrentNicknameUseCaseTests { + + @Test func trimsAndPersistsNickname() async throws { + let repository = PlayerRepositorySpy() + let sut = await SaveCurrentNicknameUseCase(repository: repository) + + let nickname = try await sut.execute(nickname: " kiyo ") + + #expect(nickname == "kiyo") + #expect(await repository.currentNickname() == "kiyo") + #expect(await repository.savedNicknames() == ["kiyo"]) + } + + @Test func throwsWhenNicknameIsEmptyAfterTrimming() async { + let repository = PlayerRepositorySpy() + let sut = await SaveCurrentNicknameUseCase(repository: repository) + + do { + _ = try await sut.execute(nickname: " ") + #expect(Bool(false)) + } catch NicknameValidationError.empty { + #expect(await repository.savedNicknames().isEmpty) + } catch { + #expect(Bool(false)) + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/UserDefaultsPlayerRepositoryTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/UserDefaultsPlayerRepositoryTests.swift new file mode 100644 index 0000000000..76f0756c4f --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/UserDefaultsPlayerRepositoryTests.swift @@ -0,0 +1,58 @@ +// +// UserDefaultsPlayerRepositoryTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation +import Testing +@testable import ios_quiz_challenge + +struct UserDefaultsPlayerRepositoryTests { + + @Test func storesAndLoadsCurrentNickname() async { + let userDefaults = makeUserDefaults() + let sut = await UserDefaultsPlayerRepository(userDefaults: userDefaults) + + await sut.setCurrentNickname("maria") + + #expect(await sut.currentNickname() == "maria") + } + + @Test func rankingIsSortedByScoreDescendingAndNicknameAscending() async throws { + let userDefaults = makeUserDefaults() + let sut = await UserDefaultsPlayerRepository(userDefaults: userDefaults) + + try await sut.save(score: 8, for: "Bruno") + try await sut.save(score: 10, for: "Ana") + try await sut.save(score: 10, for: "Carlos") + + let ranking = await sut.ranking() + + #expect(ranking.map(\.nickname) == ["Ana", "Carlos", "Bruno"]) + #expect(ranking.map(\.score) == [10, 10, 8]) + } + + @Test func keepsBestScoreForSameNicknameIgnoringCase() async throws { + let userDefaults = makeUserDefaults() + let sut = await UserDefaultsPlayerRepository(userDefaults: userDefaults) + + try await sut.save(score: 4, for: "Ana") + try await sut.save(score: 7, for: "ana") + try await sut.save(score: 5, for: "ANA") + + let ranking = await sut.ranking() + + #expect(ranking.count == 1) + #expect(ranking.first?.nickname == "ANA") + #expect(ranking.first?.score == 7) + } +} + +private func makeUserDefaults() -> UserDefaults { + let suiteName = "ios-quiz-challenge-tests-\(UUID().uuidString)" + let userDefaults = UserDefaults(suiteName: suiteName)! + userDefaults.removePersistentDomain(forName: suiteName) + return userDefaults +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/QuizEntrySnapshotTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/QuizEntrySnapshotTests.swift new file mode 100644 index 0000000000..f21c6f6d93 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/QuizEntrySnapshotTests.swift @@ -0,0 +1,47 @@ +// +// QuizEntrySnapshotTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +@testable import ios_quiz_challenge + +final class QuizEntrySnapshotTests: ViewSnapshotTestCase { + + @MainActor + func testEmptyEntryScreenSnapshot() throws { + try assertSnapshot( + matching: makeSUT(), + named: "entry-empty" + ) + } + + @MainActor + func testFilledEntryScreenSnapshot() throws { + try assertSnapshot( + matching: makeSUT(nickname: "kiyo", canContinue: true), + named: "entry-filled" + ) + } +} + +@MainActor +private extension QuizEntrySnapshotTests { + func makeSUT( + nickname: String = "", + canContinue: Bool = false + ) -> QuizEntryView { + let state = QuizEntryViewState() + let interactor = QuizEntryInteractorSpy() + state.display( + nickname: nickname, + canContinue: canContinue + ) + + return QuizEntryView( + state: state, + interactor: interactor + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/__Snapshots__/QuizEntrySnapshotTests/entry-empty-iPhone15.png b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/__Snapshots__/QuizEntrySnapshotTests/entry-empty-iPhone15.png new file mode 100644 index 0000000000..7b3dedec83 Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/__Snapshots__/QuizEntrySnapshotTests/entry-empty-iPhone15.png differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/__Snapshots__/QuizEntrySnapshotTests/entry-filled-iPhone15.png b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/__Snapshots__/QuizEntrySnapshotTests/entry-filled-iPhone15.png new file mode 100644 index 0000000000..f28093b8d3 Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Entry/View/__Snapshots__/QuizEntrySnapshotTests/entry-filled-iPhone15.png differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/AnswerQuizQuestionUseCaseTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/AnswerQuizQuestionUseCaseTests.swift new file mode 100644 index 0000000000..beb0e373ac --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/AnswerQuizQuestionUseCaseTests.swift @@ -0,0 +1,48 @@ +// +// AnswerQuizQuestionUseCaseTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +struct AnswerQuizQuestionUseCaseTests { + + @Test func returnsAnswerResultAndPassesPayloadToService() async throws { + let service = QuizServiceSpy() + service.answerResult = .success( + AnswerQuizQuestionResponseDTO(result: false) + ) + let sut = await AnswerQuizQuestionUseCase(service: service) + + let isCorrect = try await sut.execute( + questionID: "question-1", + answer: "Dynamox" + ) + + #expect(isCorrect == false) + #expect(service.answerCalls.count == 1) + #expect(service.answerCalls.first?.questionID == "question-1") + #expect(service.answerCalls.first?.answer == "Dynamox") + } + + @Test func propagatesServiceError() async { + let service = QuizServiceSpy() + service.answerResult = .failure(TestError.expected) + let sut = await AnswerQuizQuestionUseCase(service: service) + + do { + _ = try await sut.execute( + questionID: "question-1", + answer: "A" + ) + #expect(Bool(false)) + } catch TestError.expected { + #expect(service.answerCalls.count == 1) + } catch { + #expect(Bool(false)) + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/FetchQuizQuestionUseCaseTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/FetchQuizQuestionUseCaseTests.swift new file mode 100644 index 0000000000..b484b21aaa --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/FetchQuizQuestionUseCaseTests.swift @@ -0,0 +1,48 @@ +// +// FetchQuizQuestionUseCaseTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +struct FetchQuizQuestionUseCaseTests { + + @Test func mapsQuestionDTOToDomainModel() async throws { + let service = QuizServiceSpy() + service.questionResult = .success( + QuizQuestionDTO( + id: "42", + statement: "What is the answer?", + options: ["One", "Two"] + ) + ) + let sut = await FetchQuizQuestionUseCase(service: service) + + let question = try await sut.execute(questionNumber: 3) + + #expect(question.id == "42") + #expect(question.number == 3) + #expect(question.title == "What is the answer?") + #expect(question.imageName == nil) + #expect(question.options.map(\.title) == ["One", "Two"]) + #expect(service.fetchQuestionCallCount == 1) + } + + @Test func propagatesServiceError() async { + let service = QuizServiceSpy() + service.questionResult = .failure(TestError.expected) + let sut = await FetchQuizQuestionUseCase(service: service) + + do { + _ = try await sut.execute(questionNumber: 1) + #expect(Bool(false)) + } catch TestError.expected { + #expect(service.fetchQuestionCallCount == 1) + } catch { + #expect(Bool(false)) + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/QuizInteractorTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/QuizInteractorTests.swift new file mode 100644 index 0000000000..5de8e8c959 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/QuizInteractorTests.swift @@ -0,0 +1,329 @@ +// +// QuizInteractorTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +@MainActor +struct QuizInteractorTests { + + @Test func loadQuestionFetchesFirstQuestionAndPresentsLoadingState() async { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(true) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router + ) + + await sut.loadQuestion() + + #expect(fetchQuestion.receivedQuestionNumbers == [1]) + #expect( + presenter.events == [ + .loading, + .question(question), + .remainingSeconds(120) + ] + ) + } + + @Test func loadQuestionDoesNotAdvanceTimerWhileFetching() async { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + fetchQuestion.delayNanoseconds = 5_000_000 + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(true) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router, + quizDurationSeconds: 1, + timerTickNanoseconds: 1_000_000 + ) + + await sut.loadQuestion() + + #expect(!presenter.events.contains(.remainingSeconds(0))) + #expect(router.finishedScores.isEmpty) + } + + @Test func loadQuestionDoesNotFetchAgainWhenQuestionIsAlreadyLoaded() async { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(true) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router + ) + + await sut.loadQuestion() + await sut.loadQuestion() + + #expect(fetchQuestion.receivedQuestionNumbers == [1]) + } + + @Test func selectingCorrectAnswerUpdatesScoreAndFinishesQuiz() async { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(true) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router, + totalQuestions: 1 + ) + + await sut.loadQuestion() + await sut.selectAnswer(question.options[0]) + + #expect(answerQuestion.receivedAnswers.count == 1) + #expect(answerQuestion.receivedAnswers.first?.questionID == question.id) + #expect(answerQuestion.receivedAnswers.first?.answer == question.options[0].title) + #expect(presenter.events.contains(.answering(question.options[0].id))) + #expect(presenter.events.contains(.score(1))) + #expect(presenter.events.contains(.answerResult(true))) + #expect(router.finishedScores == [120]) + } + + @Test func answerFailurePresentsErrorAndAllowsRetry() async { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .failure(TestError.expected) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router + ) + + await sut.loadQuestion() + await sut.selectAnswer(question.options[0]) + answerQuestion.result = .success(false) + await sut.selectAnswer(question.options[1]) + let didPresentAnswerError = presenter.events.contains { event in + if case .answerError = event { + return true + } + + return false + } + + #expect(answerQuestion.receivedAnswers.count == 2) + #expect(answerQuestion.receivedAnswers[0].answer == question.options[0].title) + #expect(answerQuestion.receivedAnswers[1].answer == question.options[1].title) + #expect(didPresentAnswerError) + #expect(presenter.events.contains(.answerResult(false))) + } + + @Test func answerFailurePausesTimer() async throws { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .failure(TestError.expected) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router, + quizDurationSeconds: 1, + timerTickNanoseconds: 1_000_000 + ) + + await sut.loadQuestion() + await sut.selectAnswer(question.options[0]) + try await Task.sleep(nanoseconds: 5_000_000) + + #expect(!presenter.events.contains(.remainingSeconds(0))) + #expect(router.finishedScores.isEmpty) + } + + @Test func nextQuestionFailureAfterAnswerPresentsQuestionError() async { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(true) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router, + totalQuestions: 2 + ) + + await sut.loadQuestion() + fetchQuestion.result = .failure(TestError.expected) + await sut.selectAnswer(question.options[0]) + let didPresentQuestionError = presenter.events.contains { event in + if case .error = event { + return true + } + + return false + } + let didPresentAnswerError = presenter.events.contains { event in + if case .answerError = event { + return true + } + + return false + } + + #expect(fetchQuestion.receivedQuestionNumbers == [1, 2]) + #expect(didPresentQuestionError) + #expect(!didPresentAnswerError) + } + + @Test func nextQuestionLoadingAfterAnswerDoesNotAdvanceTimer() async { + let firstQuestion = makeQuestion(number: 1) + let nextQuestion = makeQuestion(number: 2) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(firstQuestion) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(false) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router, + totalQuestions: 2, + quizDurationSeconds: 1, + timerTickNanoseconds: 1_000_000 + ) + + await sut.loadQuestion() + fetchQuestion.result = .success(nextQuestion) + fetchQuestion.delayNanoseconds = 5_000_000 + await sut.selectAnswer(firstQuestion.options[0]) + + #expect(fetchQuestion.receivedQuestionNumbers == [1, 2]) + #expect(!presenter.events.contains(.remainingSeconds(0))) + #expect(router.finishedScores.isEmpty) + } + + @Test func quizFinishesWhenTimerReachesZero() async throws { + let question = makeQuestion(number: 1) + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(question) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(true) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router, + quizDurationSeconds: 1, + timerTickNanoseconds: 1_000_000 + ) + + await sut.loadQuestion() + await Task.yield() + try await Task.sleep(nanoseconds: 5_000_000) + + #expect(presenter.events.contains(.remainingSeconds(0))) + #expect(router.finishedScores == [0]) + } + + @Test func closeRoutesBack() { + let fetchQuestion = FetchQuizQuestionUseCaseSpy( + result: .success(makeQuestion()) + ) + let answerQuestion = AnswerQuizQuestionUseCaseSpy( + result: .success(true) + ) + let presenter = QuizPresenterSpy() + let router = QuizRouterSpy() + let sut = makeSUT( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion, + presenter: presenter, + router: router + ) + + sut.close() + + #expect(router.closeCallCount == 1) + } +} + +private extension QuizInteractorTests { + func makeSUT( + fetchQuestion: FetchQuizQuestionUseCaseSpy, + answerQuestion: AnswerQuizQuestionUseCaseSpy, + presenter: QuizPresenterSpy, + router: QuizRouterSpy, + totalQuestions: Int = 10, + quizDurationSeconds: Int = 120, + timerTickNanoseconds: UInt64 = 1_000_000_000 + ) -> QuizInteractor { + QuizInteractor( + useCases: .init( + fetchQuestion: fetchQuestion, + answerQuestion: answerQuestion + ), + presenter: presenter, + router: router, + totalQuestions: totalQuestions, + quizDurationSeconds: quizDurationSeconds, + timerTickNanoseconds: timerTickNanoseconds, + answerResultDelayNanoseconds: 0 + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/View/QuizOptionsPanelSnapshotTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/View/QuizOptionsPanelSnapshotTests.swift new file mode 100644 index 0000000000..a4a9cf54d8 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/View/QuizOptionsPanelSnapshotTests.swift @@ -0,0 +1,62 @@ +// +// QuizOptionsPanelSnapshotTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import SwiftUI +@testable import ios_quiz_challenge + +final class QuizOptionsPanelSnapshotTests: ViewSnapshotTestCase { + + @MainActor + func testAnsweredOptionsPanelSnapshot() throws { + let options = makeOptions() + + try assertSnapshot( + matching: makeSUT( + options: options, + selectedOptionID: options[1].id, + answerResult: true + ), + named: "answered-options-panel" + ) + } +} + +@MainActor +private extension QuizOptionsPanelSnapshotTests { + func makeSUT( + options: [QuizOption], + selectedOptionID: QuizOption.ID?, + answerResult: Bool? + ) -> some View { + let answerSpy = QuizAnswerSpy() + + return ZStack { + Color.white + + QuizOptionsPanel( + options: options, + selectedOptionID: selectedOptionID, + answerResult: answerResult, + isAnswering: false, + showPinkShadow: true, + showPurpleCard: true, + visibleOptionCount: options.count, + onAnswer: answerSpy.answer + ) + .padding(.horizontal, 18) + } + } + + func makeOptions() -> [QuizOption] { + [ + QuizOption(id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!, title: "Google"), + QuizOption(id: UUID(uuidString: "00000000-0000-0000-0000-000000000002")!, title: "Dynamox"), + QuizOption(id: UUID(uuidString: "00000000-0000-0000-0000-000000000003")!, title: "Spotify"), + QuizOption(id: UUID(uuidString: "00000000-0000-0000-0000-000000000004")!, title: "Amazon") + ] + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/View/__Snapshots__/QuizOptionsPanelSnapshotTests/answered-options-panel-iPhone15.png b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/View/__Snapshots__/QuizOptionsPanelSnapshotTests/answered-options-panel-iPhone15.png new file mode 100644 index 0000000000..c62739943d Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Quiz/View/__Snapshots__/QuizOptionsPanelSnapshotTests/answered-options-panel-iPhone15.png differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/LoadRankingUseCaseTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/LoadRankingUseCaseTests.swift new file mode 100644 index 0000000000..3bbcfa7fb8 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/LoadRankingUseCaseTests.swift @@ -0,0 +1,26 @@ +// +// LoadRankingUseCaseTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +struct LoadRankingUseCaseTests { + + @Test func returnsRepositoryRanking() async { + let repository = PlayerRepositorySpy() + let expectedScores = [ + PlayerScore(nickname: "Ana", score: 10), + PlayerScore(nickname: "Bia", score: 8) + ] + await repository.setScores(expectedScores) + let sut = await LoadRankingUseCase(repository: repository) + + let scores = await sut.execute() + + #expect(scores == expectedScores) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/RankingInteractorTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/RankingInteractorTests.swift new file mode 100644 index 0000000000..d9ecc8f9d5 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/RankingInteractorTests.swift @@ -0,0 +1,62 @@ +// +// RankingInteractorTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +@MainActor +struct RankingInteractorTests { + + @Test func loadPresentsLoadingAndScores() async { + let scores = [ + PlayerScore(nickname: "Ana", score: 10), + PlayerScore(nickname: "Bia", score: 8) + ] + let loadRanking = LoadRankingUseCaseSpy(scores: scores) + let presenter = RankingPresenterSpy() + let router = RankingRouterSpy() + let sut = makeSUT( + loadRanking: loadRanking, + presenter: presenter, + router: router + ) + + await sut.load() + + #expect(loadRanking.executeCallCount == 1) + #expect(presenter.events == [.loading, .scores(scores)]) + } + + @Test func closeRoutesBack() { + let loadRanking = LoadRankingUseCaseSpy(scores: []) + let presenter = RankingPresenterSpy() + let router = RankingRouterSpy() + let sut = makeSUT( + loadRanking: loadRanking, + presenter: presenter, + router: router + ) + + sut.close() + + #expect(router.closeCallCount == 1) + } +} + +private extension RankingInteractorTests { + func makeSUT( + loadRanking: LoadRankingUseCaseSpy, + presenter: RankingPresenterSpy, + router: RankingRouterSpy + ) -> RankingInteractor { + RankingInteractor( + useCases: .init(loadRanking: loadRanking), + presenter: presenter, + router: router + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/RankingSnapshotTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/RankingSnapshotTests.swift new file mode 100644 index 0000000000..d5ce361a29 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/RankingSnapshotTests.swift @@ -0,0 +1,47 @@ +// +// RankingSnapshotTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +@testable import ios_quiz_challenge + +final class RankingSnapshotTests: ViewSnapshotTestCase { + + @MainActor + func testEmptyRankingScreenSnapshot() throws { + try assertSnapshot( + matching: makeSUT(scores: []), + named: "ranking-empty" + ) + } + + @MainActor + func testRankingWithCurrentPlayerSnapshot() throws { + try assertSnapshot( + matching: makeSUT( + scores: [ + PlayerScore(nickname: "kiyo", score: 8), + PlayerScore(nickname: "ana", score: 6) + ] + ), + named: "ranking-current-player" + ) + } +} + +@MainActor +private extension RankingSnapshotTests { + func makeSUT(scores: [PlayerScore]) -> RankingView { + let state = RankingViewState() + let interactor = RankingInteractorSpy() + state.display(scores: scores) + + return RankingView( + state: state, + interactor: interactor, + nickname: "kiyo" + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/__Snapshots__/RankingSnapshotTests/ranking-current-player-iPhone15.png b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/__Snapshots__/RankingSnapshotTests/ranking-current-player-iPhone15.png new file mode 100644 index 0000000000..77d0c6e972 Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/__Snapshots__/RankingSnapshotTests/ranking-current-player-iPhone15.png differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/__Snapshots__/RankingSnapshotTests/ranking-empty-iPhone15.png b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/__Snapshots__/RankingSnapshotTests/ranking-empty-iPhone15.png new file mode 100644 index 0000000000..8220307eca Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Ranking/View/__Snapshots__/RankingSnapshotTests/ranking-empty-iPhone15.png differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/QuizResultInteractorTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/QuizResultInteractorTests.swift new file mode 100644 index 0000000000..20074d4fe1 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/QuizResultInteractorTests.swift @@ -0,0 +1,107 @@ +// +// QuizResultInteractorTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +@MainActor +struct QuizResultInteractorTests { + + @Test func loadSavesScoreAndPresentsSuccess() async { + let saveResult = SaveQuizResultUseCaseSpy() + let presenter = QuizResultPresenterSpy() + let router = QuizResultRouterSpy() + let sut = makeSUT( + saveResult: saveResult, + presenter: presenter, + router: router + ) + + await sut.load() + + #expect(saveResult.calls.count == 1) + #expect(saveResult.calls.first?.nickname == "kiyo") + #expect(saveResult.calls.first?.score == 8) + #expect(presenter.events == [.saving, .saved]) + } + + @Test func loadDoesNotSaveTwice() async { + let saveResult = SaveQuizResultUseCaseSpy() + let presenter = QuizResultPresenterSpy() + let router = QuizResultRouterSpy() + let sut = makeSUT( + saveResult: saveResult, + presenter: presenter, + router: router + ) + + await sut.load() + await sut.load() + + #expect(saveResult.calls.count == 1) + #expect(presenter.events == [.saving, .saved]) + } + + @Test func loadPresentsSaveError() async { + let saveResult = SaveQuizResultUseCaseSpy() + saveResult.error = TestError.expected + let presenter = QuizResultPresenterSpy() + let router = QuizResultRouterSpy() + let sut = makeSUT( + saveResult: saveResult, + presenter: presenter, + router: router + ) + + await sut.load() + + #expect(saveResult.calls.count == 1) + #expect(presenter.events.count == 2) + #expect(presenter.events.first == .saving) + + if case .some(.error) = presenter.events.last { + #expect(true) + } else { + #expect(Bool(false)) + } + } + + @Test func routeActionsAreDelegatedToRouter() { + let saveResult = SaveQuizResultUseCaseSpy() + let presenter = QuizResultPresenterSpy() + let router = QuizResultRouterSpy() + let sut = makeSUT( + saveResult: saveResult, + presenter: presenter, + router: router + ) + + sut.restartQuiz() + sut.openRanking() + sut.close() + + #expect(router.restartCallCount == 1) + #expect(router.openRankingCallCount == 1) + #expect(router.closeCallCount == 1) + } +} + +private extension QuizResultInteractorTests { + func makeSUT( + saveResult: SaveQuizResultUseCaseSpy, + presenter: QuizResultPresenterSpy, + router: QuizResultRouterSpy + ) -> QuizResultInteractor { + QuizResultInteractor( + nickname: "kiyo", + score: 8, + useCases: .init(saveResult: saveResult), + presenter: presenter, + router: router + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/SaveQuizResultUseCaseTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/SaveQuizResultUseCaseTests.swift new file mode 100644 index 0000000000..0c5e79d7b8 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/SaveQuizResultUseCaseTests.swift @@ -0,0 +1,45 @@ +// +// SaveQuizResultUseCaseTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Testing +@testable import ios_quiz_challenge + +struct SaveQuizResultUseCaseTests { + + @Test func savesScoreForNickname() async throws { + let repository = PlayerRepositorySpy() + let sut = await SaveQuizResultUseCase(repository: repository) + + try await sut.execute( + nickname: "kiyo", + score: 9 + ) + + let saves = await repository.savedScores() + #expect(saves.count == 1) + #expect(saves.first?.nickname == "kiyo") + #expect(saves.first?.score == 9) + } + + @Test func propagatesRepositoryError() async { + let repository = PlayerRepositorySpy() + await repository.setSaveError(TestError.expected) + let sut = await SaveQuizResultUseCase(repository: repository) + + do { + try await sut.execute( + nickname: "kiyo", + score: 9 + ) + #expect(Bool(false)) + } catch TestError.expected { + #expect(true) + } catch { + #expect(Bool(false)) + } + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/View/QuizResultSnapshotTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/View/QuizResultSnapshotTests.swift new file mode 100644 index 0000000000..b2b329dde2 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/View/QuizResultSnapshotTests.swift @@ -0,0 +1,36 @@ +// +// QuizResultSnapshotTests.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +@testable import ios_quiz_challenge + +final class QuizResultSnapshotTests: ViewSnapshotTestCase { + + @MainActor + func testResultScreenSnapshot() throws { + try assertSnapshot( + matching: makeSUT(), + named: "result-score-8" + ) + } +} + +@MainActor +private extension QuizResultSnapshotTests { + func makeSUT() -> QuizResultView { + let state = QuizResultViewState() + let interactor = QuizResultInteractorSpy() + state.displaySavedScore() + + return QuizResultView( + state: state, + interactor: interactor, + nickname: "kiyo", + score: 8, + totalQuestions: 10 + ) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/View/__Snapshots__/QuizResultSnapshotTests/result-score-8-iPhone15.png b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/View/__Snapshots__/QuizResultSnapshotTests/result-score-8-iPhone15.png new file mode 100644 index 0000000000..8c1524ae5a Binary files /dev/null and b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Result/View/__Snapshots__/QuizResultSnapshotTests/result-score-8-iPhone15.png differ diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/SnapshotDoubles.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/SnapshotDoubles.swift new file mode 100644 index 0000000000..26b087a89d --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/SnapshotDoubles.swift @@ -0,0 +1,78 @@ +// +// SnapshotDoubles.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +@testable import ios_quiz_challenge + +@MainActor +final class QuizEntryInteractorSpy: QuizEntryInteracting { + private(set) var loadCallCount = 0 + private(set) var updatedNicknames: [String] = [] + private(set) var startQuizCallCount = 0 + private(set) var openRankingCallCount = 0 + + func load() async { + loadCallCount += 1 + } + + func updateNickname(_ nickname: String) { + updatedNicknames.append(nickname) + } + + func startQuiz() async { + startQuizCallCount += 1 + } + + func openRanking() async { + openRankingCallCount += 1 + } +} + +@MainActor +final class QuizResultInteractorSpy: QuizResultInteracting { + private(set) var loadCallCount = 0 + private(set) var restartQuizCallCount = 0 + private(set) var openRankingCallCount = 0 + private(set) var closeCallCount = 0 + + func load() async { + loadCallCount += 1 + } + + func restartQuiz() { + restartQuizCallCount += 1 + } + + func openRanking() { + openRankingCallCount += 1 + } + + func close() { + closeCallCount += 1 + } +} + +@MainActor +final class RankingInteractorSpy: RankingInteracting { + private(set) var loadCallCount = 0 + private(set) var closeCallCount = 0 + + func load() async { + loadCallCount += 1 + } + + func close() { + closeCallCount += 1 + } +} + +final class QuizAnswerSpy { + private(set) var answeredOptions: [QuizOption] = [] + + func answer(_ option: QuizOption) { + answeredOptions.append(option) + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/TestDoubles.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/TestDoubles.swift new file mode 100644 index 0000000000..e1fd0e01d2 --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/TestDoubles.swift @@ -0,0 +1,314 @@ +// +// TestDoubles.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import Foundation +@testable import ios_quiz_challenge + +enum TestError: Error, Equatable { + case expected +} + +actor PlayerRepositorySpy: PlayerRepository { + var currentNicknameValue: String? + var scores: [PlayerScore] = [] + var saveError: Error? + + private var setCurrentNicknameCalls: [String] = [] + private var saveCalls: [(score: Int, nickname: String)] = [] + + func setCurrentNickname(_ nickname: String) async { + setCurrentNicknameCalls.append(nickname) + currentNicknameValue = nickname + } + + func currentNickname() async -> String? { + currentNicknameValue + } + + func save( + score: Int, + for nickname: String + ) async throws { + if let saveError { + throw saveError + } + + saveCalls.append( + (score: score, nickname: nickname) + ) + } + + func ranking() async -> [PlayerScore] { + scores + } + + func savedNicknames() -> [String] { + setCurrentNicknameCalls + } + + func savedScores() -> [(score: Int, nickname: String)] { + saveCalls + } + + func setScores(_ scores: [PlayerScore]) { + self.scores = scores + } + + func setSaveError(_ error: Error?) { + saveError = error + } +} + +final class QuizServiceSpy: QuizServiceProtocol { + var questionResult: Result = .success( + QuizQuestionDTO( + id: "question-id", + statement: "Question?", + options: ["A", "B", "C"] + ) + ) + var answerResult: Result = .success( + AnswerQuizQuestionResponseDTO(result: true) + ) + + private(set) var fetchQuestionCallCount = 0 + private(set) var answerCalls: [(questionID: String, answer: String)] = [] + + func fetchQuestion() async throws -> QuizQuestionDTO { + fetchQuestionCallCount += 1 + return try questionResult.get() + } + + func answerQuestion( + questionID: String, + answer: String + ) async throws -> AnswerQuizQuestionResponseDTO { + answerCalls.append( + (questionID: questionID, answer: answer) + ) + return try answerResult.get() + } +} + +final class FetchQuizQuestionUseCaseSpy: FetchQuizQuestionUseCaseProtocol { + var result: Result + var delayNanoseconds: UInt64 = 0 + private(set) var receivedQuestionNumbers: [Int] = [] + + init(result: Result) { + self.result = result + } + + func execute(questionNumber: Int) async throws -> QuizQuestion { + receivedQuestionNumbers.append(questionNumber) + + if delayNanoseconds > 0 { + try? await Task.sleep(nanoseconds: delayNanoseconds) + } + + return try result.get() + } +} + +final class AnswerQuizQuestionUseCaseSpy: AnswerQuizQuestionUseCaseProtocol { + var result: Result + private(set) var receivedAnswers: [(questionID: String, answer: String)] = [] + + init(result: Result) { + self.result = result + } + + func execute( + questionID: String, + answer: String + ) async throws -> Bool { + receivedAnswers.append( + (questionID: questionID, answer: answer) + ) + return try result.get() + } +} + +@MainActor +final class QuizPresenterSpy: QuizPresenting { + enum Event: Equatable { + case loading + case question(QuizQuestion) + case score(Int) + case remainingSeconds(Int) + case answering(QuizOption.ID) + case answerResult(Bool) + case error(String) + case answerError(String) + } + + private(set) var events: [Event] = [] + + func presentLoading() { + events.append(.loading) + } + + func present(question: QuizQuestion) { + events.append(.question(question)) + } + + func present(score: Int) { + events.append(.score(score)) + } + + func present(remainingSeconds: Int) { + events.append(.remainingSeconds(remainingSeconds)) + } + + func presentAnswering(optionID: QuizOption.ID) { + events.append(.answering(optionID)) + } + + func presentAnswerResult(isCorrect: Bool) { + events.append(.answerResult(isCorrect)) + } + + func present(error: Error) { + events.append(.error(error.localizedDescription)) + } + + func presentAnswerError(_ error: Error) { + events.append(.answerError(error.localizedDescription)) + } +} + +@MainActor +final class QuizRouterSpy: QuizRouting { + private(set) var closeCallCount = 0 + private(set) var finishedScores: [Int] = [] + + func close() { + closeCallCount += 1 + } + + func finishQuiz(score: Int) { + finishedScores.append(score) + } +} + +final class SaveQuizResultUseCaseSpy: SaveQuizResultUseCaseProtocol { + var error: Error? + private(set) var calls: [(nickname: String, score: Int)] = [] + + func execute( + nickname: String, + score: Int + ) async throws { + calls.append( + (nickname: nickname, score: score) + ) + + if let error { + throw error + } + } +} + +@MainActor +final class QuizResultPresenterSpy: QuizResultPresenting { + enum Event: Equatable { + case saving + case saved + case error(String) + } + + private(set) var events: [Event] = [] + + func presentSavingScore() { + events.append(.saving) + } + + func presentSavedScore() { + events.append(.saved) + } + + func presentSaveError(_ error: Error) { + events.append(.error(error.localizedDescription)) + } +} + +@MainActor +final class QuizResultRouterSpy: QuizResultRouting { + private(set) var restartCallCount = 0 + private(set) var openRankingCallCount = 0 + private(set) var closeCallCount = 0 + + func restartQuiz() { + restartCallCount += 1 + } + + func openRanking() { + openRankingCallCount += 1 + } + + func close() { + closeCallCount += 1 + } +} + +final class LoadRankingUseCaseSpy: LoadRankingUseCaseProtocol { + var scores: [PlayerScore] + private(set) var executeCallCount = 0 + + init(scores: [PlayerScore]) { + self.scores = scores + } + + func execute() async -> [PlayerScore] { + executeCallCount += 1 + return scores + } +} + +@MainActor +final class RankingPresenterSpy: RankingPresenting { + enum Event: Equatable { + case loading + case scores([PlayerScore]) + } + + private(set) var events: [Event] = [] + + func presentLoading() { + events.append(.loading) + } + + func present(scores: [PlayerScore]) { + events.append(.scores(scores)) + } +} + +@MainActor +final class RankingRouterSpy: RankingRouting { + private(set) var closeCallCount = 0 + + func close() { + closeCallCount += 1 + } +} + +func makeQuestion( + id: String = "question-id", + number: Int = 1, + options: [QuizOption] = [ + QuizOption(id: UUID(uuidString: "00000000-0000-0000-0000-000000000001")!, title: "A"), + QuizOption(id: UUID(uuidString: "00000000-0000-0000-0000-000000000002")!, title: "B") + ] +) -> QuizQuestion { + QuizQuestion( + id: id, + number: number, + title: "Question?", + imageName: nil, + options: options + ) +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/ViewSnapshotTesting.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/ViewSnapshotTesting.swift new file mode 100644 index 0000000000..8b814448ea --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeTests/Support/ViewSnapshotTesting.swift @@ -0,0 +1,362 @@ +// +// ViewSnapshotTesting.swift +// ios-quiz-challengeTests +// +// Created by João Marcus Dionisio Araujo on 16/07/26. +// + +import SwiftUI +import UIKit +import XCTest + +struct ViewSnapshotConfiguration { + let name: String + let size: CGSize + + static let iPhone15 = ViewSnapshotConfiguration( + name: "iPhone15", + size: CGSize(width: 393, height: 852) + ) +} + +class ViewSnapshotTestCase: XCTestCase { + + override func setUpWithError() throws { + try super.setUpWithError() + continueAfterFailure = false + } + + @MainActor + func assertSnapshot( + matching view: Content, + named name: String, + record: Bool = false, + configuration: ViewSnapshotConfiguration = .iPhone15, + filePath: StaticString = #filePath, + line: UInt = #line + ) throws { + let data = renderPNGData( + from: view, + size: configuration.size + ) + let snapshotURL = snapshotFileURL( + named: name, + configuration: configuration, + filePath: filePath + ) + + if record { + try write(data, to: snapshotURL) + attachImage( + data, + named: "Recorded \(name)" + ) + XCTFail( + "Recorded snapshot at \(snapshotURL.path). Disable record mode before committing or running CI.", + file: filePath, + line: line + ) + return + } + + guard FileManager.default.fileExists( + atPath: snapshotURL.path + ) else { + attachImage( + data, + named: "Missing \(name)" + ) + XCTFail( + "Missing snapshot at \(snapshotURL.path). Set record: true to record it.", + file: filePath, + line: line + ) + return + } + + let referenceData = try Data(contentsOf: snapshotURL) + + guard data == referenceData else { + let diffData = makeDiffPNGData( + referenceData: referenceData, + receivedData: data + ) + + if let comparisonData = makeComparisonPNGData( + referenceData: referenceData, + receivedData: data, + diffData: diffData + ) { + attachImage( + comparisonData, + named: "Snapshot Failure \(name)" + ) + } else { + attachImage( + data, + named: "Snapshot Failure \(name)" + ) + } + + XCTFail( + "Snapshot mismatch for \(snapshotURL.lastPathComponent). See Snapshot Failure attachment.", + file: filePath, + line: line + ) + return + } + } +} + +private extension ViewSnapshotTestCase { + + @MainActor + func renderPNGData( + from view: Content, + size: CGSize + ) -> Data { + let hostingController = UIHostingController( + rootView: view + .environment(\.colorScheme, .light) + ) + let window = UIWindow( + frame: CGRect(origin: .zero, size: size) + ) + window.rootViewController = hostingController + window.makeKeyAndVisible() + + hostingController.view.frame = window.bounds + hostingController.view.backgroundColor = .clear + hostingController.view.setNeedsLayout() + hostingController.view.layoutIfNeeded() + + let image = UIGraphicsImageRenderer( + size: size + ).image { _ in + hostingController.view.drawHierarchy( + in: hostingController.view.bounds, + afterScreenUpdates: true + ) + } + + return image.pngData() ?? Data() + } + + func snapshotFileURL( + named name: String, + configuration: ViewSnapshotConfiguration, + filePath: StaticString + ) -> URL { + let testFileURL = URL( + fileURLWithPath: String(describing: filePath) + ) + let testFileName = testFileURL + .deletingPathExtension() + .lastPathComponent + let snapshotDirectory = testFileURL + .deletingLastPathComponent() + .appendingPathComponent("__Snapshots__") + .appendingPathComponent(testFileName) + + return snapshotDirectory + .appendingPathComponent("\(sanitized(name))-\(configuration.name).png") + } + + func sanitized(_ name: String) -> String { + name + .replacingOccurrences(of: "(", with: "") + .replacingOccurrences(of: ")", with: "") + .replacingOccurrences(of: " ", with: "-") + .replacingOccurrences(of: "/", with: "-") + } + + func write( + _ data: Data, + to url: URL + ) throws { + try FileManager.default.createDirectory( + at: url.deletingLastPathComponent(), + withIntermediateDirectories: true + ) + + try data.write(to: url) + } + + func attachImage( + _ data: Data, + named name: String + ) { + guard let image = UIImage(data: data) else { + return + } + + let attachment = XCTAttachment(image: image) + attachment.name = name + attachment.lifetime = .keepAlways + add(attachment) + } + + func makeComparisonPNGData( + referenceData: Data, + receivedData: Data, + diffData: Data? + ) -> Data? { + guard + let referenceImage = UIImage(data: referenceData), + let receivedImage = UIImage(data: receivedData) + else { + return nil + } + + let diffImage = diffData.flatMap(UIImage.init(data:)) + let images = [referenceImage, receivedImage] + [diffImage].compactMap { $0 } + let labelHeight: CGFloat = 36 + let spacing: CGFloat = 8 + let maxHeight = images.map(\.size.height).max() ?? 0 + let totalWidth = images.map(\.size.width).reduce(0, +) + + spacing * CGFloat(images.count - 1) + let size = CGSize( + width: totalWidth, + height: maxHeight + labelHeight + ) + + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { context in + UIColor.white.setFill() + context.cgContext.fill( + CGRect(origin: .zero, size: size) + ) + + var x: CGFloat = 0 + let labels = diffImage == nil + ? ["Reference", "Received"] + : ["Reference", "Received", "Diff"] + + for (index, image) in images.enumerated() { + labels[index].draw( + at: CGPoint(x: x + 8, y: 8), + withAttributes: [ + .font: UIFont.boldSystemFont(ofSize: 18), + .foregroundColor: UIColor.black + ] + ) + + image.draw(at: CGPoint(x: x, y: labelHeight)) + x += image.size.width + spacing + } + } + + return image.pngData() + } + + func makeDiffPNGData( + referenceData: Data, + receivedData: Data + ) -> Data? { + guard + let referenceImage = UIImage(data: referenceData)?.cgImage, + let receivedImage = UIImage(data: receivedData)?.cgImage + else { + return nil + } + + let width = max(referenceImage.width, receivedImage.width) + let height = max(referenceImage.height, receivedImage.height) + + guard + let referencePixels = pixels(from: referenceImage, width: width, height: height), + let receivedPixels = pixels(from: receivedImage, width: width, height: height) + else { + return nil + } + + var diffPixels = [UInt8]( + repeating: 255, + count: width * height * 4 + ) + + for offset in stride(from: 0, to: diffPixels.count, by: 4) { + let matches = referencePixels[offset] == receivedPixels[offset] + && referencePixels[offset + 1] == receivedPixels[offset + 1] + && referencePixels[offset + 2] == receivedPixels[offset + 2] + && referencePixels[offset + 3] == receivedPixels[offset + 3] + + if matches { + diffPixels[offset] = 245 + diffPixels[offset + 1] = 245 + diffPixels[offset + 2] = 245 + diffPixels[offset + 3] = 255 + } else { + diffPixels[offset] = 255 + diffPixels[offset + 1] = 0 + diffPixels[offset + 2] = 255 + diffPixels[offset + 3] = 255 + } + } + + return pngData(from: diffPixels, width: width, height: height) + } + + func pixels( + from image: CGImage, + width: Int, + height: Int + ) -> [UInt8]? { + var pixels = [UInt8](repeating: 0, count: width * height * 4) + let colorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue + + let didDraw = pixels.withUnsafeMutableBytes { buffer in + guard let context = CGContext( + data: buffer.baseAddress, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: bitmapInfo + ) else { + return false + } + + context.draw( + image, + in: CGRect(x: 0, y: 0, width: image.width, height: image.height) + ) + return true + } + + return didDraw ? pixels : nil + } + + func pngData( + from pixels: [UInt8], + width: Int, + height: Int + ) -> Data? { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue + let data = Data(pixels) + + guard + let provider = CGDataProvider(data: data as CFData), + let image = CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo), + provider: provider, + decode: nil, + shouldInterpolate: false, + intent: .defaultIntent + ) + else { + return nil + } + + return UIImage(cgImage: image).pngData() + } +} diff --git a/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeUITests/ios_quiz_challengeUITestsLaunchTests.swift b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeUITests/ios_quiz_challengeUITestsLaunchTests.swift new file mode 100644 index 0000000000..5a81efe39f --- /dev/null +++ b/ios-quiz-challenge/ios-quiz-challenge/ios-quiz-challengeUITests/ios_quiz_challengeUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// ios_quiz_challengeUITestsLaunchTests.swift +// ios-quiz-challengeUITests +// +// Created by João Marcus Dionisio Araujo on 12/07/26. +// + +import XCTest + +final class ios_quiz_challengeUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + false + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + 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) + } +}