diff --git a/.fastlane/.env b/.fastlane/.env deleted file mode 100644 index 53a6133..0000000 --- a/.fastlane/.env +++ /dev/null @@ -1,2 +0,0 @@ -FASTLANE_HIDE_TIMESTAMP=true -FASTLANE_HIDE_CHANGELOG=true diff --git a/.fastlane/Fastfile b/.fastlane/Fastfile deleted file mode 100644 index 3f1c543..0000000 --- a/.fastlane/Fastfile +++ /dev/null @@ -1,10 +0,0 @@ -skip_docs - -lane :test do |options| - record_mode = options[:record].nil? ? 'false' : 'true' - scan( - scheme: 'HackerNews', - devices: ['iPhone 7', 'iPhone 11 Pro Max'], - xcargs: "RECORD_MODE='#{record_mode}'" - ) -end diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml new file mode 100644 index 0000000..0d044e6 --- /dev/null +++ b/.github/workflows/ios.yml @@ -0,0 +1,55 @@ +name: "HackerNews" + +on: + push: + branches: + - dev + - main + pull_request: + paths: + - '.swiftlint.yml' + - ".github/workflows/**" + - "HackerNews/**" + - "HackerNewsTests/**" + - "Modules/**" + +jobs: + iOS: + name: + runs-on: macos-14 + strategy: + fail-fast: false + matrix: + include: + - destination: "OS=17.0,name=iPhone 14 Pro" + name: "iOS" + scheme: "Debug" + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Dependencies + run: make setup_build_tools + - name: Generate resources + run: make swiftgen + - name: Generate project + run: xcodegen generate + - name: Select Xcode version + run: sudo xcode-select -s '/Applications/Xcode_15.4.app/Contents/Developer' + - name: Run tests + run: | + bundle add fastlane + bundle exec fastlane test + + discover-typos: + name: Discover Typos + runs-on: macOS-12 + env: + DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer + steps: + - uses: actions/checkout@v4 + - name: Discover typos + run: | + export PATH="$PATH:/Library/Frameworks/Python.framework/Versions/3.11/bin" + python3 -m pip install --upgrade pip + python3 -m pip install codespell + codespell --ignore-words-list="hart,inout,msdos,sur" --skip="./.build/*,./.git/*,./fastlane/*" diff --git a/.gitignore b/.gitignore index 46cecc8..b1721a5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,17 @@ # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore -## Build generated +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ - -## Various settings +*.moved-aside *.pbxuser !default.pbxuser *.mode1v3 @@ -15,16 +21,11 @@ DerivedData/ !default.mode2v3 *.perspectivev3 !default.perspectivev3 -xcuserdata/ -!project.pbxproj - -## Other -*.moved-aside -*.xccheckout -*.xcscmblueprint ## Obj-C/Swift specific *.hmap + +## App packaging *.ipa *.dSYM.zip *.dSYM @@ -38,6 +39,13 @@ playground.xcworkspace # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + .build/ # CocoaPods @@ -46,31 +54,52 @@ playground.xcworkspace # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # -Pods/ -Podfile.lock -.Podfile.swp +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts -Carthage/Build +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ # fastlane # -# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the -# screenshots whenever they are needed. +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html -fastlane/screenshots +fastlane/screenshots/**/*.png fastlane/test_output -# Other -.DS_Store -fabric.apikey -fabric.buildsecret -GoogleService-Info.plist +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +# Modules + +Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/Classes/Strings.swift +Modules/Common/DesignKit/Sources/DesignKit/Classes/Design/Generated/Colors.swift +Modules/Common/DesignKit/Sources/DesignKit/Classes/Design/Generated/Fonts.swift +Modules/Features/Settings/Sources/Settings/Classes/Generated/Assets.swift + +# Others + +.cache +*.xcodeproj +*.env.default +*.p8 +fastlane/api-key.json diff --git a/.sourcery.yml b/.sourcery.yml deleted file mode 100644 index 649d4bc..0000000 --- a/.sourcery.yml +++ /dev/null @@ -1,5 +0,0 @@ -sources: - - Features/NetworkManager/NetworkManager/Models -templates: - - Sourcery/Templates -output: Features/NetworkManager/NetworkManager/Models diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..895918f --- /dev/null +++ b/.swiftformat @@ -0,0 +1,64 @@ +# Stream rules + +--swiftversion 5.7 + +# Use 'swiftformat --options' to list all of the possible options + +--header "\nHackerNews\nCopyright © {created.year} Nikita Vasilev. All rights reserved.\n//" + +--enable blankLinesBetweenScopes +--enable blankLinesAtStartOfScope +--enable blankLinesAtEndOfScope +--enable blankLinesAroundMark +--enable anyObjectProtocol +--enable consecutiveBlankLines +--enable consecutiveSpaces +--enable duplicateImports +--enable elseOnSameLine +--enable emptyBraces +--enable initCoderUnavailable +--enable leadingDelimiters +--enable numberFormatting +--enable preferKeyPath +--enable redundantBreak +--enable redundantFileprivate +--enable redundantGet +--enable redundantInit +--enable redundantLet +--enable redundantLetError +--enable redundantNilInit +--enable redundantObjc +--enable redundantParens +--enable redundantPattern +--enable redundantRawValues +--enable redundantReturn +--enable redundantSelf +--enable redundantVoidReturnType +--enable semicolons +--enable sortedImports +--enable sortedSwitchCases +--enable spaceAroundBraces +--enable spaceAroundBrackets +--enable spaceAroundComments +--enable spaceAroundGenerics +--enable spaceAroundOperators +--enable spaceInsideBraces +--enable spaceInsideBrackets +--enable spaceInsideComments +--enable spaceInsideGenerics +--enable spaceInsideParens +--enable strongOutlets +--enable strongifiedSelf +--enable todos +--enable trailingClosures +--enable unusedArguments +--enable void +--enable markTypes +--enable isEmpty +--enable redundantExtensionACL + +# format options + +--wraparguments before-first +--wrapcollections before-first +--maxwidth 140 \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml index 88c55e7..e2b040c 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,44 +1,126 @@ -opt_in_rules: - - private_outlet - - force_unwrapping - - strong_iboutlet - - private_action +excluded: + - .build + - .swiftpm + - .cache + - "Modules/Common/HackerNewsLocalization" + +# Rules + +disabled_rules: + - trailing_comma + - todo + - opening_brace + - identifier_name + - large_tuple + - multiple_closures_with_trailing_closure + - attributes + - blanket_disable_command +opt_in_rules: # some rules are only opt-in + - anyobject_protocol + - array_init + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type - discouraged_object_literal + - empty_collection_literal - empty_count - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - explicit_init + - fallthrough + - fatal_error_message + - file_name + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - ibinspectable_in_extension - identical_operands - - - unneeded_parentheses_in_closure_argument - - let_var_whitespace - - yoda_condition - - closure_spacing - - collection_alignment + - implicit_return + - inert_defer + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - multiline_arguments + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - multiline_parameters_brackets + - no_space_in_method_call - operator_usage_whitespace - - closure_end_indentation - - file_name_no_space + - optional_enum_case_matching + - orphaned_doc_comment + - overridden_super_call + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - prefixed_toplevel_constant + - private_action + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_objc_attribute + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strict_fileprivate + - switch_case_on_newline + - toggle_bool + - unavailable_function + - unneeded_parentheses_in_closure_argument - unowned_variable_capture - - contains_over_filter_count - - contains_over_filter_is_empty - - contains_over_range_nil_comparison - - empty_collection_literal -disabled_rules: - - trailing_whitespace - - type_name + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition + +force_cast: warning +force_try: warning + +analyzer_rules: + - unused_import + - unused_declaration + +line_length: + warning: 130 + error: 200 + +type_body_length: + warning: 300 + error: 400 + +file_length: + warning: 500 + error: 1200 + +function_body_length: + warning: 30 + error: 50 + +nesting: + type_level: + warning: 2 + statement_level: + warning: 10 -# Rule Config -line_length: 150 -identifier_name: - min_length: 1 - max_length: - warning: 40 - error: 60 type_name: - min_length: 3 max_length: - warning: 80 - error: 100 - -excluded: - - Pods - - R.generated.swift + warning: 50 + error: 60 diff --git a/Features/HNService/HNService/HNService.xcodeproj/project.pbxproj b/Features/HNService/HNService/HNService.xcodeproj/project.pbxproj deleted file mode 100644 index 0c03187..0000000 --- a/Features/HNService/HNService/HNService.xcodeproj/project.pbxproj +++ /dev/null @@ -1,754 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 5A4D0B2C249FBA8D0076A781 /* NetworkManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A4D0B2B249FBA8D0076A781 /* NetworkManager.framework */; }; - 5A5177D124C4D3D5005856C4 /* StoryTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5177D024C4D3D5005856C4 /* StoryTypeFactory.swift */; }; - 5A5177D424C4DED3005856C4 /* StoryTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5177D024C4D3D5005856C4 /* StoryTypeFactory.swift */; }; - 5AEE9B88249A9FE900929A85 /* HNService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AEE9B7E249A9FE900929A85 /* HNService.framework */; }; - 5AEE9B8D249A9FE900929A85 /* HNServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B8C249A9FE900929A85 /* HNServiceTests.swift */; }; - 5AEE9B8F249A9FE900929A85 /* HNService.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AEE9B81249A9FE900929A85 /* HNService.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 5AEE9BB0249AA31000929A85 /* HNService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA5249AA06400929A85 /* HNService.swift */; }; - 5AEE9BB1249AA31200929A85 /* HNServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA6249AA06400929A85 /* HNServiceProtocol.swift */; }; - 5AEE9BB2249AA31400929A85 /* TopStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA3249AA06400929A85 /* TopStoriesResource.swift */; }; - 5AEE9BB3249AA31800929A85 /* AskStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9C249AA06400929A85 /* AskStoriesResource.swift */; }; - 5AEE9BB4249AA31800929A85 /* BestStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9D249AA06400929A85 /* BestStoriesResource.swift */; }; - 5AEE9BB5249AA31800929A85 /* CommentResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9E249AA06400929A85 /* CommentResource.swift */; }; - 5AEE9BB6249AA31800929A85 /* ItemResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9F249AA06400929A85 /* ItemResource.swift */; }; - 5AEE9BB7249AA31800929A85 /* NewsResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA0249AA06400929A85 /* NewsResource.swift */; }; - 5AEE9BB8249AA31800929A85 /* NewStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA1249AA06400929A85 /* NewStoriesResource.swift */; }; - 5AEE9BB9249AA31800929A85 /* ShowStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA2249AA06400929A85 /* ShowStoriesResource.swift */; }; - 5AEE9BBA249AA31A00929A85 /* CommentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B98249AA06400929A85 /* CommentModel.swift */; }; - 5AEE9BBB249AA31A00929A85 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B99249AA06400929A85 /* Item.swift */; }; - 5AEE9BBC249AA31A00929A85 /* PostModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9A249AA06400929A85 /* PostModel.swift */; }; - 5AEE9BBD249AA32700929A85 /* BaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BAA249AA06400929A85 /* BaseService.swift */; }; - 5AEE9BBE249AA32900929A85 /* BaseResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA8249AA06400929A85 /* BaseResource.swift */; }; - 5AEE9BC1249AA41700929A85 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BC0249AA41700929A85 /* Constants.swift */; }; - 5AF495AE249D5EE700E469A2 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BC0249AA41700929A85 /* Constants.swift */; }; - 5AF495AF249D5EE700E469A2 /* CommentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B98249AA06400929A85 /* CommentModel.swift */; }; - 5AF495B0249D5EE700E469A2 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B99249AA06400929A85 /* Item.swift */; }; - 5AF495B1249D5EE700E469A2 /* PostModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9A249AA06400929A85 /* PostModel.swift */; }; - 5AF495B2249D5EE700E469A2 /* AskStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9C249AA06400929A85 /* AskStoriesResource.swift */; }; - 5AF495B3249D5EE700E469A2 /* BestStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9D249AA06400929A85 /* BestStoriesResource.swift */; }; - 5AF495B4249D5EE700E469A2 /* CommentResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9E249AA06400929A85 /* CommentResource.swift */; }; - 5AF495B5249D5EE700E469A2 /* ItemResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9B9F249AA06400929A85 /* ItemResource.swift */; }; - 5AF495B6249D5EE700E469A2 /* NewsResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA0249AA06400929A85 /* NewsResource.swift */; }; - 5AF495B7249D5EE700E469A2 /* NewStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA1249AA06400929A85 /* NewStoriesResource.swift */; }; - 5AF495B8249D5EE700E469A2 /* ShowStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA2249AA06400929A85 /* ShowStoriesResource.swift */; }; - 5AF495B9249D5EE700E469A2 /* TopStoriesResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA3249AA06400929A85 /* TopStoriesResource.swift */; }; - 5AF495BA249D5EE700E469A2 /* HNService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA5249AA06400929A85 /* HNService.swift */; }; - 5AF495BB249D5EE700E469A2 /* HNServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA6249AA06400929A85 /* HNServiceProtocol.swift */; }; - 5AF495BC249D5EE700E469A2 /* BaseResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BA8249AA06400929A85 /* BaseResource.swift */; }; - 5AF495BD249D5EE700E469A2 /* BaseService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BAA249AA06400929A85 /* BaseService.swift */; }; - 5AF495BE249D5EEC00E469A2 /* HNService.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AEE9B81249A9FE900929A85 /* HNService.h */; settings = {ATTRIBUTES = (Public, ); }; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 5AEE9B89249A9FE900929A85 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 5AEE9B75249A9FE900929A85 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 5AEE9B7D249A9FE900929A85; - remoteInfo = HNService; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 5A4D0B2E249FBA8D0076A781 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 5A4D0B2B249FBA8D0076A781 /* NetworkManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = NetworkManager.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5A5177D024C4D3D5005856C4 /* StoryTypeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryTypeFactory.swift; sourceTree = ""; }; - 5AEE9B7E249A9FE900929A85 /* HNService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = HNService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5AEE9B81249A9FE900929A85 /* HNService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HNService.h; sourceTree = ""; }; - 5AEE9B82249A9FE900929A85 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5AEE9B87249A9FE900929A85 /* HNServiceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HNServiceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 5AEE9B8C249A9FE900929A85 /* HNServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HNServiceTests.swift; sourceTree = ""; }; - 5AEE9B8E249A9FE900929A85 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5AEE9B98249AA06400929A85 /* CommentModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommentModel.swift; sourceTree = ""; }; - 5AEE9B99249AA06400929A85 /* Item.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; - 5AEE9B9A249AA06400929A85 /* PostModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostModel.swift; sourceTree = ""; }; - 5AEE9B9C249AA06400929A85 /* AskStoriesResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AskStoriesResource.swift; sourceTree = ""; }; - 5AEE9B9D249AA06400929A85 /* BestStoriesResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BestStoriesResource.swift; sourceTree = ""; }; - 5AEE9B9E249AA06400929A85 /* CommentResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentResource.swift; sourceTree = ""; }; - 5AEE9B9F249AA06400929A85 /* ItemResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemResource.swift; sourceTree = ""; }; - 5AEE9BA0249AA06400929A85 /* NewsResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsResource.swift; sourceTree = ""; }; - 5AEE9BA1249AA06400929A85 /* NewStoriesResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStoriesResource.swift; sourceTree = ""; }; - 5AEE9BA2249AA06400929A85 /* ShowStoriesResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowStoriesResource.swift; sourceTree = ""; }; - 5AEE9BA3249AA06400929A85 /* TopStoriesResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopStoriesResource.swift; sourceTree = ""; }; - 5AEE9BA5249AA06400929A85 /* HNService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HNService.swift; sourceTree = ""; }; - 5AEE9BA6249AA06400929A85 /* HNServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HNServiceProtocol.swift; sourceTree = ""; }; - 5AEE9BA8249AA06400929A85 /* BaseResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseResource.swift; sourceTree = ""; }; - 5AEE9BAA249AA06400929A85 /* BaseService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseService.swift; sourceTree = ""; }; - 5AEE9BC0249AA41700929A85 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 5AF49579249D5D1400E469A2 /* HNService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = HNService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 5AEE9B7B249A9FE900929A85 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A4D0B2C249FBA8D0076A781 /* NetworkManager.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AEE9B84249A9FE900929A85 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AEE9B88249A9FE900929A85 /* HNService.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF49576249D5D1400E469A2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 5A4D0B2A249FBA8D0076A781 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5A4D0B2B249FBA8D0076A781 /* NetworkManager.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 5AEE9B74249A9FE900929A85 = { - isa = PBXGroup; - children = ( - 5AEE9B80249A9FE900929A85 /* HNService */, - 5AEE9B8B249A9FE900929A85 /* HNServiceTests */, - 5AEE9B7F249A9FE900929A85 /* Products */, - 5A4D0B2A249FBA8D0076A781 /* Frameworks */, - ); - sourceTree = ""; - }; - 5AEE9B7F249A9FE900929A85 /* Products */ = { - isa = PBXGroup; - children = ( - 5AEE9B7E249A9FE900929A85 /* HNService.framework */, - 5AEE9B87249A9FE900929A85 /* HNServiceTests.xctest */, - 5AF49579249D5D1400E469A2 /* HNService.framework */, - ); - name = Products; - sourceTree = ""; - }; - 5AEE9B80249A9FE900929A85 /* HNService */ = { - isa = PBXGroup; - children = ( - 5AEE9BBF249AA40B00929A85 /* Common */, - 5AEE9BA7249AA06400929A85 /* HNService */, - 5AEE9BAB249AA06400929A85 /* BaseService */, - 5AEE9B81249A9FE900929A85 /* HNService.h */, - 5AEE9B82249A9FE900929A85 /* Info.plist */, - ); - path = HNService; - sourceTree = ""; - }; - 5AEE9B8B249A9FE900929A85 /* HNServiceTests */ = { - isa = PBXGroup; - children = ( - 5AEE9B8C249A9FE900929A85 /* HNServiceTests.swift */, - 5AEE9B8E249A9FE900929A85 /* Info.plist */, - ); - path = HNServiceTests; - sourceTree = ""; - }; - 5AEE9B9B249AA06400929A85 /* Responses */ = { - isa = PBXGroup; - children = ( - 5AEE9B98249AA06400929A85 /* CommentModel.swift */, - 5AEE9B99249AA06400929A85 /* Item.swift */, - 5AEE9B9A249AA06400929A85 /* PostModel.swift */, - ); - path = Responses; - sourceTree = ""; - }; - 5AEE9BA4249AA06400929A85 /* Resources */ = { - isa = PBXGroup; - children = ( - 5AEE9B9C249AA06400929A85 /* AskStoriesResource.swift */, - 5AEE9B9D249AA06400929A85 /* BestStoriesResource.swift */, - 5AEE9B9E249AA06400929A85 /* CommentResource.swift */, - 5AEE9B9F249AA06400929A85 /* ItemResource.swift */, - 5AEE9BA0249AA06400929A85 /* NewsResource.swift */, - 5AEE9BA1249AA06400929A85 /* NewStoriesResource.swift */, - 5AEE9BA2249AA06400929A85 /* ShowStoriesResource.swift */, - 5AEE9BA3249AA06400929A85 /* TopStoriesResource.swift */, - ); - path = Resources; - sourceTree = ""; - }; - 5AEE9BA7249AA06400929A85 /* HNService */ = { - isa = PBXGroup; - children = ( - 5AEE9B9B249AA06400929A85 /* Responses */, - 5AEE9BA4249AA06400929A85 /* Resources */, - 5AEE9BA5249AA06400929A85 /* HNService.swift */, - 5AEE9BA6249AA06400929A85 /* HNServiceProtocol.swift */, - ); - path = HNService; - sourceTree = ""; - }; - 5AEE9BA9249AA06400929A85 /* Resources */ = { - isa = PBXGroup; - children = ( - 5AEE9BA8249AA06400929A85 /* BaseResource.swift */, - ); - path = Resources; - sourceTree = ""; - }; - 5AEE9BAB249AA06400929A85 /* BaseService */ = { - isa = PBXGroup; - children = ( - 5AEE9BA9249AA06400929A85 /* Resources */, - 5AEE9BAA249AA06400929A85 /* BaseService.swift */, - ); - path = BaseService; - sourceTree = ""; - }; - 5AEE9BBF249AA40B00929A85 /* Common */ = { - isa = PBXGroup; - children = ( - 5AEE9BC0249AA41700929A85 /* Constants.swift */, - 5A5177D024C4D3D5005856C4 /* StoryTypeFactory.swift */, - ); - path = Common; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 5AEE9B79249A9FE900929A85 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AEE9B8F249A9FE900929A85 /* HNService.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF49574249D5D1400E469A2 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF495BE249D5EEC00E469A2 /* HNService.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 5AEE9B7D249A9FE900929A85 /* HNService */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5AEE9B92249A9FE900929A85 /* Build configuration list for PBXNativeTarget "HNService" */; - buildPhases = ( - 5AEE9B79249A9FE900929A85 /* Headers */, - 5AEE9B7A249A9FE900929A85 /* Sources */, - 5AEE9B7B249A9FE900929A85 /* Frameworks */, - 5AEE9B7C249A9FE900929A85 /* Resources */, - 5A4D0B2E249FBA8D0076A781 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = HNService; - productName = HNService; - productReference = 5AEE9B7E249A9FE900929A85 /* HNService.framework */; - productType = "com.apple.product-type.framework"; - }; - 5AEE9B86249A9FE900929A85 /* HNServiceTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5AEE9B95249A9FE900929A85 /* Build configuration list for PBXNativeTarget "HNServiceTests" */; - buildPhases = ( - 5AEE9B83249A9FE900929A85 /* Sources */, - 5AEE9B84249A9FE900929A85 /* Frameworks */, - 5AEE9B85249A9FE900929A85 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 5AEE9B8A249A9FE900929A85 /* PBXTargetDependency */, - ); - name = HNServiceTests; - productName = HNServiceTests; - productReference = 5AEE9B87249A9FE900929A85 /* HNServiceTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 5AF49578249D5D1400E469A2 /* HNService watchOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5AF49580249D5D1400E469A2 /* Build configuration list for PBXNativeTarget "HNService watchOS" */; - buildPhases = ( - 5AF49574249D5D1400E469A2 /* Headers */, - 5AF49575249D5D1400E469A2 /* Sources */, - 5AF49576249D5D1400E469A2 /* Frameworks */, - 5AF49577249D5D1400E469A2 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "HNService watchOS"; - productName = "HNService watchOS"; - productReference = 5AF49579249D5D1400E469A2 /* HNService.framework */; - productType = "com.apple.product-type.framework"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 5AEE9B75249A9FE900929A85 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 1150; - ORGANIZATIONNAME = "Никита Васильев"; - TargetAttributes = { - 5AEE9B7D249A9FE900929A85 = { - CreatedOnToolsVersion = 11.5; - }; - 5AEE9B86249A9FE900929A85 = { - CreatedOnToolsVersion = 11.5; - }; - 5AF49578249D5D1400E469A2 = { - CreatedOnToolsVersion = 11.5; - }; - }; - }; - buildConfigurationList = 5AEE9B78249A9FE900929A85 /* Build configuration list for PBXProject "HNService" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 5AEE9B74249A9FE900929A85; - productRefGroup = 5AEE9B7F249A9FE900929A85 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 5AEE9B7D249A9FE900929A85 /* HNService */, - 5AEE9B86249A9FE900929A85 /* HNServiceTests */, - 5AF49578249D5D1400E469A2 /* HNService watchOS */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 5AEE9B7C249A9FE900929A85 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AEE9B85249A9FE900929A85 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF49577249D5D1400E469A2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 5AEE9B7A249A9FE900929A85 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AEE9BB0249AA31000929A85 /* HNService.swift in Sources */, - 5AEE9BB5249AA31800929A85 /* CommentResource.swift in Sources */, - 5AEE9BB9249AA31800929A85 /* ShowStoriesResource.swift in Sources */, - 5AEE9BB7249AA31800929A85 /* NewsResource.swift in Sources */, - 5AEE9BBD249AA32700929A85 /* BaseService.swift in Sources */, - 5AEE9BBB249AA31A00929A85 /* Item.swift in Sources */, - 5A5177D124C4D3D5005856C4 /* StoryTypeFactory.swift in Sources */, - 5AEE9BBC249AA31A00929A85 /* PostModel.swift in Sources */, - 5AEE9BC1249AA41700929A85 /* Constants.swift in Sources */, - 5AEE9BB1249AA31200929A85 /* HNServiceProtocol.swift in Sources */, - 5AEE9BBE249AA32900929A85 /* BaseResource.swift in Sources */, - 5AEE9BB2249AA31400929A85 /* TopStoriesResource.swift in Sources */, - 5AEE9BB6249AA31800929A85 /* ItemResource.swift in Sources */, - 5AEE9BBA249AA31A00929A85 /* CommentModel.swift in Sources */, - 5AEE9BB4249AA31800929A85 /* BestStoriesResource.swift in Sources */, - 5AEE9BB3249AA31800929A85 /* AskStoriesResource.swift in Sources */, - 5AEE9BB8249AA31800929A85 /* NewStoriesResource.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AEE9B83249A9FE900929A85 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AEE9B8D249A9FE900929A85 /* HNServiceTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF49575249D5D1400E469A2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF495AE249D5EE700E469A2 /* Constants.swift in Sources */, - 5AF495AF249D5EE700E469A2 /* CommentModel.swift in Sources */, - 5AF495B0249D5EE700E469A2 /* Item.swift in Sources */, - 5AF495B1249D5EE700E469A2 /* PostModel.swift in Sources */, - 5AF495B2249D5EE700E469A2 /* AskStoriesResource.swift in Sources */, - 5AF495B3249D5EE700E469A2 /* BestStoriesResource.swift in Sources */, - 5A5177D424C4DED3005856C4 /* StoryTypeFactory.swift in Sources */, - 5AF495B4249D5EE700E469A2 /* CommentResource.swift in Sources */, - 5AF495B5249D5EE700E469A2 /* ItemResource.swift in Sources */, - 5AF495B6249D5EE700E469A2 /* NewsResource.swift in Sources */, - 5AF495B7249D5EE700E469A2 /* NewStoriesResource.swift in Sources */, - 5AF495B8249D5EE700E469A2 /* ShowStoriesResource.swift in Sources */, - 5AF495B9249D5EE700E469A2 /* TopStoriesResource.swift in Sources */, - 5AF495BA249D5EE700E469A2 /* HNService.swift in Sources */, - 5AF495BB249D5EE700E469A2 /* HNServiceProtocol.swift in Sources */, - 5AF495BC249D5EE700E469A2 /* BaseResource.swift in Sources */, - 5AF495BD249D5EE700E469A2 /* BaseService.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 5AEE9B8A249A9FE900929A85 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 5AEE9B7D249A9FE900929A85 /* HNService */; - targetProxy = 5AEE9B89249A9FE900929A85 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 5AEE9B90249A9FE900929A85 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - 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_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; - CURRENT_PROJECT_VERSION = 1; - 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 = ( - "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 = 13.5; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - 5AEE9B91249A9FE900929A85 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - 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_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; - CURRENT_PROJECT_VERSION = 1; - 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_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 = 13.5; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - 5AEE9B93249A9FE900929A85 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = HNService/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HNService; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 5AEE9B94249A9FE900929A85 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = HNService/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HNService; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 5AEE9B96249A9FE900929A85 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HNServiceTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HNServiceTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 5AEE9B97249A9FE900929A85 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HNServiceTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HNServiceTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 5AF4957E249D5D1400E469A2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = HNService/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.nikitavasilev.HNService-watchOS"; - PRODUCT_NAME = HNService; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Debug; - }; - 5AF4957F249D5D1400E469A2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = HNService/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "com.nikitavasilev.HNService-watchOS"; - PRODUCT_NAME = HNService; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 5AEE9B78249A9FE900929A85 /* Build configuration list for PBXProject "HNService" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AEE9B90249A9FE900929A85 /* Debug */, - 5AEE9B91249A9FE900929A85 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 5AEE9B92249A9FE900929A85 /* Build configuration list for PBXNativeTarget "HNService" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AEE9B93249A9FE900929A85 /* Debug */, - 5AEE9B94249A9FE900929A85 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 5AEE9B95249A9FE900929A85 /* Build configuration list for PBXNativeTarget "HNServiceTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AEE9B96249A9FE900929A85 /* Debug */, - 5AEE9B97249A9FE900929A85 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 5AF49580249D5D1400E469A2 /* Build configuration list for PBXNativeTarget "HNService watchOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AF4957E249D5D1400E469A2 /* Debug */, - 5AF4957F249D5D1400E469A2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 5AEE9B75249A9FE900929A85 /* Project object */; -} diff --git a/Features/HNService/HNService/HNService.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Features/HNService/HNService/HNService.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index dd52653..0000000 --- a/Features/HNService/HNService/HNService.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Features/HNService/HNService/HNService.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Features/HNService/HNService/HNService.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/Features/HNService/HNService/HNService.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Features/HNService/HNService/HNService/BaseService/BaseService.swift b/Features/HNService/HNService/HNService/BaseService/BaseService.swift deleted file mode 100644 index d7376d3..0000000 --- a/Features/HNService/HNService/HNService/BaseService/BaseService.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// BaseService.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import NetworkManager - -public protocol NetworkService { - func load(resource: R, completion: @escaping (R.ModelType) -> Void, fail: @escaping (Error) -> Void) - func load(resources: [R], completion: @escaping ([R.ModelType]) -> Void, fail: @escaping (Error) -> Void) - func cancelAllTasks() -} - -public class BaseService: NetworkService { - let session: NetworkSession - let networkManager: NetworkManager - - public init(networkManager: NetworkManager = NetworkManager(), session: NetworkSession = URLSession.shared) { - self.session = session - self.networkManager = networkManager - } - - public func load(resource: R, completion: @escaping (R.ModelType) -> Void, fail: @escaping (Error) -> Void) { - networkManager.fetch(resource, completion: completion, fail: fail) - } - - public func load(resources: [R], completion: @escaping ([R.ModelType]) -> Void, fail: @escaping (Error) -> Void) { - let group = DispatchGroup() - var responces: [R.ModelType] = [] - - for resource in resources { - group.enter() - - load(resource: resource, completion: { model in - responces.append(model) - group.leave() - }, fail: { error in - fail(error) - }) - } - - group.notify(queue: .main) { - completion(responces) - } - } - - public func cancelAllTasks() { - networkManager.cancelAllTasks() - } -} diff --git a/Features/HNService/HNService/HNService/BaseService/Resources/BaseResource.swift b/Features/HNService/HNService/HNService/BaseService/Resources/BaseResource.swift deleted file mode 100644 index 00bb6fc..0000000 --- a/Features/HNService/HNService/HNService/BaseService/Resources/BaseResource.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// BaseResource.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import NetworkManager - -public class BaseResource: APIResource { - public typealias ModelType = Model - - public var baseURL: URL { - // swiftlint:disable force_unwrapping - return URL(string: Constants.Endpoints.base)! - // swiftlint:enable force_unwrapping - } - - public var path: String { - return "" - } - - public var httpHeaders: HTTPHeaders? { - return nil - } - - public var bodyParameters: Parameters? { - return nil - } - - public var httpMethod: HTTPMethod { - return .get - } - - public var task: HTTPTask { - return .request - } - - public var cachePolicy: URLRequest.CachePolicy { - return .reloadIgnoringLocalAndRemoteCacheData - } - - public var timeout: TimeInterval { - return 30 - } -} diff --git a/Features/HNService/HNService/HNService/Common/Constants.swift b/Features/HNService/HNService/HNService/Common/Constants.swift deleted file mode 100644 index 17c17c6..0000000 --- a/Features/HNService/HNService/HNService/Common/Constants.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Constants.swift -// HNService -// -// Created by Никита Васильев on 17.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct Constants { - struct Endpoints { - static let base = "https://hacker-news.firebaseio.com/v0/" - static let topStories = "topstories.json" - static let bestStories = "beststories.json" - static let newStories = "newstories.json" - static let askStories = "askstories.json" - static let showStories = "showstories.json" - } -} diff --git a/Features/HNService/HNService/HNService/Common/StoryTypeFactory.swift b/Features/HNService/HNService/HNService/Common/StoryTypeFactory.swift deleted file mode 100644 index 5fb9b22..0000000 --- a/Features/HNService/HNService/HNService/Common/StoryTypeFactory.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// StoryTypeFactory.swift -// HNService -// -// Created by Никита Васильев on 19.07.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public enum StoryType: Int { - case new - case best - case top - case ask - case show -} - -enum StoryTypeFactory { - static func resource(for type: StoryType) -> BaseResource<[Int]> { - switch type { - case .new: - return NewStoriesResource() - case .best: - return BestStoriesResource() - case .top: - return TopStoriesResource() - case .ask: - return AskStoriesResource() - case .show: - return ShowStoriesResource() - } - } -} diff --git a/Features/HNService/HNService/HNService/HNService.h b/Features/HNService/HNService/HNService/HNService.h deleted file mode 100644 index 59a2eae..0000000 --- a/Features/HNService/HNService/HNService/HNService.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// HNService.h -// HNService -// -// Created by Никита Васильев on 17.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -#import - -//! Project version number for HNService. -FOUNDATION_EXPORT double HNServiceVersionNumber; - -//! Project version string for HNService. -FOUNDATION_EXPORT const unsigned char HNServiceVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/Features/HNService/HNService/HNService/HNService/HNService.swift b/Features/HNService/HNService/HNService/HNService/HNService.swift deleted file mode 100644 index 00809aa..0000000 --- a/Features/HNService/HNService/HNService/HNService/HNService.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// HNService.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import NetworkManager - -final public class HNService: BaseService { } - -// MARK: HNServiceProtocol -extension HNService: HNServiceProtocol { - public func fetchIds(for type: StoryType, completion: @escaping ([Int]) -> Void, fail: @escaping (Error) -> Void) { - let resource = StoryTypeFactory.resource(for: type) - load(resource: resource, completion: completion, fail: fail) - } - - public func loadPosts(with ids: [Int], completion: @escaping ([PostModel]) -> Void, fail: @escaping (Error) -> Void) { - let resources = ids.map(NewsResource.init) - load(resources: resources, completion: completion, fail: fail) - } - - public func loadPost(with id: Int, completion: @escaping (PostModel) -> Void, fail: @escaping (Error) -> Void) { - let resource = NewsResource(id: id) - load(resource: resource, completion: completion, fail: fail) - } - - public func loadComment(with id: Int, completion: @escaping (CommentModel) -> Void, fail: @escaping (Error) -> Void) { - let resource = CommentResource(id: id) - load(resource: resource, completion: completion, fail: fail) - } - - public func loadComments(with id: Int, completion: @escaping (CommentModel) -> Void, fail: @escaping (Error) -> Void) { - loadComment(with: id, completion: { comment in - var commentObject = comment - - guard !commentObject.kids.isEmpty else { - completion(commentObject) - return - } - - var subcomments: [CommentModel] = [] - - let group = DispatchGroup() - - for id in comment.kids { - group.enter() - self.loadComments(with: id, completion: { model in - subcomments.append(model) - group.leave() - }, fail: { error in - fail(error) - return - }) - } - - group.notify(queue: .main) { - commentObject.comments = subcomments - completion(commentObject) - } - }, fail: { error in - fail(error) - }) - } -} diff --git a/Features/HNService/HNService/HNService/HNService/HNServiceProtocol.swift b/Features/HNService/HNService/HNService/HNService/HNServiceProtocol.swift deleted file mode 100644 index 4198cd5..0000000 --- a/Features/HNService/HNService/HNService/HNService/HNServiceProtocol.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// HNServiceProtocol.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public protocol HNServiceProtocol { - func fetchIds(for type: StoryType, completion: @escaping ([Int]) -> Void, fail: @escaping (Error) -> Void) - func loadPosts(with ids: [Int], completion: @escaping ([PostModel]) -> Void, fail: @escaping (Error) -> Void) - func loadComments(with id: Int, completion: @escaping (CommentModel) -> Void, fail: @escaping (Error) -> Void) - func cancelAllTasks() -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/AskStoriesResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/AskStoriesResource.swift deleted file mode 100644 index 152608e..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/AskStoriesResource.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AskStoriesResource.swift -// HackerNews -// -// Created by Никита Васильев on 02.05.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class AskStoriesResource: BaseResource<[Int]> { - override public var path: String { - return Constants.Endpoints.askStories - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/BestStoriesResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/BestStoriesResource.swift deleted file mode 100644 index c01acc5..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/BestStoriesResource.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// BestStoriesResource.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class BestStoriesResource: BaseResource<[Int]> { - override public var path: String { - return Constants.Endpoints.bestStories - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/CommentResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/CommentResource.swift deleted file mode 100644 index b33ccb6..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/CommentResource.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// CommentResource.swift -// HackerNews -// -// Created by Никита Васильев on 24.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class CommentResource: BaseResource { - // MARK: Public Properties - - /// A `Int` value that contains the item id. - let id: Int - - // MARK: Override - override public var path: String { - return "item/\(id).json" - } - - // MARK: Initialization - - /// Create a new `NewsResource` instance. - /// - /// - Parameter id: A `Int` value that contains the item id. - init(id: Int) { - self.id = id - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/ItemResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/ItemResource.swift deleted file mode 100644 index 2af781f..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/ItemResource.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ItemResource.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class ItemResource: BaseResource { - - // MARK: Public Properties - - /// A `Int` value that contains the item id. - let id: Int - - // MARK: Override - override public var path: String { - return "item/\(id).json" - } - - // MARK: Initialization - - /// Create a new `ItemResource` instance. - /// - /// - Parameter id: A `Int` value that contains the item id. - init(id: Int) { - self.id = id - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/NewStoriesResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/NewStoriesResource.swift deleted file mode 100644 index d19094b..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/NewStoriesResource.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// NewStoriesResource.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class NewStoriesResource: BaseResource<[Int]> { - override public var path: String { - return Constants.Endpoints.newStories - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/NewsResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/NewsResource.swift deleted file mode 100644 index 1a460e1..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/NewsResource.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// NewsResource.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class NewsResource: BaseResource { - // MARK: Public Properties - - /// A `Int` value that contains the item id. - let id: Int - - // MARK: Override - override public var path: String { - return "item/\(id).json" - } - - // MARK: Initialization - - /// Create a new `NewsResource` instance. - /// - /// - Parameter id: A `Int` value that contains the item id. - init(id: Int) { - self.id = id - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/ShowStoriesResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/ShowStoriesResource.swift deleted file mode 100644 index 77c9634..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/ShowStoriesResource.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ShowStoriesResource.swift -// HackerNews -// -// Created by Никита Васильев on 05.05.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class ShowStoriesResource: BaseResource<[Int]> { - override public var path: String { - return Constants.Endpoints.showStories - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Resources/TopStoriesResource.swift b/Features/HNService/HNService/HNService/HNService/Resources/TopStoriesResource.swift deleted file mode 100644 index 0a3b8bd..0000000 --- a/Features/HNService/HNService/HNService/HNService/Resources/TopStoriesResource.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// TopStoriesResource.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class TopStoriesResource: BaseResource<[Int]> { - override public var path: String { - return Constants.Endpoints.topStories - } -} diff --git a/Features/HNService/HNService/HNService/HNService/Responses/CommentModel.swift b/Features/HNService/HNService/HNService/HNService/Responses/CommentModel.swift deleted file mode 100644 index cdf1dac..0000000 --- a/Features/HNService/HNService/HNService/HNService/Responses/CommentModel.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// CommentModel.swift -// NetworkManager -// -// Created by Никита Васильев on 11/11/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import Foundation - -public struct CommentModel { - public let id: Int - public let by: String? - public let text: String? - public let time: Int? - public let ids: [Int]? - public let kids: [Int] - public let deleted: Bool - - public var level: Int = 0 - public var comments: [CommentModel] = [] -} - -// MARK: Decodable -extension CommentModel: Decodable { - private enum CodingKeys: String, CodingKey { - case id - case by - case text - case time - case kids - case ids - case deleted - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(Int.self, forKey: .id) - by = try? container.decode(String.self, forKey: .by) - text = try? container.decode(String.self, forKey: .text) - time = try? container.decode(Int.self, forKey: .time) - kids = (try? container.decode([Int].self, forKey: .kids)) ?? [] - ids = try? container.decode([Int].self, forKey: .ids) - deleted = (try? container.decode(Bool.self, forKey: .deleted)) ?? false - } -} - -extension CommentModel { -// func commentPublishTime() -> String? { -// guard let time = time else { return nil } -// let date = Date(timeIntervalSince1970: TimeInterval(time)) -// return Date().timeAgo(from: date) -// } -} diff --git a/Features/HNService/HNService/HNService/HNService/Responses/Item.swift b/Features/HNService/HNService/HNService/HNService/Responses/Item.swift deleted file mode 100644 index e8ed96a..0000000 --- a/Features/HNService/HNService/HNService/HNService/Responses/Item.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Item.swift -// NetworkManager -// -// Created by Никита Васильев on 29/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import Foundation - -// sourcery: ItemRepresentable -public struct ItemModel { - - // MARK: - Properties - - /// The username of the item's author. - public var by: String? - - /// The item's unique id. - public var id: Int? - - /// The URL of the story. - public var url: String? - - /// The title of the story, poll or job. - public var title: String? - - /// The story's score, or the votes for a pollopt. - public var score: Int? - - /// true if the item is deleted. - public var isDeleted: Bool? - - /// The type of item. One of "job", "story", "comment", "poll", or "pollopt". - public var type: [String]? - - /// Creation date of the item, in Unix Time. - public var time: Int? - - /// The comment, story or poll text. HTML. - public var text: String? - - /// true if the item is dead. - public var isDead: Bool? - - /// The comment's parent: either another comment or the relevant story. - public var parent: Int? - - /// The pollopt's associated poll. - public var poll: Int? - - /// The ids of the item's comments, in ranked display order. - public var kids: [Int]? - - /// A list of related pollopts, in display order. - public var parts: [Int]? - - /// In the case of stories or polls, the total comment count. - public var descendants: Int? -} - -// sourcery:inline:auto:Item.ItemRepresentable -extension ItemModel: Decodable { - private enum CodingKeys: String, CodingKey { - case by - case id - case url - case title - case score - case isDeleted - case type - case time - case text - case isDead - case parent - case poll - case kids - case parts - case descendants - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - by = try? container.decode(String.self, forKey: .by) - id = try? container.decode(Int.self, forKey: .id) - url = try? container.decode(String.self, forKey: .url) - title = try? container.decode(String.self, forKey: .title) - score = try? container.decode(Int.self, forKey: .score) - isDeleted = try? container.decode(Bool.self, forKey: .isDeleted) - type = try? container.decode([String].self, forKey: .type) - time = try? container.decode(Int.self, forKey: .time) - text = try? container.decode(String.self, forKey: .text) - isDead = try? container.decode(Bool.self, forKey: .isDead) - parent = try? container.decode(Int.self, forKey: .parent) - poll = try? container.decode(Int.self, forKey: .poll) - kids = try? container.decode([Int].self, forKey: .kids) - parts = try? container.decode([Int].self, forKey: .parts) - descendants = try? container.decode(Int.self, forKey: .descendants) - } -} -// sourcery:end diff --git a/Features/HNService/HNService/HNService/HNService/Responses/PostModel.swift b/Features/HNService/HNService/HNService/HNService/Responses/PostModel.swift deleted file mode 100644 index 1b22227..0000000 --- a/Features/HNService/HNService/HNService/HNService/Responses/PostModel.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// NewsModel.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public struct PostModel { - - /// The item's unique id. - public let id: Int - - /// The title of the story, poll or job. - public let title: String? - - /// The story's score, or the votes for a pollopt. - public let score: Int? - - /// The username of the item's author. - public let by: String? - - /// The URL of the story. - public let url: String? - - /// The ids of the item's comments, in ranked display order. - public var kids: [Int] - - /// Creation date of the item, in Unix Time. - public var time: Int? -} - -// MARK: Decodable -extension PostModel: Decodable { - private enum CodingKeys: String, CodingKey { - case id - case title - case score - case by - case url - case kids - case time - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(Int.self, forKey: .id) - title = try? container.decode(String.self, forKey: .title) - score = try? container.decode(Int.self, forKey: .score) - by = try? container.decode(String.self, forKey: .by) - url = try? container.decode(String.self, forKey: .url) - kids = (try? container.decode([Int].self, forKey: .kids)) ?? [] - time = try? container.decode(Int.self, forKey: .time) - } -} - -//extension PostModel { -//// func newsPublishTime() -> String? { -//// guard let time = time else { return nil } -//// let date = Date(timeIntervalSince1970: TimeInterval(time)) -//// return Date().timeAgo(from: date) -//// } -//} - -// MARK: Equatable -extension PostModel: Equatable { - static public func == (lhs: PostModel, rhs: PostModel) -> Bool { - return lhs.id == rhs.id - } -} diff --git a/Features/HNService/HNService/HNService/Info.plist b/Features/HNService/HNService/HNService/Info.plist deleted file mode 100644 index 9bcb244..0000000 --- a/Features/HNService/HNService/HNService/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - - diff --git a/Features/HNService/HNService/HNServiceTests/HNServiceTests.swift b/Features/HNService/HNService/HNServiceTests/HNServiceTests.swift deleted file mode 100644 index 904ee04..0000000 --- a/Features/HNService/HNService/HNServiceTests/HNServiceTests.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// HNServiceTests.swift -// HNServiceTests -// -// Created by Никита Васильев on 17.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest -@testable import HNService - -class HNServiceTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Features/HNService/HNService/HNServiceTests/Info.plist b/Features/HNService/HNService/HNServiceTests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/Features/HNService/HNService/HNServiceTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Features/NetworkManager/NetworkManager watchOS/Info.plist b/Features/NetworkManager/NetworkManager watchOS/Info.plist deleted file mode 100644 index 9bcb244..0000000 --- a/Features/NetworkManager/NetworkManager watchOS/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - - diff --git a/Features/NetworkManager/NetworkManager watchOS/NetworkManager_watchOS.h b/Features/NetworkManager/NetworkManager watchOS/NetworkManager_watchOS.h deleted file mode 100644 index aa384fa..0000000 --- a/Features/NetworkManager/NetworkManager watchOS/NetworkManager_watchOS.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// NetworkManager_watchOS.h -// NetworkManager watchOS -// -// Created by Никита Васильев on 19.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -#import - -//! Project version number for NetworkManager_watchOS. -FOUNDATION_EXPORT double NetworkManager_watchOSVersionNumber; - -//! Project version string for NetworkManager_watchOS. -FOUNDATION_EXPORT const unsigned char NetworkManager_watchOSVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/Features/NetworkManager/NetworkManager.xcodeproj/project.pbxproj b/Features/NetworkManager/NetworkManager.xcodeproj/project.pbxproj deleted file mode 100644 index 7f5c071..0000000 --- a/Features/NetworkManager/NetworkManager.xcodeproj/project.pbxproj +++ /dev/null @@ -1,876 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 50; - objects = { - -/* Begin PBXBuildFile section */ - 5A1CADD8244AF3E7002D0114 /* NetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADD7244AF3E7002D0114 /* NetworkRequest.swift */; }; - 5A1CADDA244AF415002D0114 /* APIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADD9244AF415002D0114 /* APIRequest.swift */; }; - 5A1CADDE244AF461002D0114 /* APIResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADDD244AF461002D0114 /* APIResource.swift */; }; - 5A1CADE0244AFB1C002D0114 /* URLParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADDF244AFB1B002D0114 /* URLParameterEncoder.swift */; }; - 5A1CADE4244B1412002D0114 /* JSONParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADE3244B1412002D0114 /* JSONParameterEncoder.swift */; }; - 5A1CADE6244B2210002D0114 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADE5244B2210002D0114 /* HTTPMethod.swift */; }; - 5A1CADE8244B3D8B002D0114 /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADE7244B3D8B002D0114 /* HTTPTask.swift */; }; - 5A1CADF6244B598D002D0114 /* JSONParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADF5244B598D002D0114 /* JSONParameterEncoderTests.swift */; }; - 5A5A8115244B82D500BF554D /* URLParameterEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A8114244B82D500BF554D /* URLParameterEncoderTests.swift */; }; - 5A5A8118244B89B100BF554D /* APIRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A8117244B89B100BF554D /* APIRequestTests.swift */; }; - 5A5A811B244B8DF700BF554D /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A811A244B8DF700BF554D /* MockNetworkSession.swift */; }; - 5A5A811D244B8F6700BF554D /* MockDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A811C244B8F6700BF554D /* MockDataTask.swift */; }; - 5A5A811F244B911A00BF554D /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A811E244B911A00BF554D /* NetworkSession.swift */; }; - 5A5A8121244B95C000BF554D /* MockResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A8120244B95C000BF554D /* MockResource.swift */; }; - 5A5A8123244B960500BF554D /* MockModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A8122244B960500BF554D /* MockModel.swift */; }; - 5A5A8125244BA3D800BF554D /* MockCorruptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A8124244BA3D800BF554D /* MockCorruptModel.swift */; }; - 5A5A8127244BA5FD00BF554D /* MockCorruptParametersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A8126244BA5FD00BF554D /* MockCorruptParametersModel.swift */; }; - 5A8054CD244CB665005E4F96 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054CC244CB665005E4F96 /* NetworkManager.swift */; }; - 5A8054D0244CC0AD005E4F96 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054CF244CC0AD005E4F96 /* NetworkError.swift */; }; - 5A8054D2244CC1EE005E4F96 /* NetworkManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054D1244CC1EE005E4F96 /* NetworkManagerProtocol.swift */; }; - 5A8054D6244CC322005E4F96 /* NetworkManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054D5244CC322005E4F96 /* NetworkManagerTests.swift */; }; - 5AF49591249D5DE500E469A2 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054CF244CC0AD005E4F96 /* NetworkError.swift */; }; - 5AF49592249D5DE500E469A2 /* JSONParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADE3244B1412002D0114 /* JSONParameterEncoder.swift */; }; - 5AF49593249D5DE500E469A2 /* URLParameterEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADDF244AFB1B002D0114 /* URLParameterEncoder.swift */; }; - 5AF49594249D5DE500E469A2 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054CC244CB665005E4F96 /* NetworkManager.swift */; }; - 5AF49595249D5DE500E469A2 /* NetworkManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054D1244CC1EE005E4F96 /* NetworkManagerProtocol.swift */; }; - 5AF49596249D5DE500E469A2 /* APIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADD9244AF415002D0114 /* APIRequest.swift */; }; - 5AF49597249D5DE500E469A2 /* NetworkRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADD7244AF3E7002D0114 /* NetworkRequest.swift */; }; - 5AF49598249D5DE500E469A2 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5A811E244B911A00BF554D /* NetworkSession.swift */; }; - 5AF49599249D5DE500E469A2 /* APIResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADDD244AF461002D0114 /* APIResource.swift */; }; - 5AF4959A249D5DE500E469A2 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADE5244B2210002D0114 /* HTTPMethod.swift */; }; - 5AF4959B249D5DE500E469A2 /* HTTPTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A1CADE7244B3D8B002D0114 /* HTTPTask.swift */; }; - 5AF4959C249D5DEB00E469A2 /* NetworkManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FA2E3621373CCC00C073A3 /* NetworkManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 6656BEBD045976CA2CEC4AE3 /* Pods_NetworkManagerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7529AD9DE31E87E29A292FBF /* Pods_NetworkManagerTests.framework */; }; - 872E5E0096182FF74FFD0912 /* Pods_NetworkManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D6396BAC443EB0FD6D4A57DB /* Pods_NetworkManager.framework */; }; - D0FA2E3D21373CCC00C073A3 /* NetworkManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0FA2E3321373CCC00C073A3 /* NetworkManager.framework */; }; - D0FA2E4421373CCC00C073A3 /* NetworkManager.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FA2E3621373CCC00C073A3 /* NetworkManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - D0FA2E3E21373CCC00C073A3 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D0FA2E2A21373CCC00C073A3 /* Project object */; - proxyType = 1; - remoteGlobalIDString = D0FA2E3221373CCC00C073A3; - remoteInfo = NetworkManager; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 3F35EF52AF6F725871B0D98E /* Pods-NetworkManagerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkManagerTests.debug.xcconfig"; path = "../../Pods/Target Support Files/Pods-NetworkManagerTests/Pods-NetworkManagerTests.debug.xcconfig"; sourceTree = ""; }; - 56E40017254E0EE6E38E7E99 /* Pods-NetworkManager.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkManager.debug.xcconfig"; path = "../../Pods/Target Support Files/Pods-NetworkManager/Pods-NetworkManager.debug.xcconfig"; sourceTree = ""; }; - 5A1CADD7244AF3E7002D0114 /* NetworkRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequest.swift; sourceTree = ""; }; - 5A1CADD9244AF415002D0114 /* APIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequest.swift; sourceTree = ""; }; - 5A1CADDD244AF461002D0114 /* APIResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIResource.swift; sourceTree = ""; }; - 5A1CADDF244AFB1B002D0114 /* URLParameterEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLParameterEncoder.swift; sourceTree = ""; }; - 5A1CADE3244B1412002D0114 /* JSONParameterEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONParameterEncoder.swift; sourceTree = ""; }; - 5A1CADE5244B2210002D0114 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; - 5A1CADE7244B3D8B002D0114 /* HTTPTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPTask.swift; sourceTree = ""; }; - 5A1CADF5244B598D002D0114 /* JSONParameterEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONParameterEncoderTests.swift; sourceTree = ""; }; - 5A5A8114244B82D500BF554D /* URLParameterEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLParameterEncoderTests.swift; sourceTree = ""; }; - 5A5A8117244B89B100BF554D /* APIRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequestTests.swift; sourceTree = ""; }; - 5A5A811A244B8DF700BF554D /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = ""; }; - 5A5A811C244B8F6700BF554D /* MockDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDataTask.swift; sourceTree = ""; }; - 5A5A811E244B911A00BF554D /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = ""; }; - 5A5A8120244B95C000BF554D /* MockResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockResource.swift; sourceTree = ""; }; - 5A5A8122244B960500BF554D /* MockModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockModel.swift; sourceTree = ""; }; - 5A5A8124244BA3D800BF554D /* MockCorruptModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCorruptModel.swift; sourceTree = ""; }; - 5A5A8126244BA5FD00BF554D /* MockCorruptParametersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCorruptParametersModel.swift; sourceTree = ""; }; - 5A8054CC244CB665005E4F96 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; - 5A8054CF244CC0AD005E4F96 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; - 5A8054D1244CC1EE005E4F96 /* NetworkManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerProtocol.swift; sourceTree = ""; }; - 5A8054D5244CC322005E4F96 /* NetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerTests.swift; sourceTree = ""; }; - 5AF49589249D5DDB00E469A2 /* NetworkManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NetworkManager.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5EA4D6BA42570055750886FD /* Pods-NetworkManagerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkManagerTests.release.xcconfig"; path = "../../Pods/Target Support Files/Pods-NetworkManagerTests/Pods-NetworkManagerTests.release.xcconfig"; sourceTree = ""; }; - 7529AD9DE31E87E29A292FBF /* Pods_NetworkManagerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkManagerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - C1326FB6653D5411D03275E7 /* Pods-NetworkManager.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NetworkManager.release.xcconfig"; path = "../../Pods/Target Support Files/Pods-NetworkManager/Pods-NetworkManager.release.xcconfig"; sourceTree = ""; }; - D0FA2E3321373CCC00C073A3 /* NetworkManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = NetworkManager.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D0FA2E3621373CCC00C073A3 /* NetworkManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NetworkManager.h; sourceTree = ""; }; - D0FA2E3721373CCC00C073A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D0FA2E3C21373CCC00C073A3 /* NetworkManagerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NetworkManagerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - D0FA2E4321373CCC00C073A3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D6396BAC443EB0FD6D4A57DB /* Pods_NetworkManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NetworkManager.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 5AF49586249D5DDB00E469A2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D0FA2E2F21373CCC00C073A3 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 872E5E0096182FF74FFD0912 /* Pods_NetworkManager.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D0FA2E3921373CCC00C073A3 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D0FA2E3D21373CCC00C073A3 /* NetworkManager.framework in Frameworks */, - 6656BEBD045976CA2CEC4AE3 /* Pods_NetworkManagerTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 5A1CADEB244B4B67002D0114 /* Service */ = { - isa = PBXGroup; - children = ( - 5A1CADDD244AF461002D0114 /* APIResource.swift */, - 5A1CADE5244B2210002D0114 /* HTTPMethod.swift */, - 5A1CADE7244B3D8B002D0114 /* HTTPTask.swift */, - ); - path = Service; - sourceTree = ""; - }; - 5A1CADEC244B4BA8002D0114 /* Encoding */ = { - isa = PBXGroup; - children = ( - 5A1CADE3244B1412002D0114 /* JSONParameterEncoder.swift */, - 5A1CADDF244AFB1B002D0114 /* URLParameterEncoder.swift */, - ); - path = Encoding; - sourceTree = ""; - }; - 5A1CADED244B4BBC002D0114 /* Request */ = { - isa = PBXGroup; - children = ( - 5A1CADD9244AF415002D0114 /* APIRequest.swift */, - 5A1CADD7244AF3E7002D0114 /* NetworkRequest.swift */, - 5A5A811E244B911A00BF554D /* NetworkSession.swift */, - ); - path = Request; - sourceTree = ""; - }; - 5A1CADF7244B5992002D0114 /* Encoding */ = { - isa = PBXGroup; - children = ( - 5A1CADF5244B598D002D0114 /* JSONParameterEncoderTests.swift */, - 5A5A8114244B82D500BF554D /* URLParameterEncoderTests.swift */, - ); - path = Encoding; - sourceTree = ""; - }; - 5A5A8116244B899F00BF554D /* Request */ = { - isa = PBXGroup; - children = ( - 5A5A8117244B89B100BF554D /* APIRequestTests.swift */, - ); - path = Request; - sourceTree = ""; - }; - 5A5A8119244B8DEA00BF554D /* Mocks */ = { - isa = PBXGroup; - children = ( - 5A5A812F244BAD5B00BF554D /* Models */, - 5A5A812E244BAD4D00BF554D /* Requests */, - 5A5A812D244BAD3E00BF554D /* Services */, - ); - path = Mocks; - sourceTree = ""; - }; - 5A5A8128244BAB8E00BF554D /* Tests */ = { - isa = PBXGroup; - children = ( - 5A1CADF7244B5992002D0114 /* Encoding */, - 5A8054D4244CC318005E4F96 /* Manager */, - 5A5A8116244B899F00BF554D /* Request */, - ); - path = Tests; - sourceTree = ""; - }; - 5A5A812D244BAD3E00BF554D /* Services */ = { - isa = PBXGroup; - children = ( - 5A5A8124244BA3D800BF554D /* MockCorruptModel.swift */, - 5A5A8126244BA5FD00BF554D /* MockCorruptParametersModel.swift */, - 5A5A8120244B95C000BF554D /* MockResource.swift */, - ); - path = Services; - sourceTree = ""; - }; - 5A5A812E244BAD4D00BF554D /* Requests */ = { - isa = PBXGroup; - children = ( - 5A5A811C244B8F6700BF554D /* MockDataTask.swift */, - 5A5A811A244B8DF700BF554D /* MockNetworkSession.swift */, - ); - path = Requests; - sourceTree = ""; - }; - 5A5A812F244BAD5B00BF554D /* Models */ = { - isa = PBXGroup; - children = ( - 5A5A8122244B960500BF554D /* MockModel.swift */, - ); - path = Models; - sourceTree = ""; - }; - 5A8054CE244CC01D005E4F96 /* Manager */ = { - isa = PBXGroup; - children = ( - 5A8054CC244CB665005E4F96 /* NetworkManager.swift */, - 5A8054D1244CC1EE005E4F96 /* NetworkManagerProtocol.swift */, - ); - path = Manager; - sourceTree = ""; - }; - 5A8054D4244CC318005E4F96 /* Manager */ = { - isa = PBXGroup; - children = ( - 5A8054D5244CC322005E4F96 /* NetworkManagerTests.swift */, - ); - path = Manager; - sourceTree = ""; - }; - 5A8054D7244CC847005E4F96 /* Common */ = { - isa = PBXGroup; - children = ( - 5A8054CF244CC0AD005E4F96 /* NetworkError.swift */, - ); - path = Common; - sourceTree = ""; - }; - 7689145F0EFA253BA66799C6 /* Pods */ = { - isa = PBXGroup; - children = ( - 56E40017254E0EE6E38E7E99 /* Pods-NetworkManager.debug.xcconfig */, - C1326FB6653D5411D03275E7 /* Pods-NetworkManager.release.xcconfig */, - 3F35EF52AF6F725871B0D98E /* Pods-NetworkManagerTests.debug.xcconfig */, - 5EA4D6BA42570055750886FD /* Pods-NetworkManagerTests.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - D0FA2E2921373CCC00C073A3 = { - isa = PBXGroup; - children = ( - D0FA2E3521373CCC00C073A3 /* NetworkManager */, - D0FA2E4021373CCC00C073A3 /* NetworkManagerTests */, - D0FA2E3421373CCC00C073A3 /* Products */, - 7689145F0EFA253BA66799C6 /* Pods */, - D6A71F658000A3AAD1C84BED /* Frameworks */, - ); - sourceTree = ""; - }; - D0FA2E3421373CCC00C073A3 /* Products */ = { - isa = PBXGroup; - children = ( - D0FA2E3321373CCC00C073A3 /* NetworkManager.framework */, - D0FA2E3C21373CCC00C073A3 /* NetworkManagerTests.xctest */, - 5AF49589249D5DDB00E469A2 /* NetworkManager.framework */, - ); - name = Products; - sourceTree = ""; - }; - D0FA2E3521373CCC00C073A3 /* NetworkManager */ = { - isa = PBXGroup; - children = ( - 5A8054D7244CC847005E4F96 /* Common */, - 5A1CADEC244B4BA8002D0114 /* Encoding */, - 5A8054CE244CC01D005E4F96 /* Manager */, - 5A1CADED244B4BBC002D0114 /* Request */, - 5A1CADEB244B4B67002D0114 /* Service */, - D0FA2E3721373CCC00C073A3 /* Info.plist */, - D0FA2E3621373CCC00C073A3 /* NetworkManager.h */, - ); - path = NetworkManager; - sourceTree = ""; - }; - D0FA2E4021373CCC00C073A3 /* NetworkManagerTests */ = { - isa = PBXGroup; - children = ( - 5A5A8119244B8DEA00BF554D /* Mocks */, - 5A5A8128244BAB8E00BF554D /* Tests */, - D0FA2E4321373CCC00C073A3 /* Info.plist */, - ); - path = NetworkManagerTests; - sourceTree = ""; - }; - D6A71F658000A3AAD1C84BED /* Frameworks */ = { - isa = PBXGroup; - children = ( - D6396BAC443EB0FD6D4A57DB /* Pods_NetworkManager.framework */, - 7529AD9DE31E87E29A292FBF /* Pods_NetworkManagerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXHeadersBuildPhase section */ - 5AF49584249D5DDB00E469A2 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF4959C249D5DEB00E469A2 /* NetworkManager.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D0FA2E3021373CCC00C073A3 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - D0FA2E4421373CCC00C073A3 /* NetworkManager.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXHeadersBuildPhase section */ - -/* Begin PBXNativeTarget section */ - 5AF49588249D5DDB00E469A2 /* NetworkManager watchOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5AF4958E249D5DDB00E469A2 /* Build configuration list for PBXNativeTarget "NetworkManager watchOS" */; - buildPhases = ( - 5AF49584249D5DDB00E469A2 /* Headers */, - 5AF49585249D5DDB00E469A2 /* Sources */, - 5AF49586249D5DDB00E469A2 /* Frameworks */, - 5AF49587249D5DDB00E469A2 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "NetworkManager watchOS"; - productName = "NetworkManager watchOS"; - productReference = 5AF49589249D5DDB00E469A2 /* NetworkManager.framework */; - productType = "com.apple.product-type.framework"; - }; - D0FA2E3221373CCC00C073A3 /* NetworkManager */ = { - isa = PBXNativeTarget; - buildConfigurationList = D0FA2E4721373CCC00C073A3 /* Build configuration list for PBXNativeTarget "NetworkManager" */; - buildPhases = ( - 79C5514A7014DA5F355EF42E /* [CP] Check Pods Manifest.lock */, - D0FA2E2E21373CCC00C073A3 /* Sources */, - D0FA2E2F21373CCC00C073A3 /* Frameworks */, - D0FA2E3021373CCC00C073A3 /* Headers */, - D0FA2E3121373CCC00C073A3 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = NetworkManager; - productName = NetworkManager; - productReference = D0FA2E3321373CCC00C073A3 /* NetworkManager.framework */; - productType = "com.apple.product-type.framework"; - }; - D0FA2E3B21373CCC00C073A3 /* NetworkManagerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = D0FA2E4A21373CCC00C073A3 /* Build configuration list for PBXNativeTarget "NetworkManagerTests" */; - buildPhases = ( - 35DEBCB7C67852BAAFA8CD3B /* [CP] Check Pods Manifest.lock */, - D0FA2E3821373CCC00C073A3 /* Sources */, - D0FA2E3921373CCC00C073A3 /* Frameworks */, - D0FA2E3A21373CCC00C073A3 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - D0FA2E3F21373CCC00C073A3 /* PBXTargetDependency */, - ); - name = NetworkManagerTests; - productName = NetworkManagerTests; - productReference = D0FA2E3C21373CCC00C073A3 /* NetworkManagerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - D0FA2E2A21373CCC00C073A3 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0940; - LastUpgradeCheck = 0940; - ORGANIZATIONNAME = "Никита Васильев"; - TargetAttributes = { - 5AF49588249D5DDB00E469A2 = { - CreatedOnToolsVersion = 11.5; - }; - D0FA2E3221373CCC00C073A3 = { - CreatedOnToolsVersion = 9.4.1; - LastSwiftMigration = 0940; - }; - D0FA2E3B21373CCC00C073A3 = { - CreatedOnToolsVersion = 9.4.1; - LastSwiftMigration = 1130; - }; - }; - }; - buildConfigurationList = D0FA2E2D21373CCC00C073A3 /* Build configuration list for PBXProject "NetworkManager" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = D0FA2E2921373CCC00C073A3; - productRefGroup = D0FA2E3421373CCC00C073A3 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - D0FA2E3221373CCC00C073A3 /* NetworkManager */, - D0FA2E3B21373CCC00C073A3 /* NetworkManagerTests */, - 5AF49588249D5DDB00E469A2 /* NetworkManager watchOS */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 5AF49587249D5DDB00E469A2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D0FA2E3121373CCC00C073A3 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D0FA2E3A21373CCC00C073A3 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 35DEBCB7C67852BAAFA8CD3B /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NetworkManagerTests-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; - }; - 79C5514A7014DA5F355EF42E /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-NetworkManager-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 */ - 5AF49585249D5DDB00E469A2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF49591249D5DE500E469A2 /* NetworkError.swift in Sources */, - 5AF49592249D5DE500E469A2 /* JSONParameterEncoder.swift in Sources */, - 5AF49593249D5DE500E469A2 /* URLParameterEncoder.swift in Sources */, - 5AF49594249D5DE500E469A2 /* NetworkManager.swift in Sources */, - 5AF49595249D5DE500E469A2 /* NetworkManagerProtocol.swift in Sources */, - 5AF49596249D5DE500E469A2 /* APIRequest.swift in Sources */, - 5AF49597249D5DE500E469A2 /* NetworkRequest.swift in Sources */, - 5AF49598249D5DE500E469A2 /* NetworkSession.swift in Sources */, - 5AF49599249D5DE500E469A2 /* APIResource.swift in Sources */, - 5AF4959A249D5DE500E469A2 /* HTTPMethod.swift in Sources */, - 5AF4959B249D5DE500E469A2 /* HTTPTask.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D0FA2E2E21373CCC00C073A3 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A8054D2244CC1EE005E4F96 /* NetworkManagerProtocol.swift in Sources */, - 5A8054CD244CB665005E4F96 /* NetworkManager.swift in Sources */, - 5A1CADD8244AF3E7002D0114 /* NetworkRequest.swift in Sources */, - 5A8054D0244CC0AD005E4F96 /* NetworkError.swift in Sources */, - 5A1CADE6244B2210002D0114 /* HTTPMethod.swift in Sources */, - 5A1CADE4244B1412002D0114 /* JSONParameterEncoder.swift in Sources */, - 5A1CADE8244B3D8B002D0114 /* HTTPTask.swift in Sources */, - 5A1CADE0244AFB1C002D0114 /* URLParameterEncoder.swift in Sources */, - 5A5A811F244B911A00BF554D /* NetworkSession.swift in Sources */, - 5A1CADDA244AF415002D0114 /* APIRequest.swift in Sources */, - 5A1CADDE244AF461002D0114 /* APIResource.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D0FA2E3821373CCC00C073A3 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A8054D6244CC322005E4F96 /* NetworkManagerTests.swift in Sources */, - 5A1CADF6244B598D002D0114 /* JSONParameterEncoderTests.swift in Sources */, - 5A5A8127244BA5FD00BF554D /* MockCorruptParametersModel.swift in Sources */, - 5A5A8115244B82D500BF554D /* URLParameterEncoderTests.swift in Sources */, - 5A5A811B244B8DF700BF554D /* MockNetworkSession.swift in Sources */, - 5A5A8125244BA3D800BF554D /* MockCorruptModel.swift in Sources */, - 5A5A8123244B960500BF554D /* MockModel.swift in Sources */, - 5A5A811D244B8F6700BF554D /* MockDataTask.swift in Sources */, - 5A5A8118244B89B100BF554D /* APIRequestTests.swift in Sources */, - 5A5A8121244B95C000BF554D /* MockResource.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - D0FA2E3F21373CCC00C073A3 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D0FA2E3221373CCC00C073A3 /* NetworkManager */; - targetProxy = D0FA2E3E21373CCC00C073A3 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 5AF4958F249D5DDB00E469A2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = NetworkManager/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.nikitavasilev.NetworkManager-watchOS"; - PRODUCT_NAME = NetworkManager; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Debug; - }; - 5AF49590249D5DDB00E469A2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = NetworkManager/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.nikitavasilev.NetworkManager-watchOS"; - PRODUCT_NAME = NetworkManager; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Release; - }; - D0FA2E4521373CCC00C073A3 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - 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_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; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - 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 = ( - "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 = 11.4; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - D0FA2E4621373CCC00C073A3 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - 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_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; - CODE_SIGN_IDENTITY = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 1; - 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_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 = 11.4; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; - D0FA2E4821373CCC00C073A3 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 56E40017254E0EE6E38E7E99 /* Pods-NetworkManager.debug.xcconfig */; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = NetworkManager/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.NetworkManager; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - D0FA2E4921373CCC00C073A3 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C1326FB6653D5411D03275E7 /* Pods-NetworkManager.release.xcconfig */; - buildSettings = { - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_IDENTITY = ""; - CODE_SIGN_STYLE = Automatic; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = A8WE5LL2GU; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = NetworkManager/Info.plist; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.NetworkManager; - PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - D0FA2E4B21373CCC00C073A3 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F35EF52AF6F725871B0D98E /* Pods-NetworkManagerTests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = NetworkManagerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.NetworkManagerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - D0FA2E4C21373CCC00C073A3 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 5EA4D6BA42570055750886FD /* Pods-NetworkManagerTests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = NetworkManagerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.NetworkManagerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 5AF4958E249D5DDB00E469A2 /* Build configuration list for PBXNativeTarget "NetworkManager watchOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AF4958F249D5DDB00E469A2 /* Debug */, - 5AF49590249D5DDB00E469A2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D0FA2E2D21373CCC00C073A3 /* Build configuration list for PBXProject "NetworkManager" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D0FA2E4521373CCC00C073A3 /* Debug */, - D0FA2E4621373CCC00C073A3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D0FA2E4721373CCC00C073A3 /* Build configuration list for PBXNativeTarget "NetworkManager" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D0FA2E4821373CCC00C073A3 /* Debug */, - D0FA2E4921373CCC00C073A3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D0FA2E4A21373CCC00C073A3 /* Build configuration list for PBXNativeTarget "NetworkManagerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D0FA2E4B21373CCC00C073A3 /* Debug */, - D0FA2E4C21373CCC00C073A3 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = D0FA2E2A21373CCC00C073A3 /* Project object */; -} diff --git a/Features/NetworkManager/NetworkManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Features/NetworkManager/NetworkManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 3ef3dc1..0000000 --- a/Features/NetworkManager/NetworkManager.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Features/NetworkManager/NetworkManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Features/NetworkManager/NetworkManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/Features/NetworkManager/NetworkManager.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Features/NetworkManager/NetworkManager.xcodeproj/xcshareddata/xcschemes/NetworkManager.xcscheme b/Features/NetworkManager/NetworkManager.xcodeproj/xcshareddata/xcschemes/NetworkManager.xcscheme deleted file mode 100644 index 16993a9..0000000 --- a/Features/NetworkManager/NetworkManager.xcodeproj/xcshareddata/xcschemes/NetworkManager.xcscheme +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Features/NetworkManager/NetworkManager/Common/NetworkError.swift b/Features/NetworkManager/NetworkManager/Common/NetworkError.swift deleted file mode 100644 index 37d89d4..0000000 --- a/Features/NetworkManager/NetworkManager/Common/NetworkError.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// NetworkError.swift -// NetworkManager -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -/// Network errors -public enum NetworkError: String, Error { - /// For encoding errors, such as failing to encode parameters. - case encodingFailed = "Parameter encoding failed." - - /// For decoding errors, such as failing to decode a response from the server. - case decodingFailed = "Parameter decoding failed." - - /// For not valid URL. - case missingURL = "URL is nil." - - /// For client side errors, such as failing to build a request to the server. - case client = "Building request is failed." -} diff --git a/Features/NetworkManager/NetworkManager/Encoding/JSONParameterEncoder.swift b/Features/NetworkManager/NetworkManager/Encoding/JSONParameterEncoder.swift deleted file mode 100644 index 52817c3..0000000 --- a/Features/NetworkManager/NetworkManager/Encoding/JSONParameterEncoder.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// JSONParameterEncoder.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -/// JSON encoder. -public struct JSONParameterEncoder: ParameterEncoder { - public static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws { - do { - let data = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted) - urlRequest.httpBody = data - - if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { - urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") - } - - } catch { - throw NetworkError.encodingFailed - } - } -} diff --git a/Features/NetworkManager/NetworkManager/Encoding/URLParameterEncoder.swift b/Features/NetworkManager/NetworkManager/Encoding/URLParameterEncoder.swift deleted file mode 100644 index da46558..0000000 --- a/Features/NetworkManager/NetworkManager/Encoding/URLParameterEncoder.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// URLParameterEncoder.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public typealias Parameters = [String: Any] - -public protocol ParameterEncoder { - /// Encode items for the URL in the order in which they appear in the original query string. - /// - /// - Parameters: - /// - urlRequest: A URL load request that is independent of protocol or URL scheme. - /// - parameters: The object from which to generate items. - /// - /// - Throws: `NetworkError.missingURL` if url request contains is not valid URL. - static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws -} - -/// URL encoder. -public struct URLParameterEncoder: ParameterEncoder { - public static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws { - guard let url = urlRequest.url else { throw NetworkError.missingURL } - - if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty { - urlComponents.queryItems = [URLQueryItem]() - - for (key, value) in parameters { - let queryItem = URLQueryItem(name: key, value: "\(value)".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)) - urlComponents.queryItems?.append(queryItem) - } - - if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { - urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type") - } - - urlRequest.url = urlComponents.url - } - } -} diff --git a/Features/NetworkManager/NetworkManager/Info.plist b/Features/NetworkManager/NetworkManager/Info.plist deleted file mode 100644 index 1007fd9..0000000 --- a/Features/NetworkManager/NetworkManager/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - diff --git a/Features/NetworkManager/NetworkManager/Manager/NetworkManager.swift b/Features/NetworkManager/NetworkManager/Manager/NetworkManager.swift deleted file mode 100644 index 1ce9e14..0000000 --- a/Features/NetworkManager/NetworkManager/Manager/NetworkManager.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// NetworkManager.swift -// NetworkManager -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final public class NetworkManager { - - // MARK: Public Properties - - /// A `NetworkSession` value that contains the current HTTP session. - let session: NetworkSession - - // MARK: Initialization - - /// Create a new `NetworkManager` instance. - /// - /// - Parameter session: A `NetworkSession` value that contains the current HTTP session. - public init(session: NetworkSession = URLSession.shared) { - self.session = session - } -} - -// MARK: NetworkManagerProtocol -extension NetworkManager: NetworkManagerProtocol { - public func fetch(_ resource: R, completion: @escaping (R.ModelType) -> Void, fail: @escaping (Error) -> Void) { - let request = APIRequest(resource: resource) - - guard let urlRequest = try? request.buildRequest() else { - return - } - - let task = session.dataTask(with: urlRequest) { (data: Data?, _, error: Error?) in - if let error = error { - DispatchQueue.main.async { - fail(error) - } - return - } - - guard let data = data, let model = try? JSONDecoder().decode(R.ModelType.self, from: data) else { - DispatchQueue.main.async { - fail(NetworkError.decodingFailed) - } - return - } - - DispatchQueue.main.async { - completion(model) - } - } - task.resume() - } - - public func cancelAllTasks() { - session.cancelAllTasks() - } -} diff --git a/Features/NetworkManager/NetworkManager/Manager/NetworkManagerProtocol.swift b/Features/NetworkManager/NetworkManager/Manager/NetworkManagerProtocol.swift deleted file mode 100644 index 30d992f..0000000 --- a/Features/NetworkManager/NetworkManager/Manager/NetworkManagerProtocol.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NetworkManagerProtocol.swift -// NetworkManager -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -protocol NetworkManagerProtocol { - /// A `NetworkSession` value that contains the current HTTP session. - var session: NetworkSession { get } - - /// Creates a task that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion. - /// - /// - Parameters: - /// - resource: An `APIResource` resource object that provides the URL, cache policy, request type, body data or body stream, and so on. - /// - completion: A block object to be executed when the load ends. - /// - fail: A block object to be execute when the load fails. - func fetch(_ resource: R, completion: @escaping (R.ModelType) -> Void, fail: @escaping (Error) -> Void) - - /// Cancel all tasks. - func cancelAllTasks() -} diff --git a/Features/NetworkManager/NetworkManager/Models/Item.swift b/Features/NetworkManager/NetworkManager/Models/Item.swift deleted file mode 100644 index a0dfdf5..0000000 --- a/Features/NetworkManager/NetworkManager/Models/Item.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Item.swift -// NetworkManager -// -// Created by Никита Васильев on 29/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// -// -import Foundation - -// sourcery: ItemRepresentable -public struct Item { - - // MARK: - Properties - - /// The username of the item's author. - public var by: String? - - /// The item's unique id. - public var id: Int? - - /// The URL of the story. - public var url: String? - - /// The title of the story, poll or job. - public var title: String? - - /// The story's score, or the votes for a pollopt. - public var score: Int? - - /// true if the item is deleted. - public var isDeleted: Bool? - - /// The type of item. One of "job", "story", "comment", "poll", or "pollopt". - public var type: [String]? - - /// Creation date of the item, in Unix Time. - public var time: Int? - - /// The comment, story or poll text. HTML. - public var text: String? - - /// true if the item is dead. - public var isDead: Bool? - - /// The comment's parent: either another comment or the relevant story. - public var parent: Int? - - /// The pollopt's associated poll. - public var poll: Int? - - /// The ids of the item's comments, in ranked display order. - public var kids: [Int]? - - /// A list of related pollopts, in display order. - public var parts: [Int]? - - /// In the case of stories or polls, the total comment count. - public var descendants: Int? -} - -// sourcery:inline:auto:Item.ItemRepresentable -extension Item: Decodable { - private enum CodingKeys: String, CodingKey { - case by - case id - case url - case title - case score - case isDeleted - case type - case time - case text - case isDead - case parent - case poll - case kids - case parts - case descendants - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - by = try? container.decode(String.self, forKey: .by) - id = try? container.decode(Int.self, forKey: .id) - url = try? container.decode(String.self, forKey: .url) - title = try? container.decode(String.self, forKey: .title) - score = try? container.decode(Int.self, forKey: .score) - isDeleted = try? container.decode(Bool.self, forKey: .isDeleted) - type = try? container.decode([String].self, forKey: .type) - time = try? container.decode(Int.self, forKey: .time) - text = try? container.decode(String.self, forKey: .text) - isDead = try? container.decode(Bool.self, forKey: .isDead) - parent = try? container.decode(Int.self, forKey: .parent) - poll = try? container.decode(Int.self, forKey: .poll) - kids = try? container.decode([Int].self, forKey: .kids) - parts = try? container.decode([Int].self, forKey: .parts) - descendants = try? container.decode(Int.self, forKey: .descendants) - } -} -// sourcery:end diff --git a/Features/NetworkManager/NetworkManager/NetworkManager.h b/Features/NetworkManager/NetworkManager/NetworkManager.h deleted file mode 100644 index 9d64f88..0000000 --- a/Features/NetworkManager/NetworkManager/NetworkManager.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// NetworkManager.h -// NetworkManager -// -// Created by Никита Васильев on 29/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -#import - -//! Project version number for NetworkManager. -FOUNDATION_EXPORT double NetworkManagerVersionNumber; - -//! Project version string for NetworkManager. -FOUNDATION_EXPORT const unsigned char NetworkManagerVersionString[]; - -// In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/Features/NetworkManager/NetworkManager/Request/APIRequest.swift b/Features/NetworkManager/NetworkManager/Request/APIRequest.swift deleted file mode 100644 index 4a936d8..0000000 --- a/Features/NetworkManager/NetworkManager/Request/APIRequest.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// APIRequest.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public struct APIRequest { - - // MARK: Public Properties - - /// An `APIResource` resource object that provides the URL, cache policy, request type, body data or body stream, and so on. - let resource: Resource - - /// Create a new `APIRequest` instance. - /// - /// - Parameters: - /// - resource: An `APIResource` resource object that provides the URL, cache policy, request type, body data or body stream, and so on. - /// - session: A `URLSession` value that contains the current HTTP session. - public init(resource: Resource) { - self.resource = resource - } -} - -// MARK: NetworkRequest -extension APIRequest: NetworkRequest { } diff --git a/Features/NetworkManager/NetworkManager/Request/NetworkRequest.swift b/Features/NetworkManager/NetworkManager/Request/NetworkRequest.swift deleted file mode 100644 index 294f668..0000000 --- a/Features/NetworkManager/NetworkManager/Request/NetworkRequest.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// NetworkRequest.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -/// Network request -protocol NetworkRequest { - associatedtype Resource: APIResource - - /// An `APIResource` resource object that provides the URL, cache policy, request type, body data or body stream, and so on. - var resource: Resource { get } - - /// Build `URLRequest` from resource. - /// - /// - Parameter resource: An `APIResource` resource object that provides the URL, cache policy, request type, body data or body stream, and so on. - /// - /// - Returns: A URL load request that is independent of protocol or URL scheme. - /// - /// - Throws: `NetworkError.encodingFailed`. - func buildRequest() throws -> URLRequest? -} - -extension NetworkRequest { - func buildRequest() throws -> URLRequest? { - var request = URLRequest(url: resource.baseURL.appendingPathComponent(resource.path), - cachePolicy: resource.cachePolicy, - timeoutInterval: resource.timeout) - - request.httpMethod = resource.httpMethod.rawValue - - do { - switch resource.task { - case .request: - request.setValue("application/json", forHTTPHeaderField: "Content-Type") - case .requestParameters(let bodyParameters, let urlParameters): - try self.configureParameters(bodyParameters: bodyParameters, urlParameters: urlParameters, urlRequest: &request) - case .requestParametersAndHeaders(let bodyParameters, let urlParameters, let headers): - self.addAdditionalHeaders(additionalHeaders: headers, request: &request) - try self.configureParameters(bodyParameters: bodyParameters, urlParameters: urlParameters, urlRequest: &request) - } - - return request - } catch { - throw error - } - } - - /// Configure the `urlRequest` with the specified parameters. - /// - /// - Parameters: - /// - bodyParameters: The data sent as the message body of a request, such as for an HTTP POST request. - /// - urlParameters: The items for the URL in the order in which they appear in the original query string. - /// - urlRequest: A URL load request that is independent of protocol or URL scheme. - /// - /// - Throws: `NetworkError.encodingFailed`. - fileprivate func configureParameters(bodyParameters: Parameters?, urlParameters: Parameters?, urlRequest: inout URLRequest) throws { - do { - if let bodyParameters = bodyParameters { - try JSONParameterEncoder.encode(urlRequest: &urlRequest, with: bodyParameters) - } - - if let urlParameters = urlParameters { - try URLParameterEncoder.encode(urlRequest: &urlRequest, with: urlParameters) - } - } catch { - throw error - } - } - - /// Add additional headers to the request. - /// - /// - Parameters: - /// - additionalHeaders: Sets a value for the header field. - /// - request: A URL load request that is independent of protocol or URL scheme. - fileprivate func addAdditionalHeaders(additionalHeaders: HTTPHeaders?, request: inout URLRequest) { - guard let headers = additionalHeaders else { return } - - for (key, value) in headers { - request.setValue(value, forHTTPHeaderField: key) - } - } -} diff --git a/Features/NetworkManager/NetworkManager/Request/NetworkSession.swift b/Features/NetworkManager/NetworkManager/Request/NetworkSession.swift deleted file mode 100644 index a171801..0000000 --- a/Features/NetworkManager/NetworkManager/Request/NetworkSession.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// NetworkSession.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public protocol NetworkSession: class { - /// Creates a task that retrieves the contents of the specified URL, then calls a handler upon completion. - /// - /// - Parameters: - /// - request: The URL to be retrieved. - /// - completionHandler: The completion handler to call when the load request is complete. This handler is executed on the delegate queue. - func dataTask(with request: URLRequest, - completionHandler: @escaping (Data?, URLResponse?, Swift.Error?) -> Swift.Void) -> URLSessionDataTask - - /// Asynchronously calls a completion callback with all tasks in a session - func getAllTasks(completionHandler: @escaping ([URLSessionTask]) -> Void) - - /// Cancel all tasks. - func cancelAllTasks() -} - -// MARK: NetworkSession -extension URLSession: NetworkSession { - public func cancelAllTasks() { - self.getAllTasks { $0.forEach { $0.cancel() } } - } -} diff --git a/Features/NetworkManager/NetworkManager/Service/APIResource.swift b/Features/NetworkManager/NetworkManager/Service/APIResource.swift deleted file mode 100644 index 7f21de2..0000000 --- a/Features/NetworkManager/NetworkManager/Service/APIResource.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// APIResource.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public typealias HTTPHeaders = [String: String] - -public protocol APIResource { - associatedtype ModelType: Decodable - - /// The URL for the request. - var baseURL: URL { get } - - /// The path to endpoint. - var path: String { get } - - /// The value for the header field. - var httpHeaders: HTTPHeaders? { get } - - /// The parameters for the URL in the order in which they appear in the original query string. - var bodyParameters: Parameters? { get } - - /// The HTTP request method. - var httpMethod: HTTPMethod { get } - - /// The task type. - var task: HTTPTask { get } - - /// The cache policy for the request - var cachePolicy: URLRequest.CachePolicy { get } - - /// The timeout interval for the request. The default is 60.0. - var timeout: TimeInterval { get } -} - -public extension APIResource { - var bodyParameters: Parameters? { - return nil - } - - var cachePolicy: URLRequest.CachePolicy { - return .useProtocolCachePolicy - } - - var timeout: TimeInterval { - return 60 - } - - var httpMethod: HTTPMethod { - return .get - } - - var task: HTTPTask { - return .request - } - - var httpHeaders: HTTPHeaders? { - return nil - } -} diff --git a/Features/NetworkManager/NetworkManager/Service/HTTPMethod.swift b/Features/NetworkManager/NetworkManager/Service/HTTPMethod.swift deleted file mode 100644 index e7a2532..0000000 --- a/Features/NetworkManager/NetworkManager/Service/HTTPMethod.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// HTTPMethod.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public enum HTTPMethod: String { - case get = "GET" - case post = "POST" - case put = "PUT" - case patch = "PATCH" - case delete = "DELETE" -} diff --git a/Features/NetworkManager/NetworkManager/Service/HTTPTask.swift b/Features/NetworkManager/NetworkManager/Service/HTTPTask.swift deleted file mode 100644 index 613deee..0000000 --- a/Features/NetworkManager/NetworkManager/Service/HTTPTask.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// HTTPTask.swift -// NetworkManager -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -public enum HTTPTask { - case request - case requestParameters(bodyParameters: Parameters?, urlParameters: Parameters?) - case requestParametersAndHeaders(bodyParameters: Parameters?, urlParameters: Parameters?, headers: HTTPHeaders?) -} diff --git a/Features/NetworkManager/NetworkManagerTests/Info.plist b/Features/NetworkManager/NetworkManagerTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/Features/NetworkManager/NetworkManagerTests/Mocks/Models/MockModel.swift b/Features/NetworkManager/NetworkManagerTests/Mocks/Models/MockModel.swift deleted file mode 100644 index e118f53..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Mocks/Models/MockModel.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// MockModel.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct MockModel: Codable { - let id: String - let name: String -} - -// MARK: Equatable -extension MockModel: Equatable { } diff --git a/Features/NetworkManager/NetworkManagerTests/Mocks/Requests/MockDataTask.swift b/Features/NetworkManager/NetworkManagerTests/Mocks/Requests/MockDataTask.swift deleted file mode 100644 index f32cea0..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Mocks/Requests/MockDataTask.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// MockDataTask.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final class MockDataTask: URLSessionDataTask { - - typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void - - // MARK: Private Properties - private let data: Data? - private let urlResponse: URLResponse? - private let responseError: Error? - - // MARK: Public Properties - var completionHandler: CompletionHandler? - - // MARK: Initialization - init(data: Data?, urlResponse: URLResponse?, error: Error?) { - self.data = data - self.urlResponse = urlResponse - self.responseError = error - super.init() - } - - // MARK: Override - override func resume() { - DispatchQueue.main.async { - self.completionHandler?(self.data, self.urlResponse, self.responseError) - } - } -} diff --git a/Features/NetworkManager/NetworkManagerTests/Mocks/Requests/MockNetworkSession.swift b/Features/NetworkManager/NetworkManagerTests/Mocks/Requests/MockNetworkSession.swift deleted file mode 100644 index d6badcf..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Mocks/Requests/MockNetworkSession.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// MockNetworkSession.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -@testable import NetworkManager - -final class MockNetworkSession { - // MARK: Public Properties - var request: URLRequest? - - // MARK: Private Properties - private var dataTask: MockDataTask - - // MARK: Initialization - init(data: Data?, urlResponse: URLResponse?, error: Error?) { - dataTask = MockDataTask(data: data, urlResponse: urlResponse, error: error) - } -} - -// MARK: NetworkSession -extension MockNetworkSession: NetworkSession { - func cancelAllTasks() { - - } - - func getAllTasks(completionHandler: @escaping ([URLSessionTask]) -> Void) { - - } - - func dataTask(with request: URLRequest, - completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { - self.request = request - dataTask.completionHandler = completionHandler - return dataTask - } -} diff --git a/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockCorruptModel.swift b/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockCorruptModel.swift deleted file mode 100644 index dda3526..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockCorruptModel.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// MockCorruptModel.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -@testable import NetworkManager - -struct MockCorruptParametersModel: APIResource { - typealias ModelType = MockModel - - var baseURL: URL { - // swiftlint:disable force_unwrapping - return URL(string: "https://test.com/")! - // swiftlint:enable force_unwrapping - } - - var path: String { - return "path/" - } - - var bodyParameters: Parameters? { - return [corruptDataString: corruptDataString] - } - - var task: HTTPTask { - .requestParameters(bodyParameters: bodyParameters, urlParameters: nil) - } - - // swiftlint:disable force_unwrapping - private let corruptDataString = String(bytes: [0xD8, 0x00] as [UInt8], encoding: .utf16BigEndian)! - // swiftlint:enable force_unwrapping -} diff --git a/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockCorruptParametersModel.swift b/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockCorruptParametersModel.swift deleted file mode 100644 index 354af57..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockCorruptParametersModel.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// MockCorruptParametersModel.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -@testable import NetworkManager - -struct MockCorruptParametersWithHeaderResource: APIResource { - typealias ModelType = MockModel - - var baseURL: URL { - // swiftlint:disable force_unwrapping - return URL(string: "https://test.com/")! - // swiftlint:enable force_unwrapping - } - - var path: String { - return "path/" - } - - var bodyParameters: Parameters? { - return [corruptDataString: corruptDataString] - } - - var task: HTTPTask { - .requestParametersAndHeaders(bodyParameters: bodyParameters, urlParameters: urlParameters, headers: httpHeaders) - } - - var httpHeaders: HTTPHeaders? { - return ["some": "text"] - } - - // swiftlint:disable force_unwrapping - private let corruptDataString = String(bytes: [0xD8, 0x00] as [UInt8], encoding: .utf16BigEndian)! - // swiftlint:enable force_unwrapping - - private var urlParameters: Parameters { - return [corruptDataString: corruptDataString] - } -} diff --git a/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockResource.swift b/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockResource.swift deleted file mode 100644 index 38923ca..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Mocks/Services/MockResource.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// MockResource.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -@testable import NetworkManager - -struct MockResource: APIResource { - typealias ModelType = MockModel - - var baseURL: URL { - // swiftlint:disable force_unwrapping - return URL(string: "https://test.com/")! - // swiftlint:enable force_unwrapping - } - - var path: String { - return "path/" - } -} - -// MARK: Equatable -extension MockResource: Equatable { } diff --git a/Features/NetworkManager/NetworkManagerTests/Tests/Encoding/JSONParameterEncoderTests.swift b/Features/NetworkManager/NetworkManagerTests/Tests/Encoding/JSONParameterEncoderTests.swift deleted file mode 100644 index d3fd539..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Tests/Encoding/JSONParameterEncoderTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// JSONParameterEncoderTests.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest -@testable import NetworkManager - -class JSONParameterEncoderTests: XCTestCase { - - // MARK: Private Properties - private let url = URL(string: "https://google.com/") - - // MARK: Initialization - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - // MARK: Tests - func testThatJSONEncodingSuccessfully() throws { - let url = try XCTUnwrap(self.url, "Couldn't instantiate url") - - var urlRequest = URLRequest(url: url) - let parameters: Parameters = [ - "userID": 1, - "Name": "Max", - "EMail": "test@test.com" - ] - - do { - try JSONParameterEncoder.encode(urlRequest: &urlRequest, with: parameters) - - let fullURL = try XCTUnwrap(urlRequest.url, "urlRequest url is nil") - XCTAssertEqual(fullURL.absoluteURL, url) - - let body = try XCTUnwrap(urlRequest.httpBody, "body in urlRequest is nil") - let json = try JSONSerialization.jsonObject(with: body, options: .mutableContainers) - XCTAssertNotNil(json, "Unable to deserialize json") - } catch { - XCTFail(error.localizedDescription) - } - } - - func testThatJSONEncodingFailedWhenParametersInNotValid() throws { - let url = try XCTUnwrap(self.url, "Couldn't instantiate url") - let failedString = try XCTUnwrap(String(bytes: [0xD8, 0x00] as [UInt8], encoding: .utf16BigEndian), "Couldn't create a string") - var urlRequest = URLRequest(url: url) - let parameters: Parameters = [failedString: failedString] - - XCTAssertThrowsError(try JSONParameterEncoder.encode(urlRequest: &urlRequest, with: parameters)) - } -} diff --git a/Features/NetworkManager/NetworkManagerTests/Tests/Encoding/URLParameterEncoderTests.swift b/Features/NetworkManager/NetworkManagerTests/Tests/Encoding/URLParameterEncoderTests.swift deleted file mode 100644 index 54f57b3..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Tests/Encoding/URLParameterEncoderTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// URLParameterEncoderTests.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest -@testable import NetworkManager - -class URLParameterEncoderTests: XCTestCase { - - // MARK: Private Properties - private let url = URL(string: "https://google.com/") - - // MARK: Initialization - override func setUp() { - super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - // MARK: Tests - func testThatURLParameterEncodingSuccessfully() throws { - let url = try XCTUnwrap(self.url, "Couldn't instantiate url") - - var urlRequest = URLRequest(url: url) - let parameters: Parameters = [ - "userID": 1, - "Name": "Max", - "EMail": "test@test.com" - ] - - do { - try URLParameterEncoder.encode(urlRequest: &urlRequest, with: parameters) - - let fullURL = try XCTUnwrap(urlRequest.url, "urlRequest url is nil") - let expectedUrl = "\(url.absoluteString)?EMail=test%2540test.com&userID=1&Name=Max" - - XCTAssertEqual(fullURL.absoluteString.sorted(), expectedUrl.sorted(), "String doesn't equal") - } catch { - XCTFail(error.localizedDescription) - } - } - - func testThatURLParameterEncodingFailWhenURLIsNotValid() throws { - let parameters: Parameters = [:] - let url = try XCTUnwrap(self.url, "Couldn't instantiate url") - var urlRequest = URLRequest(url: url) - urlRequest.url = nil - - XCTAssertThrowsError(try URLParameterEncoder.encode(urlRequest: &urlRequest, with: parameters)) - } -} diff --git a/Features/NetworkManager/NetworkManagerTests/Tests/Manager/NetworkManagerTests.swift b/Features/NetworkManager/NetworkManagerTests/Tests/Manager/NetworkManagerTests.swift deleted file mode 100644 index cedee0f..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Tests/Manager/NetworkManagerTests.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// NetworkManagerTests.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest -@testable import NetworkManager - -class NetworkManagerTests: XCTestCase { - - // MARK: Private Properties - private let resource = MockResource() - private let model = MockModel(id: "123", name: "Max") - private var session: NetworkSession! - private var networkManager: NetworkManagerProtocol! - - // MARK: Initialization - override func setUp() { - super.setUp() - - let data = try? JSONEncoder().encode(model) - session = MockNetworkSession(data: data, urlResponse: nil, error: nil) - networkManager = NetworkManager(session: session) - } - - override func tearDown() { - super.tearDown() - } - - // MARK: Tests - func testThatNetworkManagerInitCorrect() { - XCTAssertTrue(session === networkManager.session, "session and networkManager.session should be equal") - } - - func testThatDecodingResponseIsCorrect() { - networkManager.fetch(resource, completion: { mock in - XCTAssertEqual(mock, self.model, "mock should be equal to self.mock") - }, fail: { error in - XCTFail(error.localizedDescription) - }) - } - - func testThatDecodingResponseFailedWhenDataIsCorrupt() { - let session = MockNetworkSession(data: nil, urlResponse: nil, error: nil) - let networkManager = NetworkManager(session: session) - - networkManager.fetch(resource, completion: { _ in - XCTFail("Loading ") - }, fail: { error in - if case NetworkError.decodingFailed = error { - XCTAssert(true) - return - } - XCTFail("error should be equal to NetworkError.decodingFailed") - }) - } - - func testThatDecodingResponseFailedWhenErrorOccured() { - let data = try? JSONEncoder().encode(model) - let session = MockNetworkSession(data: data, urlResponse: nil, error: NetworkError.decodingFailed) - let networkManager = NetworkManager(session: session) - - networkManager.fetch(resource, completion: { _ in - XCTFail("Loading ") - }, fail: { error in - if case NetworkError.decodingFailed = error { - XCTAssert(true) - return - } - XCTFail("error should be equal to NetworkError.decodingFailed") - }) - } - - func testThatLoadingFailedWhenAPIResourceParametersIsCorrupt() { - let corruptResource = MockCorruptParametersModel() - - networkManager.fetch(corruptResource, completion: { _ in - XCTFail("Loading with corrupt resource should be failed") - }, fail: { _ in - XCTAssert(true) - }) - } - - func testThatLoadingFailedWhenAPIResourceParametersWithHeaderIsCorrupt() { - let corruptResource = MockCorruptParametersWithHeaderResource() - - networkManager.fetch(corruptResource, completion: { _ in - XCTFail("Loading with corrupt resource should be failed") - }, fail: { _ in - XCTAssert(true) - }) - } -} diff --git a/Features/NetworkManager/NetworkManagerTests/Tests/Request/APIRequestTests.swift b/Features/NetworkManager/NetworkManagerTests/Tests/Request/APIRequestTests.swift deleted file mode 100644 index 0e41947..0000000 --- a/Features/NetworkManager/NetworkManagerTests/Tests/Request/APIRequestTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// APIRequestTests.swift -// NetworkManagerTests -// -// Created by Никита Васильев on 18.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest -@testable import NetworkManager - -class APIRequestTests: XCTestCase { - - // MARK: Private Properties - private let resource = MockResource() - private var apiRequest: APIRequest! - - // MARK: Initialization - override func setUp() { - super.setUp() - apiRequest = APIRequest(resource: resource) - } - - override func tearDown() { - super.tearDown() - } - - // MARK: Tests - func testThatAPIRequestInitCorrect() { - XCTAssertEqual(resource, apiRequest.resource, "resource in apiRequest should be equal to resource") - } - - func testThatURLRequestBuildSuccessfully() throws { - let request = try XCTUnwrap(apiRequest.buildRequest()) - let fullURL = try XCTUnwrap(request.url, "urlRequest url is nil") - let resourceURL = resource.baseURL.appendingPathComponent(resource.path) - XCTAssertEqual(fullURL.absoluteString.sorted(), resourceURL.absoluteString.sorted(), "request url is not correct") - } -} diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..c0d2f61 --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source 'https://rubygems.org' + +gem "dotenv" +gem "fastlane" +gem "slather" + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) \ No newline at end of file diff --git a/HackerNews.xcodeproj/project.pbxproj b/HackerNews.xcodeproj/project.pbxproj deleted file mode 100644 index 4d452e0..0000000 --- a/HackerNews.xcodeproj/project.pbxproj +++ /dev/null @@ -1,3293 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 52; - objects = { - -/* Begin PBXBuildFile section */ - 0D1603CAFAEA30A156862ED1 /* SettingsConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DB1BC9C41A88EF3EA1E8B3 /* SettingsConfiguratorTests.swift */; }; - 0E95E3FEBFD9EFBA7A696CFD /* ThemeModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D7F843BFB3D0ECBFC7E16D /* ThemeModuleInput.swift */; }; - 0EC6C9C75C06A1962156C50B /* Pods_HackerNews.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F10F7222A47FB417E6C4588 /* Pods_HackerNews.framework */; }; - 106C017560B2C1FF27A88E13 /* CommentsPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B08574C37377B86D4DD66004 /* CommentsPresenterTests.swift */; }; - 11247932183A52868E71490D /* ThemeViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C822CF4F21F2BE3AD24BF1 /* ThemeViewTests.swift */; }; - 1735864EEEEE601B855C3A57 /* ThemeAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020C7913C10E5D047B6F6F12 /* ThemeAssembly.swift */; }; - 1AFA6585B5891D01D02A3D1F /* SettingsPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 481BE5B37B60EBEA91C05322 /* SettingsPresenterTests.swift */; }; - 1B478549D7C1F1E80DD61905 /* SettingsRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880050B23E0763C56310A265 /* SettingsRouterTests.swift */; }; - 223CB8777BF3B1F04F665350 /* StoriesPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B0973524E7C669B5C27E93D /* StoriesPresenterTests.swift */; }; - 227D42543579B790BDF32914 /* CommentsModuleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADB423B936F932A5F5C43F9F /* CommentsModuleInput.swift */; }; - 23B60C899980EFD06CDBEFE6 /* CommentsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48D032C79286B642547280DF /* CommentsRouter.swift */; }; - 2B5AC3B732D57493E958B015 /* ThemeInteractorInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF23845D0D60ECAF198876E /* ThemeInteractorInput.swift */; }; - 2C5287CA0F51E7CC0998F056 /* StoriesRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75288D891967908B4B1CF34C /* StoriesRouter.swift */; }; - 37B144B44710E04D53D38B1F /* SettingsViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756C7453871B7DB5374C91D0 /* SettingsViewInput.swift */; }; - 384F0821219E07D30029ADDD /* Alertable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384F0820219E07D30029ADDD /* Alertable.swift */; }; - 385B495311D6CBD5EB3F44E7 /* SettingsInteractorInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84559136E2CC1694F565A308 /* SettingsInteractorInput.swift */; }; - 3867CB6F215FB264003C0658 /* Date+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3867CB6E215FB264003C0658 /* Date+.swift */; }; - 38AB0BDD12801ED62ABDAA88 /* ThemePresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABC193D2B780816BABAF6D7 /* ThemePresenterTests.swift */; }; - 38F7969A2198A2710083EFEF /* String+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F796992198A2710083EFEF /* String+.swift */; }; - 3C63C91DAA87693F4AF304FD /* StoriesConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7D49B681E9B168FE7B4F96 /* StoriesConfiguratorTests.swift */; }; - 41A294D7045C082B25DA58AF /* CommentsAssemblyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E57EB5317778D5553A2E21B9 /* CommentsAssemblyTests.swift */; }; - 42DDF700C568557D9721B39E /* ThemeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE4653D7772D620D3CBF1B1 /* ThemeRouter.swift */; }; - 437795427873CF1A213CABF6 /* SettingsInteractorOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1949A91778A123B5EF93CC54 /* SettingsInteractorOutput.swift */; }; - 4D878B9B487FDD6CA0B50886 /* ThemeRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30051C967D26D38780E7A57 /* ThemeRouterTests.swift */; }; - 4EFF808EC08E9CCA2D33FC20 /* Theme.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C923B096F47F2D687D4445C3 /* Theme.storyboard */; }; - 4F95CC6EFE3DB85351335A0A /* CommentsRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E089D733E432016A28791194 /* CommentsRouterTests.swift */; }; - 4FBF586447B5D27D3FFE8695 /* StoriesViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C54504D79E3203454D65767 /* StoriesViewInput.swift */; }; - 53E3C81A7457949576309607 /* SettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A682BDFD6027AEE7D726F878 /* SettingsInteractor.swift */; }; - 5A00D8A2244655560013B3F5 /* Themeable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A00D8A1244655560013B3F5 /* Themeable.swift */; }; - 5A030954245E1C900004D669 /* SkeletonCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A030953245E1C900004D669 /* SkeletonCellViewModel.swift */; }; - 5A0D25DB248BF42F006718D4 /* MainTabBarPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25DA248BF42F006718D4 /* MainTabBarPresenterTests.swift */; }; - 5A0D25DD248BF46F006718D4 /* MainTabBarAssemblyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25DC248BF46F006718D4 /* MainTabBarAssemblyTests.swift */; }; - 5A0D25DF248BF5A8006718D4 /* MainTabBarConfuguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25DE248BF5A8006718D4 /* MainTabBarConfuguratorTests.swift */; }; - 5A0D25E1248BF606006718D4 /* MainTabBarViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25E0248BF606006718D4 /* MainTabBarViewTests.swift */; }; - 5A0D25E4248BF969006718D4 /* ThemeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25E3248BF969006718D4 /* ThemeManagerTests.swift */; }; - 5A0D25E7248C084A006718D4 /* WeakObjectSetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25E6248C084A006718D4 /* WeakObjectSetTests.swift */; }; - 5A0D25EA248C09C2006718D4 /* WeakObjectSetPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25E9248C09C2006718D4 /* WeakObjectSetPerformanceTests.swift */; }; - 5A0D25F2248C190D006718D4 /* MockThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A0D25F1248C190D006718D4 /* MockThemeManager.swift */; }; - 5A116C7924A2927B00E27834 /* NetworkManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AF495A1249D5E2300E469A2 /* NetworkManager.framework */; }; - 5A116C7A24A2927B00E27834 /* NetworkManager.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5AF495A1249D5E2300E469A2 /* NetworkManager.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 5A14BC5624899CAE008E9B7B /* MockContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A14BC5524899CAE008E9B7B /* MockContainer.swift */; }; - 5A1CA33624AD3B4B006EA300 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5A1CA33524AD3B4B006EA300 /* GoogleService-Info.plist */; }; - 5A25EF3A2455870900AC7C56 /* BaseCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A25EF392455870900AC7C56 /* BaseCellModel.swift */; }; - 5A25EF3C2455888700AC7C56 /* PostCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A25EF3B2455888700AC7C56 /* PostCellViewModel.swift */; }; - 5A25EF3E2455894A00AC7C56 /* CommentCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A25EF3D2455894A00AC7C56 /* CommentCellViewModel.swift */; }; - 5A435C0B243A27F5009C5355 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A435C0A243A27F5009C5355 /* ThemeManager.swift */; }; - 5A435C0D243A286A009C5355 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A435C0C243A286A009C5355 /* Theme.swift */; }; - 5A435C0F243A2E0B009C5355 /* Stylesheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A435C0E243A2E0B009C5355 /* Stylesheet.swift */; }; - 5A435C11243A2E34009C5355 /* Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A435C10243A2E34009C5355 /* Style.swift */; }; - 5A43892F247F10EC002D3111 /* WeakObjectSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A43892E247F10EC002D3111 /* WeakObjectSet.swift */; }; - 5A438931247F110B002D3111 /* WeakObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A438930247F110B002D3111 /* WeakObject.swift */; }; - 5A4D0B13249FA0090076A781 /* CommentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A4D0B12249FA0090076A781 /* CommentCell.swift */; }; - 5A4D0B15249FA0670076A781 /* CommentsInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A4D0B14249FA0670076A781 /* CommentsInterfaceController.swift */; }; - 5A4D0B18249FA9E20076A781 /* String+Decode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A4D0B17249FA9E20076A781 /* String+Decode.swift */; }; - 5A4D0B1D249FAF140076A781 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5A4D0B1F249FAF140076A781 /* Localizable.strings */; }; - 5A4D0B22249FAF560076A781 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A4D0B21249FAF560076A781 /* String+Localized.swift */; }; - 5A4D0B28249FBA3B0076A781 /* HNService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A4D0B27249FBA3B0076A781 /* HNService.framework */; }; - 5A4D0B29249FBA3B0076A781 /* HNService.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5A4D0B27249FBA3B0076A781 /* HNService.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 5A4D0B31249FC23D0076A781 /* HNService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A4D0B30249FC23D0076A781 /* HNService.framework */; }; - 5A5177CF24C4D289005856C4 /* StoryType+AllValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5177CE24C4D289005856C4 /* StoryType+AllValues.swift */; }; - 5A52212124479D01003B799D /* ThemeSelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A52212024479D00003B799D /* ThemeSelectableTableViewCell.swift */; }; - 5A52212324479D1B003B799D /* ThemeSelectableTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5A52212224479D1B003B799D /* ThemeSelectableTableViewCell.xib */; }; - 5A5C0AA0241ECB7D007B4FCF /* ServiceLocatorConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5C0A9F241ECB7D007B4FCF /* ServiceLocatorConfigurator.swift */; }; - 5A5C0AA2241ECBF9007B4FCF /* ServiceLocatorAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A5C0AA1241ECBF9007B4FCF /* ServiceLocatorAssembly.swift */; }; - 5A6FF6C324182BDF003F9B9D /* StoriesConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A6FF6C224182BDF003F9B9D /* StoriesConfigurator.swift */; }; - 5A7347E5242E9549005B394F /* SettingsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A7347E4242E9549005B394F /* SettingsTableViewCell.swift */; }; - 5A7347E7242E9562005B394F /* SettingsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5A7347E6242E9562005B394F /* SettingsTableViewCell.xib */; }; - 5A7347EB242EA926005B394F /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A7347EA242EA926005B394F /* SettingsManager.swift */; }; - 5A8054E9244CD98B005E4F96 /* PostTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8054E8244CD98B005E4F96 /* PostTableViewCell.swift */; }; - 5A8054EB244CD9A0005E4F96 /* PostTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5A8054EA244CD9A0005E4F96 /* PostTableViewCell.xib */; }; - 5A8BF1682449ABAB00215883 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5A8BF16A2449ABAB00215883 /* Localizable.strings */; }; - 5A8BF16E2449AC3F00215883 /* String+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8BF16D2449AC3F00215883 /* String+Localized.swift */; }; - 5A8BF1752449D78900215883 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8BF1742449D78900215883 /* RootSplitViewController.swift */; }; - 5A8BF1772449D79600215883 /* RootViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8BF1762449D79600215883 /* RootViewInput.swift */; }; - 5A8BF1792449D7A500215883 /* RootViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8BF1782449D7A500215883 /* RootViewOutput.swift */; }; - 5A8BF17C2449D7BF00215883 /* RootPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8BF17B2449D7BF00215883 /* RootPresenter.swift */; }; - 5A8BF1902449F00E00215883 /* SettingsListData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8BF18F2449F00E00215883 /* SettingsListData.swift */; }; - 5A8F6B052499722000484C18 /* NotificationCenter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A8F6B042499722000484C18 /* NotificationCenter.framework */; }; - 5A8F6B082499722000484C18 /* TopStoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A8F6B072499722000484C18 /* TopStoriesViewController.swift */; }; - 5A8F6B0B2499722000484C18 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5A8F6B092499722000484C18 /* MainInterface.storyboard */; }; - 5A8F6B0F2499722000484C18 /* HackerNewsTodayExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5A8F6B032499722000484C18 /* HackerNewsTodayExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 5A94A2E92439383D00918198 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A94A2E82439383D00918198 /* Constants.swift */; }; - 5A9C965224425B4A002AC0F5 /* Poppins-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C963E24425B2F002AC0F5 /* Poppins-Bold.ttf */; }; - 5A9C965424425B4A002AC0F5 /* Poppins-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C964324425B30002AC0F5 /* Poppins-ExtraBold.ttf */; }; - 5A9C965624425B4A002AC0F5 /* Poppins-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C964C24425B32002AC0F5 /* Poppins-ExtraLight.ttf */; }; - 5A9C965824425B4A002AC0F5 /* Poppins-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C964924425B31002AC0F5 /* Poppins-Italic.ttf */; }; - 5A9C965924425B4A002AC0F5 /* Poppins-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C964424425B30002AC0F5 /* Poppins-Light.ttf */; }; - 5A9C965B24425B4A002AC0F5 /* Poppins-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C963F24425B2F002AC0F5 /* Poppins-Medium.ttf */; }; - 5A9C965D24425B4A002AC0F5 /* Poppins-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C964B24425B32002AC0F5 /* Poppins-Regular.ttf */; }; - 5A9C965E24425B4A002AC0F5 /* Poppins-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 5A9C964524425B30002AC0F5 /* Poppins-SemiBold.ttf */; }; - 5A9C966324425C69002AC0F5 /* FontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9C966224425C69002AC0F5 /* FontStyle.swift */; }; - 5A9C966524425D07002AC0F5 /* Fonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9C966424425D07002AC0F5 /* Fonts.swift */; }; - 5A9C966724425E35002AC0F5 /* Font+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9C966624425E35002AC0F5 /* Font+.swift */; }; - 5A9C966D2442608E002AC0F5 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5A9C966C2442608E002AC0F5 /* Colors.swift */; }; - 5AA02BEA244F658400B5B15F /* String+Attributed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AA02BE9244F658400B5B15F /* String+Attributed.swift */; }; - 5AAC707D249024BC005AFFC4 /* UITest+Helprers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC707C249024BC005AFFC4 /* UITest+Helprers.swift */; }; - 5AAC707F2490252C005AFFC4 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC707E2490252C005AFFC4 /* App.swift */; }; - 5AAC708224902C26005AFFC4 /* UITestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC708124902C26005AFFC4 /* UITestFactory.swift */; }; - 5AAC708524902CCC005AFFC4 /* SettingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC708424902CCC005AFFC4 /* SettingScreen.swift */; }; - 5AAC708724902D4B005AFFC4 /* SettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC708624902D4B005AFFC4 /* SettingsUITests.swift */; }; - 5AAC708924902F88005AFFC4 /* ThemesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC708824902F88005AFFC4 /* ThemesScreen.swift */; }; - 5AAC708B24903190005AFFC4 /* StoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC708A24903190005AFFC4 /* StoryScreen.swift */; }; - 5AAC708E249035F1005AFFC4 /* SettingsShapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC708D249035F1005AFFC4 /* SettingsShapshotTests.swift */; }; - 5AAC709324903DFF005AFFC4 /* UIViewController+Safe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC709224903DFE005AFFC4 /* UIViewController+Safe.swift */; }; - 5AAC9112245090C60041006F /* Array+Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC9111245090C60041006F /* Array+Range.swift */; }; - 5AAC91202450E8830041006F /* HNImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC911F2450E8830041006F /* HNImageView.swift */; }; - 5AAC91232450E89F0041006F /* SkeletonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AAC91222450E89F0041006F /* SkeletonCell.swift */; }; - 5AAC91252450E8A90041006F /* SkeletonCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5AAC91242450E8A90041006F /* SkeletonCell.xib */; }; - 5AB1139B2456253700332B5F /* LoadingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AB1139A2456253700332B5F /* LoadingFooterView.swift */; }; - 5AB25D8D2451ADD2009C05ED /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AB25D8C2451ADD0009C05ED /* UIViewController+.swift */; }; - 5AC0CC272454710E0060F5C9 /* CommentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC0CC262454710E0060F5C9 /* CommentCell.swift */; }; - 5AC0CC29245472610060F5C9 /* CommentCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5AC0CC28245472610060F5C9 /* CommentCell.xib */; }; - 5AC10D20248AE7D000132EB4 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D1F248AE7D000132EB4 /* TestData.swift */; }; - 5AC10D23248AE98B00132EB4 /* PostModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D22248AE98B00132EB4 /* PostModelTests.swift */; }; - 5AC10D27248AEC2400132EB4 /* Data+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D26248AEC2400132EB4 /* Data+.swift */; }; - 5AC10D29248AF52F00132EB4 /* CommentModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D28248AF52F00132EB4 /* CommentModelTests.swift */; }; - 5AC10D2B248AF65F00132EB4 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D2A248AF65F00132EB4 /* Utils.swift */; }; - 5AC10D32248AF71000132EB4 /* RootPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D31248AF71000132EB4 /* RootPresenterTests.swift */; }; - 5AC10D35248AF82900132EB4 /* RootAssemblyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D34248AF82900132EB4 /* RootAssemblyTests.swift */; }; - 5AC10D38248AF90300132EB4 /* RootConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D37248AF90300132EB4 /* RootConfiguratorTests.swift */; }; - 5AC10D3B248AFA3D00132EB4 /* RootViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC10D3A248AFA3D00132EB4 /* RootViewTests.swift */; }; - 5AC892B0248D5C6F00AFC5AC /* HNServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC892AF248D5C6F00AFC5AC /* HNServiceMock.swift */; }; - 5AC892B2248D797300AFC5AC /* UnitTestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AC892B1248D797300AFC5AC /* UnitTestError.swift */; }; - 5ACA6C472454E2C2007E89BF /* Queue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACA6C462454E2C2007E89BF /* Queue.swift */; }; - 5ACB43D424484BA00010465B /* ThemeUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACB43D324484BA00010465B /* ThemeUpdatable.swift */; }; - 5ACB43D9244855F30010465B /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACB43D8244855F30010465B /* Weak.swift */; }; - 5ACB43DB244862500010465B /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACB43DA244862500010465B /* ImageType.swift */; }; - 5ACB43DD244883E90010465B /* Optional+Throwable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACB43DC244883E90010465B /* Optional+Throwable.swift */; }; - 5ACB43DF244886E30010465B /* MainTabBarViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACB43DE244886E30010465B /* MainTabBarViewOutput.swift */; }; - 5ACB43E22448872A0010465B /* MainTabBarPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACB43E12448872A0010465B /* MainTabBarPresenter.swift */; }; - 5ACB43E42448875C0010465B /* MainTabBarViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ACB43E32448875C0010465B /* MainTabBarViewInput.swift */; }; - 5AD8D59624532F6100D7E1F1 /* Comments.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5AD8D59524532F6100D7E1F1 /* Comments.storyboard */; }; - 5AE2EDCD24103C9B0011A198 /* RootModuleAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDCC24103C9B0011A198 /* RootModuleAssembly.swift */; }; - 5AE2EDD024103CDB0011A198 /* RootConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDCF24103CDB0011A198 /* RootConfigurator.swift */; }; - 5AE2EDD424103EED0011A198 /* MainTabBarConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDD324103EED0011A198 /* MainTabBarConfigurator.swift */; }; - 5AE2EDD724103F1D0011A198 /* MainTabBarAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDD624103F1D0011A198 /* MainTabBarAssembly.swift */; }; - 5AE2EDDA241040420011A198 /* TabBarViewProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDD9241040420011A198 /* TabBarViewProtocol.swift */; }; - 5AE2EDDD241040A40011A198 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDDC241040A40011A198 /* MainTabBarViewController.swift */; }; - 5AE2EDE0241045D60011A198 /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDDF241045D60011A198 /* Presentable.swift */; }; - 5AE2EDE2241046210011A198 /* UINavigationController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDE1241046210011A198 /* UINavigationController+.swift */; }; - 5AE2EDE4241046730011A198 /* TransitionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDE3241046730011A198 /* TransitionHandler.swift */; }; - 5AE2EDE8241047660011A198 /* R.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE2EDE7241047650011A198 /* R.generated.swift */; }; - 5AE95B682461F49C00DAA009 /* UIView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE95B672461F49C00DAA009 /* UIView+.swift */; }; - 5AEE9ADC249A851900929A85 /* TopStoryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9ADB249A851900929A85 /* TopStoryTableViewCell.swift */; }; - 5AEE9ADE249A852F00929A85 /* TopStoryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5AEE9ADD249A852F00929A85 /* TopStoryTableViewCell.xib */; }; - 5AEE9BC6249AB22500929A85 /* RateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEE9BC5249AB22500929A85 /* RateView.swift */; }; - 5AF4954F249D5CC000E469A2 /* HackerNewsWatch WatchKit App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 5AF4954E249D5CC000E469A2 /* HackerNewsWatch WatchKit App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 5AF49555249D5CC000E469A2 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5AF49553249D5CC000E469A2 /* Interface.storyboard */; }; - 5AF49557249D5CC500E469A2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5AF49556249D5CC500E469A2 /* Assets.xcassets */; }; - 5AF4955E249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 5AF4955D249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 5AF49565249D5CC500E469A2 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF49564249D5CC500E469A2 /* ExtensionDelegate.swift */; }; - 5AF49567249D5CC800E469A2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5AF49566249D5CC800E469A2 /* Assets.xcassets */; }; - 5AF495A2249D5E2300E469A2 /* NetworkManager.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AF495A1249D5E2300E469A2 /* NetworkManager.framework */; }; - 5AF495A3249D5E2300E469A2 /* NetworkManager.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5AF495A1249D5E2300E469A2 /* NetworkManager.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 5AF495C1249D600800E469A2 /* TopStoriesInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF495C0249D600800E469A2 /* TopStoriesInterfaceController.swift */; }; - 5AF495C4249D642100E469A2 /* PostCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AF495C3249D642100E469A2 /* PostCell.swift */; }; - 5CBE8D8383348E881F3F800A /* CommentsConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4E29D3A2F2C24826F905C2A /* CommentsConfigurator.swift */; }; - 5D7937310DC67C2D0A5D7B26 /* SettingsViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AECF0419410CC923AC4242E /* SettingsViewOutput.swift */; }; - 6D4A7BBE429B8E09377B7DE7 /* CommentsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC457690D01D25C8B43C69EE /* CommentsPresenter.swift */; }; - 6F86307F7A43F5636A2A8702 /* SettingsInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D636BD0FA1E64C87B4348CAC /* SettingsInteractorTests.swift */; }; - 71D3EE6174A29CCC96AF27FC /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C8FD75009999F70D8BED054F /* Settings.storyboard */; }; - 74A73D457753C052AE5B8A6F /* SettingsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C4B2F3CE40C5B4BBB8DFDD /* SettingsRouter.swift */; }; - 7618E63336284456181FC902 /* ThemePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88D4AD8613343B832D8C6308 /* ThemePresenter.swift */; }; - 79C0892943C9CCCC93DE4683 /* ThemeInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9357ED5A64187C76057B33B /* ThemeInteractorTests.swift */; }; - 7ACEA30CDD9C2EC5E4304163 /* StoriesRouterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98EAC67E1A9739CA65400E95 /* StoriesRouterTests.swift */; }; - 7BB7FDA10247EA18C604DBC2 /* CommentsViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06783D5310AB3073753EB966 /* CommentsViewTests.swift */; }; - 7C61D1FC15958476BCBDB865 /* CommentsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E8726255A3D84E5CBFC402A /* CommentsInteractor.swift */; }; - 7C9258D74C6FC05A982ECDAE /* ThemeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFB1FA6087E3BE89323CEB /* ThemeViewController.swift */; }; - 7E61868B4DA2FA7FB7E7C37C /* CommentsViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FEF91989794A3C7AC72B9FF /* CommentsViewOutput.swift */; }; - 7EC5DDFA7D277736127F645F /* CommentsConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4180EE33349724472CC83552 /* CommentsConfiguratorTests.swift */; }; - 8058C73658389F9105345824 /* CommentsInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587E7F5215B3B2B613C7AFAF /* CommentsInteractorTests.swift */; }; - 80A064256ECA8ACEA6CAB352 /* SettingsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86BAEAE1E6A9BDB269B528B7 /* SettingsAssembly.swift */; }; - 841F0F20A6C107848D230D68 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173DA34D18916D6FCA182FA3 /* SettingsViewController.swift */; }; - 85FD344F5BC1934F615CFAC9 /* SettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD6806965BB5158014D0AAB /* SettingsPresenter.swift */; }; - 87235FA1D54F1BBD55496993 /* StoriesViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25768042F1949ED23D54DF4 /* StoriesViewTests.swift */; }; - 8AA0F3452ED1C28896F8CDA8 /* ThemeViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 254B44A617EBDBDEC696224C /* ThemeViewInput.swift */; }; - 9217DDCCAA0BF7EACAFA0B62 /* StoriesInteractorInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5688482AF43266B89E7A0A7B /* StoriesInteractorInput.swift */; }; - 946BE207880921E25E93302B /* CommentsRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8400E6F8B807CF3B0E7A117 /* CommentsRouterInput.swift */; }; - 96808C88C39A94F772EAE01F /* ThemeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0E27AFCA2403A03ED302C2C /* ThemeInteractor.swift */; }; - 99FF3F4214BEF893002901F4 /* CommentsViewInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BE2E7CCBA31084C76C3274C /* CommentsViewInput.swift */; }; - A2973A58E5B273EFDDA3199A /* StoriesRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58FA4A86F5D21BC625349CD /* StoriesRouterInput.swift */; }; - A583AFD99ACCED9C4F18E5C8 /* CommentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F739F4CAB4001886132AD3C7 /* CommentsViewController.swift */; }; - AA1B09AF7685666005E3B301 /* ThemeInteractorOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = D349D0CD9C2060043398C027 /* ThemeInteractorOutput.swift */; }; - AAE00CC387A3A7599BFDA77F /* CommentsAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = B579C0416DDA2AD457DC8042 /* CommentsAssembly.swift */; }; - B29F0F598619422FE8A87738 /* CommentsInteractorOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCEE42028E4D75974DFEBFB7 /* CommentsInteractorOutput.swift */; }; - B2A18F8423F3459CE46FDCE1 /* StoriesInteractorOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EED532098A8A2AF98ED6E80 /* StoriesInteractorOutput.swift */; }; - B375711E66BCACF11B39AFFF /* SettingsRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9694DB55717A8BA99A82C027 /* SettingsRouterInput.swift */; }; - B7A71D6D5CC68FEAB62EC9C3 /* ThemeConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B757722416640AE5DA2D5B6 /* ThemeConfiguratorTests.swift */; }; - BCBA5819B2598079B48DDA9C /* ThemeConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C08F6F4829CAEB8EF67D15A /* ThemeConfigurator.swift */; }; - C72C67A3801D1099E2E78982 /* StoriesAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A4FDD125295BD464D56161 /* StoriesAssembly.swift */; }; - CB02B826066E9E278B544AB0 /* StoriesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD9175C3D9C3474F09BEE4F /* StoriesViewController.swift */; }; - CB877EC84D37205766B4EC6D /* Pods_HackerNewsTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BAF3412A178EA5EB164E53D3 /* Pods_HackerNewsTests.framework */; }; - CFB9F0778AD778B3121CC343 /* ThemeAssemblyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559CCB6107A00A4B58CBC7A5 /* ThemeAssemblyTests.swift */; }; - D07204F421309FC600E4B1ED /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07204F321309FC600E4B1ED /* AppDelegate.swift */; }; - D07204FB21309FC900E4B1ED /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D07204FA21309FC900E4B1ED /* Assets.xcassets */; }; - D07204FE21309FC900E4B1ED /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D07204FC21309FC900E4B1ED /* LaunchScreen.storyboard */; }; - D072051421309FC900E4B1ED /* HackerNewsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072051321309FC900E4B1ED /* HackerNewsUITests.swift */; }; - D07205252130A09C00E4B1ED /* Color+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07205242130A09C00E4B1ED /* Color+.swift */; }; - D07205272130A0AF00E4B1ED /* UIImage+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07205262130A0AF00E4B1ED /* UIImage+.swift */; }; - D07205292130A10400E4B1ED /* NibLoadableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07205282130A10400E4B1ED /* NibLoadableView.swift */; }; - D072052B2130A11A00E4B1ED /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072052A2130A11A00E4B1ED /* ReusableView.swift */; }; - D072052D2130A13800E4B1ED /* UITableView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072052C2130A13800E4B1ED /* UITableView+.swift */; }; - D072052F2130A15200E4B1ED /* UITableViewCell+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D072052E2130A15200E4B1ED /* UITableViewCell+.swift */; }; - D07205312130A17100E4B1ED /* UIViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07205302130A17100E4B1ED /* UIViewControllerFactory.swift */; }; - D078561297ACC3EAD4FC08EA /* StoriesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCED8BB0967B1933D8335489 /* StoriesPresenter.swift */; }; - D12DE9103766E9D49A9F2B37 /* SettingsViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C322EE9386899B3C63540FAD /* SettingsViewTests.swift */; }; - D266EC5FA8619EC9119612B6 /* CommentsInteractorInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78C4FBC5B114C6A6B02D7E43 /* CommentsInteractorInput.swift */; }; - D6F2CD32688DA91CBA8DF3F6 /* Pods_HackerNewsUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 941A683CEF3511E341FC3BDA /* Pods_HackerNewsUITests.framework */; }; - D746902D625B40CF3177A9A3 /* StoriesViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5263AF6EF05EEDA880DA524F /* StoriesViewOutput.swift */; }; - E4D5B4ABDCD0124D7352915F /* ThemeViewOutput.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6AE570ED443C0AA530DDBC6 /* ThemeViewOutput.swift */; }; - EDE03E5231E7355B1352B22D /* StoriesAssemblyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C465F457451BB047F53DF5D7 /* StoriesAssemblyTests.swift */; }; - EF0A179C2312F9F08DD9B53B /* SettingsAssemblyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B05D558803A1AD4B7FABDB /* SettingsAssemblyTests.swift */; }; - EF595E98A7E35C576C647499 /* StoriesInteractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD3B924ABCD34C1BE81504C /* StoriesInteractorTests.swift */; }; - F5A0C84E03E8AD683E2A10AB /* ThemeRouterInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CC01CE435879B2D5F78B13 /* ThemeRouterInput.swift */; }; - F82094BAB4970E016398315E /* SettingsConfigurator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86A1D9B7E0433BA1D3C63F7D /* SettingsConfigurator.swift */; }; - FAEE86FF44686D0C9C73A091 /* StoriesInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A76C4A1BBE4B4F8C6FCEC3 /* StoriesInteractor.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 5A8F6B0D2499722000484C18 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D07204E821309FC600E4B1ED /* Project object */; - proxyType = 1; - remoteGlobalIDString = 5A8F6B022499722000484C18; - remoteInfo = HackerNewsTodayExtension; - }; - 5AF49550249D5CC000E469A2 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D07204E821309FC600E4B1ED /* Project object */; - proxyType = 1; - remoteGlobalIDString = 5AF4954D249D5CC000E469A2; - remoteInfo = "HackerNewsWatch WatchKit App"; - }; - 5AF4955F249D5CC500E469A2 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D07204E821309FC600E4B1ED /* Project object */; - proxyType = 1; - remoteGlobalIDString = 5AF4955C249D5CC500E469A2; - remoteInfo = "HackerNewsWatch WatchKit Extension"; - }; - D072050521309FC900E4B1ED /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D07204E821309FC600E4B1ED /* Project object */; - proxyType = 1; - remoteGlobalIDString = D07204EF21309FC600E4B1ED; - remoteInfo = HackerNews; - }; - D072051021309FC900E4B1ED /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = D07204E821309FC600E4B1ED /* Project object */; - proxyType = 1; - remoteGlobalIDString = D07204EF21309FC600E4B1ED; - remoteInfo = HackerNews; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 5A8F6B102499722000484C18 /* Embed App Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 5A8F6B0F2499722000484C18 /* HackerNewsTodayExtension.appex in Embed App Extensions */, - ); - name = "Embed App Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF4956C249D5CC800E469A2 /* Embed App Extensions */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 13; - files = ( - 5AF4955E249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension.appex in Embed App Extensions */, - ); - name = "Embed App Extensions"; - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF49570249D5CC800E469A2 /* Embed Watch Content */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(CONTENTS_FOLDER_PATH)/Watch"; - dstSubfolderSpec = 16; - files = ( - 5AF4954F249D5CC000E469A2 /* HackerNewsWatch WatchKit App.app in Embed Watch Content */, - ); - name = "Embed Watch Content"; - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF495A4249D5E2300E469A2 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 5AF495A3249D5E2300E469A2 /* NetworkManager.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - D02555AC213C6F7400822BFE /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 5A4D0B29249FBA3B0076A781 /* HNService.framework in Embed Frameworks */, - 5A116C7A24A2927B00E27834 /* NetworkManager.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 00C822CF4F21F2BE3AD24BF1 /* ThemeViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeViewTests.swift; sourceTree = ""; }; - 020C7913C10E5D047B6F6F12 /* ThemeAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeAssembly.swift; sourceTree = ""; }; - 06783D5310AB3073753EB966 /* CommentsViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsViewTests.swift; sourceTree = ""; }; - 173DA34D18916D6FCA182FA3 /* SettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; - 1949A91778A123B5EF93CC54 /* SettingsInteractorOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsInteractorOutput.swift; sourceTree = ""; }; - 1E8726255A3D84E5CBFC402A /* CommentsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsInteractor.swift; sourceTree = ""; }; - 254B44A617EBDBDEC696224C /* ThemeViewInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeViewInput.swift; sourceTree = ""; }; - 2BD9175C3D9C3474F09BEE4F /* StoriesViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesViewController.swift; sourceTree = ""; }; - 2BE2E7CCBA31084C76C3274C /* CommentsViewInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsViewInput.swift; sourceTree = ""; }; - 2C08F6F4829CAEB8EF67D15A /* ThemeConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeConfigurator.swift; sourceTree = ""; }; - 384F0820219E07D30029ADDD /* Alertable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alertable.swift; sourceTree = ""; }; - 3867CB6E215FB264003C0658 /* Date+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+.swift"; sourceTree = ""; }; - 38F796992198A2710083EFEF /* String+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+.swift"; sourceTree = ""; }; - 3B757722416640AE5DA2D5B6 /* ThemeConfiguratorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeConfiguratorTests.swift; sourceTree = ""; }; - 3F2AA6A6FB80106F288FEE85 /* Pods-HackerNews.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HackerNews.debug.xcconfig"; path = "Target Support Files/Pods-HackerNews/Pods-HackerNews.debug.xcconfig"; sourceTree = ""; }; - 4180EE33349724472CC83552 /* CommentsConfiguratorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsConfiguratorTests.swift; sourceTree = ""; }; - 481BE5B37B60EBEA91C05322 /* SettingsPresenterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsPresenterTests.swift; sourceTree = ""; }; - 48D032C79286B642547280DF /* CommentsRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsRouter.swift; sourceTree = ""; }; - 5263AF6EF05EEDA880DA524F /* StoriesViewOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesViewOutput.swift; sourceTree = ""; }; - 54EFB1FA6087E3BE89323CEB /* ThemeViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeViewController.swift; sourceTree = ""; }; - 559CCB6107A00A4B58CBC7A5 /* ThemeAssemblyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeAssemblyTests.swift; sourceTree = ""; }; - 5688482AF43266B89E7A0A7B /* StoriesInteractorInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesInteractorInput.swift; sourceTree = ""; }; - 587E7F5215B3B2B613C7AFAF /* CommentsInteractorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsInteractorTests.swift; sourceTree = ""; }; - 5A00D8A1244655560013B3F5 /* Themeable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Themeable.swift; sourceTree = ""; }; - 5A030953245E1C900004D669 /* SkeletonCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCellViewModel.swift; sourceTree = ""; }; - 5A0D25DA248BF42F006718D4 /* MainTabBarPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarPresenterTests.swift; sourceTree = ""; }; - 5A0D25DC248BF46F006718D4 /* MainTabBarAssemblyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarAssemblyTests.swift; sourceTree = ""; }; - 5A0D25DE248BF5A8006718D4 /* MainTabBarConfuguratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarConfuguratorTests.swift; sourceTree = ""; }; - 5A0D25E0248BF606006718D4 /* MainTabBarViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewTests.swift; sourceTree = ""; }; - 5A0D25E3248BF969006718D4 /* ThemeManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManagerTests.swift; sourceTree = ""; }; - 5A0D25E6248C084A006718D4 /* WeakObjectSetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakObjectSetTests.swift; sourceTree = ""; }; - 5A0D25E9248C09C2006718D4 /* WeakObjectSetPerformanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakObjectSetPerformanceTests.swift; sourceTree = ""; }; - 5A0D25F1248C190D006718D4 /* MockThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockThemeManager.swift; sourceTree = ""; }; - 5A14BC5524899CAE008E9B7B /* MockContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockContainer.swift; sourceTree = ""; }; - 5A1CA33524AD3B4B006EA300 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 5A25EF392455870900AC7C56 /* BaseCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCellModel.swift; sourceTree = ""; }; - 5A25EF3B2455888700AC7C56 /* PostCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCellViewModel.swift; sourceTree = ""; }; - 5A25EF3D2455894A00AC7C56 /* CommentCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCellViewModel.swift; sourceTree = ""; }; - 5A435C0A243A27F5009C5355 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; - 5A435C0C243A286A009C5355 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; - 5A435C0E243A2E0B009C5355 /* Stylesheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stylesheet.swift; sourceTree = ""; }; - 5A435C10243A2E34009C5355 /* Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Style.swift; sourceTree = ""; }; - 5A43892E247F10EC002D3111 /* WeakObjectSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakObjectSet.swift; sourceTree = ""; }; - 5A438930247F110B002D3111 /* WeakObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeakObject.swift; sourceTree = ""; }; - 5A4D0B12249FA0090076A781 /* CommentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCell.swift; sourceTree = ""; }; - 5A4D0B14249FA0670076A781 /* CommentsInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsInterfaceController.swift; sourceTree = ""; }; - 5A4D0B17249FA9E20076A781 /* String+Decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Decode.swift"; sourceTree = ""; }; - 5A4D0B1E249FAF140076A781 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 5A4D0B20249FAF190076A781 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; - 5A4D0B21249FAF560076A781 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; - 5A4D0B26249FB9880076A781 /* Hacker News.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Hacker News.entitlements"; sourceTree = ""; }; - 5A4D0B27249FBA3B0076A781 /* HNService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = HNService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5A4D0B30249FC23D0076A781 /* HNService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = HNService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5A5177CE24C4D289005856C4 /* StoryType+AllValues.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoryType+AllValues.swift"; sourceTree = ""; }; - 5A52212024479D00003B799D /* ThemeSelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeSelectableTableViewCell.swift; sourceTree = ""; }; - 5A52212224479D1B003B799D /* ThemeSelectableTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ThemeSelectableTableViewCell.xib; sourceTree = ""; }; - 5A5C0A9F241ECB7D007B4FCF /* ServiceLocatorConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocatorConfigurator.swift; sourceTree = ""; }; - 5A5C0AA1241ECBF9007B4FCF /* ServiceLocatorAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceLocatorAssembly.swift; sourceTree = ""; }; - 5A6FF6C224182BDF003F9B9D /* StoriesConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoriesConfigurator.swift; sourceTree = ""; }; - 5A7347E4242E9549005B394F /* SettingsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTableViewCell.swift; sourceTree = ""; }; - 5A7347E6242E9562005B394F /* SettingsTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SettingsTableViewCell.xib; sourceTree = ""; }; - 5A7347EA242EA926005B394F /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = ""; }; - 5A8054E8244CD98B005E4F96 /* PostTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTableViewCell.swift; sourceTree = ""; }; - 5A8054EA244CD9A0005E4F96 /* PostTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PostTableViewCell.xib; sourceTree = ""; }; - 5A8BF1642449AB5400215883 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; - 5A8BF16B2449ABBD00215883 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; - 5A8BF16D2449AC3F00215883 /* String+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localized.swift"; sourceTree = ""; }; - 5A8BF1742449D78900215883 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; - 5A8BF1762449D79600215883 /* RootViewInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewInput.swift; sourceTree = ""; }; - 5A8BF1782449D7A500215883 /* RootViewOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewOutput.swift; sourceTree = ""; }; - 5A8BF17B2449D7BF00215883 /* RootPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPresenter.swift; sourceTree = ""; }; - 5A8BF18F2449F00E00215883 /* SettingsListData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListData.swift; sourceTree = ""; }; - 5A8BF194244A111000215883 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 5A8F6B032499722000484C18 /* HackerNewsTodayExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HackerNewsTodayExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 5A8F6B042499722000484C18 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; - 5A8F6B072499722000484C18 /* TopStoriesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopStoriesViewController.swift; sourceTree = ""; }; - 5A8F6B0A2499722000484C18 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - 5A8F6B0C2499722000484C18 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5A94A2E82439383D00918198 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 5A9C963E24425B2F002AC0F5 /* Poppins-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Bold.ttf"; sourceTree = ""; }; - 5A9C963F24425B2F002AC0F5 /* Poppins-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Medium.ttf"; sourceTree = ""; }; - 5A9C964324425B30002AC0F5 /* Poppins-ExtraBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-ExtraBold.ttf"; sourceTree = ""; }; - 5A9C964424425B30002AC0F5 /* Poppins-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Light.ttf"; sourceTree = ""; }; - 5A9C964524425B30002AC0F5 /* Poppins-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-SemiBold.ttf"; sourceTree = ""; }; - 5A9C964924425B31002AC0F5 /* Poppins-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Italic.ttf"; sourceTree = ""; }; - 5A9C964B24425B32002AC0F5 /* Poppins-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-Regular.ttf"; sourceTree = ""; }; - 5A9C964C24425B32002AC0F5 /* Poppins-ExtraLight.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Poppins-ExtraLight.ttf"; sourceTree = ""; }; - 5A9C966224425C69002AC0F5 /* FontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontStyle.swift; sourceTree = ""; }; - 5A9C966424425D07002AC0F5 /* Fonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fonts.swift; sourceTree = ""; }; - 5A9C966624425E35002AC0F5 /* Font+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+.swift"; sourceTree = ""; }; - 5A9C966C2442608E002AC0F5 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; - 5AA02BE9244F658400B5B15F /* String+Attributed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Attributed.swift"; sourceTree = ""; }; - 5AAC707C249024BC005AFFC4 /* UITest+Helprers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITest+Helprers.swift"; sourceTree = ""; }; - 5AAC707E2490252C005AFFC4 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; - 5AAC708124902C26005AFFC4 /* UITestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestFactory.swift; sourceTree = ""; }; - 5AAC708424902CCC005AFFC4 /* SettingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingScreen.swift; sourceTree = ""; }; - 5AAC708624902D4B005AFFC4 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = ""; }; - 5AAC708824902F88005AFFC4 /* ThemesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemesScreen.swift; sourceTree = ""; }; - 5AAC708A24903190005AFFC4 /* StoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryScreen.swift; sourceTree = ""; }; - 5AAC708D249035F1005AFFC4 /* SettingsShapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsShapshotTests.swift; sourceTree = ""; }; - 5AAC709224903DFE005AFFC4 /* UIViewController+Safe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Safe.swift"; sourceTree = ""; }; - 5AAC9111245090C60041006F /* Array+Range.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Range.swift"; sourceTree = ""; }; - 5AAC911F2450E8830041006F /* HNImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HNImageView.swift; sourceTree = ""; }; - 5AAC91222450E89F0041006F /* SkeletonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCell.swift; sourceTree = ""; }; - 5AAC91242450E8A90041006F /* SkeletonCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SkeletonCell.xib; sourceTree = ""; }; - 5AB1139A2456253700332B5F /* LoadingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingFooterView.swift; sourceTree = ""; }; - 5AB25D8C2451ADD0009C05ED /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; - 5AC0CC262454710E0060F5C9 /* CommentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentCell.swift; sourceTree = ""; }; - 5AC0CC28245472610060F5C9 /* CommentCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CommentCell.xib; sourceTree = ""; }; - 5AC10D1F248AE7D000132EB4 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; - 5AC10D22248AE98B00132EB4 /* PostModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostModelTests.swift; sourceTree = ""; }; - 5AC10D26248AEC2400132EB4 /* Data+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+.swift"; sourceTree = ""; }; - 5AC10D28248AF52F00132EB4 /* CommentModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentModelTests.swift; sourceTree = ""; }; - 5AC10D2A248AF65F00132EB4 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - 5AC10D31248AF71000132EB4 /* RootPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootPresenterTests.swift; sourceTree = ""; }; - 5AC10D34248AF82900132EB4 /* RootAssemblyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootAssemblyTests.swift; sourceTree = ""; }; - 5AC10D37248AF90300132EB4 /* RootConfiguratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfiguratorTests.swift; sourceTree = ""; }; - 5AC10D3A248AFA3D00132EB4 /* RootViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewTests.swift; sourceTree = ""; }; - 5AC892AF248D5C6F00AFC5AC /* HNServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HNServiceMock.swift; sourceTree = ""; }; - 5AC892B1248D797300AFC5AC /* UnitTestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitTestError.swift; sourceTree = ""; }; - 5ACA6C462454E2C2007E89BF /* Queue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Queue.swift; sourceTree = ""; }; - 5ACB43D324484BA00010465B /* ThemeUpdatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeUpdatable.swift; sourceTree = ""; }; - 5ACB43D8244855F30010465B /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; - 5ACB43DA244862500010465B /* ImageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageType.swift; sourceTree = ""; }; - 5ACB43DC244883E90010465B /* Optional+Throwable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Throwable.swift"; sourceTree = ""; }; - 5ACB43DE244886E30010465B /* MainTabBarViewOutput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewOutput.swift; sourceTree = ""; }; - 5ACB43E12448872A0010465B /* MainTabBarPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarPresenter.swift; sourceTree = ""; }; - 5ACB43E32448875C0010465B /* MainTabBarViewInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewInput.swift; sourceTree = ""; }; - 5AD8D59524532F6100D7E1F1 /* Comments.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Comments.storyboard; sourceTree = ""; }; - 5AE2EDCC24103C9B0011A198 /* RootModuleAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootModuleAssembly.swift; sourceTree = ""; }; - 5AE2EDCF24103CDB0011A198 /* RootConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootConfigurator.swift; sourceTree = ""; }; - 5AE2EDD324103EED0011A198 /* MainTabBarConfigurator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarConfigurator.swift; sourceTree = ""; }; - 5AE2EDD624103F1D0011A198 /* MainTabBarAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarAssembly.swift; sourceTree = ""; }; - 5AE2EDD9241040420011A198 /* TabBarViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarViewProtocol.swift; sourceTree = ""; }; - 5AE2EDDC241040A40011A198 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = ""; }; - 5AE2EDDF241045D60011A198 /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; }; - 5AE2EDE1241046210011A198 /* UINavigationController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+.swift"; sourceTree = ""; }; - 5AE2EDE3241046730011A198 /* TransitionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransitionHandler.swift; sourceTree = ""; }; - 5AE2EDE7241047650011A198 /* R.generated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R.generated.swift; sourceTree = ""; }; - 5AE95B672461F49C00DAA009 /* UIView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+.swift"; sourceTree = ""; }; - 5AEE9ADB249A851900929A85 /* TopStoryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopStoryTableViewCell.swift; sourceTree = ""; }; - 5AEE9ADD249A852F00929A85 /* TopStoryTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TopStoryTableViewCell.xib; sourceTree = ""; }; - 5AEE9BC5249AB22500929A85 /* RateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RateView.swift; sourceTree = ""; }; - 5AF4954B249D5CC000E469A2 /* HackerNewsWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HackerNewsWatch.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 5AF4954E249D5CC000E469A2 /* HackerNewsWatch WatchKit App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "HackerNewsWatch WatchKit App.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 5AF49554249D5CC000E469A2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; - 5AF49556249D5CC500E469A2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 5AF49558249D5CC500E469A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5AF4955D249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HackerNewsWatch WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - 5AF49564249D5CC500E469A2 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = ""; }; - 5AF49566249D5CC800E469A2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 5AF49568249D5CC800E469A2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 5AF495A1249D5E2300E469A2 /* NetworkManager.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = NetworkManager.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 5AF495C0249D600800E469A2 /* TopStoriesInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopStoriesInterfaceController.swift; sourceTree = ""; }; - 5AF495C3249D642100E469A2 /* PostCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCell.swift; sourceTree = ""; }; - 5B0973524E7C669B5C27E93D /* StoriesPresenterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesPresenterTests.swift; sourceTree = ""; }; - 6588DDD3BA46AA474D11E747 /* Pods-HackerNewsUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HackerNewsUITests.debug.xcconfig"; path = "Target Support Files/Pods-HackerNewsUITests/Pods-HackerNewsUITests.debug.xcconfig"; sourceTree = ""; }; - 6C54504D79E3203454D65767 /* StoriesViewInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesViewInput.swift; sourceTree = ""; }; - 6EED532098A8A2AF98ED6E80 /* StoriesInteractorOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesInteractorOutput.swift; sourceTree = ""; }; - 6FEF91989794A3C7AC72B9FF /* CommentsViewOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsViewOutput.swift; sourceTree = ""; }; - 71C4B2F3CE40C5B4BBB8DFDD /* SettingsRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsRouter.swift; sourceTree = ""; }; - 75288D891967908B4B1CF34C /* StoriesRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesRouter.swift; sourceTree = ""; }; - 756C7453871B7DB5374C91D0 /* SettingsViewInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsViewInput.swift; sourceTree = ""; }; - 7830BC11140D5520485785C7 /* Pods-HackerNewsTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HackerNewsTests.debug.xcconfig"; path = "Target Support Files/Pods-HackerNewsTests/Pods-HackerNewsTests.debug.xcconfig"; sourceTree = ""; }; - 78C4FBC5B114C6A6B02D7E43 /* CommentsInteractorInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsInteractorInput.swift; sourceTree = ""; }; - 7AD3B924ABCD34C1BE81504C /* StoriesInteractorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesInteractorTests.swift; sourceTree = ""; }; - 7AECF0419410CC923AC4242E /* SettingsViewOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsViewOutput.swift; sourceTree = ""; }; - 7F10F7222A47FB417E6C4588 /* Pods_HackerNews.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HackerNews.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 84559136E2CC1694F565A308 /* SettingsInteractorInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsInteractorInput.swift; sourceTree = ""; }; - 84DB1BC9C41A88EF3EA1E8B3 /* SettingsConfiguratorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsConfiguratorTests.swift; sourceTree = ""; }; - 86A1D9B7E0433BA1D3C63F7D /* SettingsConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsConfigurator.swift; sourceTree = ""; }; - 86BAEAE1E6A9BDB269B528B7 /* SettingsAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsAssembly.swift; sourceTree = ""; }; - 87973D774E9854C755CEE98C /* Pods-HackerNewsTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HackerNewsTests.release.xcconfig"; path = "Target Support Files/Pods-HackerNewsTests/Pods-HackerNewsTests.release.xcconfig"; sourceTree = ""; }; - 880050B23E0763C56310A265 /* SettingsRouterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsRouterTests.swift; sourceTree = ""; }; - 88D4AD8613343B832D8C6308 /* ThemePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemePresenter.swift; sourceTree = ""; }; - 941A683CEF3511E341FC3BDA /* Pods_HackerNewsUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HackerNewsUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9694DB55717A8BA99A82C027 /* SettingsRouterInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsRouterInput.swift; sourceTree = ""; }; - 98EAC67E1A9739CA65400E95 /* StoriesRouterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesRouterTests.swift; sourceTree = ""; }; - A30051C967D26D38780E7A57 /* ThemeRouterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeRouterTests.swift; sourceTree = ""; }; - A4E29D3A2F2C24826F905C2A /* CommentsConfigurator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsConfigurator.swift; sourceTree = ""; }; - A682BDFD6027AEE7D726F878 /* SettingsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsInteractor.swift; sourceTree = ""; }; - A6B05D558803A1AD4B7FABDB /* SettingsAssemblyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsAssemblyTests.swift; sourceTree = ""; }; - A8400E6F8B807CF3B0E7A117 /* CommentsRouterInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsRouterInput.swift; sourceTree = ""; }; - A8A4FDD125295BD464D56161 /* StoriesAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesAssembly.swift; sourceTree = ""; }; - ADB423B936F932A5F5C43F9F /* CommentsModuleInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsModuleInput.swift; sourceTree = ""; }; - B08574C37377B86D4DD66004 /* CommentsPresenterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsPresenterTests.swift; sourceTree = ""; }; - B0E27AFCA2403A03ED302C2C /* ThemeInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeInteractor.swift; sourceTree = ""; }; - B3A76C4A1BBE4B4F8C6FCEC3 /* StoriesInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesInteractor.swift; sourceTree = ""; }; - B579C0416DDA2AD457DC8042 /* CommentsAssembly.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsAssembly.swift; sourceTree = ""; }; - B74DFCA04DB8F96532311217 /* Pods-HackerNews.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HackerNews.release.xcconfig"; path = "Target Support Files/Pods-HackerNews/Pods-HackerNews.release.xcconfig"; sourceTree = ""; }; - B8CC01CE435879B2D5F78B13 /* ThemeRouterInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeRouterInput.swift; sourceTree = ""; }; - BAE4653D7772D620D3CBF1B1 /* ThemeRouter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeRouter.swift; sourceTree = ""; }; - BAF3412A178EA5EB164E53D3 /* Pods_HackerNewsTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_HackerNewsTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - BB7D49B681E9B168FE7B4F96 /* StoriesConfiguratorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesConfiguratorTests.swift; sourceTree = ""; }; - BCEE42028E4D75974DFEBFB7 /* CommentsInteractorOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsInteractorOutput.swift; sourceTree = ""; }; - C0D7F843BFB3D0ECBFC7E16D /* ThemeModuleInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeModuleInput.swift; sourceTree = ""; }; - C322EE9386899B3C63540FAD /* SettingsViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsViewTests.swift; sourceTree = ""; }; - C465F457451BB047F53DF5D7 /* StoriesAssemblyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesAssemblyTests.swift; sourceTree = ""; }; - C58FA4A86F5D21BC625349CD /* StoriesRouterInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesRouterInput.swift; sourceTree = ""; }; - C8FD75009999F70D8BED054F /* Settings.storyboard */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; - C923B096F47F2D687D4445C3 /* Theme.storyboard */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.storyboard; path = Theme.storyboard; sourceTree = ""; }; - C9357ED5A64187C76057B33B /* ThemeInteractorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeInteractorTests.swift; sourceTree = ""; }; - CC457690D01D25C8B43C69EE /* CommentsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsPresenter.swift; sourceTree = ""; }; - D07204F021309FC600E4B1ED /* Hacker News.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hacker News.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - D07204F321309FC600E4B1ED /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - D07204FA21309FC900E4B1ED /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - D07204FD21309FC900E4B1ED /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - D07204FF21309FC900E4B1ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D072050421309FC900E4B1ED /* HackerNewsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HackerNewsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - D072050A21309FC900E4B1ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D072050F21309FC900E4B1ED /* HackerNewsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = HackerNewsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - D072051321309FC900E4B1ED /* HackerNewsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HackerNewsUITests.swift; sourceTree = ""; }; - D072051521309FC900E4B1ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - D07205242130A09C00E4B1ED /* Color+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+.swift"; sourceTree = ""; }; - D07205262130A0AF00E4B1ED /* UIImage+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+.swift"; sourceTree = ""; }; - D07205282130A10400E4B1ED /* NibLoadableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibLoadableView.swift; sourceTree = ""; }; - D072052A2130A11A00E4B1ED /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; - D072052C2130A13800E4B1ED /* UITableView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+.swift"; sourceTree = ""; }; - D072052E2130A15200E4B1ED /* UITableViewCell+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableViewCell+.swift"; sourceTree = ""; }; - D07205302130A17100E4B1ED /* UIViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerFactory.swift; sourceTree = ""; }; - D349D0CD9C2060043398C027 /* ThemeInteractorOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeInteractorOutput.swift; sourceTree = ""; }; - D636BD0FA1E64C87B4348CAC /* SettingsInteractorTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsInteractorTests.swift; sourceTree = ""; }; - DBD6806965BB5158014D0AAB /* SettingsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsPresenter.swift; sourceTree = ""; }; - DCED8BB0967B1933D8335489 /* StoriesPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesPresenter.swift; sourceTree = ""; }; - DCF23845D0D60ECAF198876E /* ThemeInteractorInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeInteractorInput.swift; sourceTree = ""; }; - E089D733E432016A28791194 /* CommentsRouterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsRouterTests.swift; sourceTree = ""; }; - E25768042F1949ED23D54DF4 /* StoriesViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StoriesViewTests.swift; sourceTree = ""; }; - E57EB5317778D5553A2E21B9 /* CommentsAssemblyTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsAssemblyTests.swift; sourceTree = ""; }; - EA33A5A998886E55C869116C /* Pods-HackerNewsUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-HackerNewsUITests.release.xcconfig"; path = "Target Support Files/Pods-HackerNewsUITests/Pods-HackerNewsUITests.release.xcconfig"; sourceTree = ""; }; - EABC193D2B780816BABAF6D7 /* ThemePresenterTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemePresenterTests.swift; sourceTree = ""; }; - F6AE570ED443C0AA530DDBC6 /* ThemeViewOutput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ThemeViewOutput.swift; sourceTree = ""; }; - F739F4CAB4001886132AD3C7 /* CommentsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommentsViewController.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 5A8F6B002499722000484C18 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A4D0B31249FC23D0076A781 /* HNService.framework in Frameworks */, - 5A8F6B052499722000484C18 /* NotificationCenter.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF4955A249D5CC500E469A2 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF495A2249D5E2300E469A2 /* NetworkManager.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 7E5D7F0B32F6770A9DC5C348 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D07204ED21309FC600E4B1ED /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A116C7924A2927B00E27834 /* NetworkManager.framework in Frameworks */, - 5A4D0B28249FBA3B0076A781 /* HNService.framework in Frameworks */, - 0EC6C9C75C06A1962156C50B /* Pods_HackerNews.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D072050121309FC900E4B1ED /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - CB877EC84D37205766B4EC6D /* Pods_HackerNewsTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D072050C21309FC900E4B1ED /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D6F2CD32688DA91CBA8DF3F6 /* Pods_HackerNewsUITests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - F9BF5BB4A1DA7287E0E16974 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 00F38B09D4B98C61F5590884 /* Theme */ = { - isa = PBXGroup; - children = ( - 405582666EB49F385EAF9943 /* Assembly */, - 57FC2037D99FFC15A2FF423C /* Configurator */, - 7DF2FB279A3CEB704F9C2957 /* Interactor */, - 6D2F340C19BF8C42715F26FE /* Presenter */, - 6289C622A5C242AFEFBC86F6 /* Router */, - BF1CFA0B924B072CD4984313 /* View */, - ); - path = Theme; - sourceTree = ""; - }; - 0303F88D837CD698A93720BB /* Router */ = { - isa = PBXGroup; - children = ( - 880050B23E0763C56310A265 /* SettingsRouterTests.swift */, - ); - path = Router; - sourceTree = ""; - }; - 05D6E6B921AAF09B758FB04E /* Assembly */ = { - isa = PBXGroup; - children = ( - B579C0416DDA2AD457DC8042 /* CommentsAssembly.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 10AF3DE3F260614A7751A18D /* Presenter */ = { - isa = PBXGroup; - children = ( - 5B0973524E7C669B5C27E93D /* StoriesPresenterTests.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 171EBB474B759BA4CA4C7E9C /* Configurator */ = { - isa = PBXGroup; - children = ( - A4E29D3A2F2C24826F905C2A /* CommentsConfigurator.swift */, - ADB423B936F932A5F5C43F9F /* CommentsModuleInput.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 1E06CF2D837EB1F31F5EDE5E /* Presenter */ = { - isa = PBXGroup; - children = ( - B08574C37377B86D4DD66004 /* CommentsPresenterTests.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 2CEA7D26636B60D4D590C779 /* Stories */ = { - isa = PBXGroup; - children = ( - 9949B397CEE3E580720BE48F /* Assembly */, - 5A6FF6C6241833D6003F9B9D /* Configurator */, - 2F14444915C298B8F325B11F /* Interactor */, - 7D87A71A50611BAFAE9E4AAF /* Presenter */, - F7BBA7FD73C0C57982AF8E1E /* Router */, - 5A5177D224C4D846005856C4 /* View */, - ); - path = Stories; - sourceTree = ""; - }; - 2F14444915C298B8F325B11F /* Interactor */ = { - isa = PBXGroup; - children = ( - 5688482AF43266B89E7A0A7B /* StoriesInteractorInput.swift */, - 6EED532098A8A2AF98ED6E80 /* StoriesInteractorOutput.swift */, - B3A76C4A1BBE4B4F8C6FCEC3 /* StoriesInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - 33F5257E0620ED9C2F5B0838 /* Router */ = { - isa = PBXGroup; - children = ( - 9694DB55717A8BA99A82C027 /* SettingsRouterInput.swift */, - 71C4B2F3CE40C5B4BBB8DFDD /* SettingsRouter.swift */, - ); - path = Router; - sourceTree = ""; - }; - 3F8D45DA94E05083F9573FC9 /* Assembly */ = { - isa = PBXGroup; - children = ( - C465F457451BB047F53DF5D7 /* StoriesAssemblyTests.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 405582666EB49F385EAF9943 /* Assembly */ = { - isa = PBXGroup; - children = ( - 559CCB6107A00A4B58CBC7A5 /* ThemeAssemblyTests.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 408D06C99C4F8E674CC20AC6 /* Router */ = { - isa = PBXGroup; - children = ( - 98EAC67E1A9739CA65400E95 /* StoriesRouterTests.swift */, - ); - path = Router; - sourceTree = ""; - }; - 478CE303851F3D5B589B31EB /* Comments */ = { - isa = PBXGroup; - children = ( - 05D6E6B921AAF09B758FB04E /* Assembly */, - 171EBB474B759BA4CA4C7E9C /* Configurator */, - 74A51D6ACEBD7DADE9FA7651 /* Interactor */, - 4BA48C2EF2D9D17B9E9989D4 /* Presenter */, - CABAC464F58D823684601050 /* Router */, - 607D0D5CF88A32286BBB7E19 /* View */, - ); - path = Comments; - sourceTree = ""; - }; - 4BA48C2EF2D9D17B9E9989D4 /* Presenter */ = { - isa = PBXGroup; - children = ( - CC457690D01D25C8B43C69EE /* CommentsPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 51265A8D36D0DA1622A208BB /* Interactor */ = { - isa = PBXGroup; - children = ( - D636BD0FA1E64C87B4348CAC /* SettingsInteractorTests.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - 5479375AC2AFE078E7500714 /* Assembly */ = { - isa = PBXGroup; - children = ( - A6B05D558803A1AD4B7FABDB /* SettingsAssemblyTests.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 57FC2037D99FFC15A2FF423C /* Configurator */ = { - isa = PBXGroup; - children = ( - 3B757722416640AE5DA2D5B6 /* ThemeConfiguratorTests.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 5A0D25E2248BF94D006718D4 /* Managers */ = { - isa = PBXGroup; - children = ( - 5A0D25E3248BF969006718D4 /* ThemeManagerTests.swift */, - ); - path = Managers; - sourceTree = ""; - }; - 5A0D25E5248C0831006718D4 /* Utils */ = { - isa = PBXGroup; - children = ( - 5A0D25E6248C084A006718D4 /* WeakObjectSetTests.swift */, - ); - path = Utils; - sourceTree = ""; - }; - 5A0D25E8248C09A8006718D4 /* PerformanceTests */ = { - isa = PBXGroup; - children = ( - 5A0D25E9248C09C2006718D4 /* WeakObjectSetPerformanceTests.swift */, - ); - path = PerformanceTests; - sourceTree = ""; - }; - 5A14BC5424899CA0008E9B7B /* Mocks */ = { - isa = PBXGroup; - children = ( - 5A14BC5524899CAE008E9B7B /* MockContainer.swift */, - 5AC10D1F248AE7D000132EB4 /* TestData.swift */, - 5A0D25F1248C190D006718D4 /* MockThemeManager.swift */, - 5AC892AF248D5C6F00AFC5AC /* HNServiceMock.swift */, - 5AC892B1248D797300AFC5AC /* UnitTestError.swift */, - ); - path = Mocks; - sourceTree = ""; - }; - 5A1CADEE244B4BDC002D0114 /* Models */ = { - isa = PBXGroup; - children = ( - 5A25EF392455870900AC7C56 /* BaseCellModel.swift */, - 5A25EF3B2455888700AC7C56 /* PostCellViewModel.swift */, - 5A25EF3D2455894A00AC7C56 /* CommentCellViewModel.swift */, - 5A030953245E1C900004D669 /* SkeletonCellViewModel.swift */, - ); - path = Models; - sourceTree = ""; - }; - 5A435C09243A27E7009C5355 /* ThemeManager */ = { - isa = PBXGroup; - children = ( - 5A435C0A243A27F5009C5355 /* ThemeManager.swift */, - 5A435C0C243A286A009C5355 /* Theme.swift */, - 5A00D8A1244655560013B3F5 /* Themeable.swift */, - ); - path = ThemeManager; - sourceTree = ""; - }; - 5A43892D247F10E1002D3111 /* Weak */ = { - isa = PBXGroup; - children = ( - 5A43892E247F10EC002D3111 /* WeakObjectSet.swift */, - 5A438930247F110B002D3111 /* WeakObject.swift */, - ); - path = Weak; - sourceTree = ""; - }; - 5A4D0B11249F9FD90076A781 /* Cells */ = { - isa = PBXGroup; - children = ( - 5AF495C3249D642100E469A2 /* PostCell.swift */, - 5A4D0B12249FA0090076A781 /* CommentCell.swift */, - ); - path = Cells; - sourceTree = ""; - }; - 5A4D0B16249FA9D20076A781 /* Extension */ = { - isa = PBXGroup; - children = ( - 5A4D0B17249FA9E20076A781 /* String+Decode.swift */, - 5A4D0B21249FAF560076A781 /* String+Localized.swift */, - ); - path = Extension; - sourceTree = ""; - }; - 5A4D0B19249FAEE60076A781 /* Resources */ = { - isa = PBXGroup; - children = ( - 5A4D0B1A249FAEEE0076A781 /* Strings */, - ); - path = Resources; - sourceTree = ""; - }; - 5A4D0B1A249FAEEE0076A781 /* Strings */ = { - isa = PBXGroup; - children = ( - 5A4D0B1F249FAF140076A781 /* Localizable.strings */, - ); - path = Strings; - sourceTree = ""; - }; - 5A5177D224C4D846005856C4 /* View */ = { - isa = PBXGroup; - children = ( - 5A8054E6244CD963005E4F96 /* Views */, - 6C54504D79E3203454D65767 /* StoriesViewInput.swift */, - 5263AF6EF05EEDA880DA524F /* StoriesViewOutput.swift */, - 2BD9175C3D9C3474F09BEE4F /* StoriesViewController.swift */, - ); - path = View; - sourceTree = ""; - }; - 5A5177D324C4DE81005856C4 /* View */ = { - isa = PBXGroup; - children = ( - 5AEE9ADA249A84E600929A85 /* Views */, - 5A8F6B072499722000484C18 /* TopStoriesViewController.swift */, - 5A8F6B092499722000484C18 /* MainInterface.storyboard */, - ); - path = View; - sourceTree = ""; - }; - 5A52211E24479CD7003B799D /* Views */ = { - isa = PBXGroup; - children = ( - 5A52211F24479CE4003B799D /* ThemeSelectableCell */, - ); - path = Views; - sourceTree = ""; - }; - 5A52211F24479CE4003B799D /* ThemeSelectableCell */ = { - isa = PBXGroup; - children = ( - 5A52212024479D00003B799D /* ThemeSelectableTableViewCell.swift */, - 5A52212224479D1B003B799D /* ThemeSelectableTableViewCell.xib */, - ); - path = ThemeSelectableCell; - sourceTree = ""; - }; - 5A5221312447B87C003B799D /* Themeable */ = { - isa = PBXGroup; - children = ( - 5ACB43D324484BA00010465B /* ThemeUpdatable.swift */, - ); - path = Themeable; - sourceTree = ""; - }; - 5A6EFDF302D74AB07328A1DB /* Presenter */ = { - isa = PBXGroup; - children = ( - 88D4AD8613343B832D8C6308 /* ThemePresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 5A6FF6C6241833D6003F9B9D /* Configurator */ = { - isa = PBXGroup; - children = ( - 5A6FF6C224182BDF003F9B9D /* StoriesConfigurator.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 5A7347E2242E9529005B394F /* Views */ = { - isa = PBXGroup; - children = ( - 5A7347E3242E952F005B394F /* SettingsTableViewCell */, - ); - path = Views; - sourceTree = ""; - }; - 5A7347E3242E952F005B394F /* SettingsTableViewCell */ = { - isa = PBXGroup; - children = ( - 5A7347E4242E9549005B394F /* SettingsTableViewCell.swift */, - 5A7347E6242E9562005B394F /* SettingsTableViewCell.xib */, - ); - path = SettingsTableViewCell; - sourceTree = ""; - }; - 5A7347E8242EA915005B394F /* Managers */ = { - isa = PBXGroup; - children = ( - 5A435C09243A27E7009C5355 /* ThemeManager */, - 5A7347E9242EA91A005B394F /* SettingsManager */, - ); - path = Managers; - sourceTree = ""; - }; - 5A7347E9242EA91A005B394F /* SettingsManager */ = { - isa = PBXGroup; - children = ( - 5A7347EA242EA926005B394F /* SettingsManager.swift */, - ); - path = SettingsManager; - sourceTree = ""; - }; - 5A8054E6244CD963005E4F96 /* Views */ = { - isa = PBXGroup; - children = ( - 5A8054E7244CD974005E4F96 /* PostTableViewCell */, - ); - path = Views; - sourceTree = ""; - }; - 5A8054E7244CD974005E4F96 /* PostTableViewCell */ = { - isa = PBXGroup; - children = ( - 5A8054E8244CD98B005E4F96 /* PostTableViewCell.swift */, - 5A8054EA244CD9A0005E4F96 /* PostTableViewCell.xib */, - ); - path = PostTableViewCell; - sourceTree = ""; - }; - 5A8BF15D2449A55800215883 /* Colors */ = { - isa = PBXGroup; - children = ( - D07205242130A09C00E4B1ED /* Color+.swift */, - ); - path = Colors; - sourceTree = ""; - }; - 5A8BF1632449AAAE00215883 /* Strings */ = { - isa = PBXGroup; - children = ( - 5A8BF16A2449ABAB00215883 /* Localizable.strings */, - ); - path = Strings; - sourceTree = ""; - }; - 5A8BF16C2449AC1000215883 /* Strings */ = { - isa = PBXGroup; - children = ( - 38F796992198A2710083EFEF /* String+.swift */, - 5A8BF16D2449AC3F00215883 /* String+Localized.swift */, - 5AA02BE9244F658400B5B15F /* String+Attributed.swift */, - ); - path = Strings; - sourceTree = ""; - }; - 5A8BF1732449D77A00215883 /* View */ = { - isa = PBXGroup; - children = ( - 5A8BF1742449D78900215883 /* RootSplitViewController.swift */, - 5A8BF1762449D79600215883 /* RootViewInput.swift */, - 5A8BF1782449D7A500215883 /* RootViewOutput.swift */, - ); - path = View; - sourceTree = ""; - }; - 5A8BF17A2449D7B700215883 /* Presenter */ = { - isa = PBXGroup; - children = ( - 5A8BF17B2449D7BF00215883 /* RootPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 5A8BF18C2449EFAF00215883 /* ListData */ = { - isa = PBXGroup; - children = ( - 5A8BF18F2449F00E00215883 /* SettingsListData.swift */, - ); - path = ListData; - sourceTree = ""; - }; - 5A8BF198244A243C00215883 /* Modules */ = { - isa = PBXGroup; - children = ( - 6F1BA349F19C9A2E833041AD /* Theme */, - 5A8BF199244A244300215883 /* Main */, - ); - path = Modules; - sourceTree = ""; - }; - 5A8BF199244A244300215883 /* Main */ = { - isa = PBXGroup; - children = ( - 6A8B5540D2AF6417A560F91D /* Assembly */, - 750568379C9702DD8CEB6CF3 /* Configurator */, - B3E1253FC3E95FF62EFE7583 /* Interactor */, - E5CFC53F02B307E3C1CE3226 /* Presenter */, - 33F5257E0620ED9C2F5B0838 /* Router */, - C97C569A10F809DFFA9F89E1 /* View */, - ); - path = Main; - sourceTree = ""; - }; - 5A8F6B062499722000484C18 /* HackerNewsTodayExtension */ = { - isa = PBXGroup; - children = ( - 5A5177D324C4DE81005856C4 /* View */, - 5A8F6B0C2499722000484C18 /* Info.plist */, - ); - path = HackerNewsTodayExtension; - sourceTree = ""; - }; - 5A9C966824425E62002AC0F5 /* Fonts */ = { - isa = PBXGroup; - children = ( - 5A9C966624425E35002AC0F5 /* Font+.swift */, - 5A9C966424425D07002AC0F5 /* Fonts.swift */, - 5A9C966224425C69002AC0F5 /* FontStyle.swift */, - ); - path = Fonts; - sourceTree = ""; - }; - 5AAC707B249024A4005AFFC4 /* Helpers */ = { - isa = PBXGroup; - children = ( - 5AAC707C249024BC005AFFC4 /* UITest+Helprers.swift */, - 5AAC707E2490252C005AFFC4 /* App.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - 5AAC708024902C16005AFFC4 /* UITestFactory */ = { - isa = PBXGroup; - children = ( - 5AAC708324902CB6005AFFC4 /* Screens */, - 5AAC708124902C26005AFFC4 /* UITestFactory.swift */, - ); - path = UITestFactory; - sourceTree = ""; - }; - 5AAC708324902CB6005AFFC4 /* Screens */ = { - isa = PBXGroup; - children = ( - 5AAC708424902CCC005AFFC4 /* SettingScreen.swift */, - 5AAC708824902F88005AFFC4 /* ThemesScreen.swift */, - 5AAC708A24903190005AFFC4 /* StoryScreen.swift */, - ); - path = Screens; - sourceTree = ""; - }; - 5AAC708C249035DC005AFFC4 /* SnapshotTests */ = { - isa = PBXGroup; - children = ( - 5AAC708D249035F1005AFFC4 /* SettingsShapshotTests.swift */, - ); - path = SnapshotTests; - sourceTree = ""; - }; - 5AAC709124903DED005AFFC4 /* UIViewController */ = { - isa = PBXGroup; - children = ( - 5AE2EDE1241046210011A198 /* UINavigationController+.swift */, - 5AB25D8C2451ADD0009C05ED /* UIViewController+.swift */, - 5AAC709224903DFE005AFFC4 /* UIViewController+Safe.swift */, - ); - path = UIViewController; - sourceTree = ""; - }; - 5AAC911D2450E82E0041006F /* Cells */ = { - isa = PBXGroup; - children = ( - 5AAC91212450E8940041006F /* LoadingCell */, - ); - path = Cells; - sourceTree = ""; - }; - 5AAC91212450E8940041006F /* LoadingCell */ = { - isa = PBXGroup; - children = ( - 5AAC91222450E89F0041006F /* SkeletonCell.swift */, - 5AAC91242450E8A90041006F /* SkeletonCell.xib */, - ); - path = LoadingCell; - sourceTree = ""; - }; - 5AB113992456252400332B5F /* LoadingFooterView */ = { - isa = PBXGroup; - children = ( - 5AB1139A2456253700332B5F /* LoadingFooterView.swift */, - ); - path = LoadingFooterView; - sourceTree = ""; - }; - 5AC0CC25245470FF0060F5C9 /* CommentsCell */ = { - isa = PBXGroup; - children = ( - 5AC0CC262454710E0060F5C9 /* CommentCell.swift */, - 5AC0CC28245472610060F5C9 /* CommentCell.xib */, - ); - path = CommentsCell; - sourceTree = ""; - }; - 5AC10D21248AE96100132EB4 /* Model */ = { - isa = PBXGroup; - children = ( - 5AC10D22248AE98B00132EB4 /* PostModelTests.swift */, - 5AC10D28248AF52F00132EB4 /* CommentModelTests.swift */, - ); - path = Model; - sourceTree = ""; - }; - 5AC10D24248AEC0800132EB4 /* UnitTests */ = { - isa = PBXGroup; - children = ( - 5A0D25E2248BF94D006718D4 /* Managers */, - 5AC10D21248AE96100132EB4 /* Model */, - 5AC10D2D248AF6CE00132EB4 /* Modules */, - 5A0D25E5248C0831006718D4 /* Utils */, - ); - path = UnitTests; - sourceTree = ""; - }; - 5AC10D25248AEC1900132EB4 /* Extensions */ = { - isa = PBXGroup; - children = ( - 5AC10D26248AEC2400132EB4 /* Data+.swift */, - 5AC10D2A248AF65F00132EB4 /* Utils.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 5AC10D2D248AF6CE00132EB4 /* Modules */ = { - isa = PBXGroup; - children = ( - D898A4361F1DEE4AFE83D893 /* Comments */, - 5AC10D2E248AF6E600132EB4 /* Main */, - 5AC10D2F248AF6EC00132EB4 /* Root */, - EE3CB5B53F2B7A16F50CD2FA /* Settings */, - C2570B1A87F07AE586E2A92C /* Stories */, - 00F38B09D4B98C61F5590884 /* Theme */, - ); - path = Modules; - sourceTree = ""; - }; - 5AC10D2E248AF6E600132EB4 /* Main */ = { - isa = PBXGroup; - children = ( - 5AC10D3E248AFA8700132EB4 /* Assembly */, - 5AC10D3F248AFA8C00132EB4 /* Configurator */, - 5AC10D3C248AFA7E00132EB4 /* Presenter */, - 5AC10D3D248AFA8300132EB4 /* View */, - ); - path = Main; - sourceTree = ""; - }; - 5AC10D2F248AF6EC00132EB4 /* Root */ = { - isa = PBXGroup; - children = ( - 5AC10D33248AF79700132EB4 /* Assembly */, - 5AC10D36248AF8F300132EB4 /* Configurator */, - 5AC10D30248AF6F900132EB4 /* Presenter */, - 5AC10D39248AFA3200132EB4 /* View */, - ); - path = Root; - sourceTree = ""; - }; - 5AC10D30248AF6F900132EB4 /* Presenter */ = { - isa = PBXGroup; - children = ( - 5AC10D31248AF71000132EB4 /* RootPresenterTests.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 5AC10D33248AF79700132EB4 /* Assembly */ = { - isa = PBXGroup; - children = ( - 5AC10D34248AF82900132EB4 /* RootAssemblyTests.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 5AC10D36248AF8F300132EB4 /* Configurator */ = { - isa = PBXGroup; - children = ( - 5AC10D37248AF90300132EB4 /* RootConfiguratorTests.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 5AC10D39248AFA3200132EB4 /* View */ = { - isa = PBXGroup; - children = ( - 5AC10D3A248AFA3D00132EB4 /* RootViewTests.swift */, - ); - path = View; - sourceTree = ""; - }; - 5AC10D3C248AFA7E00132EB4 /* Presenter */ = { - isa = PBXGroup; - children = ( - 5A0D25DA248BF42F006718D4 /* MainTabBarPresenterTests.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 5AC10D3D248AFA8300132EB4 /* View */ = { - isa = PBXGroup; - children = ( - 5A0D25E0248BF606006718D4 /* MainTabBarViewTests.swift */, - ); - path = View; - sourceTree = ""; - }; - 5AC10D3E248AFA8700132EB4 /* Assembly */ = { - isa = PBXGroup; - children = ( - 5A0D25DC248BF46F006718D4 /* MainTabBarAssemblyTests.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 5AC10D3F248AFA8C00132EB4 /* Configurator */ = { - isa = PBXGroup; - children = ( - 5A0D25DE248BF5A8006718D4 /* MainTabBarConfuguratorTests.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 5ACA6C452454E2B1007E89BF /* DataStructures */ = { - isa = PBXGroup; - children = ( - 5ACA6C462454E2C2007E89BF /* Queue.swift */, - ); - path = DataStructures; - sourceTree = ""; - }; - 5ACB43E02448871F0010465B /* Presenter */ = { - isa = PBXGroup; - children = ( - 5ACB43E12448872A0010465B /* MainTabBarPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 5AD9C02F244E3C50007F8769 /* Views */ = { - isa = PBXGroup; - children = ( - 5AAC911F2450E8830041006F /* HNImageView.swift */, - 5AAC911D2450E82E0041006F /* Cells */, - 5AB113992456252400332B5F /* LoadingFooterView */, - ); - path = Views; - sourceTree = ""; - }; - 5AE2EDC424103B450011A198 /* Classes */ = { - isa = PBXGroup; - children = ( - 5AE2EDC524103B510011A198 /* ApplicationLayer */, - 5AE2EDC724103C220011A198 /* BusinessLayer */, - 5AE2EDC624103C0C0011A198 /* PresentationLayer */, - ); - path = Classes; - sourceTree = ""; - }; - 5AE2EDC524103B510011A198 /* ApplicationLayer */ = { - isa = PBXGroup; - children = ( - D07204F321309FC600E4B1ED /* AppDelegate.swift */, - 5A5C0A9F241ECB7D007B4FCF /* ServiceLocatorConfigurator.swift */, - 5A5C0AA1241ECBF9007B4FCF /* ServiceLocatorAssembly.swift */, - ); - path = ApplicationLayer; - sourceTree = ""; - }; - 5AE2EDC624103C0C0011A198 /* PresentationLayer */ = { - isa = PBXGroup; - children = ( - 478CE303851F3D5B589B31EB /* Comments */, - 5AE2EDC824103C330011A198 /* Main */, - C26D41CD92E58E7F27B1529B /* Settings */, - 2CEA7D26636B60D4D590C779 /* Stories */, - ); - path = PresentationLayer; - sourceTree = ""; - }; - 5AE2EDC724103C220011A198 /* BusinessLayer */ = { - isa = PBXGroup; - children = ( - 5A7347E8242EA915005B394F /* Managers */, - 5A9C966C2442608E002AC0F5 /* Colors.swift */, - 5A94A2E82439383D00918198 /* Constants.swift */, - ); - path = BusinessLayer; - sourceTree = ""; - }; - 5AE2EDC824103C330011A198 /* Main */ = { - isa = PBXGroup; - children = ( - 5AE2EDC924103C3B0011A198 /* Modules */, - ); - path = Main; - sourceTree = ""; - }; - 5AE2EDC924103C3B0011A198 /* Modules */ = { - isa = PBXGroup; - children = ( - 5AE2EDCA24103C400011A198 /* Root */, - ); - path = Modules; - sourceTree = ""; - }; - 5AE2EDCA24103C400011A198 /* Root */ = { - isa = PBXGroup; - children = ( - 5AE2EDCB24103C520011A198 /* Assembly */, - 5AE2EDCE24103CCC0011A198 /* Configurator */, - 5A8BF17A2449D7B700215883 /* Presenter */, - 5A8BF1732449D77A00215883 /* View */, - ); - path = Root; - sourceTree = ""; - }; - 5AE2EDCB24103C520011A198 /* Assembly */ = { - isa = PBXGroup; - children = ( - 5AE2EDCC24103C9B0011A198 /* RootModuleAssembly.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 5AE2EDCE24103CCC0011A198 /* Configurator */ = { - isa = PBXGroup; - children = ( - 5AE2EDCF24103CDB0011A198 /* RootConfigurator.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 5AE2EDD124103ED20011A198 /* MainTabBar */ = { - isa = PBXGroup; - children = ( - 5ACB43E02448871F0010465B /* Presenter */, - 5AE2EDDB2410409A0011A198 /* View */, - 5AE2EDD524103F100011A198 /* Assembly */, - 5AE2EDD224103EDC0011A198 /* Configurator */, - ); - path = MainTabBar; - sourceTree = ""; - }; - 5AE2EDD224103EDC0011A198 /* Configurator */ = { - isa = PBXGroup; - children = ( - 5AE2EDD324103EED0011A198 /* MainTabBarConfigurator.swift */, - 5AE2EDD9241040420011A198 /* TabBarViewProtocol.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 5AE2EDD524103F100011A198 /* Assembly */ = { - isa = PBXGroup; - children = ( - 5AE2EDD624103F1D0011A198 /* MainTabBarAssembly.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 5AE2EDD82410400F0011A198 /* Controllers */ = { - isa = PBXGroup; - children = ( - 5AE2EDD124103ED20011A198 /* MainTabBar */, - ); - path = Controllers; - sourceTree = ""; - }; - 5AE2EDDB2410409A0011A198 /* View */ = { - isa = PBXGroup; - children = ( - 5AE2EDDC241040A40011A198 /* MainTabBarViewController.swift */, - 5ACB43DE244886E30010465B /* MainTabBarViewOutput.swift */, - 5ACB43E32448875C0010465B /* MainTabBarViewInput.swift */, - ); - path = View; - sourceTree = ""; - }; - 5AEE9ADA249A84E600929A85 /* Views */ = { - isa = PBXGroup; - children = ( - 5AEE9BC4249AB21B00929A85 /* RateView */, - 5AEE9BC2249AB1A100929A85 /* TopStoryTableViewCell */, - ); - path = Views; - sourceTree = ""; - }; - 5AEE9BC2249AB1A100929A85 /* TopStoryTableViewCell */ = { - isa = PBXGroup; - children = ( - 5AEE9ADB249A851900929A85 /* TopStoryTableViewCell.swift */, - 5AEE9ADD249A852F00929A85 /* TopStoryTableViewCell.xib */, - ); - path = TopStoryTableViewCell; - sourceTree = ""; - }; - 5AEE9BC4249AB21B00929A85 /* RateView */ = { - isa = PBXGroup; - children = ( - 5AEE9BC5249AB22500929A85 /* RateView.swift */, - ); - path = RateView; - sourceTree = ""; - }; - 5AF49552249D5CC000E469A2 /* HackerNewsWatch WatchKit App */ = { - isa = PBXGroup; - children = ( - 5AF49553249D5CC000E469A2 /* Interface.storyboard */, - 5AF49556249D5CC500E469A2 /* Assets.xcassets */, - 5AF49558249D5CC500E469A2 /* Info.plist */, - ); - path = "HackerNewsWatch WatchKit App"; - sourceTree = ""; - }; - 5AF49561249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension */ = { - isa = PBXGroup; - children = ( - 5A4D0B19249FAEE60076A781 /* Resources */, - 5AF495C2249D641700E469A2 /* Common */, - 5AF495BF249D5FEC00E469A2 /* Controllers */, - 5AF49564249D5CC500E469A2 /* ExtensionDelegate.swift */, - 5AF49566249D5CC800E469A2 /* Assets.xcassets */, - 5AF49568249D5CC800E469A2 /* Info.plist */, - ); - path = "HackerNewsWatch WatchKit Extension"; - sourceTree = ""; - }; - 5AF495BF249D5FEC00E469A2 /* Controllers */ = { - isa = PBXGroup; - children = ( - 5AF495C0249D600800E469A2 /* TopStoriesInterfaceController.swift */, - 5A4D0B14249FA0670076A781 /* CommentsInterfaceController.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - 5AF495C2249D641700E469A2 /* Common */ = { - isa = PBXGroup; - children = ( - 5A4D0B16249FA9D20076A781 /* Extension */, - 5A4D0B11249F9FD90076A781 /* Cells */, - ); - path = Common; - sourceTree = ""; - }; - 5F7B60CE9584FD15023856EF /* Interactor */ = { - isa = PBXGroup; - children = ( - DCF23845D0D60ECAF198876E /* ThemeInteractorInput.swift */, - D349D0CD9C2060043398C027 /* ThemeInteractorOutput.swift */, - B0E27AFCA2403A03ED302C2C /* ThemeInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - 607D0D5CF88A32286BBB7E19 /* View */ = { - isa = PBXGroup; - children = ( - 5AC0CC25245470FF0060F5C9 /* CommentsCell */, - 5AD8D59524532F6100D7E1F1 /* Comments.storyboard */, - 2BE2E7CCBA31084C76C3274C /* CommentsViewInput.swift */, - 6FEF91989794A3C7AC72B9FF /* CommentsViewOutput.swift */, - F739F4CAB4001886132AD3C7 /* CommentsViewController.swift */, - ); - path = View; - sourceTree = ""; - }; - 6289C622A5C242AFEFBC86F6 /* Router */ = { - isa = PBXGroup; - children = ( - A30051C967D26D38780E7A57 /* ThemeRouterTests.swift */, - ); - path = Router; - sourceTree = ""; - }; - 6920FF250F4B00CBCD6E23D8 /* Configurator */ = { - isa = PBXGroup; - children = ( - 84DB1BC9C41A88EF3EA1E8B3 /* SettingsConfiguratorTests.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 6A8B5540D2AF6417A560F91D /* Assembly */ = { - isa = PBXGroup; - children = ( - 86BAEAE1E6A9BDB269B528B7 /* SettingsAssembly.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 6B0E681D8751E4A9EBA5A2F0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5A4D0B30249FC23D0076A781 /* HNService.framework */, - 5A4D0B27249FBA3B0076A781 /* HNService.framework */, - 5AF495A1249D5E2300E469A2 /* NetworkManager.framework */, - 7F10F7222A47FB417E6C4588 /* Pods_HackerNews.framework */, - BAF3412A178EA5EB164E53D3 /* Pods_HackerNewsTests.framework */, - 941A683CEF3511E341FC3BDA /* Pods_HackerNewsUITests.framework */, - 5A8F6B042499722000484C18 /* NotificationCenter.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 6D2F340C19BF8C42715F26FE /* Presenter */ = { - isa = PBXGroup; - children = ( - EABC193D2B780816BABAF6D7 /* ThemePresenterTests.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 6F1BA349F19C9A2E833041AD /* Theme */ = { - isa = PBXGroup; - children = ( - F64D71AF10C836EF6A629A3C /* View */, - 5A6EFDF302D74AB07328A1DB /* Presenter */, - 5F7B60CE9584FD15023856EF /* Interactor */, - DD7F98E770A0C5A7291E2E46 /* Router */, - D6992840279A502F9163DA87 /* Assembly */, - C6BA8318124D62871DF0FCE3 /* Configurator */, - ); - path = Theme; - sourceTree = ""; - }; - 74A51D6ACEBD7DADE9FA7651 /* Interactor */ = { - isa = PBXGroup; - children = ( - 78C4FBC5B114C6A6B02D7E43 /* CommentsInteractorInput.swift */, - BCEE42028E4D75974DFEBFB7 /* CommentsInteractorOutput.swift */, - 1E8726255A3D84E5CBFC402A /* CommentsInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - 750568379C9702DD8CEB6CF3 /* Configurator */ = { - isa = PBXGroup; - children = ( - 86A1D9B7E0433BA1D3C63F7D /* SettingsConfigurator.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 78602166658AE59F2654534E /* Configurator */ = { - isa = PBXGroup; - children = ( - BB7D49B681E9B168FE7B4F96 /* StoriesConfiguratorTests.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 7D87A71A50611BAFAE9E4AAF /* Presenter */ = { - isa = PBXGroup; - children = ( - DCED8BB0967B1933D8335489 /* StoriesPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 7DF2FB279A3CEB704F9C2957 /* Interactor */ = { - isa = PBXGroup; - children = ( - C9357ED5A64187C76057B33B /* ThemeInteractorTests.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - 8374DF91BCDE049A6A0900E1 /* Presenter */ = { - isa = PBXGroup; - children = ( - 481BE5B37B60EBEA91C05322 /* SettingsPresenterTests.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - 86FCBB2F753081B319542B3F /* View */ = { - isa = PBXGroup; - children = ( - E25768042F1949ED23D54DF4 /* StoriesViewTests.swift */, - ); - path = View; - sourceTree = ""; - }; - 8BC97F3A84EFEA640D8278F3 /* Pods */ = { - isa = PBXGroup; - children = ( - 3F2AA6A6FB80106F288FEE85 /* Pods-HackerNews.debug.xcconfig */, - B74DFCA04DB8F96532311217 /* Pods-HackerNews.release.xcconfig */, - 7830BC11140D5520485785C7 /* Pods-HackerNewsTests.debug.xcconfig */, - 87973D774E9854C755CEE98C /* Pods-HackerNewsTests.release.xcconfig */, - 6588DDD3BA46AA474D11E747 /* Pods-HackerNewsUITests.debug.xcconfig */, - EA33A5A998886E55C869116C /* Pods-HackerNewsUITests.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 8F455115444380D63C5240D8 /* Configurator */ = { - isa = PBXGroup; - children = ( - 4180EE33349724472CC83552 /* CommentsConfiguratorTests.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - 9949B397CEE3E580720BE48F /* Assembly */ = { - isa = PBXGroup; - children = ( - A8A4FDD125295BD464D56161 /* StoriesAssembly.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - 9C5EF7717DB64AB6F788A9E2 /* Interactor */ = { - isa = PBXGroup; - children = ( - 7AD3B924ABCD34C1BE81504C /* StoriesInteractorTests.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - B3E1253FC3E95FF62EFE7583 /* Interactor */ = { - isa = PBXGroup; - children = ( - 84559136E2CC1694F565A308 /* SettingsInteractorInput.swift */, - 1949A91778A123B5EF93CC54 /* SettingsInteractorOutput.swift */, - A682BDFD6027AEE7D726F878 /* SettingsInteractor.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - BF1CFA0B924B072CD4984313 /* View */ = { - isa = PBXGroup; - children = ( - 00C822CF4F21F2BE3AD24BF1 /* ThemeViewTests.swift */, - ); - path = View; - sourceTree = ""; - }; - C2570B1A87F07AE586E2A92C /* Stories */ = { - isa = PBXGroup; - children = ( - 3F8D45DA94E05083F9573FC9 /* Assembly */, - 78602166658AE59F2654534E /* Configurator */, - 9C5EF7717DB64AB6F788A9E2 /* Interactor */, - 10AF3DE3F260614A7751A18D /* Presenter */, - 408D06C99C4F8E674CC20AC6 /* Router */, - 86FCBB2F753081B319542B3F /* View */, - ); - path = Stories; - sourceTree = ""; - }; - C26D41CD92E58E7F27B1529B /* Settings */ = { - isa = PBXGroup; - children = ( - 5A8BF198244A243C00215883 /* Modules */, - ); - path = Settings; - sourceTree = ""; - }; - C6BA8318124D62871DF0FCE3 /* Configurator */ = { - isa = PBXGroup; - children = ( - 2C08F6F4829CAEB8EF67D15A /* ThemeConfigurator.swift */, - C0D7F843BFB3D0ECBFC7E16D /* ThemeModuleInput.swift */, - ); - path = Configurator; - sourceTree = ""; - }; - C97C569A10F809DFFA9F89E1 /* View */ = { - isa = PBXGroup; - children = ( - 5A7347E2242E9529005B394F /* Views */, - 756C7453871B7DB5374C91D0 /* SettingsViewInput.swift */, - 7AECF0419410CC923AC4242E /* SettingsViewOutput.swift */, - 173DA34D18916D6FCA182FA3 /* SettingsViewController.swift */, - C8FD75009999F70D8BED054F /* Settings.storyboard */, - ); - path = View; - sourceTree = ""; - }; - CABAC464F58D823684601050 /* Router */ = { - isa = PBXGroup; - children = ( - A8400E6F8B807CF3B0E7A117 /* CommentsRouterInput.swift */, - 48D032C79286B642547280DF /* CommentsRouter.swift */, - ); - path = Router; - sourceTree = ""; - }; - D026E8602131ADE000512C32 /* Style */ = { - isa = PBXGroup; - children = ( - 5A9C966824425E62002AC0F5 /* Fonts */, - 5A435C10243A2E34009C5355 /* Style.swift */, - 5A435C0E243A2E0B009C5355 /* Stylesheet.swift */, - ); - path = Style; - sourceTree = ""; - }; - D07204E721309FC600E4B1ED = { - isa = PBXGroup; - children = ( - 5AE2EDE7241047650011A198 /* R.generated.swift */, - D07204F221309FC600E4B1ED /* HackerNews */, - D072050721309FC900E4B1ED /* HackerNewsTests */, - D072051221309FC900E4B1ED /* HackerNewsUITests */, - 5A8F6B062499722000484C18 /* HackerNewsTodayExtension */, - 5AF49552249D5CC000E469A2 /* HackerNewsWatch WatchKit App */, - 5AF49561249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension */, - D07204F121309FC600E4B1ED /* Products */, - 8BC97F3A84EFEA640D8278F3 /* Pods */, - 6B0E681D8751E4A9EBA5A2F0 /* Frameworks */, - ); - sourceTree = ""; - }; - D07204F121309FC600E4B1ED /* Products */ = { - isa = PBXGroup; - children = ( - D07204F021309FC600E4B1ED /* Hacker News.app */, - D072050421309FC900E4B1ED /* HackerNewsTests.xctest */, - D072050F21309FC900E4B1ED /* HackerNewsUITests.xctest */, - 5A8F6B032499722000484C18 /* HackerNewsTodayExtension.appex */, - 5AF4954B249D5CC000E469A2 /* HackerNewsWatch.app */, - 5AF4954E249D5CC000E469A2 /* HackerNewsWatch WatchKit App.app */, - 5AF4955D249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension.appex */, - ); - name = Products; - sourceTree = ""; - }; - D07204F221309FC600E4B1ED /* HackerNews */ = { - isa = PBXGroup; - children = ( - D07205332130A23100E4B1ED /* Application */, - 5AE2EDC424103B450011A198 /* Classes */, - D07205212130A02E00E4B1ED /* Common */, - D07205322130A21500E4B1ED /* Resources */, - 5A1CA33524AD3B4B006EA300 /* GoogleService-Info.plist */, - 5A4D0B26249FB9880076A781 /* Hacker News.entitlements */, - ); - path = HackerNews; - sourceTree = ""; - }; - D072050721309FC900E4B1ED /* HackerNewsTests */ = { - isa = PBXGroup; - children = ( - 5AAC708C249035DC005AFFC4 /* SnapshotTests */, - 5A0D25E8248C09A8006718D4 /* PerformanceTests */, - 5AC10D25248AEC1900132EB4 /* Extensions */, - 5AC10D24248AEC0800132EB4 /* UnitTests */, - 5A14BC5424899CA0008E9B7B /* Mocks */, - D072050A21309FC900E4B1ED /* Info.plist */, - ); - path = HackerNewsTests; - sourceTree = ""; - }; - D072051221309FC900E4B1ED /* HackerNewsUITests */ = { - isa = PBXGroup; - children = ( - 5AAC708024902C16005AFFC4 /* UITestFactory */, - 5AAC707B249024A4005AFFC4 /* Helpers */, - D072051321309FC900E4B1ED /* HackerNewsUITests.swift */, - 5AAC708624902D4B005AFFC4 /* SettingsUITests.swift */, - D072051521309FC900E4B1ED /* Info.plist */, - ); - path = HackerNewsUITests; - sourceTree = ""; - }; - D07205212130A02E00E4B1ED /* Common */ = { - isa = PBXGroup; - children = ( - 5AE2EDD82410400F0011A198 /* Controllers */, - 5ACA6C452454E2B1007E89BF /* DataStructures */, - D07205222130A03800E4B1ED /* Extensions */, - 5A1CADEE244B4BDC002D0114 /* Models */, - D026E8602131ADE000512C32 /* Style */, - D07205232130A03E00E4B1ED /* Utils */, - 5AD9C02F244E3C50007F8769 /* Views */, - ); - path = Common; - sourceTree = ""; - }; - D07205222130A03800E4B1ED /* Extensions */ = { - isa = PBXGroup; - children = ( - 5AAC709124903DED005AFFC4 /* UIViewController */, - 5A8BF16C2449AC1000215883 /* Strings */, - 5A8BF15D2449A55800215883 /* Colors */, - 3867CB6E215FB264003C0658 /* Date+.swift */, - D07205262130A0AF00E4B1ED /* UIImage+.swift */, - 5AAC9111245090C60041006F /* Array+Range.swift */, - 5AE95B672461F49C00DAA009 /* UIView+.swift */, - 5A5177CE24C4D289005856C4 /* StoryType+AllValues.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - D07205232130A03E00E4B1ED /* Utils */ = { - isa = PBXGroup; - children = ( - 5A43892D247F10E1002D3111 /* Weak */, - 5A5221312447B87C003B799D /* Themeable */, - 384F0820219E07D30029ADDD /* Alertable.swift */, - D07205282130A10400E4B1ED /* NibLoadableView.swift */, - 5AE2EDDF241045D60011A198 /* Presentable.swift */, - D072052A2130A11A00E4B1ED /* ReusableView.swift */, - 5AE2EDE3241046730011A198 /* TransitionHandler.swift */, - D072052C2130A13800E4B1ED /* UITableView+.swift */, - D072052E2130A15200E4B1ED /* UITableViewCell+.swift */, - D07205302130A17100E4B1ED /* UIViewControllerFactory.swift */, - 5ACB43D8244855F30010465B /* Weak.swift */, - 5ACB43DA244862500010465B /* ImageType.swift */, - 5ACB43DC244883E90010465B /* Optional+Throwable.swift */, - ); - path = Utils; - sourceTree = ""; - }; - D07205322130A21500E4B1ED /* Resources */ = { - isa = PBXGroup; - children = ( - D07205392130ABA400E4B1ED /* Fonts */, - 5A8BF1632449AAAE00215883 /* Strings */, - D07204FA21309FC900E4B1ED /* Assets.xcassets */, - D07204FF21309FC900E4B1ED /* Info.plist */, - ); - path = Resources; - sourceTree = ""; - }; - D07205332130A23100E4B1ED /* Application */ = { - isa = PBXGroup; - children = ( - D07205342130A24200E4B1ED /* View */, - ); - path = Application; - sourceTree = ""; - }; - D07205342130A24200E4B1ED /* View */ = { - isa = PBXGroup; - children = ( - D07204FC21309FC900E4B1ED /* LaunchScreen.storyboard */, - ); - path = View; - sourceTree = ""; - }; - D07205392130ABA400E4B1ED /* Fonts */ = { - isa = PBXGroup; - children = ( - 5A9C963E24425B2F002AC0F5 /* Poppins-Bold.ttf */, - 5A9C964324425B30002AC0F5 /* Poppins-ExtraBold.ttf */, - 5A9C964C24425B32002AC0F5 /* Poppins-ExtraLight.ttf */, - 5A9C964924425B31002AC0F5 /* Poppins-Italic.ttf */, - 5A9C964424425B30002AC0F5 /* Poppins-Light.ttf */, - 5A9C963F24425B2F002AC0F5 /* Poppins-Medium.ttf */, - 5A9C964B24425B32002AC0F5 /* Poppins-Regular.ttf */, - 5A9C964524425B30002AC0F5 /* Poppins-SemiBold.ttf */, - ); - path = Fonts; - sourceTree = ""; - }; - D0C1866D3FBDA97F5349249E /* View */ = { - isa = PBXGroup; - children = ( - C322EE9386899B3C63540FAD /* SettingsViewTests.swift */, - ); - path = View; - sourceTree = ""; - }; - D6992840279A502F9163DA87 /* Assembly */ = { - isa = PBXGroup; - children = ( - 020C7913C10E5D047B6F6F12 /* ThemeAssembly.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - D898A4361F1DEE4AFE83D893 /* Comments */ = { - isa = PBXGroup; - children = ( - EF3C004688BE35D018FB74CD /* Assembly */, - 8F455115444380D63C5240D8 /* Configurator */, - DBACB8CFF0C461753F0C7715 /* Interactor */, - 1E06CF2D837EB1F31F5EDE5E /* Presenter */, - FB4D41DDBCCC27D81B1B84BE /* Router */, - EFEC19FF2BBEFBDF77D83B3C /* View */, - ); - path = Comments; - sourceTree = ""; - }; - DBACB8CFF0C461753F0C7715 /* Interactor */ = { - isa = PBXGroup; - children = ( - 587E7F5215B3B2B613C7AFAF /* CommentsInteractorTests.swift */, - ); - path = Interactor; - sourceTree = ""; - }; - DD7F98E770A0C5A7291E2E46 /* Router */ = { - isa = PBXGroup; - children = ( - B8CC01CE435879B2D5F78B13 /* ThemeRouterInput.swift */, - BAE4653D7772D620D3CBF1B1 /* ThemeRouter.swift */, - ); - path = Router; - sourceTree = ""; - }; - E5CFC53F02B307E3C1CE3226 /* Presenter */ = { - isa = PBXGroup; - children = ( - 5A8BF18C2449EFAF00215883 /* ListData */, - DBD6806965BB5158014D0AAB /* SettingsPresenter.swift */, - ); - path = Presenter; - sourceTree = ""; - }; - EE3CB5B53F2B7A16F50CD2FA /* Settings */ = { - isa = PBXGroup; - children = ( - 5479375AC2AFE078E7500714 /* Assembly */, - 6920FF250F4B00CBCD6E23D8 /* Configurator */, - 51265A8D36D0DA1622A208BB /* Interactor */, - 8374DF91BCDE049A6A0900E1 /* Presenter */, - 0303F88D837CD698A93720BB /* Router */, - D0C1866D3FBDA97F5349249E /* View */, - ); - path = Settings; - sourceTree = ""; - }; - EF3C004688BE35D018FB74CD /* Assembly */ = { - isa = PBXGroup; - children = ( - E57EB5317778D5553A2E21B9 /* CommentsAssemblyTests.swift */, - ); - path = Assembly; - sourceTree = ""; - }; - EFEC19FF2BBEFBDF77D83B3C /* View */ = { - isa = PBXGroup; - children = ( - 06783D5310AB3073753EB966 /* CommentsViewTests.swift */, - ); - path = View; - sourceTree = ""; - }; - F64D71AF10C836EF6A629A3C /* View */ = { - isa = PBXGroup; - children = ( - 5A52211E24479CD7003B799D /* Views */, - 254B44A617EBDBDEC696224C /* ThemeViewInput.swift */, - F6AE570ED443C0AA530DDBC6 /* ThemeViewOutput.swift */, - 54EFB1FA6087E3BE89323CEB /* ThemeViewController.swift */, - C923B096F47F2D687D4445C3 /* Theme.storyboard */, - ); - path = View; - sourceTree = ""; - }; - F7BBA7FD73C0C57982AF8E1E /* Router */ = { - isa = PBXGroup; - children = ( - C58FA4A86F5D21BC625349CD /* StoriesRouterInput.swift */, - 75288D891967908B4B1CF34C /* StoriesRouter.swift */, - ); - path = Router; - sourceTree = ""; - }; - FB4D41DDBCCC27D81B1B84BE /* Router */ = { - isa = PBXGroup; - children = ( - E089D733E432016A28791194 /* CommentsRouterTests.swift */, - ); - path = Router; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 5A8F6B022499722000484C18 /* HackerNewsTodayExtension */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5A8F6B132499722000484C18 /* Build configuration list for PBXNativeTarget "HackerNewsTodayExtension" */; - buildPhases = ( - 5ABCB62C24AE6E6800C42BFC /* ShellScript */, - 5A8F6AFF2499722000484C18 /* Sources */, - 5A8F6B002499722000484C18 /* Frameworks */, - 5A8F6B012499722000484C18 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = HackerNewsTodayExtension; - productName = HackerNewsTodayExtension; - productReference = 5A8F6B032499722000484C18 /* HackerNewsTodayExtension.appex */; - productType = "com.apple.product-type.app-extension"; - }; - 5AF4954A249D5CC000E469A2 /* HackerNewsWatch */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5AF49571249D5CC800E469A2 /* Build configuration list for PBXNativeTarget "HackerNewsWatch" */; - buildPhases = ( - 5AF49549249D5CC000E469A2 /* Resources */, - 5AF49570249D5CC800E469A2 /* Embed Watch Content */, - 7E5D7F0B32F6770A9DC5C348 /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 5AF49551249D5CC000E469A2 /* PBXTargetDependency */, - ); - name = HackerNewsWatch; - productName = HackerNewsWatch; - productReference = 5AF4954B249D5CC000E469A2 /* HackerNewsWatch.app */; - productType = "com.apple.product-type.application.watchapp2-container"; - }; - 5AF4954D249D5CC000E469A2 /* HackerNewsWatch WatchKit App */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5AF4956D249D5CC800E469A2 /* Build configuration list for PBXNativeTarget "HackerNewsWatch WatchKit App" */; - buildPhases = ( - 5AF4954C249D5CC000E469A2 /* Resources */, - 5AF4956C249D5CC800E469A2 /* Embed App Extensions */, - F9BF5BB4A1DA7287E0E16974 /* Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 5AF49560249D5CC500E469A2 /* PBXTargetDependency */, - ); - name = "HackerNewsWatch WatchKit App"; - productName = "HackerNewsWatch WatchKit App"; - productReference = 5AF4954E249D5CC000E469A2 /* HackerNewsWatch WatchKit App.app */; - productType = "com.apple.product-type.application.watchapp2"; - }; - 5AF4955C249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension */ = { - isa = PBXNativeTarget; - buildConfigurationList = 5AF49569249D5CC800E469A2 /* Build configuration list for PBXNativeTarget "HackerNewsWatch WatchKit Extension" */; - buildPhases = ( - 5AF49559249D5CC500E469A2 /* Sources */, - 5AF4955A249D5CC500E469A2 /* Frameworks */, - 5AF4955B249D5CC500E469A2 /* Resources */, - 5AF495A4249D5E2300E469A2 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "HackerNewsWatch WatchKit Extension"; - productName = "HackerNewsWatch WatchKit Extension"; - productReference = 5AF4955D249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension.appex */; - productType = "com.apple.product-type.watchkit2-extension"; - }; - D07204EF21309FC600E4B1ED /* HackerNews */ = { - isa = PBXNativeTarget; - buildConfigurationList = D072051821309FC900E4B1ED /* Build configuration list for PBXNativeTarget "HackerNews" */; - buildPhases = ( - FABE7829A46A52DC8DB4BBDE /* [CP] Check Pods Manifest.lock */, - 5AE2EDC3241038500011A198 /* ShellScript */, - D095C042213F22950047AB79 /* ShellScript */, - D07204EC21309FC600E4B1ED /* Sources */, - D07204ED21309FC600E4B1ED /* Frameworks */, - D07204EE21309FC600E4B1ED /* Resources */, - D02555AC213C6F7400822BFE /* Embed Frameworks */, - 0145C849EAC1D39C87F71C49 /* [CP] Embed Pods Frameworks */, - 5A8F6B102499722000484C18 /* Embed App Extensions */, - 5A1CA33724AD3E83006EA300 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 5A8F6B0E2499722000484C18 /* PBXTargetDependency */, - ); - name = HackerNews; - productName = HackerNews; - productReference = D07204F021309FC600E4B1ED /* Hacker News.app */; - productType = "com.apple.product-type.application"; - }; - D072050321309FC900E4B1ED /* HackerNewsTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = D072051B21309FC900E4B1ED /* Build configuration list for PBXNativeTarget "HackerNewsTests" */; - buildPhases = ( - 3B514FEE8CEE5EF15038C0B6 /* [CP] Check Pods Manifest.lock */, - D072050021309FC900E4B1ED /* Sources */, - D072050121309FC900E4B1ED /* Frameworks */, - D072050221309FC900E4B1ED /* Resources */, - BBDCAF6ECE196C8BEB80E685 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - D072050621309FC900E4B1ED /* PBXTargetDependency */, - ); - name = HackerNewsTests; - productName = HackerNewsTests; - productReference = D072050421309FC900E4B1ED /* HackerNewsTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - D072050E21309FC900E4B1ED /* HackerNewsUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = D072051E21309FC900E4B1ED /* Build configuration list for PBXNativeTarget "HackerNewsUITests" */; - buildPhases = ( - 0BEF9CC3AEE561F4E82EC0FF /* [CP] Check Pods Manifest.lock */, - D072050B21309FC900E4B1ED /* Sources */, - D072050C21309FC900E4B1ED /* Frameworks */, - D072050D21309FC900E4B1ED /* Resources */, - 05B6C3DA64055E257633C222 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - D072051121309FC900E4B1ED /* PBXTargetDependency */, - ); - name = HackerNewsUITests; - productName = HackerNewsUITests; - productReference = D072050F21309FC900E4B1ED /* HackerNewsUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - D07204E821309FC600E4B1ED /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 1150; - LastUpgradeCheck = 0940; - ORGANIZATIONNAME = "Никита Васильев"; - TargetAttributes = { - 5A8F6B022499722000484C18 = { - CreatedOnToolsVersion = 11.5; - }; - 5AF4954A249D5CC000E469A2 = { - CreatedOnToolsVersion = 11.5; - }; - 5AF4954D249D5CC000E469A2 = { - CreatedOnToolsVersion = 11.5; - }; - 5AF4955C249D5CC500E469A2 = { - CreatedOnToolsVersion = 11.5; - }; - D07204EF21309FC600E4B1ED = { - CreatedOnToolsVersion = 9.4.1; - }; - D072050321309FC900E4B1ED = { - CreatedOnToolsVersion = 9.4.1; - TestTargetID = D07204EF21309FC600E4B1ED; - }; - D072050E21309FC900E4B1ED = { - CreatedOnToolsVersion = 9.4.1; - TestTargetID = D07204EF21309FC600E4B1ED; - }; - }; - }; - buildConfigurationList = D07204EB21309FC600E4B1ED /* Build configuration list for PBXProject "HackerNews" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ru, - ); - mainGroup = D07204E721309FC600E4B1ED; - productRefGroup = D07204F121309FC600E4B1ED /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - D07204EF21309FC600E4B1ED /* HackerNews */, - D072050321309FC900E4B1ED /* HackerNewsTests */, - D072050E21309FC900E4B1ED /* HackerNewsUITests */, - 5A8F6B022499722000484C18 /* HackerNewsTodayExtension */, - 5AF4954A249D5CC000E469A2 /* HackerNewsWatch */, - 5AF4954D249D5CC000E469A2 /* HackerNewsWatch WatchKit App */, - 5AF4955C249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 5A8F6B012499722000484C18 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A8F6B0B2499722000484C18 /* MainInterface.storyboard in Resources */, - 5AEE9ADE249A852F00929A85 /* TopStoryTableViewCell.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF49549249D5CC000E469A2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF4954C249D5CC000E469A2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF49557249D5CC500E469A2 /* Assets.xcassets in Resources */, - 5AF49555249D5CC000E469A2 /* Interface.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF4955B249D5CC500E469A2 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF49567249D5CC800E469A2 /* Assets.xcassets in Resources */, - 5A4D0B1D249FAF140076A781 /* Localizable.strings in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D07204EE21309FC600E4B1ED /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - D07204FE21309FC900E4B1ED /* LaunchScreen.storyboard in Resources */, - D07204FB21309FC900E4B1ED /* Assets.xcassets in Resources */, - 5A9C965624425B4A002AC0F5 /* Poppins-ExtraLight.ttf in Resources */, - 5AD8D59624532F6100D7E1F1 /* Comments.storyboard in Resources */, - 5A52212324479D1B003B799D /* ThemeSelectableTableViewCell.xib in Resources */, - 5A7347E7242E9562005B394F /* SettingsTableViewCell.xib in Resources */, - 5A9C965924425B4A002AC0F5 /* Poppins-Light.ttf in Resources */, - 5A9C965224425B4A002AC0F5 /* Poppins-Bold.ttf in Resources */, - 5A9C965824425B4A002AC0F5 /* Poppins-Italic.ttf in Resources */, - 5AC0CC29245472610060F5C9 /* CommentCell.xib in Resources */, - 5A9C965B24425B4A002AC0F5 /* Poppins-Medium.ttf in Resources */, - 5A8054EB244CD9A0005E4F96 /* PostTableViewCell.xib in Resources */, - 5AAC91252450E8A90041006F /* SkeletonCell.xib in Resources */, - 5A9C965424425B4A002AC0F5 /* Poppins-ExtraBold.ttf in Resources */, - 71D3EE6174A29CCC96AF27FC /* Settings.storyboard in Resources */, - 4EFF808EC08E9CCA2D33FC20 /* Theme.storyboard in Resources */, - 5A1CA33624AD3B4B006EA300 /* GoogleService-Info.plist in Resources */, - 5A8BF1682449ABAB00215883 /* Localizable.strings in Resources */, - 5A9C965E24425B4A002AC0F5 /* Poppins-SemiBold.ttf in Resources */, - 5A9C965D24425B4A002AC0F5 /* Poppins-Regular.ttf in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D072050221309FC900E4B1ED /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D072050D21309FC900E4B1ED /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 0145C849EAC1D39C87F71C49 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-HackerNews/Pods-HackerNews-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-HackerNews/Pods-HackerNews-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HackerNews/Pods-HackerNews-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 05B6C3DA64055E257633C222 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-HackerNewsUITests/Pods-HackerNewsUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-HackerNewsUITests/Pods-HackerNewsUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HackerNewsUITests/Pods-HackerNewsUITests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 0BEF9CC3AEE561F4E82EC0FF /* [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-HackerNewsUITests-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; - }; - 3B514FEE8CEE5EF15038C0B6 /* [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-HackerNewsTests-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; - }; - 5A1CA33724AD3E83006EA300 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/FirebaseCrashlytics/run\"\n"; - }; - 5ABCB62C24AE6E6800C42BFC /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "EMBEDDED_EXTENSION_PLUGIN_PATH=\"$TARGET_BUILD_DIR/$PLUGINS_FOLDER_PATH/HackerNewsTodayExtension.appex/Frameworks\"\nif [[ -d \"$EMBEDDED_EXTENSION_PLUGIN_PATH\" ]]; then\n rm -fr \"$EMBEDDED_EXTENSION_PLUGIN_PATH\"\nfi\n"; - }; - 5AE2EDC3241038500011A198 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "$TEMP_DIR/rswift-lastrun", - ); - outputFileListPaths = ( - ); - outputPaths = ( - $SRCROOT/R.generated.swift, - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$PODS_ROOT/R.swift/rswift\" generate \"$SRCROOT/R.generated.swift\"\n"; - }; - BBDCAF6ECE196C8BEB80E685 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-HackerNewsTests/Pods-HackerNewsTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-HackerNewsTests/Pods-HackerNewsTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-HackerNewsTests/Pods-HackerNewsTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - D095C042213F22950047AB79 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; - }; - FABE7829A46A52DC8DB4BBDE /* [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-HackerNews-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 */ - 5A8F6AFF2499722000484C18 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A8F6B082499722000484C18 /* TopStoriesViewController.swift in Sources */, - 5AEE9ADC249A851900929A85 /* TopStoryTableViewCell.swift in Sources */, - 5AEE9BC6249AB22500929A85 /* RateView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 5AF49559249D5CC500E469A2 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AF49565249D5CC500E469A2 /* ExtensionDelegate.swift in Sources */, - 5A4D0B15249FA0670076A781 /* CommentsInterfaceController.swift in Sources */, - 5AF495C1249D600800E469A2 /* TopStoriesInterfaceController.swift in Sources */, - 5A4D0B18249FA9E20076A781 /* String+Decode.swift in Sources */, - 5A4D0B22249FAF560076A781 /* String+Localized.swift in Sources */, - 5AF495C4249D642100E469A2 /* PostCell.swift in Sources */, - 5A4D0B13249FA0090076A781 /* CommentCell.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D07204EC21309FC600E4B1ED /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5A7347EB242EA926005B394F /* SettingsManager.swift in Sources */, - D07205252130A09C00E4B1ED /* Color+.swift in Sources */, - 5A435C0B243A27F5009C5355 /* ThemeManager.swift in Sources */, - 5AE2EDE8241047660011A198 /* R.generated.swift in Sources */, - D07205292130A10400E4B1ED /* NibLoadableView.swift in Sources */, - 5ACB43D9244855F30010465B /* Weak.swift in Sources */, - 384F0821219E07D30029ADDD /* Alertable.swift in Sources */, - 5A5C0AA2241ECBF9007B4FCF /* ServiceLocatorAssembly.swift in Sources */, - D07205272130A0AF00E4B1ED /* UIImage+.swift in Sources */, - 5A438931247F110B002D3111 /* WeakObject.swift in Sources */, - 5AE2EDDD241040A40011A198 /* MainTabBarViewController.swift in Sources */, - 3867CB6F215FB264003C0658 /* Date+.swift in Sources */, - 5AAC91232450E89F0041006F /* SkeletonCell.swift in Sources */, - D07205312130A17100E4B1ED /* UIViewControllerFactory.swift in Sources */, - 5AE2EDCD24103C9B0011A198 /* RootModuleAssembly.swift in Sources */, - 5AAC9112245090C60041006F /* Array+Range.swift in Sources */, - 5AB1139B2456253700332B5F /* LoadingFooterView.swift in Sources */, - D072052D2130A13800E4B1ED /* UITableView+.swift in Sources */, - 5AE2EDE4241046730011A198 /* TransitionHandler.swift in Sources */, - 5A25EF3E2455894A00AC7C56 /* CommentCellViewModel.swift in Sources */, - 5AE2EDE2241046210011A198 /* UINavigationController+.swift in Sources */, - 5ACB43E42448875C0010465B /* MainTabBarViewInput.swift in Sources */, - 5AB25D8D2451ADD2009C05ED /* UIViewController+.swift in Sources */, - 5A52212124479D01003B799D /* ThemeSelectableTableViewCell.swift in Sources */, - D072052B2130A11A00E4B1ED /* ReusableView.swift in Sources */, - 5ACB43DB244862500010465B /* ImageType.swift in Sources */, - 5A435C11243A2E34009C5355 /* Style.swift in Sources */, - 5A8BF16E2449AC3F00215883 /* String+Localized.swift in Sources */, - D072052F2130A15200E4B1ED /* UITableViewCell+.swift in Sources */, - 5A94A2E92439383D00918198 /* Constants.swift in Sources */, - 5A8BF1902449F00E00215883 /* SettingsListData.swift in Sources */, - 5A7347E5242E9549005B394F /* SettingsTableViewCell.swift in Sources */, - 5AE2EDD724103F1D0011A198 /* MainTabBarAssembly.swift in Sources */, - 5AE2EDD024103CDB0011A198 /* RootConfigurator.swift in Sources */, - 5A6FF6C324182BDF003F9B9D /* StoriesConfigurator.swift in Sources */, - 38F7969A2198A2710083EFEF /* String+.swift in Sources */, - 5A25EF3C2455888700AC7C56 /* PostCellViewModel.swift in Sources */, - 5AE2EDDA241040420011A198 /* TabBarViewProtocol.swift in Sources */, - 5AE2EDD424103EED0011A198 /* MainTabBarConfigurator.swift in Sources */, - D07204F421309FC600E4B1ED /* AppDelegate.swift in Sources */, - 5ACB43DD244883E90010465B /* Optional+Throwable.swift in Sources */, - 5A435C0F243A2E0B009C5355 /* Stylesheet.swift in Sources */, - 5A00D8A2244655560013B3F5 /* Themeable.swift in Sources */, - 5A5C0AA0241ECB7D007B4FCF /* ServiceLocatorConfigurator.swift in Sources */, - 5A435C0D243A286A009C5355 /* Theme.swift in Sources */, - 4FBF586447B5D27D3FFE8695 /* StoriesViewInput.swift in Sources */, - 5AAC91202450E8830041006F /* HNImageView.swift in Sources */, - D746902D625B40CF3177A9A3 /* StoriesViewOutput.swift in Sources */, - CB02B826066E9E278B544AB0 /* StoriesViewController.swift in Sources */, - D078561297ACC3EAD4FC08EA /* StoriesPresenter.swift in Sources */, - 9217DDCCAA0BF7EACAFA0B62 /* StoriesInteractorInput.swift in Sources */, - B2A18F8423F3459CE46FDCE1 /* StoriesInteractorOutput.swift in Sources */, - FAEE86FF44686D0C9C73A091 /* StoriesInteractor.swift in Sources */, - 5A9C966D2442608E002AC0F5 /* Colors.swift in Sources */, - A2973A58E5B273EFDDA3199A /* StoriesRouterInput.swift in Sources */, - 5A8BF1792449D7A500215883 /* RootViewOutput.swift in Sources */, - 2C5287CA0F51E7CC0998F056 /* StoriesRouter.swift in Sources */, - C72C67A3801D1099E2E78982 /* StoriesAssembly.swift in Sources */, - 5AE2EDE0241045D60011A198 /* Presentable.swift in Sources */, - 37B144B44710E04D53D38B1F /* SettingsViewInput.swift in Sources */, - 5A8BF1752449D78900215883 /* RootSplitViewController.swift in Sources */, - 5ACB43DF244886E30010465B /* MainTabBarViewOutput.swift in Sources */, - 5D7937310DC67C2D0A5D7B26 /* SettingsViewOutput.swift in Sources */, - 841F0F20A6C107848D230D68 /* SettingsViewController.swift in Sources */, - 85FD344F5BC1934F615CFAC9 /* SettingsPresenter.swift in Sources */, - 5A9C966724425E35002AC0F5 /* Font+.swift in Sources */, - 5A8054E9244CD98B005E4F96 /* PostTableViewCell.swift in Sources */, - 5ACB43E22448872A0010465B /* MainTabBarPresenter.swift in Sources */, - 385B495311D6CBD5EB3F44E7 /* SettingsInteractorInput.swift in Sources */, - 437795427873CF1A213CABF6 /* SettingsInteractorOutput.swift in Sources */, - 5AE95B682461F49C00DAA009 /* UIView+.swift in Sources */, - 53E3C81A7457949576309607 /* SettingsInteractor.swift in Sources */, - 5ACA6C472454E2C2007E89BF /* Queue.swift in Sources */, - B375711E66BCACF11B39AFFF /* SettingsRouterInput.swift in Sources */, - 5A9C966324425C69002AC0F5 /* FontStyle.swift in Sources */, - 74A73D457753C052AE5B8A6F /* SettingsRouter.swift in Sources */, - 80A064256ECA8ACEA6CAB352 /* SettingsAssembly.swift in Sources */, - 5A9C966524425D07002AC0F5 /* Fonts.swift in Sources */, - 5A8BF17C2449D7BF00215883 /* RootPresenter.swift in Sources */, - F82094BAB4970E016398315E /* SettingsConfigurator.swift in Sources */, - 8AA0F3452ED1C28896F8CDA8 /* ThemeViewInput.swift in Sources */, - E4D5B4ABDCD0124D7352915F /* ThemeViewOutput.swift in Sources */, - 7C9258D74C6FC05A982ECDAE /* ThemeViewController.swift in Sources */, - 5AA02BEA244F658400B5B15F /* String+Attributed.swift in Sources */, - 5A8BF1772449D79600215883 /* RootViewInput.swift in Sources */, - 5A25EF3A2455870900AC7C56 /* BaseCellModel.swift in Sources */, - 7618E63336284456181FC902 /* ThemePresenter.swift in Sources */, - 5A030954245E1C900004D669 /* SkeletonCellViewModel.swift in Sources */, - 2B5AC3B732D57493E958B015 /* ThemeInteractorInput.swift in Sources */, - AA1B09AF7685666005E3B301 /* ThemeInteractorOutput.swift in Sources */, - 96808C88C39A94F772EAE01F /* ThemeInteractor.swift in Sources */, - F5A0C84E03E8AD683E2A10AB /* ThemeRouterInput.swift in Sources */, - 42DDF700C568557D9721B39E /* ThemeRouter.swift in Sources */, - 5ACB43D424484BA00010465B /* ThemeUpdatable.swift in Sources */, - 5A43892F247F10EC002D3111 /* WeakObjectSet.swift in Sources */, - 1735864EEEEE601B855C3A57 /* ThemeAssembly.swift in Sources */, - BCBA5819B2598079B48DDA9C /* ThemeConfigurator.swift in Sources */, - 0E95E3FEBFD9EFBA7A696CFD /* ThemeModuleInput.swift in Sources */, - 99FF3F4214BEF893002901F4 /* CommentsViewInput.swift in Sources */, - 7E61868B4DA2FA7FB7E7C37C /* CommentsViewOutput.swift in Sources */, - A583AFD99ACCED9C4F18E5C8 /* CommentsViewController.swift in Sources */, - 5AC0CC272454710E0060F5C9 /* CommentCell.swift in Sources */, - 6D4A7BBE429B8E09377B7DE7 /* CommentsPresenter.swift in Sources */, - D266EC5FA8619EC9119612B6 /* CommentsInteractorInput.swift in Sources */, - 5A5177CF24C4D289005856C4 /* StoryType+AllValues.swift in Sources */, - B29F0F598619422FE8A87738 /* CommentsInteractorOutput.swift in Sources */, - 7C61D1FC15958476BCBDB865 /* CommentsInteractor.swift in Sources */, - 946BE207880921E25E93302B /* CommentsRouterInput.swift in Sources */, - 23B60C899980EFD06CDBEFE6 /* CommentsRouter.swift in Sources */, - AAE00CC387A3A7599BFDA77F /* CommentsAssembly.swift in Sources */, - 5CBE8D8383348E881F3F800A /* CommentsConfigurator.swift in Sources */, - 227D42543579B790BDF32914 /* CommentsModuleInput.swift in Sources */, - 5AAC709324903DFF005AFFC4 /* UIViewController+Safe.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D072050021309FC900E4B1ED /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AC10D38248AF90300132EB4 /* RootConfiguratorTests.swift in Sources */, - 5AC10D23248AE98B00132EB4 /* PostModelTests.swift in Sources */, - EDE03E5231E7355B1352B22D /* StoriesAssemblyTests.swift in Sources */, - 87235FA1D54F1BBD55496993 /* StoriesViewTests.swift in Sources */, - EF595E98A7E35C576C647499 /* StoriesInteractorTests.swift in Sources */, - 5AC10D20248AE7D000132EB4 /* TestData.swift in Sources */, - 223CB8777BF3B1F04F665350 /* StoriesPresenterTests.swift in Sources */, - 7ACEA30CDD9C2EC5E4304163 /* StoriesRouterTests.swift in Sources */, - 5AC892B2248D797300AFC5AC /* UnitTestError.swift in Sources */, - 3C63C91DAA87693F4AF304FD /* StoriesConfiguratorTests.swift in Sources */, - EF0A179C2312F9F08DD9B53B /* SettingsAssemblyTests.swift in Sources */, - D12DE9103766E9D49A9F2B37 /* SettingsViewTests.swift in Sources */, - 6F86307F7A43F5636A2A8702 /* SettingsInteractorTests.swift in Sources */, - 5AC10D35248AF82900132EB4 /* RootAssemblyTests.swift in Sources */, - 5AC10D2B248AF65F00132EB4 /* Utils.swift in Sources */, - 1AFA6585B5891D01D02A3D1F /* SettingsPresenterTests.swift in Sources */, - 1B478549D7C1F1E80DD61905 /* SettingsRouterTests.swift in Sources */, - 0D1603CAFAEA30A156862ED1 /* SettingsConfiguratorTests.swift in Sources */, - 5AC892B0248D5C6F00AFC5AC /* HNServiceMock.swift in Sources */, - 5AAC708E249035F1005AFFC4 /* SettingsShapshotTests.swift in Sources */, - 5A0D25DB248BF42F006718D4 /* MainTabBarPresenterTests.swift in Sources */, - CFB9F0778AD778B3121CC343 /* ThemeAssemblyTests.swift in Sources */, - 5AC10D29248AF52F00132EB4 /* CommentModelTests.swift in Sources */, - 11247932183A52868E71490D /* ThemeViewTests.swift in Sources */, - 79C0892943C9CCCC93DE4683 /* ThemeInteractorTests.swift in Sources */, - 38AB0BDD12801ED62ABDAA88 /* ThemePresenterTests.swift in Sources */, - 5A0D25E4248BF969006718D4 /* ThemeManagerTests.swift in Sources */, - 4D878B9B487FDD6CA0B50886 /* ThemeRouterTests.swift in Sources */, - 5A0D25F2248C190D006718D4 /* MockThemeManager.swift in Sources */, - 5A14BC5624899CAE008E9B7B /* MockContainer.swift in Sources */, - B7A71D6D5CC68FEAB62EC9C3 /* ThemeConfiguratorTests.swift in Sources */, - 5AC10D32248AF71000132EB4 /* RootPresenterTests.swift in Sources */, - 5A0D25DF248BF5A8006718D4 /* MainTabBarConfuguratorTests.swift in Sources */, - 41A294D7045C082B25DA58AF /* CommentsAssemblyTests.swift in Sources */, - 7BB7FDA10247EA18C604DBC2 /* CommentsViewTests.swift in Sources */, - 8058C73658389F9105345824 /* CommentsInteractorTests.swift in Sources */, - 106C017560B2C1FF27A88E13 /* CommentsPresenterTests.swift in Sources */, - 4F95CC6EFE3DB85351335A0A /* CommentsRouterTests.swift in Sources */, - 7EC5DDFA7D277736127F645F /* CommentsConfiguratorTests.swift in Sources */, - 5AC10D3B248AFA3D00132EB4 /* RootViewTests.swift in Sources */, - 5A0D25E1248BF606006718D4 /* MainTabBarViewTests.swift in Sources */, - 5A0D25E7248C084A006718D4 /* WeakObjectSetTests.swift in Sources */, - 5AC10D27248AEC2400132EB4 /* Data+.swift in Sources */, - 5A0D25EA248C09C2006718D4 /* WeakObjectSetPerformanceTests.swift in Sources */, - 5A0D25DD248BF46F006718D4 /* MainTabBarAssemblyTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - D072050B21309FC900E4B1ED /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5AAC708924902F88005AFFC4 /* ThemesScreen.swift in Sources */, - 5AAC708524902CCC005AFFC4 /* SettingScreen.swift in Sources */, - 5AAC708224902C26005AFFC4 /* UITestFactory.swift in Sources */, - 5AAC707F2490252C005AFFC4 /* App.swift in Sources */, - 5AAC707D249024BC005AFFC4 /* UITest+Helprers.swift in Sources */, - 5AAC708B24903190005AFFC4 /* StoryScreen.swift in Sources */, - 5AAC708724902D4B005AFFC4 /* SettingsUITests.swift in Sources */, - D072051421309FC900E4B1ED /* HackerNewsUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 5A8F6B0E2499722000484C18 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - platformFilter = ios; - target = 5A8F6B022499722000484C18 /* HackerNewsTodayExtension */; - targetProxy = 5A8F6B0D2499722000484C18 /* PBXContainerItemProxy */; - }; - 5AF49551249D5CC000E469A2 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 5AF4954D249D5CC000E469A2 /* HackerNewsWatch WatchKit App */; - targetProxy = 5AF49550249D5CC000E469A2 /* PBXContainerItemProxy */; - }; - 5AF49560249D5CC500E469A2 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 5AF4955C249D5CC500E469A2 /* HackerNewsWatch WatchKit Extension */; - targetProxy = 5AF4955F249D5CC500E469A2 /* PBXContainerItemProxy */; - }; - D072050621309FC900E4B1ED /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D07204EF21309FC600E4B1ED /* HackerNews */; - targetProxy = D072050521309FC900E4B1ED /* PBXContainerItemProxy */; - }; - D072051121309FC900E4B1ED /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D07204EF21309FC600E4B1ED /* HackerNews */; - targetProxy = D072051021309FC900E4B1ED /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 5A4D0B1F249FAF140076A781 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - 5A4D0B1E249FAF140076A781 /* en */, - 5A4D0B20249FAF190076A781 /* ru */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - 5A8BF16A2449ABAB00215883 /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - 5A8BF16B2449ABBD00215883 /* ru */, - 5A8BF194244A111000215883 /* en */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - 5A8F6B092499722000484C18 /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 5A8F6B0A2499722000484C18 /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; - 5AF49553249D5CC000E469A2 /* Interface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 5AF49554249D5CC000E469A2 /* Base */, - ); - name = Interface.storyboard; - sourceTree = ""; - }; - D07204FC21309FC900E4B1ED /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - D07204FD21309FC900E4B1ED /* Base */, - 5A8BF1642449AB5400215883 /* ru */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 5A8F6B112499722000484C18 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2312; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HackerNewsTodayExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 2.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNews.HackerNewsTodayExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 5A8F6B122499722000484C18 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2312; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HackerNewsTodayExtension/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 2.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNews.HackerNewsTodayExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 5AF4956A249D5CC800E469A2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = "HackerNewsWatch WatchKit Extension/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsWatch.watchkitapp.watchkitextension; - PRODUCT_NAME = "${TARGET_NAME}"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Debug; - }; - 5AF4956B249D5CC800E469A2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = "HackerNewsWatch WatchKit Extension/Info.plist"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsWatch.watchkitapp.watchkitextension; - PRODUCT_NAME = "${TARGET_NAME}"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Release; - }; - 5AF4956E249D5CC800E469A2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - IBSC_MODULE = HackerNewsWatch_WatchKit_Extension; - INFOPLIST_FILE = "HackerNewsWatch WatchKit App/Info.plist"; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsWatch.watchkitapp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Debug; - }; - 5AF4956F249D5CC800E469A2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - IBSC_MODULE = HackerNewsWatch_WatchKit_Extension; - INFOPLIST_FILE = "HackerNewsWatch WatchKit App/Info.plist"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsWatch.watchkitapp; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SKIP_INSTALL = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 6.2; - }; - name = Release; - }; - 5AF49572249D5CC800E469A2 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = A8WE5LL2GU; - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsWatch; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 5AF49573249D5CC800E469A2 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = A8WE5LL2GU; - MARKETING_VERSION = 1.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsWatch; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - D072051621309FC900E4B1ED /* 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_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; - CODE_SIGN_IDENTITY = "iPhone Developer"; - 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 = ( - "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 = 10.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - D072051721309FC900E4B1ED /* 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_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; - CODE_SIGN_IDENTITY = "iPhone Developer"; - 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_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 = 10.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - D072051921309FC900E4B1ED /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 3F2AA6A6FB80106F288FEE85 /* Pods-HackerNews.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = "HackerNews/Hacker News.entitlements"; - CODE_SIGN_STYLE = Automatic; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = "$(SRCROOT)/HackerNews/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNews; - PRODUCT_MODULE_NAME = HackerNews; - PRODUCT_NAME = "Hacker News"; - SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - D072051A21309FC900E4B1ED /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = B74DFCA04DB8F96532311217 /* Pods-HackerNews.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CODE_SIGN_ENTITLEMENTS = "HackerNews/Hacker News.entitlements"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = "$(SRCROOT)/HackerNews/Resources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 2.0; - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNews; - PRODUCT_MODULE_NAME = HackerNews; - PRODUCT_NAME = "Hacker News"; - SUPPORTS_MACCATALYST = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - D072051C21309FC900E4B1ED /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7830BC11140D5520485785C7 /* Pods-HackerNewsTests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HackerNewsTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hacker News.app/Hacker News"; - }; - name = Debug; - }; - D072051D21309FC900E4B1ED /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 87973D774E9854C755CEE98C /* Pods-HackerNewsTests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HackerNewsTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Hacker News.app/Hacker News"; - }; - name = Release; - }; - D072051F21309FC900E4B1ED /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 6588DDD3BA46AA474D11E747 /* Pods-HackerNewsUITests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HackerNewsUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = HackerNews; - }; - name = Debug; - }; - D072052021309FC900E4B1ED /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = EA33A5A998886E55C869116C /* Pods-HackerNewsUITests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = A8WE5LL2GU; - INFOPLIST_FILE = HackerNewsUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.nikitavasilev.HackerNewsUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = HackerNews; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 5A8F6B132499722000484C18 /* Build configuration list for PBXNativeTarget "HackerNewsTodayExtension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5A8F6B112499722000484C18 /* Debug */, - 5A8F6B122499722000484C18 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 5AF49569249D5CC800E469A2 /* Build configuration list for PBXNativeTarget "HackerNewsWatch WatchKit Extension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AF4956A249D5CC800E469A2 /* Debug */, - 5AF4956B249D5CC800E469A2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 5AF4956D249D5CC800E469A2 /* Build configuration list for PBXNativeTarget "HackerNewsWatch WatchKit App" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AF4956E249D5CC800E469A2 /* Debug */, - 5AF4956F249D5CC800E469A2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 5AF49571249D5CC800E469A2 /* Build configuration list for PBXNativeTarget "HackerNewsWatch" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 5AF49572249D5CC800E469A2 /* Debug */, - 5AF49573249D5CC800E469A2 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D07204EB21309FC600E4B1ED /* Build configuration list for PBXProject "HackerNews" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D072051621309FC900E4B1ED /* Debug */, - D072051721309FC900E4B1ED /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D072051821309FC900E4B1ED /* Build configuration list for PBXNativeTarget "HackerNews" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D072051921309FC900E4B1ED /* Debug */, - D072051A21309FC900E4B1ED /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D072051B21309FC900E4B1ED /* Build configuration list for PBXNativeTarget "HackerNewsTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D072051C21309FC900E4B1ED /* Debug */, - D072051D21309FC900E4B1ED /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - D072051E21309FC900E4B1ED /* Build configuration list for PBXNativeTarget "HackerNewsUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - D072051F21309FC900E4B1ED /* Debug */, - D072052021309FC900E4B1ED /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = D07204E821309FC600E4B1ED /* Project object */; -} diff --git a/HackerNews.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/HackerNews.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index cbbc691..0000000 --- a/HackerNews.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/HackerNews.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HackerNews.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/HackerNews.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNews.xcscheme b/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNews.xcscheme deleted file mode 100644 index aa7c383..0000000 --- a/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNews.xcscheme +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNewsTodayExtension.xcscheme b/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNewsTodayExtension.xcscheme deleted file mode 100644 index aea34e9..0000000 --- a/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNewsTodayExtension.xcscheme +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNewsWatch WatchKit App.xcscheme b/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNewsWatch WatchKit App.xcscheme deleted file mode 100644 index 2fa786e..0000000 --- a/HackerNews.xcodeproj/xcshareddata/xcschemes/HackerNewsWatch WatchKit App.xcscheme +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews.xcworkspace/contents.xcworkspacedata b/HackerNews.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 4e96450..0000000 --- a/HackerNews.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/HackerNews.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/HackerNews.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/HackerNews/Application/View/Base.lproj/LaunchScreen.storyboard b/HackerNews/Application/View/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index 20df96e..0000000 --- a/HackerNews/Application/View/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - Poppins-Bold - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Application/View/ru.lproj/LaunchScreen.strings b/HackerNews/Application/View/ru.lproj/LaunchScreen.strings deleted file mode 100644 index f44cac1..0000000 --- a/HackerNews/Application/View/ru.lproj/LaunchScreen.strings +++ /dev/null @@ -1,3 +0,0 @@ - -/* Class = "UILabel"; text = "Hacker News"; ObjectID = "0ZE-OB-y6P"; */ -"0ZE-OB-y6P.text" = "Hacker News"; diff --git a/HackerNews/Classes/AppDelegate.swift b/HackerNews/Classes/AppDelegate.swift new file mode 100644 index 0000000..f0bf5a2 --- /dev/null +++ b/HackerNews/Classes/AppDelegate.swift @@ -0,0 +1,18 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import DesignKit +import Pulse +import UIKit + +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + #if DEBUG + URLSessionProxyDelegate.enableAutomaticRegistration() + #endif + FontFamily.registerAllCustomFonts() + return true + } +} diff --git a/HackerNews/Classes/ApplicationLayer/AppDelegate.swift b/HackerNews/Classes/ApplicationLayer/AppDelegate.swift deleted file mode 100644 index 5566908..0000000 --- a/HackerNews/Classes/ApplicationLayer/AppDelegate.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// AppDelegate.swift -// HackerNews -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit -import Firebase - -@UIApplicationMain -class AppDelegate: UIResponder { - - // MARK: Public Properties - var window: UIWindow? - let serviceLocatorConfigurator = ServiceLocatorConfigurator() -} - -// MARK: UIApplicationDelegate -extension AppDelegate: UIApplicationDelegate { - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - window = UIWindow(frame: UIScreen.main.bounds) - window?.makeKeyAndVisible() - - if let window = window { - let rootConfigurator = serviceLocatorConfigurator.assembler.resolver.resolve(RootConfigurator.self) - rootConfigurator?.installIntoWindow(window) - } - - ThemeManager.shared.theme = Theme(rawValue: SettingsManager.shared.currentTheme ?? "") ?? .dark - - FirebaseApp.configure() - - return true - } -} - -extension AppDelegate { - static var currentDelegate: AppDelegate { - // swiftlint:disable force_cast - return UIApplication.shared.delegate as! AppDelegate - // swiftlint:enable force_cast - } - - static var currentWindow: UIWindow { - // swiftlint:disable force_unwrapping - return currentDelegate.window! - // swiftlint:enable force_unwrapping - } - - static var currentApplication: UIApplication { - return UIApplication.shared - } -} diff --git a/HackerNews/Classes/ApplicationLayer/ServiceLocatorAssembly.swift b/HackerNews/Classes/ApplicationLayer/ServiceLocatorAssembly.swift deleted file mode 100644 index fb4b373..0000000 --- a/HackerNews/Classes/ApplicationLayer/ServiceLocatorAssembly.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ServiceLocatorAssembly.swift -// HackerNews -// -// Created by Никита Васильев on 15.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import Swinject - -final class ServiceLocatorAssembly: Assembly { - func assemble(container: Container) { - container.register(RootConfigurator.self) { _ in - let delegate = AppDelegate.currentDelegate - return RootConfigurator(parentAssembler: delegate.serviceLocatorConfigurator.assembler) - } - } -} diff --git a/HackerNews/Classes/ApplicationLayer/ServiceLocatorConfigurator.swift b/HackerNews/Classes/ApplicationLayer/ServiceLocatorConfigurator.swift deleted file mode 100644 index eea1028..0000000 --- a/HackerNews/Classes/ApplicationLayer/ServiceLocatorConfigurator.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ServiceLocatorConfigurator.swift -// HackerNews -// -// Created by Никита Васильев on 15.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import Swinject -import HNService - -final class ServiceLocatorConfigurator { - // MARK: Public Properties - let assembler: Assembler - let container = Container(parent: nil) - - // MARK: Initialization - init() { - container.register(ThemeManager.self) { _ in - return ThemeManager.shared - } - - container.register(HNServiceProtocol.self) { _ in - return HNService() - } - - assembler = Assembler(container: container) - assembler.apply(assembly: ServiceLocatorAssembly()) - } -} diff --git a/HackerNews/Classes/ApplicationRoot/Assembly/ApplicationAssembly.swift b/HackerNews/Classes/ApplicationRoot/Assembly/ApplicationAssembly.swift new file mode 100644 index 0000000..e1519a1 --- /dev/null +++ b/HackerNews/Classes/ApplicationRoot/Assembly/ApplicationAssembly.swift @@ -0,0 +1,49 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +// MARK: - IApplicationAssembly + +protocol IApplicationAssembly { + func assemble() -> AnyView +} + +// MARK: - ApplicationAssembly + +final class ApplicationAssembly: IApplicationAssembly { + // MARK: Properties + + private var dependencies: IDependenciesAssembly + + // MARK: Initialization + + init(dependencies: IDependenciesAssembly) { + self.dependencies = dependencies + } + + // MARK: IApplicationAssembly + + func assemble() -> AnyView { + VStack { + if UIDevice.current.userInterfaceIdiom == .phone { + tabbarView + } else { + contentView + } + }.eraseToAnyView() + } + + private var tabbarView: AnyView { + RootTabBarAssembly( + homePublicAssembly: dependencies.homePublicAssembly, + settingsPublicAssembly: dependencies.settingsPublicAssembly + ).assemble() + } + + private var contentView: AnyView { + dependencies.homePublicAssembly.assemble() + } +} diff --git a/HackerNews/Classes/ApplicationRoot/Assembly/DependenciesAssembly.swift b/HackerNews/Classes/ApplicationRoot/Assembly/DependenciesAssembly.swift new file mode 100644 index 0000000..c86b6e7 --- /dev/null +++ b/HackerNews/Classes/ApplicationRoot/Assembly/DependenciesAssembly.swift @@ -0,0 +1,35 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Home +import HomeInterfaces +import NetworkLayer +import NetworkLayerInterfaces +import Settings +import SettingsInterfaces + +// MARK: - IDependenciesAssembly + +protocol IDependenciesAssembly { + var homePublicAssembly: IHomePublicAssembly { get } + var settingsPublicAssembly: ISettingsPublicAssembly { get } + var networkAssembly: INetworkLayerAssembly { get } +} + +// MARK: - DependenciesAssembly + +final class DependenciesAssembly: IDependenciesAssembly { + var homePublicAssembly: IHomePublicAssembly { + HomePublicAssembly(requestProcessor: networkAssembly.assemble(), settingsAssembly: settingsPublicAssembly) + } + + var networkAssembly: INetworkLayerAssembly { + NetworkLayerAssembly() + } + + var settingsPublicAssembly: ISettingsPublicAssembly { + SettingsPublicAssembly() + } +} diff --git a/HackerNews/Classes/ApplicationRoot/View/TabBar/Factory/RootTabBarViewFactory.swift b/HackerNews/Classes/ApplicationRoot/View/TabBar/Factory/RootTabBarViewFactory.swift new file mode 100644 index 0000000..8030baa --- /dev/null +++ b/HackerNews/Classes/ApplicationRoot/View/TabBar/Factory/RootTabBarViewFactory.swift @@ -0,0 +1,33 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import HomeInterfaces +import SettingsInterfaces +import SwiftUI + +struct RootTabBarViewFactory { + // MARK: Properties + + private let homePublicAssembly: IHomePublicAssembly + private let settingsPublicAssembly: ISettingsPublicAssembly + + // MARK: Initialization + + init(homePublicAssembly: IHomePublicAssembly, settingsPublicAssembly: ISettingsPublicAssembly) { + self.homePublicAssembly = homePublicAssembly + self.settingsPublicAssembly = settingsPublicAssembly + } + + // MARK: Internal + + func view(for tab: Tab) -> some View { + switch tab { + case .home: + return homePublicAssembly.assemble() + case .settings: + return settingsPublicAssembly.assemble() + } + } +} diff --git a/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarAssembly.swift b/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarAssembly.swift new file mode 100644 index 0000000..c29b0c2 --- /dev/null +++ b/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarAssembly.swift @@ -0,0 +1,50 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import HomeInterfaces +import SettingsInterfaces +import SwiftUI +import UIExtensions + +// MARK: - IRootTabBarAssembly + +protocol IRootTabBarAssembly { + func assemble() -> AnyView +} + +// MARK: - RootTabBarAssembly + +final class RootTabBarAssembly: IRootTabBarAssembly { + // MARK: Private + + private let homePublicAssembly: IHomePublicAssembly + private let settingsPublicAssembly: ISettingsPublicAssembly + + private lazy var store: Store = .init( + initialState: RootTabBarViewStore.State(tabs: [.home, .settings], tab: .home) + ) { + RootTabBarViewStore() + } + + // MARK: Initialization + + init(homePublicAssembly: IHomePublicAssembly, settingsPublicAssembly: ISettingsPublicAssembly) { + self.homePublicAssembly = homePublicAssembly + self.settingsPublicAssembly = settingsPublicAssembly + } + + // MARK: IRootTabBarAssembly + + func assemble() -> AnyView { + RootTabBarView( + store: store, + viewFactory: RootTabBarViewFactory( + homePublicAssembly: homePublicAssembly, + settingsPublicAssembly: settingsPublicAssembly + ) + ).eraseToAnyView() + } +} diff --git a/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarView.swift b/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarView.swift new file mode 100644 index 0000000..4ff5ef7 --- /dev/null +++ b/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarView.swift @@ -0,0 +1,73 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import SwiftUI + +// MARK: - RootTabBarView + +struct RootTabBarView: View { + // MARK: Properties + + private let store: StoreOf + private let viewFactory: RootTabBarViewFactory + + // MARK: Initialization + + init(store: StoreOf, viewFactory: RootTabBarViewFactory) { + self.store = store + self.viewFactory = viewFactory + } + + // MARK: View + + var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + TabView(selection: viewStore.binding(send: { .tab($0.tab) })) { + tabs(in: viewStore) + } + .tint(.orange) + } + } + + // MARK: Private + + @ViewBuilder + private func tabItemView(_ tab: Tab, @ViewBuilder content: @escaping () -> some View) -> some View { + content() + .tabItem { + Label(title: { Text(tab.name) }, icon: { Image(systemName: tab.iconSystemName) }) + } + } + + private func tabs(in viewStore: ViewStore) -> some View { + ForEach(viewStore.tabs) { tab in + tabItemView(tab) { + viewFactory.view(for: tab) + } + } + } +} + +// MARK: - ContentView_Previews + +// #if DEBUG +// +// import Home +// +// struct ContentView_Previews: PreviewProvider { +// static var previews: some View { +// RootTabBarView( +// store: Store( +// initialState: RootTabBarViewStore.State(tabs: [.home, .search, .settings], tab: .home) +// ) { +// RootTabBarViewStore() +// }, +// viewFactory: RootTabBarViewFactory(homePublicAssembly: HomePublicAssembly(requestProcessor: <#IRequestProcessor#>)) +// ) +// } +// } +// +// #endif diff --git a/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarViewStore.swift b/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarViewStore.swift new file mode 100644 index 0000000..f692c67 --- /dev/null +++ b/HackerNews/Classes/ApplicationRoot/View/TabBar/RootTabBarViewStore.swift @@ -0,0 +1,28 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import Foundation +import OrderedCollections + +struct RootTabBarViewStore: Reducer { + struct State: Equatable, Hashable { + @BindingState var tabs: OrderedSet + @BindingState var tab: Tab + } + + enum Action { + case tab(Tab) + } + + var body: some ReducerOf { + Reduce { _, action in + switch action { + case .tab: + return .none + } + } + } +} diff --git a/HackerNews/Classes/ApplicationRoot/View/TabBar/Tab.swift b/HackerNews/Classes/ApplicationRoot/View/TabBar/Tab.swift new file mode 100644 index 0000000..4303167 --- /dev/null +++ b/HackerNews/Classes/ApplicationRoot/View/TabBar/Tab.swift @@ -0,0 +1,32 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Foundation +import HackerNewsLocalization + +enum Tab: Int, Identifiable { + case home + case settings + + var id: Self { self } + + var name: String { + switch self { + case .home: + return L10n.TabBar.Item.home + case .settings: + return L10n.TabBar.Item.settings + } + } + + var iconSystemName: String { + switch self { + case .home: + return "house" + case .settings: + return "gearshape" + } + } +} diff --git a/HackerNews/Classes/BusinessLayer/Colors.swift b/HackerNews/Classes/BusinessLayer/Colors.swift deleted file mode 100644 index 18a15fb..0000000 --- a/HackerNews/Classes/BusinessLayer/Colors.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Colors.swift -// HackerNews -// -// Created by Никита Васильев on 11.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -enum Colors { - static let white: UIColor = { - return .fromRGB(255, 255, 255) - }() - - static let black: UIColor = { - return .fromRGB(0, 0, 0) - }() - - static let darkGray: UIColor = { - return .fromRGB(43, 43, 43) - }() - - static let greyish: UIColor = { - return .fromRGB(229, 229, 229) - }() - - static let greyishBrown: UIColor = { - return .fromRGB(50, 50, 50) - }() - - static let gray: UIColor = { - return .fromRGB(112, 112, 112) - }() - - static let lightGray: UIColor = { - return .fromRGB(242, 242, 247) - }() - - static let warmGrayLight: UIColor = { - return .fromRGB(153, 153, 153) - }() - - static let warmGray: UIColor = { - return .fromRGB(248, 248, 248) - }() - - static let lightOrange: UIColor = { - return .fromRGB(255, 149, 0) - }() - - static let darkOrange: UIColor = { - return .fromRGB(255, 159, 10) - }() - - static let gray20: UIColor = { - return .fromRGBA(199, 201, 209, 0.2) - }() -} diff --git a/HackerNews/Classes/BusinessLayer/Constants.swift b/HackerNews/Classes/BusinessLayer/Constants.swift deleted file mode 100644 index 1eb4530..0000000 --- a/HackerNews/Classes/BusinessLayer/Constants.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Constants.swift -// HackerNews -// -// Created by Никита Васильев on 05.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct Constants { - struct Settings { - static let theme: String = "Theme" - } - - struct Links { - static let feedbackURL: String = "https://www.github.com/nik3212/HackerNews" - static let appstoreURL: String = "itms-apps://itunes.apple.com/app/id1442922669" - static let imageExtractorURL: String = "https://thumbnail-extractor.herokuapp.com/?url=" - } -} diff --git a/HackerNews/Classes/BusinessLayer/Managers/SettingsManager/SettingsManager.swift b/HackerNews/Classes/BusinessLayer/Managers/SettingsManager/SettingsManager.swift deleted file mode 100644 index b8b003b..0000000 --- a/HackerNews/Classes/BusinessLayer/Managers/SettingsManager/SettingsManager.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SettingsManager.swift -// HackerNews -// -// Created by Никита Васильев on 28.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final class SettingsManager { - - // MARK: Public Propreties - - /// <#Description#> - static let shared = SettingsManager() - - // MARK: Initialization - private init() { } - - // MARK: Public Properties - - /// A `String` value that contains the current theme name. - var currentTheme: String? { - set { UserDefaults.standard.set(newValue, forKey: Constants.Settings.theme) } - get { UserDefaults.standard.string(forKey: Constants.Settings.theme) } - } -} diff --git a/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/Theme.swift b/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/Theme.swift deleted file mode 100644 index e72cb86..0000000 --- a/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/Theme.swift +++ /dev/null @@ -1,245 +0,0 @@ -// -// Theme.swift -// HackerNews -// -// Created by Никита Васильев on 05.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import Skeleton - -enum Theme: String { - /// Light theme - case light = "Light" - - /// Dark theme - case dark = "Dark" - - /// <#Description#> - static let `default` = Theme.dark - - /// Create a new Theme instance. - /// - /// - Parameter name: A `String` value that contains the theme name. - init(name: String) { - self = Theme(rawValue: name) ?? .default - } -} - -extension Theme { - var navigationBar: Style { - switch self { - case .dark: - return Stylesheet.NavigationBar.dark - case .light: - return Stylesheet.NavigationBar.light - } - } - - var tabBar: Style { - switch self { - case .dark: - return Stylesheet.TabBar.dark - case .light: - return Stylesheet.TabBar.light - } - } - - var segmentedControl: Style { - switch self { - case .dark: - return Stylesheet.SegmentedControl.dark - case .light: - return Stylesheet.SegmentedControl.light - } - } - - var activityIndicator: Style { - switch self { - case .dark: - return Stylesheet.ActivityIndicator.dark - case .light: - return Stylesheet.ActivityIndicator.light - } - } - - var cellOne: Style { - switch self { - case .dark: - return Stylesheet.Cell.darkWithOrangeTint - case .light: - return Stylesheet.Cell.lightWithOrangeTint - } - } - - var cellTwo: Style { - switch self { - case .dark: - return Stylesheet.Cell.darkWithGrayTint - case .light: - return Stylesheet.Cell.lightWithGrayTint - } - } - - var postCell: Style { - switch self { - case .dark: - return Stylesheet.Cell.darkPostCell - case .light: - return Stylesheet.Cell.lightPostCell - } - } - - var postTitle: Style { - switch self { - case .dark: - return Stylesheet.Label.darkPostTitleText - case .light: - return Stylesheet.Label.lightPostTitleText - } - } - - var refreshControl: Style { - switch self { - case .dark: - return Stylesheet.RefreshControl.dark - case .light: - return Stylesheet.RefreshControl.light - } - } - - var baseSettingsTitle: Style { - switch self { - case .dark: - return Stylesheet.Label.darkSettingsBaseText - case .light: - return Stylesheet.Label.lightSettingsBaseText - } - } - - var baseTableViewHeaderTitle: Style { - switch self { - case .dark: - return Stylesheet.Label.darkTableViewHeaderText - case .light: - return Stylesheet.Label.lightTableViewHeaderText - } - } - - var infoSettingsTitle: Style { - switch self { - case .dark: - return Stylesheet.Label.infoSettingsText - case .light: - return Stylesheet.Label.infoSettingsText - } - } - - var tableView: Style { - switch self { - case .dark: - return Stylesheet.TableView.dark - case .light: - return Stylesheet.TableView.light - } - } - - var tableViewHeader: Style { - switch self { - case .dark: - return Stylesheet.TableViewHeader.dark - case .light: - return Stylesheet.TableViewHeader.light - } - } - - var view: Style { - switch self { - case .dark: - return Stylesheet.View.dark - case .light: - return Stylesheet.View.light - } - } - - var statusBar: UIStatusBarStyle { - switch self { - case .dark: - return .lightContent - case .light: - if #available(iOS 13.0, *) { - return .darkContent - } else { - return .default - } - } - } - - var skeleton: Style { - switch self { - case .dark: - return Stylesheet.Skeleton.dark - case .light: - return Stylesheet.Skeleton.light - } - } - - func emptySetTitle(title: String) -> NSAttributedString { - let color = self == .dark ? Colors.lightGray : Colors.darkGray - return Stylesheet.AttributedString.emptySetTitle(string: title, color: color) - } - - func emptySetDescription(text: String) -> NSAttributedString { - let color = self == .dark ? Colors.lightGray : Colors.darkGray - return Stylesheet.AttributedString.emptySetDescription(string: text, color: color) - } - - func commentTitle(username: String, time: String) -> NSAttributedString { - let usernameColor = self == .dark ? Colors.lightOrange : Colors.darkOrange - let timeColor = self == .dark ? Colors.lightGray : Colors.darkGray - let dotString = Stylesheet.AttributedString.postDescription(string: " • ", color: timeColor) - - let attributedUsername = Stylesheet.AttributedString.commentTitle(string: username, color: usernameColor) - let attributedTime = Stylesheet.AttributedString.commentTitle(string: time, color: timeColor) - - return attributedUsername + dotString + attributedTime - } - - func commentText(text: String) -> NSAttributedString { - let textColor = self == .dark ? Colors.lightGray : Colors.darkGray - return Stylesheet.AttributedString.commentText(string: text, color: textColor) - } - - func noCommentsText(text: String) -> NSAttributedString { - let textColor = self == .dark ? Colors.lightGray : Colors.darkGray - return Stylesheet.AttributedString.noCommentsText(string: text, color: textColor) - } - - func postDescriptionTitle(score: String?, username: String?, time: String?) -> NSAttributedString { - let baseColor = self == .dark ? Colors.lightGray : Colors.darkGray - let dotString = Stylesheet.AttributedString.postDescription(string: " • ", color: baseColor) - - var results: [NSAttributedString] = [] - - if let score = score { - if let icon = R.image.pointsIcon() { - results.append(Stylesheet.AttributedString.postDescription(icon: icon, color: baseColor)) - results.append(NSAttributedString(string: " ")) - } - results.append(Stylesheet.AttributedString.postDescription(string: score, color: baseColor)) - results.append(dotString) - } - - if let username = username { - results.append(Stylesheet.AttributedString.postDescription(string: username, color: baseColor)) - results.append(dotString) - } - - if let time = time { - results.append(Stylesheet.AttributedString.postDescription(string: time, color: baseColor)) - } - - return results.reduce(NSAttributedString()) { $0 + $1 } - } -} diff --git a/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/ThemeManager.swift b/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/ThemeManager.swift deleted file mode 100644 index 165060c..0000000 --- a/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/ThemeManager.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// ThemeManager.swift -// HackerNews -// -// Created by Никита Васильев on 05.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol ThemeManagerProtocol: AnyObject { - var theme: Theme { get set } - var themes: [Theme] { get } - - func addObserver(_ observer: ThemeObserver) - func removeObserver(_ observer: ThemeObserver) -} - -final class ThemeManager: ThemeManagerProtocol { - - // MARK: Public Properties - - /// Returns the shared theme manager object. - static let shared = ThemeManager() - - /// A `Theme` value that contains the current application theme. - var theme: Theme = Theme(rawValue: SettingsManager.shared.currentTheme ?? "") ?? .dark { - didSet { - UIView.animate(withDuration: 0.3) { [weak self] in - guard let `self` = self else { return } - - for observer in self.observers.allObjects { - guard let weakObserver = observer as? ThemeObserver else { return } - weakObserver.themeDidChange(self.theme) - } - } - SettingsManager.shared.currentTheme = theme.rawValue - } - } - - /// All application themes. - let themes: [Theme] = [.light, .dark] - - /// Contains observers. - private(set) var observers = WeakObjectSet() - - // MARK: Intialization - private init() { } - - // MARK: Public Methods - - /// Add observer to theme manager. - /// - /// - Parameter observer: A `ThemeObserver` value that contains the object that will receive notification that theme was changed. - func addObserver(_ observer: ThemeObserver) { - guard !observers.contains(observer as AnyObject) else { - return - } - - observers.append(observer as AnyObject) - } - - /// Remove observer from manager. - /// - /// - Parameter observer: The object to be delete. - func removeObserver(_ observer: ThemeObserver) { - observers.remove(observer as AnyObject) - } -} diff --git a/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/Themeable.swift b/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/Themeable.swift deleted file mode 100644 index c3510b8..0000000 --- a/HackerNews/Classes/BusinessLayer/Managers/ThemeManager/Themeable.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// Themeable.swift -// HackerNews -// -// Created by Никита Васильев on 14.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -protocol ThemeObserver: class { - func themeDidChange(_ theme: Theme) -} diff --git a/HackerNews/Classes/HackerNewsApp.swift b/HackerNews/Classes/HackerNewsApp.swift new file mode 100644 index 0000000..ebcf790 --- /dev/null +++ b/HackerNews/Classes/HackerNewsApp.swift @@ -0,0 +1,36 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import Home +import PulseUI +import SwiftUI +import UIExtensions + +@main +struct HackerNewsApp: App { + // MARK: Properties + + @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + @State private var showConsole = false + + private let assembly: IApplicationAssembly = ApplicationAssembly(dependencies: DependenciesAssembly()) + + // MARK: View + + var body: some Scene { + WindowGroup { + self.assembly.assemble() + .sheet(isPresented: $showConsole) { + NavigationView { + ConsoleView() + } + } + .onShake { + showConsole.toggle() + } + } + } +} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Assembly/CommentsAssembly.swift b/HackerNews/Classes/PresentationLayer/Comments/Assembly/CommentsAssembly.swift deleted file mode 100644 index 98fac30..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Assembly/CommentsAssembly.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// CommentsAssembly.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Swinject -import HNService - -final class CommentsModuleAssembly: Assembly { - func assemble(container: Container) { - container.register(CommentsInteractor.self) { (resolver, presenter: CommentsPresenter) in - let interactor = CommentsInteractor() - interactor.output = presenter - interactor.networkService = resolver.resolve(HNServiceProtocol.self) - return interactor - } - - container.register(CommentsRouter.self) { (_, viewController: CommentsViewController) in - let router = CommentsRouter() - router.transitionHandler = viewController - return router - } - - container.register(CommentsModuleInput.self) { (resolver, viewController: CommentsViewController, post: PostModel) in - let presenter = CommentsPresenter() - - presenter.view = viewController - presenter.interactor = resolver.resolve(CommentsInteractor.self, argument: presenter) - presenter.router = resolver.resolve(CommentsRouter.self, argument: viewController) - presenter.themeManager = resolver.resolve(ThemeManager.self) - presenter.post = post - return presenter - } - - container.register(CommentsViewController.self) { (resolver, post: PostModel) in - let viewController = R.storyboard.comments().instantiateViewController(type: CommentsViewController.self) - viewController.output = resolver.resolve(CommentsModuleInput.self, arguments: viewController, post) as? CommentsPresenter - return viewController - } - } -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Configurator/CommentsConfigurator.swift b/HackerNews/Classes/PresentationLayer/Comments/Configurator/CommentsConfigurator.swift deleted file mode 100644 index 2bb5339..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Configurator/CommentsConfigurator.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// CommentsConfigurator.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Swinject -import struct HNService.PostModel - -final class CommentsConfigurator { - // MARK: Private Properties - private var viewController: CommentsViewController? - private let assembler: Assembler - - // MARK: Initialization - init(parentAssembler: Assembler) { - assembler = Assembler([CommentsModuleAssembly()], parent: parentAssembler) - } - - // MARK: Public Methods - func configure(post: PostModel) -> CommentsViewController? { - if let viewController = assembler.resolver.resolve(CommentsViewController.self, argument: post) { - return viewController - } - return nil - } -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Configurator/CommentsModuleInput.swift b/HackerNews/Classes/PresentationLayer/Comments/Configurator/CommentsModuleInput.swift deleted file mode 100644 index 6b2b713..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Configurator/CommentsModuleInput.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// CommentsModuleInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -protocol CommentsModuleInput: class { - func showDetail(from viewController: UIViewController) -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractor.swift b/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractor.swift deleted file mode 100644 index 1b0787c..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractor.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// CommentsInteractor.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation -import protocol HNService.HNServiceProtocol - -class CommentsInteractor { - // MARK: Public Proeprties - weak var output: CommentsInteractorOutput! - var networkService: HNServiceProtocol? -} - -// MARK: CommentsInteractorInput -extension CommentsInteractor: CommentsInteractorInput { - func fetchComments(for id: Int) { - networkService?.loadComments(with: id, completion: { [weak self] comment in - self?.output.fetchCommentsSuccess(comment) - }, fail: { [weak self] error in - self?.output.fetchCommentsFail(error: error) - }) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractorInput.swift b/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractorInput.swift deleted file mode 100644 index c103e13..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractorInput.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// CommentsInteractorInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import protocol HNService.HNServiceProtocol - -protocol CommentsInteractorInput { - var output: CommentsInteractorOutput! { get set } - var networkService: HNServiceProtocol? { get set } - - func fetchComments(for id: Int) -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractorOutput.swift b/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractorOutput.swift deleted file mode 100644 index c7495f9..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Interactor/CommentsInteractorOutput.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// CommentsInteractorOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import struct HNService.CommentModel - -protocol CommentsInteractorOutput: class { - func fetchCommentsSuccess(_ comment: CommentModel) - func fetchCommentsFail(error: Error) -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Presenter/CommentsPresenter.swift b/HackerNews/Classes/PresentationLayer/Comments/Presenter/CommentsPresenter.swift deleted file mode 100644 index 47d6509..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Presenter/CommentsPresenter.swift +++ /dev/null @@ -1,201 +0,0 @@ -// -// CommentsPresenter.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit -import struct HNService.CommentModel -import struct HNService.PostModel - -final class CommentsPresenter { - // MARK: Public Properties - weak var view: CommentsViewInput! - var interactor: CommentsInteractorInput! - var router: CommentsRouterInput! - var themeManager: ThemeManagerProtocol! - var post: PostModel! - - // MARK: Private Properties - private var commentIds = Queue() - private var commentTexts = [String]() - private var comments: [CommentModel] = [] - private var loadingCommentId: Int? - - // MARK: Life Cycle - deinit { - themeManager.removeObserver(self) - } - - // MARK: Private Methods - private func fetchComments() { - guard let id = commentIds.dequeue(), loadingCommentId == nil else { - self.view.setLoadingIndicator(to: false) - return - } - self.loadingCommentId = id - self.interactor.fetchComments(for: id) - } - - private func prepareComments(from comment: CommentModel) { - if comment.deleted { - loadingCommentId = nil - fetchComments() - return - } - - var commentObject = comment - commentObject.level = 0 - - var sortedComments = [comment] - - self.getComments(from: (commentObject.comments, 1), to: &sortedComments) - - var indexPaths: [IndexPath] = [] - var row = self.comments.count - - sortedComments.forEach { model in - if model.deleted { - return - } - - if let text = model.text { - self.commentTexts.append(text.htmlDecoded) - } - - self.comments.append(model) - - let indexPath = IndexPath(row: row, section: Sections.comments.rawValue) - indexPaths.append(indexPath) - row += 1 - } - - self.loadingCommentId = nil - self.view.setLoadingIndicator(to: false) - self.view.hideActivityIndicator() - self.view.insertRows(at: indexPaths) - } - - private func getComments(from tuple: (graph: [CommentModel], level: Int), to comments: inout [CommentModel]) { - for var comment in tuple.graph { - comment.level = tuple.level - comments.append(comment) - getComments(from: (comment.comments, tuple.level + 1), to: &comments) - } - } -} - -// MARK: CommentsViewOutput -extension CommentsPresenter: CommentsViewOutput { - func viewIsReady() { - view.setupInitialState(title: CommentsConstants.title.localized()) - view.showActivityIndicator() - view.update(theme: themeManager.theme) - themeManager.addObserver(self) - - if post.kids.isEmpty { - view.displayMessage(text: CommentsConstants.noComments.localized()) - view.hideActivityIndicator() - } else { - commentIds.enqueue(post.kids) - fetchComments() - } - } - - func numbersOfSection() -> Int { - return 2 - } - - func numberOfRows(in section: Int) -> Int { - guard let sectionType = Sections(rawValue: section) else { return 0 } - - switch sectionType { - case .info: - return 1 - case .comments: - return loadingCommentId != nil ? 0 : comments.count - } - } - - func getPost() -> PostModel? { - return post - } - - func getModel(for indexPath: IndexPath) -> BaseCellViewModel { - guard let sectionType = Sections(rawValue: indexPath.section) else { fatalError("") } - - switch sectionType { - case .info: - return PostCellViewModel(post: post, theme: themeManager.theme, placeholderImage: .placeholder) - case .comments: - return CommentCellViewModel(comment: comments[indexPath.row], - text: commentTexts[indexPath.row], - theme: themeManager.theme) - } - } - - func didSelectRow(at indexPath: IndexPath) { - guard let section = Sections(rawValue: indexPath.section) else { return } - - if section == .info, let url = post.url { - router.openPost(from: url) - } - } - - func willDisplay(for indexPath: IndexPath) { - guard let sectionType = Sections(rawValue: indexPath.section) else { return } - - let isLastRow = sectionType == .comments && indexPath.row == comments.count - 1 - - if isLastRow && !commentIds.isEmpty { - view.setLoadingIndicator(to: true) - fetchComments() - } - } - - func heightForRow(at indexPath: IndexPath) -> CGFloat { - return 95.0 - } -} - -// MARK: CommentsInteractorOutput -extension CommentsPresenter: CommentsInteractorOutput { - func fetchCommentsSuccess(_ comment: CommentModel) { - prepareComments(from: comment) - } - - func fetchCommentsFail(error: Error) { - - } -} - -// MARK: CommentsModuleInput -extension CommentsPresenter: CommentsModuleInput { - func showDetail(from viewController: UIViewController) { - view.showDetail(from: viewController) - } -} - -extension CommentsPresenter: ThemeObserver { - func themeDidChange(_ theme: Theme) { - view.update(theme: theme) - } -} - -// MARK: Constants -extension CommentsPresenter { - private enum CommentsConstants { - static let title: String = "Comments" - static let noComments: String = "No comments" - } -} - -// MARK: Sections -extension CommentsPresenter { - private enum Sections: Int { - case info - case comments - } -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Router/CommentsRouter.swift b/HackerNews/Classes/PresentationLayer/Comments/Router/CommentsRouter.swift deleted file mode 100644 index 799a3a3..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Router/CommentsRouter.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// CommentsRouter.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation -import SafariServices - -class CommentsRouter { - weak var transitionHandler: TransitionHandler? -} - -// MARK: CommentsRouterInput -extension CommentsRouter: CommentsRouterInput { - func openPost(from url: String) { - transitionHandler?.openModule({ viewController in - guard let url = URL(string: url) else { return } - let safariVC = SFSafariViewController(url: url) - viewController.present(safariVC, animated: true, completion: nil) - }) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/Router/CommentsRouterInput.swift b/HackerNews/Classes/PresentationLayer/Comments/Router/CommentsRouterInput.swift deleted file mode 100644 index c28b2b6..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/Router/CommentsRouterInput.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// CommentsRouterInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol CommentsRouterInput { - func openPost(from url: String) -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/View/Comments.storyboard b/HackerNews/Classes/PresentationLayer/Comments/View/Comments.storyboard deleted file mode 100644 index 4f19360..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/View/Comments.storyboard +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsCell/CommentCell.swift b/HackerNews/Classes/PresentationLayer/Comments/View/CommentsCell/CommentCell.swift deleted file mode 100644 index 4443538..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsCell/CommentCell.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// CommentCell.swift -// HackerNews -// -// Created by Никита Васильев on 25.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import struct HNService.CommentModel - -final class CommentCell: UITableViewCell { - - // MARK: IBOutlets - @IBOutlet private var titleLabel: UILabel! - @IBOutlet private var textView: UITextView! - @IBOutlet private var titleLabelLeadingConstraint: NSLayoutConstraint! - @IBOutlet private var textViewLeadingConstraint: NSLayoutConstraint! - - // MARK: Private Properties - private var theme: Theme? - - // MARK: Initialization - override func awakeFromNib() { - super.awakeFromNib() - textView.textContainer.lineFragmentPadding = 0 - textView.textContainerInset = .zero - } - - // MARK: Public Methods - - /// Set model data to the cell. - /// - /// - Parameter model: A `CommentModel` value that contains the cell data. - func setup(model: CommentModel, text: String) { - guard let username = model.by, let seconds = model.time else { return } - - let time = Date().timeAgo(from: seconds) - - titleLabel.attributedText = theme?.commentTitle(username: username, time: time) - textView.attributedText = theme?.commentText(text: text) - updatePadding(with: model.level) - } - - /// Apply theme to cell. - /// - /// - Parameter theme: A `Theme` value that contains the current application theme. - func apply(theme: Theme) { - self.theme = theme - theme.postTitle.apply(to: titleLabel) - theme.postCell.apply(to: self) - theme.view.apply(to: textView) - theme.view.apply(to: titleLabel) - } - - // MARK: Private Methods - - /// <#Description#> - /// - /// - Parameter level: <#level description#> - private func updatePadding(with level: Int) { - titleLabelLeadingConstraint.constant = CGFloat(level) * Metrics.padding + Metrics.padding - textViewLeadingConstraint.constant = CGFloat(level) * Metrics.padding + Metrics.padding - } -} - -// MARK: Metrics -extension CommentCell { - private struct Metrics { - static let padding: CGFloat = 15.0 - } -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsCell/CommentCell.xib b/HackerNews/Classes/PresentationLayer/Comments/View/CommentsCell/CommentCell.xib deleted file mode 100644 index 56d5717..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsCell/CommentCell.xib +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewController.swift b/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewController.swift deleted file mode 100644 index f6623c5..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewController.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// CommentsViewController.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -class CommentsViewController: UIViewController { - - // MARK: IBOutlets - @IBOutlet private var tableView: UITableView! - - // MARK: Public Properties - var output: CommentsViewOutput! - - // MARK: Private Properties - private var theme: Theme? - private var activityIndicator = UIActivityIndicatorView() - - private lazy var loadingFooterView: UIView = { - let view = UIView() - let activityIndicator = UIActivityIndicatorView() - activityIndicator.color = .black - view.addSubview(activityIndicator) - return view - }() - - override func viewDidLoad() { - super.viewDidLoad() - setup() - configureActivityIndicator() - output.viewIsReady() - self.navigationItem.leftItemsSupplementBackButton = true - } - - // MARK: Private Methods - private func setup() { - if #available(iOS 11.0, *) { - navigationItem.largeTitleDisplayMode = .never - } - - tableView.register(PostTableViewCell.self) - tableView.register(SkeletonCell.self) - tableView.register(CommentCell.self) - tableView.tableFooterView = UIView() - tableView.rowHeight = UITableView.automaticDimension - tableView.cellLayoutMarginsFollowReadableWidth = true - - if #available(iOS 11.0, *) { - tableView.estimatedRowHeight = UITableView.automaticDimension - } else { - tableView.estimatedRowHeight = Metrics.estimatedRowHeight - } - } - - private func configureActivityIndicator() { - activityIndicator.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(activityIndicator) - - NSLayoutConstraint.activate([ - activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), - activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor) - ]) - - activityIndicator.style = .gray - activityIndicator.hidesWhenStopped = true - } - - private func showLoadingIndicator() { - let spinner = UIActivityIndicatorView(style: .gray) - theme?.activityIndicator.apply(to: spinner) - spinner.startAnimating() - spinner.frame = CGRect(x: CGFloat(0), y: CGFloat(0), width: tableView.bounds.width, height: CGFloat(44)) - - tableView.tableFooterView = spinner - tableView.tableFooterView?.isHidden = false - } - - private func hideLoadingIndicator() { - tableView.tableFooterView = UIView() - } -} - -// MARK: CommentsViewInput -extension CommentsViewController: CommentsViewInput { - func setupInitialState(title: String) { - self.title = title - } - - func displayMessage(text: String) { - let backgroundView = UIView() - let textLabel = UILabel() - - textLabel.attributedText = theme?.noCommentsText(text: text) - - view.setView(textLabel) - - tableView.backgroundView = backgroundView - } - - func reloadData() { - tableView.reloadData() - } - - func showActivityIndicator() { - activityIndicator.startAnimating() - } - - func hideActivityIndicator() { - activityIndicator.stopAnimating() - } - - func insertRows(at indexPaths: [IndexPath]) { - tableView.beginUpdates() - self.tableView.setContentOffset(self.tableView.contentOffset, animated: false) - tableView.insertRows(at: indexPaths, with: .automatic) - tableView.endUpdates() - } - - func setLoadingIndicator(to state: Bool) { - state ? showLoadingIndicator() : hideLoadingIndicator() - } -} - -// MARK: UITableViewDelegte -extension CommentsViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - output.didSelectRow(at: indexPath) - tableView.deselectRow(at: indexPath, animated: true) - } -} - -// MARK: UITableViewDataSource -extension CommentsViewController: UITableViewDataSource { - func numberOfSections(in tableView: UITableView) -> Int { - return output.numbersOfSection() - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return output.numberOfRows(in: section) - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = output.getModel(for: indexPath) - return tableView.dequeueReusableCell(forIndexPath: indexPath, with: model) - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } - - func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - output.willDisplay(for: indexPath) - } -} - -// MARK: ThemeUpdatable -extension CommentsViewController: ThemeUpdatable { - func update(theme: Theme) { - self.theme = theme - - if let navigationBar = navigationController?.navigationBar { - theme.navigationBar.apply(to: navigationBar) - } - - theme.activityIndicator.apply(to: activityIndicator) - theme.tableView.apply(to: tableView) - theme.view.apply(to: view) - - if let indexPaths = tableView.indexPathsForVisibleRows, !indexPaths.isEmpty { - tableView.reloadRows(at: indexPaths, with: .none) - } - } -} - -// MARK: Constants -extension CommentsViewController { - private enum Metrics { - static let estimatedRowHeight: CGFloat = 75.0 - } -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewInput.swift b/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewInput.swift deleted file mode 100644 index 9284c2a..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewInput.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// CommentsViewInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation - -protocol CommentsViewInput: class, Presentable, ThemeUpdatable { - var output: CommentsViewOutput! { get set } - - func setupInitialState(title: String) - func displayMessage(text: String) - func reloadData() - func showActivityIndicator() - func hideActivityIndicator() - func insertRows(at indexPaths: [IndexPath]) - func setLoadingIndicator(to state: Bool) -} diff --git a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewOutput.swift b/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewOutput.swift deleted file mode 100644 index 9030f87..0000000 --- a/HackerNews/Classes/PresentationLayer/Comments/View/CommentsViewOutput.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// CommentsViewOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit -import struct HNService.PostModel - -protocol CommentsViewOutput: class { - func viewIsReady() - func numberOfRows(in section: Int) -> Int - func getPost() -> PostModel? - func getModel(for indexPath: IndexPath) -> BaseCellViewModel - func didSelectRow(at indexPath: IndexPath) - func willDisplay(for indexPath: IndexPath) - func heightForRow(at indexPath: IndexPath) -> CGFloat - func numbersOfSection() -> Int -} diff --git a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Assembly/RootModuleAssembly.swift b/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Assembly/RootModuleAssembly.swift deleted file mode 100644 index 81e9d29..0000000 --- a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Assembly/RootModuleAssembly.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// RootModuleAssembly.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Swinject - -final class RootModuleAssembly: Assembly { - func assemble(container: Container) { - container.register(RootSplitViewController.self) { (resolver, firstVC: MainTabBarViewController) in - let splitViewController = RootSplitViewController() - splitViewController.output = resolver.resolve(RootPresenter.self, argument: splitViewController) - - splitViewController.viewControllers = [firstVC] - splitViewController.preferredPrimaryColumnWidthFraction = 2 / 3 - - return splitViewController - } - - container.register(MainTabBarConfigurator.self) { resolver in - let parentAssembler = resolver.resolve(RootConfigurator.self)?.assembler - return MainTabBarConfigurator(parentAssembler: parentAssembler.unwrap()) - } - - container.register(CommentsConfigurator.self) { resolver in - let parentAssembler = resolver.resolve(RootConfigurator.self)?.assembler - return CommentsConfigurator(parentAssembler: parentAssembler.unwrap()) - } - - container.register(RootPresenter.self) { (resolver, viewController: RootSplitViewController) in - let themeManager = resolver.resolve(ThemeManager.self).unwrap() - let presenter = RootPresenter(view: viewController, themeManager: themeManager) - return presenter - } - } -} diff --git a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Configurator/RootConfigurator.swift b/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Configurator/RootConfigurator.swift deleted file mode 100644 index 0da2504..0000000 --- a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Configurator/RootConfigurator.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// RootConfigurator.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import Swinject - -final class RootConfigurator { - - // MARK: Private Properties - let assembler: Assembler - - // MARK: Intialization - init(parentAssembler: Assembler) { - assembler = Assembler([RootModuleAssembly()], parent: parentAssembler) - } - - // MARK: Public Methods - func installIntoWindow(_ window: UIWindow) { - let mainTabBarController = MainTabBarConfigurator(parentAssembler: assembler).configure() - - if let viewController = assembler.resolver.resolve(RootSplitViewController.self, argument: mainTabBarController) { - window.rootViewController = viewController - window.makeKeyAndVisible() - } - } -} diff --git a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Presenter/RootPresenter.swift b/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Presenter/RootPresenter.swift deleted file mode 100644 index b711876..0000000 --- a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/Presenter/RootPresenter.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// RootPresenter.swift -// HackerNews -// -// Created by Никита Васильев on 17.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final class RootPresenter { - // MARK: Private Properties - unowned let view: RootViewInput - let themeManager: ThemeManagerProtocol - - // MARK: Initialization - init(view: RootViewInput, themeManager: ThemeManagerProtocol) { - self.view = view - self.themeManager = themeManager - } -} - -// MARK: RootViewOutput -extension RootPresenter: RootViewOutput { - func viewIsReady() { - view.setupInitialState(theme: themeManager.theme) - themeManager.addObserver(self) - } -} - -// MARK: ThemeObserver -extension RootPresenter: ThemeObserver { - func themeDidChange(_ theme: Theme) { - view.update(theme: theme) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootSplitViewController.swift b/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootSplitViewController.swift deleted file mode 100644 index ad851d3..0000000 --- a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootSplitViewController.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// RootSplitViewController.swift -// HackerNews -// -// Created by Никита Васильев on 17.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -class RootSplitViewController: UISplitViewController { - - // MARK: Public Properties - var output: RootViewOutput! - - // MARK: Private Properties - private var statusBarStyle: UIStatusBarStyle = .default - private var theme: Theme? - - // MARK: Override Properties - override var preferredStatusBarStyle: UIStatusBarStyle { - return statusBarStyle - } - - // MARK: Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - preferredDisplayMode = .allVisible - delegate = self - output.viewIsReady() - } -} - -// MARK: RootViewInput -extension RootSplitViewController: RootViewInput { - func setupInitialState(theme: Theme) { - self.theme = theme - update(theme: theme) - } -} - -// MARK: ThemeUpdatable -extension RootSplitViewController: ThemeUpdatable { - func update(theme: Theme) { - self.theme = theme - theme.view.apply(to: view) - statusBarStyle = theme.statusBar - setNeedsStatusBarAppearanceUpdate() - } -} - -extension RootSplitViewController: UISplitViewControllerDelegate { - func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool { - if let tabController = splitViewController.viewControllers[0] as? MainTabBarViewController { - if splitViewController.traitCollection.horizontalSizeClass == .compact { - if let navController = vc as? UINavigationController, let actualVC = navController.topViewController { - tabController.selectedViewController?.show(actualVC, sender: sender) - navController.popViewController(animated: false) - } else { - tabController.selectedViewController?.show(vc, sender: sender) - } - } else { - splitViewController.viewControllers = [tabController, vc] - } - } - return true - } -} diff --git a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootViewInput.swift b/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootViewInput.swift deleted file mode 100644 index 730d54b..0000000 --- a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootViewInput.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// RootViewInput.swift -// HackerNews -// -// Created by Никита Васильев on 17.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -protocol RootViewInput: class, ThemeUpdatable { - func setupInitialState(theme: Theme) -} diff --git a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootViewOutput.swift b/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootViewOutput.swift deleted file mode 100644 index b312a69..0000000 --- a/HackerNews/Classes/PresentationLayer/Main/Modules/Root/View/RootViewOutput.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// RootViewOutput.swift -// HackerNews -// -// Created by Никита Васильев on 17.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -protocol RootViewOutput { - func viewIsReady() -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Assembly/SettingsAssembly.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Assembly/SettingsAssembly.swift deleted file mode 100644 index 2ff309d..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Assembly/SettingsAssembly.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// SettingsAssembly.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Swinject - -final class SettingsModuleAssembly: Assembly { - func assemble(container: Container) { - container.register(SettingsInteractor.self) { (_, presenter: SettingsPresenter) in - let interactor = SettingsInteractor() - interactor.output = presenter - return interactor - } - - container.register(SettingsRouter.self) { (resolver, viewController: SettingsViewController) in - let router = SettingsRouter() - router.transitionHandler = viewController - router.themeConfigurator = resolver.resolve(ThemeConfigurator.self) - return router - } - - container.register(SettingsPresenter.self) { (resolver, viewController: SettingsViewController) in - let presenter = SettingsPresenter() - - presenter.view = viewController - presenter.interactor = resolver.resolve(SettingsInteractor.self, argument: presenter) - presenter.router = resolver.resolve(SettingsRouter.self, argument: viewController) - presenter.themeManager = resolver.resolve(ThemeManager.self) - - return presenter - } - - container.register(SettingsViewController.self) { resolver in - let viewController = SettingsViewController()//R.storyboard.settings().instantiateViewController(type: SettingsViewController.self) - viewController.output = resolver.resolve(SettingsPresenter.self, argument: viewController) - return viewController - } - - container.register(ThemeConfigurator.self) { resolver in - let parentAssembler = resolver.resolve(SettingsConfigurator.self)?.assembler - return ThemeConfigurator(parentAssembler: parentAssembler.unwrap()) - } - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Configurator/SettingsConfigurator.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Configurator/SettingsConfigurator.swift deleted file mode 100644 index c2d3a5b..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Configurator/SettingsConfigurator.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// SettingsConfigurator.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Swinject - -final class SettingsConfigurator { - - // MARK: Public Properties - let assembler: Assembler - - // MARK: Initialization - init(parentAssembler: Assembler) { - assembler = Assembler([SettingsModuleAssembly()], parent: parentAssembler) - } -} - -// MARK: TabBarViewProtocol -extension SettingsConfigurator: TabBarViewProtocol { - var icon: UIImage? { - return R.image.settings() - } - - var title: String? { - return Locale.title.localized() - } - - func configureViewController() -> UIViewController { - guard let viewController = assembler.resolver.resolve(SettingsViewController.self) else { - fatalError("StoriesViewController shouldn't be nil") - } - - let item = UITabBarItem() - item.image = icon - item.title = title - - item.isAccessibilityElement = true - item.accessibilityIdentifier = "settingsTabBarItem" - - viewController.tabBarItem = item - viewController.title = item.title - - return UINavigationController(rootViewController: viewController) - } -} - -// MARK: Locale -extension SettingsConfigurator { - private enum Locale { - static let title: String = "Settings" - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractor.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractor.swift deleted file mode 100644 index e1364a3..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractor.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// SettingsInteractor.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation - -class SettingsInteractor { - weak var output: SettingsPresenter! -} - -// MARK: SettingsInteractorInput -extension SettingsInteractor: SettingsInteractorInput { - -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractorInput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractorInput.swift deleted file mode 100644 index 678fb11..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractorInput.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// SettingsInteractorInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol SettingsInteractorInput { - -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractorOutput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractorOutput.swift deleted file mode 100644 index d9eb958..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Interactor/SettingsInteractorOutput.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// SettingsInteractorOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol SettingsInteractorOutput: class { - -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Presenter/ListData/SettingsListData.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Presenter/ListData/SettingsListData.swift deleted file mode 100644 index cdbe249..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Presenter/ListData/SettingsListData.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SettingsListData.swift -// HackerNews -// -// Created by Никита Васильев on 17.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -enum SettingsListSections: Int, CaseIterable { - case themes - case help - - var title: String { - switch self { - case .themes: - return "settings.themes.header".localized() - case .help: - return "settings.help.header".localized() - } - } -} - -enum SettingsListData { - case themes - case help - case rate - - var title: String { - switch self { - case .themes: - return "settings.themes.title".localized() - case .help: - return "settings.help.title".localized() - case .rate: - return "settings.help.rate".localized() - } - } - - var navigable: Bool { - switch self { - case .themes: - return true - case .help, .rate: - return false - } - } - - var icon: UIImage? { - switch self { - case .themes: - return R.image.themeIcon() - case .help: - return R.image.help() - case .rate: - return R.image.rate() - } - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Presenter/SettingsPresenter.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Presenter/SettingsPresenter.swift deleted file mode 100644 index f6af97d..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Presenter/SettingsPresenter.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// SettingsPresenter.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -final class SettingsPresenter { - // MARK: Public Properties - weak var view: SettingsViewInput! - var interactor: SettingsInteractorInput! - var router: SettingsRouterInput! - var themeManager: ThemeManagerProtocol! - - // MARK: Private Properties - private var cells: [SettingsListSections: [SettingsListData]] = [ - .themes: [.themes], .help: [.help, .rate] - ] -} - -// MARK: SettingsViewOutput -extension SettingsPresenter: SettingsViewOutput { - func viewIsReady() { - view.setupInitialState(title: Locale.title.localized(), theme: themeManager.theme) - themeManager?.addObserver(self) - } - - func getNumberOfRows(in section: Int) -> Int { - if let tableSection = SettingsListSections(rawValue: section), let data = cells[tableSection] { - return data.count - } - return 0 - } - - func getNumberOfSections() -> Int { - return SettingsListSections.allCases.count - } - - func getTitleForHeader(in section: Int) -> String? { - return SettingsListSections(rawValue: section)?.title - } - - func didSelectRow(at indexPath: IndexPath) { - guard let section = SettingsListSections(rawValue: indexPath.section), - let cellsData = cells[section] else { - return - } - - let cell = cellsData[indexPath.row] - - switch cell { - case .themes: - router.showThemeModule() - case .help: - if let url = URL(string: Constants.Links.feedbackURL) { - router.openURL(url) - } - case .rate: - if let url = URL(string: Constants.Links.appstoreURL) { - router.openURL(url) - } - } - } - - func getModel(for indexPath: IndexPath) -> SettingsCellModel? { - guard let section = SettingsListSections(rawValue: indexPath.section), - let data = cells[section] else { - return nil - } - - let model = data[indexPath.row] - - switch model { - case .themes: - return SettingsCellModel(icon: model.icon, title: model.title, - info: themeManager.theme.rawValue.localized(), - navigatable: model.navigable) - case .help, .rate: - return SettingsCellModel(icon: model.icon, title: model.title, - info: nil, navigatable: model.navigable) - } - } -} - -// MARK: SettingsInteractorOutput -extension SettingsPresenter: SettingsInteractorOutput { - -} - -// MARK: ThemeObserver -extension SettingsPresenter: ThemeObserver { - func themeDidChange(_ theme: Theme) { - view.update(theme: theme) - } -} - -// MARK: Locale -extension SettingsPresenter { - private enum Locale { - static let title: String = "Settings" - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Router/SettingsRouter.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Router/SettingsRouter.swift deleted file mode 100644 index cb47008..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Router/SettingsRouter.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SettingsRouter.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -class SettingsRouter { - // MARK: Public Properties - weak var transitionHandler: TransitionHandler? - var themeConfigurator: ThemeConfigurator? -} - -// MARK: SettingsRouterInput -extension SettingsRouter: SettingsRouterInput { - func showThemeModule() { - transitionHandler?.openModule({ viewController in - self.themeConfigurator?.configure()?.present(from: viewController) - }) - } - - func openURL(_ url: URL) { - UIApplication.shared.open(url) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Router/SettingsRouterInput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Router/SettingsRouterInput.swift deleted file mode 100644 index e787b3b..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/Router/SettingsRouterInput.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SettingsRouterInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation - -protocol SettingsRouterInput { - func showThemeModule() - func openURL(_ url: URL) -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Settings.storyboard b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Settings.storyboard deleted file mode 100644 index 476cfc4..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Settings.storyboard +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewController.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewController.swift deleted file mode 100644 index 0b0454e..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewController.swift +++ /dev/null @@ -1,133 +0,0 @@ -// -// SettingsViewController.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -final class SettingsViewController: UIViewController { - - // MARK: IBOutlets - //@IBOutlet private var tableView: UITableView! - - // MARK: Public Properties - var output: SettingsViewOutput! - - // MARK: Private Properties - private var theme: Theme? - - private lazy var tableView: UITableView = { - let tableView = UITableView() - tableView.isAccessibilityElement = true - tableView.accessibilityIdentifier = "settingsTableView" - tableView.register(SettingsTableViewCell.self) - tableView.tableFooterView = UIView() - tableView.delegate = self - tableView.dataSource = self - tableView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(tableView) - return tableView - }() - - // MARK: Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - setup() - output.viewIsReady() - } - - // MARK: Private Methods - private func setup() { - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = true - navigationController?.navigationItem.largeTitleDisplayMode = .never - } - - view.accessibilityIdentifier = "settingsView" - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: safeTopAnchor), - tableView.leadingAnchor.constraint(equalTo: safeLeadingAnchor), - safeTrailingAnchor.constraint(equalTo: tableView.trailingAnchor), - safeBottomAnchor.constraint(equalTo: tableView.bottomAnchor) - ]) - } -} - -// MARK: SettingsViewInput -extension SettingsViewController: SettingsViewInput { - func setupInitialState(title: String, theme: Theme) { - self.title = title - self.theme = theme - update(theme: theme) - } -} - -// MARK: UITableViewDelegate -extension SettingsViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - output.didSelectRow(at: indexPath) - tableView.deselectRow(at: indexPath, animated: true) - } -} - -// MARK: UITableViewDataSource -extension SettingsViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return output.getTitleForHeader(in: section) - } - - func numberOfSections(in tableView: UITableView) -> Int { - return output.getNumberOfSections() - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return output.getNumberOfRows(in: section) - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let model = output.getModel(for: indexPath) else { return UITableViewCell() } - - let cell: SettingsTableViewCell = tableView.dequeueReusableCell(forIndexPath: indexPath) - cell.setup(model: model) - cell.isAccessibilityElement = true - cell.accessibilityIdentifier = String(format: "sTCV_%d_%d", indexPath.section, indexPath.row) - - if let theme = theme { - cell.apply(theme: theme) - } - - return cell - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return Metric.cellHeight - } - - func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) { - if let headerView = view as? UITableViewHeaderFooterView, let textLabel = headerView.textLabel { - theme?.tableViewHeader.apply(to: headerView) - theme?.baseTableViewHeaderTitle.apply(to: textLabel) - } - } -} - -// MARK: ThemeUpdatable -extension SettingsViewController: ThemeUpdatable { - func update(theme: Theme) { - self.theme = theme - theme.tableView.apply(to: tableView) - theme.view.apply(to: view) - tableView.reloadData() - } -} - -// MARK: Constants -extension SettingsViewController { - private enum Metric { - static let cellHeight: CGFloat = 48.0 - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewInput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewInput.swift deleted file mode 100644 index d67dd2f..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewInput.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// SettingsViewInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol SettingsViewInput: class, Presentable, ThemeUpdatable { - var output: SettingsViewOutput! { get set } - - func setupInitialState(title: String, theme: Theme) -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewOutput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewOutput.swift deleted file mode 100644 index 8232e5e..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/SettingsViewOutput.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// SettingsViewOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation - -protocol SettingsViewOutput: class { - func viewIsReady() - func getNumberOfRows(in section: Int) -> Int - func getNumberOfSections() -> Int - func getTitleForHeader(in section: Int) -> String? - func didSelectRow(at indexPath: IndexPath) - func getModel(for indexPath: IndexPath) -> SettingsCellModel? -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Views/SettingsTableViewCell/SettingsTableViewCell.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Views/SettingsTableViewCell/SettingsTableViewCell.swift deleted file mode 100644 index 366f49c..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Views/SettingsTableViewCell/SettingsTableViewCell.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// SettingsTableViewCell.swift -// HackerNews -// -// Created by Никита Васильев on 27.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -struct SettingsCellModel { - /// An `UIImage` value that contains the cell icon. - let icon: UIImage? - - /// A `String` value that contains the cell title. - let title: String - - /// A `String` value that contains the right text. - let info: String? - - /// A `Bool` value that contains the state of visible disclosure indicator. - let navigatable: Bool -} - -final class SettingsTableViewCell: UITableViewCell { - - // MARK: Outlets - @IBOutlet private var iconView: UIImageView! - @IBOutlet private var titleLabel: UILabel! { - didSet { - titleLabel.isAccessibilityElement = true - titleLabel.accessibilityIdentifier = "titleLabel" - } - } - @IBOutlet private var infoLabel: UILabel! { - didSet { - infoLabel.isAccessibilityElement = true - infoLabel.accessibilityIdentifier = "infoLabel" - } - } - - // MARK: Initialization - override func awakeFromNib() { - super.awakeFromNib() - selectedBackgroundView = UIView() - } - - // MARK: Override - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: true) - } - - // MARK: Public Methods - - /// Set model data to the cell. - /// - /// - Parameter model: A `SettingsCellModel` value that contains the cell data. - func setup(model: SettingsCellModel) { - iconView.image = model.icon - titleLabel.text = model.title - infoLabel.text = model.info - accessoryType = model.navigatable ? .disclosureIndicator : .none - } - - /// Apply theme to cell. - /// - /// - Parameter theme: A `Theme` value that contains the current application theme. - func apply(theme: Theme) { - theme.cellTwo.apply(to: self) - theme.baseSettingsTitle.apply(to: titleLabel) - theme.infoSettingsTitle.apply(to: infoLabel) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Views/SettingsTableViewCell/SettingsTableViewCell.xib b/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Views/SettingsTableViewCell/SettingsTableViewCell.xib deleted file mode 100644 index c783618..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Main/View/Views/SettingsTableViewCell/SettingsTableViewCell.xib +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Assembly/ThemeAssembly.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Assembly/ThemeAssembly.swift deleted file mode 100644 index ccdbcd9..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Assembly/ThemeAssembly.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// ThemeAssembly.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Swinject - -final class ThemeModuleAssembly: Assembly { - func assemble(container: Container) { - container.register(ThemeInteractor.self) { (_, presenter: ThemePresenter) in - let interactor = ThemeInteractor() - interactor.output = presenter - return interactor - } - - container.register(ThemeRouter.self) { (_, viewController: ThemeViewController) in - let router = ThemeRouter() - router.transitionHandler = viewController - return router - } - - container.register(ThemeModuleInput.self) { (resolver, viewController: ThemeViewController) in - let presenter = ThemePresenter() - - presenter.view = viewController - presenter.interactor = resolver.resolve(ThemeInteractor.self, argument: presenter) - presenter.router = resolver.resolve(ThemeRouter.self, argument: viewController) - presenter.themeManager = resolver.resolve(ThemeManager.self) - - return presenter - } - - container.register(ThemeViewController.self) { resolver in - let viewController = R.storyboard.theme().instantiateViewController(type: ThemeViewController.self) - viewController.output = resolver.resolve(ThemeModuleInput.self, argument: viewController) as? ThemePresenter - return viewController - } - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Configurator/ThemeConfigurator.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Configurator/ThemeConfigurator.swift deleted file mode 100644 index 9461ec0..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Configurator/ThemeConfigurator.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ThemeConfigurator.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Swinject - -final class ThemeConfigurator { - - // MARK: Private Properties - private var viewController: ThemeViewController? - private let assembler: Assembler - - // MARK: Initialization - init(parentAssembler: Assembler) { - assembler = Assembler([ThemeModuleAssembly()], parent: parentAssembler) - } - - // MARK: Public Methods - func configure() -> ThemeModuleInput? { - if let viewController = assembler.resolver.resolve(ThemeViewController.self) { - self.viewController = viewController - return assembler.resolver.resolve(ThemeModuleInput.self, argument: viewController) - } - - return nil - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Configurator/ThemeModuleInput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Configurator/ThemeModuleInput.swift deleted file mode 100644 index bdf951b..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Configurator/ThemeModuleInput.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ThemeModuleInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -protocol ThemeModuleInput: class { - func present(from viewController: UIViewController) -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractor.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractor.swift deleted file mode 100644 index 7e68229..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractor.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ThemeInteractor.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation - -class ThemeInteractor { - // MARK: Public Properties - weak var output: ThemePresenter! -} - -// MARK: ThemeInteractorInput -extension ThemeInteractor: ThemeInteractorInput { - -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractorInput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractorInput.swift deleted file mode 100644 index 4161fef..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractorInput.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// ThemeInteractorInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol ThemeInteractorInput { - -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractorOutput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractorOutput.swift deleted file mode 100644 index 18ece86..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Interactor/ThemeInteractorOutput.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// ThemeInteractorOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol ThemeInteractorOutput: class { - -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Presenter/ThemePresenter.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Presenter/ThemePresenter.swift deleted file mode 100644 index 58cad1a..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Presenter/ThemePresenter.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// ThemePresenter.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -class ThemePresenter { - // MARK: Public Properties - weak var view: ThemeViewInput! - var interactor: ThemeInteractorInput! - var router: ThemeRouterInput! - var themeManager: ThemeManagerProtocol! - - // MARK: Private Properties - private var themes: [(title: String, theme: Theme)] = [] - - deinit { - themeManager.removeObserver(self) - } -} - -// MARK: ThemeViewOutput -extension ThemePresenter: ThemeViewOutput { - func didSelectRow(at indexPath: IndexPath) { - themeManager.theme = themes[indexPath.row].theme - router.dismiss() - } - - func viewIsReady() { - view.setupInitialState(title: ThemeConstants.title, theme: themeManager.theme) - themeManager.addObserver(self) - themes = themeManager.themes.map({ ($0.rawValue.localized(), $0) }) - view.reloadData() - } - - func numberOfRows(in section: Int) -> Int { - return themes.count - } - - func numberOfSections() -> Int { - return 1 - } - - func getModel(for indexPath: IndexPath) -> (title: String, isSelected: Bool) { - let isSelected = themeManager.theme == themes[indexPath.row].theme - return (title: themes[indexPath.row].title, isSelected: isSelected) - } - - func titleForHeader(in section: Int) -> String { - return ThemeConstants.appearanceTitle - } -} - -// MARK: ThemeInteractorOutput -extension ThemePresenter: ThemeInteractorOutput { - -} - -// MARK: ThemeModuleInput -extension ThemePresenter: ThemeModuleInput { - func present(from viewController: UIViewController) { - view.present(from: viewController) - } -} - -// MARK: ThemeObserver -extension ThemePresenter: ThemeObserver { - func themeDidChange(_ theme: Theme) { - view.update(theme: theme) - } -} - -// MARK: Constants -extension ThemePresenter { - private enum ThemeConstants { - static let title: String = "Themes".localized() - static let appearanceTitle: String = "Appearance".localized() - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Router/ThemeRouter.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Router/ThemeRouter.swift deleted file mode 100644 index ff0dfe5..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Router/ThemeRouter.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// ThemeRouter.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -class ThemeRouter { - weak var transitionHandler: TransitionHandler? -} - -// MARK: ThemeRouterInput -extension ThemeRouter: ThemeRouterInput { - func dismiss() { - transitionHandler?.openModule({ viewController in - viewController.navigationController?.popViewController(animated: true) - }) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Router/ThemeRouterInput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Router/ThemeRouterInput.swift deleted file mode 100644 index 024cb2b..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/Router/ThemeRouterInput.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// ThemeRouterInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol ThemeRouterInput { - func dismiss() -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Theme.storyboard b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Theme.storyboard deleted file mode 100644 index 0fb7ad2..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Theme.storyboard +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewController.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewController.swift deleted file mode 100644 index ab6fd9f..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewController.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// ThemeViewController.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit - -final class ThemeViewController: UIViewController { - - // MARK: Outlets - @IBOutlet private var tableView: UITableView! - - // MARK: Public Properties - var output: ThemeViewOutput! - - // MARK: Private Properties - private var theme: Theme? - - // MARK: Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - if #available(iOS 11.0, *) { - navigationItem.largeTitleDisplayMode = .never - } - - view.accessibilityIdentifier = "themeView" - - tableView.isAccessibilityElement = true - tableView.accessibilityIdentifier = "themesTableView" - - tableView.register(ThemeSelectableTableViewCell.self) - tableView.tableFooterView = UIView() - output.viewIsReady() - } -} - -// MARK: ThemeViewInput -extension ThemeViewController: ThemeViewInput { - func setupInitialState(title: String, theme: Theme) { - self.title = title - self.theme = theme - update(theme: theme) - } - - func reloadData() { - tableView.reloadData() - } - - func update(theme: Theme) { - self.theme = theme - theme.tableView.apply(to: tableView) - theme.view.apply(to: view) - } -} - -// MARK: UITableViewDelegate -extension ThemeViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - output.didSelectRow(at: indexPath) - tableView.deselectRow(at: indexPath, animated: true) - } - - func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { - - } -} - -// MARK: UITableViewDataSource -extension ThemeViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return output.titleForHeader(in: section) - } - - func numberOfSections(in tableView: UITableView) -> Int { - return output.numberOfSections() - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return output.numberOfRows(in: section) - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: ThemeSelectableTableViewCell = tableView.dequeueReusableCell(forIndexPath: indexPath) - let model = output.getModel(for: indexPath) - cell.title = model.title - cell.accessoryType = model.isSelected ? .checkmark : .none - cell.isAccessibilityElement = true - cell.accessibilityIdentifier = String(format: "themeCell_%d_%d", indexPath.section, indexPath.row) - - if let theme = theme { - cell.apply(theme: theme) - } - - return cell - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return Metric.cellHeight - } -} - -// MARK: Constants -extension ThemeViewController { - private enum Metric { - static let cellHeight: CGFloat = 48.0 - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewInput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewInput.swift deleted file mode 100644 index a7928e3..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewInput.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// ThemeViewInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol ThemeViewInput: class, Presentable, ThemeUpdatable { - func setupInitialState(title: String, theme: Theme) - func reloadData() -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewOutput.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewOutput.swift deleted file mode 100644 index e12aa24..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/ThemeViewOutput.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// ThemeViewOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 27/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation - -protocol ThemeViewOutput: class { - func viewIsReady() - func didSelectRow(at indexPath: IndexPath) - func numberOfRows(in section: Int) -> Int - func numberOfSections() -> Int - func getModel(for indexPath: IndexPath) -> (title: String, isSelected: Bool) - func titleForHeader(in section: Int) -> String -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Views/ThemeSelectableCell/ThemeSelectableTableViewCell.swift b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Views/ThemeSelectableCell/ThemeSelectableTableViewCell.swift deleted file mode 100644 index a4dd693..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Views/ThemeSelectableCell/ThemeSelectableTableViewCell.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// ThemeSelectableTableViewCell.swift -// HackerNews -// -// Created by Никита Васильев on 15.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -final class ThemeSelectableTableViewCell: UITableViewCell { - - // MARK: Outlets - @IBOutlet private var titleLabel: UILabel! - - // MARK: Public Properties - - /// A `String` value that contains the cell title. - var title: String = "" { - didSet { - titleLabel.text = title - } - } - - // MARK: Initialization - override func awakeFromNib() { - super.awakeFromNib() - self.selectedBackgroundView = UIView() - } - - // MARK: Override - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: true) - } - - /// Apply theme to cell. - /// - /// - Parameter theme: A `Theme` value that contains the current application theme. - func apply(theme: Theme) { - theme.cellOne.apply(to: self) - theme.baseSettingsTitle.apply(to: titleLabel) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Views/ThemeSelectableCell/ThemeSelectableTableViewCell.xib b/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Views/ThemeSelectableCell/ThemeSelectableTableViewCell.xib deleted file mode 100644 index f5ba6af..0000000 --- a/HackerNews/Classes/PresentationLayer/Settings/Modules/Theme/View/Views/ThemeSelectableCell/ThemeSelectableTableViewCell.xib +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Classes/PresentationLayer/Stories/Assembly/StoriesAssembly.swift b/HackerNews/Classes/PresentationLayer/Stories/Assembly/StoriesAssembly.swift deleted file mode 100644 index fc684a9..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Assembly/StoriesAssembly.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// StoriesAssembly.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Swinject -import protocol HNService.HNServiceProtocol - -final class StoriesModuleAssembly: Assembly { - func assemble(container: Container) { - container.register(StoriesInteractor.self) { (resolver, presenter: StoriesPresenter) in - let interactor = StoriesInteractor() - interactor.output = presenter - interactor.networkService = resolver.resolve(HNServiceProtocol.self) - return interactor - } - - container.register(StoriesRouter.self) { (resolver, viewController: StoriesViewController) in - let router = StoriesRouter() - router.transitionHandler = viewController - router.commentsConfigurator = resolver.resolve(CommentsConfigurator.self) - return router - } - - container.register(StoriesPresenter.self) { (resolver, viewController: StoriesViewController) in - let presenter = StoriesPresenter() - - presenter.view = viewController - presenter.interactor = resolver.resolve(StoriesInteractor.self, argument: presenter) - presenter.router = resolver.resolve(StoriesRouter.self, argument: viewController) - presenter.themeManager = resolver.resolve(ThemeManager.self) - - return presenter - } - - container.register(StoriesViewController.self) { resolver in - let viewController = StoriesViewController() - viewController.output = resolver.resolve(StoriesPresenter.self, argument: viewController) - return viewController - } - } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/Configurator/StoriesConfigurator.swift b/HackerNews/Classes/PresentationLayer/Stories/Configurator/StoriesConfigurator.swift deleted file mode 100644 index 3bf674b..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Configurator/StoriesConfigurator.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// StoriesConfigurator.swift -// HackerNews -// -// Created by Никита Васильев on 10.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import Swinject - -final class StoriesConfigurator { - - // MARK: Private Properties - private let assembler: Assembler - - // MARK: Initialization - init(parentAssembler: Assembler) { - assembler = Assembler([StoriesModuleAssembly()], parent: parentAssembler) - } -} - -// MARK: TabBarViewProtocol -extension StoriesConfigurator: TabBarViewProtocol { - var icon: UIImage? { - return R.image.news() - } - - var title: String? { - return Locale.title.localized() - } - - func configureViewController() -> UIViewController { - guard let viewController = assembler.resolver.resolve(StoriesViewController.self) else { - fatalError("StoriesViewController shouldn't be nil") - } - - let item = UITabBarItem() - item.image = icon - item.title = title - - viewController.tabBarItem = item - viewController.title = item.title - - return UINavigationController(rootViewController: viewController) - } -} - -// MARK: Locale -extension StoriesConfigurator { - private enum Locale { - static let title: String = "Stories" - } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractor.swift b/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractor.swift deleted file mode 100644 index 652a01a..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractor.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// StoriesInteractor.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation -import NetworkManager -import enum HNService.StoryType -import protocol HNService.HNServiceProtocol - -class StoriesInteractor { - // MARK: Public Properties - weak var output: StoriesInteractorOutput? - var networkService: HNServiceProtocol? -} - -// MARK: StoriesInteractorInput -extension StoriesInteractor: StoriesInteractorInput { - func fetchIds(for type: StoryType) { - networkService?.fetchIds(for: type, completion: { [weak self] ids in - self?.output?.fetchIdsSuccess(ids) - }, fail: { [weak self] error in - self?.output?.fetchIdsFail(error: error) - }) - } - - func fetchPosts(with ids: [Int]) { - networkService?.loadPosts(with: ids, completion: { [weak self] news in - self?.output?.fetchItemsSuccess(news) - }, fail: { [weak self] error in - self?.output?.fetchItemsFailed(error: error) - }) - } - - func cancelRequests() { - networkService?.cancelAllTasks() - } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractorInput.swift b/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractorInput.swift deleted file mode 100644 index 3abbd51..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractorInput.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// StoriesInteractorInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import enum HNService.StoryType -import protocol HNService.HNServiceProtocol - -protocol StoriesInteractorInput { - var output: StoriesInteractorOutput? { get set } - var networkService: HNServiceProtocol? { get set } - - func fetchIds(for type: StoryType) - - func fetchPosts(with ids: [Int]) - func cancelRequests() -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractorOutput.swift b/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractorOutput.swift deleted file mode 100644 index 46715fd..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Interactor/StoriesInteractorOutput.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// StoriesInteractorOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import struct HNService.PostModel - -protocol StoriesInteractorOutput: class { - func fetchIdsSuccess(_ ids: [Int]) - func fetchIdsFail(error: Error) - - func fetchItemsSuccess(_ items: [PostModel]) - func fetchItemsFailed(error: Error) -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/Presenter/StoriesPresenter.swift b/HackerNews/Classes/PresentationLayer/Stories/Presenter/StoriesPresenter.swift deleted file mode 100644 index 09e41f0..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Presenter/StoriesPresenter.swift +++ /dev/null @@ -1,182 +0,0 @@ -// -// StoriesPresenter.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit -import enum HNService.StoryType -import struct HNService.PostModel - -enum SkeletonState { - case enabled - case disabled -} - -final class StoriesPresenter { - // MARK: Public Properties - weak var view: StoriesViewInput! - var interactor: StoriesInteractorInput! - var router: StoriesRouterInput! - var themeManager: ThemeManagerProtocol! - - // MARK: Private Properties - private var storyType: StoryType = .new - private var skeletonState: SkeletonState = .disabled - private var ids: [Int] = [] - private var stories: [PostModel] = [] - private var errorDescription: String? - private var isFinished: Bool = false - - private var skeletonCount: Int { - return Int(UIScreen.main.bounds.height) / 84 + 1 - } - - private var loadingIds: [Int] { - let count = self.stories.count - return Array(ids[safe:count.. Int { - return skeletonState == .enabled ? skeletonCount : stories.count - } - - func getModel(for indexPath: IndexPath) -> BaseCellViewModel { - if skeletonState == .enabled { - return SkeletonCellViewModel(theme: themeManager.theme) - } - return PostCellViewModel(post: stories[indexPath.row], - theme: themeManager.theme, - placeholderImage: .placeholder) - } - - func viewIsReady() { - skeletonState = .enabled - view.setupInitialState(title: StoriesConstants.title.localized(), - theme: themeManager.theme, - titles: StoryType.allValues.map { $0.displayName }) - view.setUserInteractorEnabled(to: false) - themeManager?.addObserver(self) - fetchStories(by: storyType) - } - - func didSelectRow(at row: Int) { - router.openCommentsModule(for: stories[row]) - } - - func didSelectImage(at row: Int) { - guard let url = stories[row].url else { return } - router.openStories(from: url) - } - - func prefetch(at indexPath: IndexPath) { - view.setLoadingIndicator(to: !isFinished) - guard !isFinished, !stories.isEmpty, indexPath.row >= stories.count - 1 else { return } - interactor.fetchPosts(with: loadingIds) - } -} - -// MARK: StoriesInteractorOutput -extension StoriesPresenter: StoriesInteractorOutput { - func fetchIdsSuccess(_ ids: [Int]) { - self.ids = ids - interactor.fetchPosts(with: loadingIds) - } - - func fetchIdsFail(error: Error) { - showError(error) - } - - func fetchItemsSuccess(_ items: [PostModel]) { - self.isFinished = items.isEmpty - self.stories.append(contentsOf: items.sorted(by: { $0.id > $1.id })) - view.setUserInteractorEnabled(to: true) - skeletonState = .disabled - view.reloadData() - view.hideRefreshControl() - } - - func fetchItemsFailed(error: Error) { - showError(error) - } - - func refreshStories() { - backToInitialState() - fetchStories(by: storyType) - } - - func getSkeletonState() -> SkeletonState { - return skeletonState - } - - func segmentedControlDidChange(to index: Int) { - guard let type = StoryType(rawValue: index) else { return } - - backToInitialState() - fetchStories(by: type) - } - - func getEmptyDataSetTitle() -> String { - return StoriesConstants.emptyTitle.localized() - } - - func getEmptyDataSetDecription() -> String { - return errorDescription ?? "" - } - - func getEmptyDataSetImage() -> Image { - return .connectionError - } -} - -// MARK: ThemeObserver -extension StoriesPresenter: ThemeObserver { - func themeDidChange(_ theme: Theme) { - view.update(theme: theme) - } -} - -// MARK: Constants -extension StoriesPresenter { - private enum StoriesConstants { - static let loadItemsCountPerOnce: Int = 20 - - static let canceledCode: Int = -999 - - static let title: String = "Stories" - static let emptyTitle: String = "No stories to show" - } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/Router/StoriesRouter.swift b/HackerNews/Classes/PresentationLayer/Stories/Router/StoriesRouter.swift deleted file mode 100644 index 1196f9b..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Router/StoriesRouter.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// StoriesRouter.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import SafariServices -import struct HNService.PostModel - -class StoriesRouter { - // MARK: Public Properties - weak var transitionHandler: TransitionHandler? - var commentsConfigurator: CommentsConfigurator? -} - -// MARK: StoriesRouterInput -extension StoriesRouter: StoriesRouterInput { - func openFilterModule(with models: [AlertActionModel]) { - transitionHandler?.openModule({ viewController in - viewController.showActionSheet(actions: models) - }) - } - - func openCommentsModule(for post: PostModel) { - transitionHandler?.openModule({ [weak self] viewController in - guard let commentsViewController = self?.commentsConfigurator?.configure(post: post) else { return } - - let nv = UINavigationController(rootViewController: commentsViewController) - viewController.showDetailViewController(nv, sender: nil) - }) - } - - func openStories(from url: String) { - transitionHandler?.openModule({ viewController in - guard let url = URL(string: url) else { return } - let safariVC = SFSafariViewController(url: url) - viewController.present(safariVC, animated: true, completion: nil) - }) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/Router/StoriesRouterInput.swift b/HackerNews/Classes/PresentationLayer/Stories/Router/StoriesRouterInput.swift deleted file mode 100644 index 0c2f475..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/Router/StoriesRouterInput.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// StoriesRouterInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation -import struct HNService.PostModel - -protocol StoriesRouterInput { - func openFilterModule(with models: [AlertActionModel]) - func openCommentsModule(for post: PostModel) - func openStories(from url: String) -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewController.swift b/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewController.swift deleted file mode 100644 index 23e6b59..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewController.swift +++ /dev/null @@ -1,245 +0,0 @@ -// -// StoriesViewController.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import UIKit -import EmptyDataSet_Swift - -final class StoriesViewController: UITableViewController { - - // MARK: Public Properties - var output: StoriesViewOutput! - - // MARK: Private Properties - private var segmentedControl = UISegmentedControl() - private var theme: Theme? - - // MARK: Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - setup() - output.viewIsReady() - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - tableView.refreshControl?.endRefreshing() - - if let indexPaths = tableView.indexPathsForVisibleRows, !indexPaths.isEmpty { - tableView.reloadRows(at: indexPaths, with: .none) - } - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - - segmentedControl.setNeedsLayout() - segmentedControl.layoutIfNeeded() - } - - // MARK: Private Methods - private func setup() { - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = true - navigationController?.navigationItem.largeTitleDisplayMode = .always - } - - navigationController?.navigationBar.isAccessibilityElement = true - navigationController?.navigationBar.accessibilityIdentifier = "storiesNavigationBar" - - tableView.register(PostTableViewCell.self) - tableView.register(SkeletonCell.self) - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = Metrics.estimatedRowHeight - tableView.refreshControl = UIRefreshControl() - tableView.tableFooterView = UIView() - tableView.emptyDataSetSource = self - tableView.emptyDataSetDelegate = self - tableView.prefetchDataSource = self - - tableView.refreshControl?.addTarget(self, action: #selector(refreshStories(_:)), for: .valueChanged) - - extendedLayoutIncludesOpaqueBars = true - edgesForExtendedLayout = [.top, .left, .right] - } - - private func configureNavigationItem(with titles: [String]) { - for (index, title) in titles.enumerated() { - segmentedControl.insertSegment(withTitle: title, at: index, animated: false) - } - - segmentedControl.selectedSegmentIndex = 0 - segmentedControl.sizeToFit() - segmentedControl.addTarget(self, action: #selector(segmentedControlDidChange(_:)), for: .valueChanged) - navigationItem.titleView = segmentedControl - } - - // MARK: Private Methods - private func showLoadingIndicator() { - let spinner = UIActivityIndicatorView(style: .gray) - theme?.activityIndicator.apply(to: spinner) - spinner.startAnimating() - spinner.frame = CGRect(x: CGFloat(0), y: CGFloat(0), width: tableView.bounds.width, height: CGFloat(44)) - - tableView.tableFooterView = spinner - tableView.tableFooterView?.isHidden = false - } - - private func hideLoadingIndicator() { - tableView.tableFooterView = UIView() - } -} - -// MARK: StoriesViewInput -extension StoriesViewController: StoriesViewInput { - func setupInitialState(title: String, theme: Theme, titles: [String]?) { - self.title = title - self.theme = theme - update(theme: theme) - - if let titles = titles { - configureNavigationItem(with: titles) - } - } - - func reloadData() { - tableView.reloadData() - } - - func setScrollEnabled(to state: Bool) { - tableView.isScrollEnabled = state - } - - func hideRefreshControl() { - tableView.refreshControl?.endRefreshing() - } - - func setUserInteractorEnabled(to state: Bool) { - tableView.isUserInteractionEnabled = state - } - - func scrollContentToTop() { - tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true) - } - - func setLoadingIndicator(to state: Bool) { - state ? showLoadingIndicator() : hideLoadingIndicator() - } -} - -// MARK: UITableViewDataSourcePrefetching -extension StoriesViewController: UITableViewDataSourcePrefetching { - func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { - for indexPath in indexPaths { - output.prefetch(at: indexPath) - } - } -} - -// MARK: UITableViewDelegate -extension StoriesViewController { - override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - output.didSelectRow(at: indexPath.row) - tableView.deselectRow(at: indexPath, animated: true) - } -} - -// MARK: UITableViewDataSource -extension StoriesViewController { - override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { - if let cell = cell as? SkeletonCell { - cell.slide(to: .right) - } - } - - override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return output.numberOfRows() - } - - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = output.getModel(for: indexPath) - return tableView.dequeueReusableCell(forIndexPath: indexPath, with: model) - } - - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension - } -} - -// MARK: EmptyDataSetSource -extension StoriesViewController: EmptyDataSetSource { - func title(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? { - return theme?.emptySetTitle(title: output.getEmptyDataSetTitle()) - } - - func description(forEmptyDataSet scrollView: UIScrollView) -> NSAttributedString? { - return theme?.emptySetDescription(text: output.getEmptyDataSetDecription()) - } - - func image(forEmptyDataSet scrollView: UIScrollView) -> UIImage? { - return output.getEmptyDataSetImage().resource - } - - func verticalOffset(forEmptyDataSet scrollView: UIScrollView) -> CGFloat { - return Metrics.verticalOffset - } - - func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView) -> Bool { - return true - } -} - -// MARK: EmptyDataSetDelegate -extension StoriesViewController: EmptyDataSetDelegate { - -} - -// MARK: PostTableViewCellDelegate -extension StoriesViewController: PostTableViewCellDelegate { - func imageDidTapped(cell: PostTableViewCell) { - guard let indexPath = tableView.indexPath(for: cell) else { return } - output.didSelectImage(at: indexPath.row) - } -} - -// MARK: ThemeUpdatable -extension StoriesViewController: ThemeUpdatable { - func update(theme: Theme) { - self.theme = theme - theme.tableView.apply(to: tableView) - theme.view.apply(to: view) - theme.segmentedControl.apply(to: segmentedControl) - - if let refreshControl = tableView.refreshControl { - theme.refreshControl.apply(to: refreshControl) - } - - if let indexPaths = tableView.indexPathsForVisibleRows, !indexPaths.isEmpty { - tableView.reloadRows(at: indexPaths, with: .none) - } - } -} - -// MARK: Constants -extension StoriesViewController { - private enum Metrics { - static let estimatedRowHeight: CGFloat = 75.0 - static let verticalOffset: CGFloat = -50.0 - } -} - -// MARK: Selectors -extension StoriesViewController { - @objc func refreshStories(_ sender: UIRefreshControl) { - output.refreshStories() - } - - @objc func segmentedControlDidChange(_ sender: UISegmentedControl) { - output.segmentedControlDidChange(to: sender.selectedSegmentIndex) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewInput.swift b/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewInput.swift deleted file mode 100644 index 33ee6c9..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewInput.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// StoriesViewInput.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -protocol StoriesViewInput: class, Presentable, ThemeUpdatable { - var output: StoriesViewOutput! { get set } - - func setupInitialState(title: String, theme: Theme, titles: [String]?) - func setUserInteractorEnabled(to state: Bool) - func scrollContentToTop() - func reloadData() - func hideRefreshControl() - func setLoadingIndicator(to state: Bool) -} - -extension StoriesViewInput { - func scrollContentToTop() { } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewOutput.swift b/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewOutput.swift deleted file mode 100644 index 1cd9bdd..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/View/StoriesViewOutput.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// StoriesViewOutput.swift -// HackerNews -// -// Created by Nikita Vasilev on 04/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Foundation - -protocol StoriesViewOutput: class { - func viewIsReady() - func prefetch(at indexPath: IndexPath) - func didSelectRow(at row: Int) - func didSelectImage(at row: Int) - func numberOfRows() -> Int - func refreshStories() - func getModel(for indexPath: IndexPath) -> BaseCellViewModel - func getSkeletonState() -> SkeletonState - func segmentedControlDidChange(to index: Int) - func getEmptyDataSetTitle() -> String - func getEmptyDataSetDecription() -> String - func getEmptyDataSetImage() -> Image -} - -extension StoriesViewOutput { - func segmentedControlDidChange(to index: Int) { } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/View/Views/PostTableViewCell/PostTableViewCell.swift b/HackerNews/Classes/PresentationLayer/Stories/View/Views/PostTableViewCell/PostTableViewCell.swift deleted file mode 100644 index f578c68..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/View/Views/PostTableViewCell/PostTableViewCell.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// PostTableViewCell.swift -// HackerNews -// -// Created by Никита Васильев on 19.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import struct HNService.PostModel - -protocol PostTableViewCellDelegate: class { - /// A block object to executed when image tapped. - func imageDidTapped(cell: PostTableViewCell) -} - -final class PostTableViewCell: UITableViewCell { - - // MARK: IBOutlets - @IBOutlet private var previewImageView: HNImageView! - @IBOutlet private var titleLabel: UILabel! - @IBOutlet private var descriptionLabel: UILabel! - @IBOutlet private var contentStackView: UIStackView! - - // MARK: Public Properties - weak var delegate: PostTableViewCellDelegate? - - // MARK: Private Properties - private var theme: Theme? - - // MARK: Initialization - override func awakeFromNib() { - super.awakeFromNib() - selectedBackgroundView = UIView() - createRecognizer() - } - - // MARK: Override - override func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: true) - } - - override func prepareForReuse() { - super.prepareForReuse() - previewImageView.cancel() - } - - // MARK: Public Methods - - /// Set model data to the cell. - /// - /// - Parameter model: A `NewsModel` value that contains the cell data. - /// - Parameter placeholder: <#placeholder description#> - func setup(model: PostModel, placeholder: Image? = nil) { - previewImageView.placeholderImage = placeholder?.resource - - var time: String = "" - - if let seconds = model.time { - time = Date().timeAgo(from: seconds) - } - - if let urlString = model.url { - previewImageView.setImage(from: URL(string: urlString)) - } - titleLabel.text = model.title - descriptionLabel.attributedText = theme?.postDescriptionTitle(score: model.score.map(String.init), - username: model.by, - time: time) - titleLabel.layer.masksToBounds = false - descriptionLabel.layer.masksToBounds = false - contentStackView.layoutIfNeeded() - } - - /// Apply theme to cell. - /// - /// - Parameter theme: A `Theme` value that contains the current application theme. - func apply(theme: Theme) { - self.theme = theme - theme.postTitle.apply(to: titleLabel) - theme.postCell.apply(to: self) - } - - // MARK: Private Methods - private func createRecognizer() { - let tap = UITapGestureRecognizer(target: self, action: #selector(imageDidTap)) - previewImageView.addGestureRecognizer(tap) - } -} - -// MARK: IBAction -extension PostTableViewCell { - @objc func imageDidTap() { - self.delegate?.imageDidTapped(cell: self) - } -} diff --git a/HackerNews/Classes/PresentationLayer/Stories/View/Views/PostTableViewCell/PostTableViewCell.xib b/HackerNews/Classes/PresentationLayer/Stories/View/Views/PostTableViewCell/PostTableViewCell.xib deleted file mode 100644 index 68d8b33..0000000 --- a/HackerNews/Classes/PresentationLayer/Stories/View/Views/PostTableViewCell/PostTableViewCell.xib +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Resources/Assets.xcassets/Placeholders/Contents.json b/HackerNews/Classes/Preview Content/Preview Assets.xcassets/Contents.json similarity index 100% rename from HackerNews/Resources/Assets.xcassets/Placeholders/Contents.json rename to HackerNews/Classes/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/HackerNews/Common/Controllers/MainTabBar/Assembly/MainTabBarAssembly.swift b/HackerNews/Common/Controllers/MainTabBar/Assembly/MainTabBarAssembly.swift deleted file mode 100644 index 48e9318..0000000 --- a/HackerNews/Common/Controllers/MainTabBar/Assembly/MainTabBarAssembly.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// MainTabBarModuleAssembly.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Swinject - -final class MainTabBarModuleAssembly: Assembly { - func assemble(container: Container) { - container.register(StoriesConfigurator.self) { resolver in - let parentAssembler = resolver.resolve(MainTabBarConfigurator.self)?.assembler - return StoriesConfigurator(parentAssembler: parentAssembler.unwrap()) - } - - container.register(SettingsConfigurator.self) { resolver in - let parentAssembler = resolver.resolve(MainTabBarConfigurator.self)?.assembler - return SettingsConfigurator(parentAssembler: parentAssembler.unwrap()) - } - - container.register(MainTabBarPresenter.self) { resolver in - let presenter = MainTabBarPresenter() - - presenter.themeManager = resolver.resolve(ThemeManager.self) - - return presenter - }.initCompleted { resolver, presenter in - presenter.view = resolver.resolve(MainTabBarViewController.self) - } - - container.register(MainTabBarViewController.self) { resolver in - let storiesViewController = resolver.resolve(StoriesConfigurator.self).unwrap().configureViewController() - let settingsViewController = resolver.resolve(SettingsConfigurator.self).unwrap().configureViewController() - - let theme = resolver.resolve(ThemeManager.self).unwrap().theme - let output = resolver.resolve(MainTabBarPresenter.self).unwrap() - - let viewController = MainTabBarViewController(theme: theme, output: output) - - viewController.viewControllers = [storiesViewController, settingsViewController] - - return viewController - } - } -} diff --git a/HackerNews/Common/Controllers/MainTabBar/Configurator/MainTabBarConfigurator.swift b/HackerNews/Common/Controllers/MainTabBar/Configurator/MainTabBarConfigurator.swift deleted file mode 100644 index 38b5bf3..0000000 --- a/HackerNews/Common/Controllers/MainTabBar/Configurator/MainTabBarConfigurator.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MainTabBarConfigurator.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import Swinject - -final class MainTabBarConfigurator { - - // MARK: Private Properties - let assembler: Assembler - - // MARK: Initialization - init(parentAssembler: Assembler) { - assembler = Assembler([MainTabBarModuleAssembly()], parent: parentAssembler) - } - - // MARK: Public Methods - func configure() -> MainTabBarViewController { - guard let viewController = assembler.resolver.resolve(MainTabBarViewController.self) else { - fatalError("Cann't resolve MainTabBarViewController") - } - return viewController - } -} diff --git a/HackerNews/Common/Controllers/MainTabBar/Configurator/TabBarViewProtocol.swift b/HackerNews/Common/Controllers/MainTabBar/Configurator/TabBarViewProtocol.swift deleted file mode 100644 index 2fe2c03..0000000 --- a/HackerNews/Common/Controllers/MainTabBar/Configurator/TabBarViewProtocol.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// TabBarViewProtocol.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol TabBarViewProtocol { - var icon: UIImage? { get } - var title: String? { get } - - func configureViewController() -> UIViewController -} diff --git a/HackerNews/Common/Controllers/MainTabBar/Presenter/MainTabBarPresenter.swift b/HackerNews/Common/Controllers/MainTabBar/Presenter/MainTabBarPresenter.swift deleted file mode 100644 index 8f912e0..0000000 --- a/HackerNews/Common/Controllers/MainTabBar/Presenter/MainTabBarPresenter.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// MainTabBarPresenter.swift -// HackerNews -// -// Created by Никита Васильев on 16.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final class MainTabBarPresenter { - // MARK: Public Properties - weak var view: MainTabBarViewInput? - var themeManager: ThemeManagerProtocol? -} - -// MARK: MainTabBarViewOutput -extension MainTabBarPresenter: MainTabBarViewOutput { - func viewIsReady() { - themeManager?.addObserver(self) - } -} - -// MARK: ThemeObserver -extension MainTabBarPresenter: ThemeObserver { - func themeDidChange(_ theme: Theme) { - view?.update(theme: theme) - } -} diff --git a/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewController.swift b/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewController.swift deleted file mode 100644 index 2102974..0000000 --- a/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewController.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// MainTabBarViewController.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -final class MainTabBarViewController: UITabBarController { - - // MARK: Public Properties - let output: MainTabBarViewOutput - - // MARK: Public Properties - private(set) var theme: Theme - - // MARK: Initialization - init(theme: Theme, output: MainTabBarViewOutput) { - self.theme = theme - self.output = output - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - // MARK: View Controller Life Cycle - override func viewDidLoad() { - super.viewDidLoad() - update(theme: theme) - output.viewIsReady() - } -} - -// MARK: MainTabBarViewInput -extension MainTabBarViewController: MainTabBarViewInput { } - -// MARK: ThemeUpdatable -extension MainTabBarViewController: ThemeUpdatable { - func update(theme: Theme) { - theme.tabBar.apply(to: self.tabBar) - theme.view.apply(to: self.view) - - guard let viewControllers = viewControllers else { return } - - for viewController in viewControllers { - if let navController = viewController as? UINavigationController { - theme.navigationBar.apply(to: navController.navigationBar) - theme.view.apply(to: navController.view) - } - } - } -} diff --git a/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewInput.swift b/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewInput.swift deleted file mode 100644 index f9d06ee..0000000 --- a/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewInput.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// MainTabBarViewInput.swift -// HackerNews -// -// Created by Никита Васильев on 16.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -protocol MainTabBarViewInput: class, ThemeUpdatable { } diff --git a/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewOutput.swift b/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewOutput.swift deleted file mode 100644 index 5c59807..0000000 --- a/HackerNews/Common/Controllers/MainTabBar/View/MainTabBarViewOutput.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// MainTabBarViewOutput.swift -// HackerNews -// -// Created by Никита Васильев on 16.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -protocol MainTabBarViewOutput { - func viewIsReady() -} diff --git a/HackerNews/Common/DataStructures/Queue.swift b/HackerNews/Common/DataStructures/Queue.swift deleted file mode 100644 index f69d384..0000000 --- a/HackerNews/Common/DataStructures/Queue.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// Queue.swift -// HackerNews -// -// Created by Никита Васильев on 26.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct Queue { - - // MARK: Private Properties - private var queue: [T?] = [] - private var head = 0 - - // MARK: Public Properties - var isEmpty: Bool { - // swiftlint:disable empty_count - return count == 0 - // swiftlint:enable empty_count - } - - var count: Int { - return queue.count - head - } - - var front: T? { - if isEmpty { return nil } - return queue[head] - } - - // MARK: Initialization - init() { } - - // MARK: Public Methods - mutating func enqueue(_ item: T) { - queue.append(item) - } - - mutating func enqueue(_ items: [T]) { - queue.append(contentsOf: items) - } - - mutating func dequeue() -> T? { - if isEmpty { return nil } - - guard head < queue.count, let item = queue[head] else { return nil } - - queue[head] = nil - head += 1 - - let percentage = Double(head) / Double(queue.count) - - if queue.count > 50 && percentage > 0.5 { - queue.removeFirst(head) - head = 0 - } - - return item - } - - mutating func removeAll() { - queue.removeAll() - head = 0 - } -} diff --git a/HackerNews/Common/Extensions/Array+Range.swift b/HackerNews/Common/Extensions/Array+Range.swift deleted file mode 100644 index abfe87f..0000000 --- a/HackerNews/Common/Extensions/Array+Range.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Array+Range.swift -// HackerNews -// -// Created by Никита Васильев on 22.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -extension Array { - subscript(safe range: Range) -> ArraySlice { - return self[Swift.min(range.startIndex, self.endIndex).. UIColor { - return .fromRGBA(r, g, b, 1.0) - } - - static func fromRGBA(_ r: Int, _ g: Int, _ b: Int, _ a: CGFloat) -> UIColor { - return UIColor(red: CGFloat(r) / 255.0, - green: CGFloat(g) / 255.0, - blue: CGFloat(b) / 255.0, - alpha: a) - } -} diff --git a/HackerNews/Common/Extensions/Date+.swift b/HackerNews/Common/Extensions/Date+.swift deleted file mode 100644 index 985df7f..0000000 --- a/HackerNews/Common/Extensions/Date+.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// Date+.swift -// HackerNews -// -// Created by Никита Васильев on 29/09/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import Foundation - -extension Date { - // Returns the number of years - func yearsCount(from date: Date) -> Int { - return Calendar.current.dateComponents([.year], from: date, to: self).year ?? 0 - } - // Returns the number of months - func monthsCount(from date: Date) -> Int { - return Calendar.current.dateComponents([.month], from: date, to: self).month ?? 0 - } - // Returns the number of weeks - func weeksCount(from date: Date) -> Int { - return Calendar.current.dateComponents([.weekOfMonth], from: date, to: self).weekOfMonth ?? 0 - } - // Returns the number of days - func daysCount(from date: Date) -> Int { - return Calendar.current.dateComponents([.day], from: date, to: self).day ?? 0 - } - // Returns the number of hours - func hoursCount(from date: Date) -> Int { - return Calendar.current.dateComponents([.hour], from: date, to: self).hour ?? 0 - } - // Returns the number of minutes - func minutesCount(from date: Date) -> Int { - return Calendar.current.dateComponents([.minute], from: date, to: self).minute ?? 0 - } - // Returns the number of seconds - func secondsCount(from date: Date) -> Int { - return Calendar.current.dateComponents([.second], from: date, to: self).second ?? 0 - } - - // Returns time ago by checking if the time differences between two dates are in year or months or weeks or days or hours or minutes or seconds - func timeAgo(from date: Date) -> String { - if yearsCount(from: date) > 0 { return "\(yearsCount(from: date)) years ago" } - if monthsCount(from: date) > 0 { return "\(monthsCount(from: date)) months ago" } - if weeksCount(from: date) > 0 { return "\(weeksCount(from: date)) weeks ago" } - if daysCount(from: date) > 0 { return "\(daysCount(from: date)) days ago" } - if hoursCount(from: date) > 0 { return "\(hoursCount(from: date)) hrs ago" } - if minutesCount(from: date) > 0 { return "\(minutesCount(from: date)) min ago" } - if secondsCount(from: date) > 0 { return "\(secondsCount(from: date)) sec ago" } - return "" - } - - func timeAgo(from seconds: Int) -> String { - let date = Date(timeIntervalSince1970: TimeInterval(seconds)) - return timeAgo(from: date) - } -} diff --git a/HackerNews/Common/Extensions/StoryType+AllValues.swift b/HackerNews/Common/Extensions/StoryType+AllValues.swift deleted file mode 100644 index dd10f1b..0000000 --- a/HackerNews/Common/Extensions/StoryType+AllValues.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// StoryType+AllValues.swift -// HackerNews -// -// Created by Никита Васильев on 19.07.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import enum HNService.StoryType - -extension StoryType { - var displayName: String { - return "\(self)".capitalizingFirstLetter() - } - - static var allValues: [Self] { - var value = 0 - var result: [Self] = [] - - while let item = StoryType(rawValue: value) { - result.append(item) - value += 1 - } - - return result - } -} diff --git a/HackerNews/Common/Extensions/Strings/String+.swift b/HackerNews/Common/Extensions/Strings/String+.swift deleted file mode 100644 index 9389a1d..0000000 --- a/HackerNews/Common/Extensions/Strings/String+.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// String+.swift -// HackerNews -// -// Created by Никита Васильев on 11/11/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import Foundation - -extension String { - var htmlDecoded: String { - let decoded = try? NSAttributedString(data: Data(utf8), options: [ - .documentType: NSAttributedString.DocumentType.html, - .characterEncoding: String.Encoding.utf8.rawValue - ], documentAttributes: nil).string - - return decoded?.trimmingCharacters(in: .whitespacesAndNewlines) ?? self - } - - func capitalizingFirstLetter() -> String { - return prefix(1).capitalized + dropFirst() - } - - mutating func capitalizeFirstLetter() { - self = self.capitalizingFirstLetter() - } -} diff --git a/HackerNews/Common/Extensions/Strings/String+Attributed.swift b/HackerNews/Common/Extensions/Strings/String+Attributed.swift deleted file mode 100644 index 6ca8968..0000000 --- a/HackerNews/Common/Extensions/Strings/String+Attributed.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// String+Attributed.swift -// HackerNews -// -// Created by Никита Васильев on 21.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -extension String { - func attributedString(withLetterSpacing spacing: CGFloat? = nil, - lineHeight: CGFloat? = nil, - textAlignment: NSTextAlignment? = nil, - textColor: UIColor? = nil, - font: UIFont? = nil) -> NSAttributedString { - let att = NSAttributedString.attributes(withLetterSpacing: spacing, - lineHeight: lineHeight, - textAlignment: textAlignment, - textColor: textColor, - font: font) - - return .init(string: self, attributes: att) - } -} - -extension NSAttributedString { - static func attributes(withLetterSpacing spacing: CGFloat? = nil, - lineHeight: CGFloat? = nil, - textAlignment: NSTextAlignment? = nil, - textColor: UIColor? = nil, - font: UIFont? = nil, - text: String? = nil) -> [NSAttributedString.Key: Any] { - var attributes: [NSAttributedString.Key: Any] = [:] - let style = NSMutableParagraphStyle() - - if let spacing = spacing { - attributes[.kern] = spacing - } - - if let lineHeight = lineHeight { - style.minimumLineHeight = lineHeight - attributes.updateValue(style, forKey: .paragraphStyle) - } - - if let textAlignment = textAlignment { - style.alignment = textAlignment - attributes.updateValue(style, forKey: .paragraphStyle) - } - - if let textColor = textColor { - attributes[.foregroundColor] = textColor - } - - if let font = font { - attributes[.font] = font - } - - return attributes - } - - static func attributedString(from image: UIImage, color: UIColor) -> NSAttributedString { - let attachment = NSTextAttachment() - let templateImage = image.withRenderingMode(.alwaysTemplate) - attachment.image = templateImage.withTint(color: color) - attachment.bounds = CGRect(x: 0, y: -2, width: image.size.width, height: image.size.height) - return NSAttributedString(attachment: attachment) - } -} - -func + (lhs: NSAttributedString, rhs: NSAttributedString) -> NSAttributedString { - // swiftlint:disable force_cast - let a = lhs.mutableCopy() as! NSMutableAttributedString - let b = rhs.mutableCopy() as! NSMutableAttributedString - - a.append(b) - - return a.copy() as! NSAttributedString - // swiftlint:enable force_cast -} - -func + (lhs: NSAttributedString, rhs: String) -> NSAttributedString { - // swiftlint:disable force_cast - let a = lhs.mutableCopy() as! NSMutableAttributedString - let b = NSMutableAttributedString(string: rhs) - - return a + b - // swiftlint:enable force_cast -} - -func + (lhs: String, rhs: NSAttributedString) -> NSAttributedString { - let a = NSMutableAttributedString(string: lhs) - // swiftlint:disable force_cast - let b = lhs.mutableCopy() as! NSMutableAttributedString - - return a + b - // swiftlint:enable force_cast -} - -func + (lhs: NSAttributedString, rhs: UIImage) -> NSAttributedString { - // swiftlint:disable force_cast - let a = lhs.mutableCopy() as! NSMutableAttributedString - let b = NSTextAttachment() - - b.image = rhs - - return a + b - // swiftlint:enable force_cast -} - -func + (lhs: NSAttributedString, rhs: NSTextAttachment) -> NSAttributedString { - // swiftlint:disable force_cast - let a = lhs.mutableCopy() as! NSMutableAttributedString - let b = NSAttributedString(attachment: rhs) - - return a + b - // swiftlint:enable force_cast -} - -func + (lhs: NSTextAttachment, rhs: NSAttributedString) -> NSAttributedString { - let a = NSAttributedString(attachment: lhs) - // swiftlint:disable force_cast - let b = rhs.mutableCopy() as! NSMutableAttributedString - - return a + b - // swiftlint:enable force_cast -} diff --git a/HackerNews/Common/Extensions/Strings/String+Localized.swift b/HackerNews/Common/Extensions/Strings/String+Localized.swift deleted file mode 100644 index b0412e3..0000000 --- a/HackerNews/Common/Extensions/Strings/String+Localized.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// String+Localized.swift -// HackerNews -// -// Created by Никита Васильев on 17.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -extension String { - func localized() -> String { - return NSLocalizedString(self, comment: "") - } - - func localizedFormat(arguments: CVarArg...) -> String { - return String(format: localized(), arguments: arguments) - } -} diff --git a/HackerNews/Common/Extensions/UIImage+.swift b/HackerNews/Common/Extensions/UIImage+.swift deleted file mode 100644 index 308220e..0000000 --- a/HackerNews/Common/Extensions/UIImage+.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// UIImage+.swift -// HackerNews -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit - -extension UIImage { - class func imageWithColor(color: UIColor) -> UIImage { - let rect = CGRect(x: 0, y: 0, width: 1.0, height: 0.5) - UIGraphicsBeginImageContextWithOptions(rect.size, false, 0) - color.setFill() - UIRectFill(rect) - let image = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage() - UIGraphicsEndImageContext() - return image - } -} - -// swiftlint:disable force_unwrapping -extension UIImage { - func withTint(color: UIColor) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, false, scale) - color.setFill() - - guard let context = UIGraphicsGetCurrentContext() else { return nil } - context.translateBy(x: 0, y: size.height) - context.scaleBy(x: 1.0, y: -1.0) - context.setBlendMode(CGBlendMode.normal) - - let rect = CGRect(origin: .zero, size: CGSize(width: size.width, height: size.height)) - context.clip(to: rect, mask: cgImage!) - context.fill(rect) - - guard let newImage = UIGraphicsGetImageFromCurrentImageContext() else { return nil } - UIGraphicsEndImageContext() - - return newImage - } -} -// swiftlint:enable force_unwrapping diff --git a/HackerNews/Common/Extensions/UIView+.swift b/HackerNews/Common/Extensions/UIView+.swift deleted file mode 100644 index 0a58498..0000000 --- a/HackerNews/Common/Extensions/UIView+.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// UIView+.swift -// HackerNews -// -// Created by Никита Васильев on 05.05.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -extension UIView { - func setView(_ view: UIView) { - view.translatesAutoresizingMaskIntoConstraints = false - addSubview(view) - - NSLayoutConstraint.activate([ - topAnchor.constraint(equalTo: view.topAnchor), - leadingAnchor.constraint(equalTo: view.leadingAnchor), - view.trailingAnchor.constraint(equalTo: trailingAnchor), - view.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - } -} diff --git a/HackerNews/Common/Extensions/UIViewController/UINavigationController+.swift b/HackerNews/Common/Extensions/UIViewController/UINavigationController+.swift deleted file mode 100644 index a3fa6a9..0000000 --- a/HackerNews/Common/Extensions/UIViewController/UINavigationController+.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// UINavigationController+.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -extension UINavigationController { - override open var preferredStatusBarStyle: UIStatusBarStyle { - return ThemeManager.shared.theme.statusBar - } - - func configureDefaultNavigationBar() { -// navigationBar.setBackgroundImage(UIImage.imageWithColor(color: .white), for: .default) -// navigationBar.shadowImage = UIImage.imageWithColor(color: .white) -// navigationBar.isTranslucent = false - navigationBar.backgroundColor = .white -// navigationBar.backIndicatorImage = R.image.backNavigation() -// navigationBar.backIndicatorTransitionMaskImage = R.image.backNavigation() -// navigationBar.titleTextAttributes = [.foregroundColor: .black, -// .font: FontStyle.header4.font()] - navigationBar.barTintColor = UIColor.white - navigationBar.tintColor = UIColor.black - navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) - navigationItem.backBarButtonItem?.title = " " - } -} diff --git a/HackerNews/Common/Extensions/UIViewController/UIViewController+.swift b/HackerNews/Common/Extensions/UIViewController/UIViewController+.swift deleted file mode 100644 index a1c5587..0000000 --- a/HackerNews/Common/Extensions/UIViewController/UIViewController+.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// UIViewController+.swift -// HackerNews -// -// Created by Никита Васильев on 23.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import UIKit - -struct AlertActionModel { - let title: String - let style: Style - let action: (() -> Void)? -} - -extension AlertActionModel { - enum Style { - case `default` - case cancel - } -} - -extension UIViewController { - func showActionSheet(title: String? = nil, message: String? = nil, - actions: [AlertActionModel], completion: (() -> Void)? = nil) { - let alert = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) - - for action in actions { - alert.addAction(UIAlertAction(actionModel: action)) - } - - self.present(alert, animated: true, completion: completion) - } -} - -extension UIAlertAction { - convenience init(actionModel: AlertActionModel) { - switch actionModel.style { - case .cancel: - self.init(title: actionModel.title, style: .cancel, handler: { _ in - actionModel.action?() - }) - case .default: - self.init(title: actionModel.title, style: .default, handler: { _ in - actionModel.action?() - }) - } - } -} diff --git a/HackerNews/Common/Extensions/UIViewController/UIViewController+Safe.swift b/HackerNews/Common/Extensions/UIViewController/UIViewController+Safe.swift deleted file mode 100644 index d183664..0000000 --- a/HackerNews/Common/Extensions/UIViewController/UIViewController+Safe.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// UIViewController+Safe.swift -// HackerNews -// -// Created by Никита Васильев on 10.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -extension UIViewController { - var safeTopAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11, *) { - return view.safeAreaLayoutGuide.topAnchor - } else { - return topLayoutGuide.bottomAnchor - } - } - - var safeBottomAnchor: NSLayoutYAxisAnchor { - if #available(iOS 11, *) { - return view.safeAreaLayoutGuide.bottomAnchor - } else { - return bottomLayoutGuide.topAnchor - } - } - - var safeLeadingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11, *) { - return view.safeAreaLayoutGuide.leadingAnchor - } else { - return view.leadingAnchor - } - } - - var safeTrailingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11, *) { - return view.safeAreaLayoutGuide.trailingAnchor - } else { - return view.trailingAnchor - } - } -} diff --git a/HackerNews/Common/Models/BaseCellModel.swift b/HackerNews/Common/Models/BaseCellModel.swift deleted file mode 100644 index e8d9977..0000000 --- a/HackerNews/Common/Models/BaseCellModel.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// BaseCellModel.swift -// HackerNews -// -// Created by Никита Васильев on 26.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit.UIView - -protocol BaseCellViewModel { - static var cellClass: UIView.Type { get } - - func setup(_ cell: UIView) -} - -protocol CellViewModel: BaseCellViewModel { - associatedtype CellClass: UIView - - func setup(on cell: CellClass) -} - -extension CellViewModel { - static var cellClass: UIView.Type { - return Self.CellClass.self - } - - func setup(_ cell: UIView) { - guard let cell = cell as? Self.CellClass else { return } - setup(on: cell) - } -} diff --git a/HackerNews/Common/Models/CommentCellViewModel.swift b/HackerNews/Common/Models/CommentCellViewModel.swift deleted file mode 100644 index 508aa55..0000000 --- a/HackerNews/Common/Models/CommentCellViewModel.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// CommentCellViewModel.swift -// HackerNews -// -// Created by Никита Васильев on 26.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import struct HNService.CommentModel - -struct CommentCellViewModel { - let comment: CommentModel - let text: String - let theme: Theme -} - -// MARK: CellViewModel -extension CommentCellViewModel: CellViewModel { - func setup(on cell: CommentCell) { - cell.apply(theme: theme) - cell.setup(model: comment, text: text) - } -} diff --git a/HackerNews/Common/Models/PostCellViewModel.swift b/HackerNews/Common/Models/PostCellViewModel.swift deleted file mode 100644 index 5b93a16..0000000 --- a/HackerNews/Common/Models/PostCellViewModel.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// PostCellViewModel.swift -// HackerNews -// -// Created by Никита Васильев on 26.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import struct HNService.PostModel - -struct PostCellViewModel { - let post: PostModel - let theme: Theme - let placeholderImage: Image -} - -// MARK: BaseCellViewModel -extension PostCellViewModel: CellViewModel { - func setup(on cell: PostTableViewCell) { - cell.apply(theme: theme) - cell.setup(model: post, placeholder: placeholderImage) - } -} diff --git a/HackerNews/Common/Models/SkeletonCellViewModel.swift b/HackerNews/Common/Models/SkeletonCellViewModel.swift deleted file mode 100644 index 7799bb7..0000000 --- a/HackerNews/Common/Models/SkeletonCellViewModel.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SkeletonCellViewModel.swift -// HackerNews -// -// Created by Никита Васильев on 03.05.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct SkeletonCellViewModel { - let theme: Theme -} - -// MARK: CellViewModel -extension SkeletonCellViewModel: CellViewModel { - func setup(on cell: SkeletonCell) { - cell.apply(theme: theme) - } -} diff --git a/HackerNews/Common/Style/Fonts/Font+.swift b/HackerNews/Common/Style/Fonts/Font+.swift deleted file mode 100644 index 6704725..0000000 --- a/HackerNews/Common/Style/Fonts/Font+.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// Font+.swift -// HackerNews -// -// Created by Никита Васильев on 11.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -import UIKit - -typealias Converter = (A) -> A - -infix operator >>>: AdditionPrecedence - -func >>> (f1: @escaping Converter, f2: @escaping Converter) -> Converter { - return { item in f2(f1(item)) } -} - -func >>> (font: UIFont, converter: Converter) -> UIFont { - return converter(font) -} - -func newFont(_ font: UIFont, with traits: UIFontDescriptor.SymbolicTraits) -> UIFont { - guard let descriptor = font.fontDescriptor.withSymbolicTraits(traits) else { - fatalError("Cannot build font descriptor with traits \(traits)") - } - return UIFont(descriptor: descriptor, size: font.pointSize) -} - -func newFont(_ font: UIFont, withSize size: CGFloat) -> UIFont { - let descriptor = font.fontDescriptor - let newFont = UIFont(descriptor: descriptor, size: size) - return newFont -} - -/// 12 font size. -func tiny(_ font: UIFont) -> UIFont { return newFont(font, withSize: 12) } -/// 13 font size. -func small(_ font: UIFont) -> UIFont { return newFont(font, withSize: 13) } -/// 15 font size. -func normalsize(_ font: UIFont) -> UIFont { return newFont(font, withSize: 15) } -/// 17 font size. -func medium(_ font: UIFont) -> UIFont { return newFont(font, withSize: 17) } -/// 22 font size. -func large(_ font: UIFont) -> UIFont { return newFont(font, withSize: 22) } -/// 34 font size. -func huge(_ font: UIFont) -> UIFont { return newFont(font, withSize: 34) } diff --git a/HackerNews/Common/Style/Fonts/FontStyle.swift b/HackerNews/Common/Style/Fonts/FontStyle.swift deleted file mode 100644 index 9416436..0000000 --- a/HackerNews/Common/Style/Fonts/FontStyle.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// FontStyle.swift -// HackerNews -// -// Created by Никита Васильев on 11.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -enum FontStyle: String { - - /// Bold 34 - case header1 - - /// Bold 22 - case header2 - - /// Semibold 17 - case header3 - - /// Regular 17 - case text1 - - /// Regular 13 - case text2 - - /// Regular 12 - case text3 - - /// - var font: UIFont { - switch self { - case .header1: - return .extraBold >>> huge - case .header2: - return .extraBold >>> large - case .header3: - return .semiBold >>> medium - case .text1: - return .regular >>> medium - case .text2: - return .regular >>> normalsize - case .text3: - return .regular >>> small - } - } - -} diff --git a/HackerNews/Common/Style/Fonts/Fonts.swift b/HackerNews/Common/Style/Fonts/Fonts.swift deleted file mode 100644 index db0838e..0000000 --- a/HackerNews/Common/Style/Fonts/Fonts.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// Fonts.swift -// HackerNews -// -// Created by Никита Васильев on 11.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -extension UIFont { - // swiftlint:disable force_unwrapping - class var light: UIFont { return UIFont(name: "Poppins-Bold", size: 16)! } - class var regular: UIFont { return UIFont(name: "Poppins-Regular", size: 16)! } - class var medium: UIFont { return UIFont(name: "Poppins-Medium", size: 16)! } - class var extraBold: UIFont { return UIFont(name: "Poppins-ExtraBold", size: 16)! } - class var extraLight: UIFont { return UIFont(name: "Poppins-ExtraLight", size: 16)! } - class var semiBold: UIFont { return UIFont(name: "Poppins-SemiBold", size: 16)! } - class var italic: UIFont { return UIFont(name: "Poppins-Italic", size: 16)! } - class var bold: UIFont { return UIFont(name: "Poppins-Bold", size: 16)! } - // swiftlint:enable force_unwrapping -} diff --git a/HackerNews/Common/Style/Style.swift b/HackerNews/Common/Style/Style.swift deleted file mode 100644 index 3b1149f..0000000 --- a/HackerNews/Common/Style/Style.swift +++ /dev/null @@ -1,82 +0,0 @@ -// -// Style.swift -// HackerNews -// -// Created by Никита Васильев on 05.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -/// An abstraction if `UIView` styling. -struct Style { - - // MARK: Public Properties - - /// The styling function that takes a `UIView` instance and performs side-effects on it. - let styling: (T) -> Void - - // MARK: Initialization - - /// Create a new `Style` instance. - /// - /// - Parameter styling: The styling function that takes a `UIView` instance and performs side-effects on it. - init(_ styling: @escaping (T) -> Void) { - self.styling = styling - } - - // MARK: Public Methods - - /// A factory method that composes multiple styles. - /// - /// - Parameter styles: The styles to compose. - /// - /// - Returns: A new `Style` that will call the input styles' - /// `styling` method in succession. - static func compose(_ styles: Style...) -> Style { - return Style { view in - for style in styles { - style.styling(view) - } - } - } - - /// Compose this style with another. - /// - /// - Parameter other: Other style to compose this style with. - /// - /// - Returns: A new `Style` which will call this style's `styling`, - /// and then the `other` style's `styling`. - func composing(with other: Style) -> Style { - return Style { view in - self.styling(view) - other.styling(view) - } - } - - /// Compose this style with another styling function. - /// - /// - Parameter otherStyling: The function to compose this style with. - /// - /// - Returns: A new `Style` which will call this style's `styling`, - /// and then the input `styling`. - func composing(with otherStyling: @escaping (T) -> Void) -> Style { - return self.composing(with: Style(otherStyling)) - } - - /// Apply this style to a UIView. - /// - /// - Parameter view: the view to style - func apply(to view: T) { - styling(view) - } - - /// Apply this style to multiple views. - /// - /// - Parameter views: the views to style. - func apply(to views: T...) { - for view in views { - styling(view) - } - } -} diff --git a/HackerNews/Common/Style/Stylesheet.swift b/HackerNews/Common/Style/Stylesheet.swift deleted file mode 100644 index 874ddf3..0000000 --- a/HackerNews/Common/Style/Stylesheet.swift +++ /dev/null @@ -1,358 +0,0 @@ -// -// Stylesheet.swift -// HackerNews -// -// Created by Никита Васильев on 05.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import Skeleton - -// swiftlint:disable type_body_length -struct Stylesheet { - enum NavigationBar { - private static let base: Style = Style { - $0.isTranslucent = false - $0.titleTextAttributes = [.font: FontStyle.header3.font] - $0.tintColor = Colors.lightOrange - - if #available (iOS 11.0, *) { - $0.largeTitleTextAttributes = [.font: FontStyle.header1.font] - } - } - - private static let white: Style = Style { - $0.barTintColor = Colors.warmGray - $0.backgroundColor = Colors.warmGray - } - - private static let whiteText: Style = Style { - guard var attributes = $0.titleTextAttributes else { return } - attributes[.foregroundColor] = Colors.lightGray - $0.titleTextAttributes = attributes - } - - private static let whiteLargeTitleText: Style = Style { - guard #available(iOS 11.0, *), var attributes = $0.largeTitleTextAttributes else { return } - attributes[.foregroundColor] = Colors.lightGray - $0.largeTitleTextAttributes = attributes - } - - private static let lightAppearance: Style = Style { - guard #available(iOS 13.0, *) else { return } - let apperance = makeLightAppearance() - $0.compactAppearance = apperance - $0.scrollEdgeAppearance = apperance - $0.standardAppearance = apperance - } - - private static let darkAppearance: Style = Style { - guard #available(iOS 13.0, *) else { return } - let apperance = makeDarkAppearance() - $0.compactAppearance = apperance - $0.scrollEdgeAppearance = apperance - $0.standardAppearance = apperance - } - - private static let blackLargeTitleText: Style = Style { - guard #available(iOS 11.0, *), var attributes = $0.largeTitleTextAttributes else { return } - attributes[.foregroundColor] = Colors.black - $0.largeTitleTextAttributes = attributes - } - - private static let blackText: Style = Style { - guard var attributes = $0.titleTextAttributes else { return } - attributes[.foregroundColor] = Colors.black - $0.titleTextAttributes = attributes - } - - private static let black: Style = Style { - $0.barTintColor = Colors.black - $0.backgroundColor = Colors.black - } - - private static let noHairline: Style = Style { - $0.shadowImage = UIImage() - } - - private static let hairline: Style = Style { - $0.shadowImage = UIImage.imageWithColor(color: Colors.gray20) - } - - @available(iOS 13.0, *) - private static func makeLightAppearance() -> UINavigationBarAppearance { - let apperance = UINavigationBarAppearance() - apperance.backgroundColor = Colors.warmGray - apperance.titleTextAttributes = [.foregroundColor: Colors.black] - apperance.largeTitleTextAttributes = [.foregroundColor: Colors.black] - return apperance - } - - @available(iOS 13.0, *) - private static func makeDarkAppearance() -> UINavigationBarAppearance { - let apperance = UINavigationBarAppearance() - apperance.backgroundColor = Colors.black - apperance.titleTextAttributes = [.foregroundColor: Colors.lightGray] - apperance.largeTitleTextAttributes = [.foregroundColor: Colors.lightGray] - return apperance - } - - static let dark: Style = Style.compose(base, black, whiteText, whiteLargeTitleText, noHairline, darkAppearance) - static let light: Style = Style.compose(base, white, blackText, blackLargeTitleText, hairline, lightAppearance) - } - - enum TabBar { - static let dark: Style = Style { - $0.barTintColor = Colors.darkGray - $0.isTranslucent = false - $0.tintColor = Colors.darkOrange - $0.unselectedItemTintColor = Colors.warmGrayLight - } - - static let light: Style = Style { - $0.barTintColor = Colors.warmGray - $0.isTranslucent = false - $0.tintColor = Colors.lightOrange - $0.unselectedItemTintColor = Colors.warmGrayLight - } - - static func itemImage(_ value: ImageType) -> Style { - return Style { - $0.image = value.resource - $0.selectedImage = value.resourceWhileSelected - $0.imageInsets = UIEdgeInsets(top: -3, left: 0, bottom: 3, right: 0) - $0.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -4) - } - } - } - - enum SegmentedControl { - private static var lightText: Style = Style { - $0.setTitleTextAttributes([.foregroundColor: Colors.white], for: .normal) - } - - private static var darkText: Style = Style { - $0.setTitleTextAttributes([.foregroundColor: Colors.black], for: .normal) - } - - private static var lightTintColor: Style = Style { - if #available(iOS 13.0, *) { - $0.selectedSegmentTintColor = Colors.white - } - } - - private static var darkTintColor: Style = Style { - if #available(iOS 13.0, *) { - $0.selectedSegmentTintColor = Colors.darkGray - } - } - - static var light: Style = Style.compose(darkText, lightTintColor) - static var dark: Style = Style.compose(lightText, darkTintColor) - } - - enum Label { - private static let header1: Style = Style { - $0.font = FontStyle.header1.font - } - - private static let header2: Style = Style { - $0.font = FontStyle.header2.font - } - - private static let header3: Style = Style { - $0.font = FontStyle.header3.font - } - - private static let text1: Style = Style { - $0.font = FontStyle.text1.font - } - - private static let white: Style = Style { - $0.textColor = Colors.white - } - - private static let darkGray: Style = Style { - $0.textColor = Colors.darkGray - } - - private static let gray: Style = Style { - $0.textColor = Colors.gray - } - - static let lightSettingsBaseText = Style.compose(text1, darkGray) - static let darkSettingsBaseText = Style.compose(text1, white) - static let infoSettingsText = Style.compose(text1, gray) - static let darkTableViewHeaderText = Style.compose(white) - static let lightTableViewHeaderText = Style.compose(darkGray) - static let lightPostTitleText = Style.compose(text1, darkGray) - static let darkPostTitleText = Style.compose(text1, white) - } - - enum View { - static let alertLight: Style = Style { - $0.tintColor = Colors.darkOrange - } - - static let alertDark: Style = Style { - $0.tintColor = Colors.lightOrange - } - - static let light: Style = Style { - $0.backgroundColor = Colors.lightGray - } - - static let dark: Style = Style { - $0.backgroundColor = Colors.black - } - } - - enum ActivityIndicator { - static let light: Style = Style { - $0.color = Colors.darkGray - } - - static let dark: Style = Style { - $0.color = Colors.lightGray - } - } - - enum Skeleton { - static let light: Style = Style { - $0.gradientLayer.colors = [Colors.greyish.cgColor, - Colors.warmGrayLight.cgColor, - Colors.greyish.cgColor] - } - - static let dark: Style = Style { - $0.gradientLayer.colors = [Colors.greyishBrown.cgColor, - Colors.gray.cgColor, - Colors.greyishBrown.cgColor] - } - } - - enum Cell { - private static let white: Style = Style { - $0.selectedBackgroundView?.backgroundColor = .lightGray - $0.backgroundColor = Colors.white - } - - private static let lightGray: Style = Style { - $0.selectedBackgroundView?.backgroundColor = .lightGray - $0.backgroundColor = Colors.lightGray - } - - private static let dark: Style = Style { - $0.selectedBackgroundView?.backgroundColor = .darkGray - $0.backgroundColor = Colors.darkGray - } - - private static let black: Style = Style { - $0.selectedBackgroundView?.backgroundColor = .darkGray - $0.backgroundColor = Colors.black - } - - private static let lightOrangeTint: Style = Style { - $0.tintColor = Colors.lightOrange - } - - private static let darkOrangeTint: Style = Style { - $0.tintColor = Colors.darkOrange - } - - private static let grayTint: Style = Style { - $0.tintColor = Colors.gray - } - - static let lightWithOrangeTint = Style.compose(white, lightOrangeTint) - static let darkWithOrangeTint = Style.compose(dark, darkOrangeTint) - static let lightWithGrayTint = Style.compose(white, grayTint) - static let darkWithGrayTint = Style.compose(dark, grayTint) - static let lightPostCell = Style.compose(lightGray, grayTint) - static let darkPostCell = Style.compose(black, grayTint) - } - - enum TableView { - private static let base: Style = Style { - $0.separatorStyle = .singleLine - } - - private static let lightSeparator: Style = Style { - $0.separatorColor = .lightGray - } - - private static let darkSeparator: Style = Style { - $0.separatorColor = .darkGray - } - - private static let darkBackground: Style = Style { - $0.backgroundColor = Colors.black - $0.indicatorStyle = .white - } - - private static let lightBackground: Style = Style { - $0.backgroundColor = Colors.lightGray - $0.indicatorStyle = .black - } - - static let light = Style.compose(base, lightBackground, lightSeparator) - static let dark = Style.compose(base, darkBackground, darkSeparator) - } - - enum RefreshControl { - static let light: Style = Style { - $0.tintColor = UIColor.darkGray - } - - static let dark: Style = Style { - $0.tintColor = UIColor.lightGray - } - } - - enum TableViewHeader { - static let light: Style = Style { - $0.contentView.backgroundColor = Colors.greyish - } - - static let dark: Style = Style { - $0.contentView.backgroundColor = Colors.greyishBrown - } - } - - enum AttributedString { - static func commentTitle(string: String, color: UIColor) -> NSAttributedString { - return string.attributedString(textAlignment: .left, textColor: color, font: FontStyle.text3.font) - } - - static func commentText(string: String, color: UIColor) -> NSAttributedString { - return string.attributedString(withLetterSpacing: 0.2, lineHeight: 8.0, - textAlignment: .left, textColor: color, - font: FontStyle.text2.font) - } - - static func noCommentsText(string: String, color: UIColor) -> NSAttributedString { - return string.attributedString(withLetterSpacing: 0.2, - textAlignment: .center, - textColor: color, - font: FontStyle.header3.font) - } - - static func postDescription(icon: UIImage, color: UIColor) -> NSAttributedString { - return NSAttributedString.attributedString(from: icon, color: color) - } - - static func postDescription(string: String, color: UIColor) -> NSAttributedString { - return string.attributedString(textAlignment: .left, textColor: color, font: FontStyle.text3.font) - } - - static func emptySetTitle(string: String, color: UIColor) -> NSAttributedString { - return string.attributedString(textAlignment: .center, textColor: color, font: FontStyle.header2.font) - } - - static func emptySetDescription(string: String, color: UIColor) -> NSAttributedString { - return string.attributedString(textAlignment: .center, textColor: color, font: FontStyle.text3.font) - } - } -} -// swiftlint:enable type_body_length diff --git a/HackerNews/Common/Utils/Alertable.swift b/HackerNews/Common/Utils/Alertable.swift deleted file mode 100644 index efccba0..0000000 --- a/HackerNews/Common/Utils/Alertable.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Alert.swift -// HackerNews -// -// Created by Никита Васильев on 15/11/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol Alertable { } -extension Alertable where Self: UIViewController { - - func showAlert(title: String, message: String) { - let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) - - let okAction = UIAlertAction(title: "OK", style: .cancel) { _ in } - alertController.addAction(okAction) - - DispatchQueue.main.async { - self.view?.window?.rootViewController?.present(alertController, - animated: true, - completion: nil) - } - } -} diff --git a/HackerNews/Common/Utils/ImageType.swift b/HackerNews/Common/Utils/ImageType.swift deleted file mode 100644 index 6b28935..0000000 --- a/HackerNews/Common/Utils/ImageType.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// ImageType.swift -// HackerNews -// -// Created by Никита Васильев on 16.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol ImageType { - var resource: UIImage? { get } - var resourceWhileSelected: UIImage? { get } -} - -enum Image: ImageType { - case settings - case filter - case connectionError - case placeholder - - var resource: UIImage? { - switch self { - case .settings: - return R.image.settings() - case .filter: - return R.image.filterIcon() - case .connectionError: - return R.image.connectionErrorIcon() - case .placeholder: - return R.image.placeholder() - } - } - - var resourceWhileSelected: UIImage? { - return nil - } -} diff --git a/HackerNews/Common/Utils/NibLoadableView.swift b/HackerNews/Common/Utils/NibLoadableView.swift deleted file mode 100644 index 8c3422a..0000000 --- a/HackerNews/Common/Utils/NibLoadableView.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// NibLoadableView.swift -// HackerNews -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol NibLoadableView: class { } - -extension UITableViewCell: NibLoadableView { } - -extension NibLoadableView where Self: UIView { - static var NibName: String { - return String(describing: self) - } -} diff --git a/HackerNews/Common/Utils/Optional+Throwable.swift b/HackerNews/Common/Utils/Optional+Throwable.swift deleted file mode 100644 index 5f8280b..0000000 --- a/HackerNews/Common/Utils/Optional+Throwable.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Optional+Throwable.swift -// HackerNews -// -// Created by Никита Васильев on 16.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -extension Optional { - func unwrap() -> Wrapped { - switch self { - case .some(let result): - return result - default: - fatalError("Cann't unwrap value") - } - } -} diff --git a/HackerNews/Common/Utils/Presentable.swift b/HackerNews/Common/Utils/Presentable.swift deleted file mode 100644 index daf3cfe..0000000 --- a/HackerNews/Common/Utils/Presentable.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// Presentable.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol Presentable { - var viewController: UIViewController { get } - var parent: UIViewController? { get } - - func present() - func presentAsRoot() - func present(from viewController: UIViewController) - func presentAsNavController() - func presentModal(from viewController: UIViewController) - func show(from viewController: UIViewController) - func dismiss() - func dismissModal() - func dismissModal(completion: @escaping () -> Void) - func dismissModal(animated: Bool, completion: @escaping () -> Void) - func popToRoot() - func showInContainer(container: UIView, in viewController: UIViewController) - func showDetail(from viewController: UIViewController) -} - -extension Presentable where Self: UIViewController { - var viewController: UIViewController { - return self - } - - var parent: UIViewController? { - return viewController.parent - } - - func present() { - AppDelegate.currentWindow.rootViewController = viewController - } - - func present(from viewController: UIViewController) { - viewController.navigationController?.pushViewController(self, animated: true) - } - - func presentAsNavController() { - let navigation = UINavigationController(rootViewController: viewController) - navigation.configureDefaultNavigationBar() - AppDelegate.currentWindow.rootViewController = navigation - } - - func presentModal(from viewController: UIViewController) { - if let presentedVC = viewController.presentedViewController { - presentedVC.dismiss(animated: false) { [weak viewController] in - viewController?.present(self, animated: false, completion: nil) - } - } else { - viewController.present(self, animated: false, completion: nil) - } - } - - func presentAsRoot() { - AppDelegate.currentWindow.rootViewController = viewController - } - - func showInContainer(container: UIView, in viewController: UIViewController) { - self.view.translatesAutoresizingMaskIntoConstraints = false - container.addSubview(self.view) - - self.view.topAnchor.constraint(equalTo: container.topAnchor).isActive = true - self.view.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true - self.view.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true - let bottomConstraint = self.view.bottomAnchor.constraint(equalTo: container.bottomAnchor) - bottomConstraint.priority = UILayoutPriority(900) - bottomConstraint.isActive = true - - viewController.addChild(self) - self.didMove(toParent: viewController) - } - - func show(from viewController: UIViewController) { - viewController.show(self, sender: viewController) - } - - func dismiss() { - _ = navigationController?.popViewController(animated: true) - } - - func dismissModal(completion: @escaping () -> Void) { - dismiss(animated: true, completion: completion) - } - - func dismissModal(animated: Bool, completion: @escaping () -> Void) { - dismiss(animated: animated, completion: completion) - } - - func dismissModal() { - dismiss(animated: true, completion: nil) - } - - func popToRoot() { - viewController.navigationController?.popToRootViewController(animated: true) - } - - func showDetail(from viewController: UIViewController) { - viewController.showDetailViewController(self, sender: nil) - } -} diff --git a/HackerNews/Common/Utils/ReusableView.swift b/HackerNews/Common/Utils/ReusableView.swift deleted file mode 100644 index 78f5744..0000000 --- a/HackerNews/Common/Utils/ReusableView.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// ReusableView.swift -// HackerNews -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol ReusableView: class {} - -extension ReusableView where Self: UIView { - static var reuseIdentifier: String { - return String(describing: self) - } -} diff --git a/HackerNews/Common/Utils/Themeable/ThemeUpdatable.swift b/HackerNews/Common/Utils/Themeable/ThemeUpdatable.swift deleted file mode 100644 index 7b9262d..0000000 --- a/HackerNews/Common/Utils/Themeable/ThemeUpdatable.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// ThemeUpdatable.swift -// HackerNews -// -// Created by Никита Васильев on 16.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol ThemeUpdatable { - func update(theme: Theme) -} diff --git a/HackerNews/Common/Utils/TransitionHandler.swift b/HackerNews/Common/Utils/TransitionHandler.swift deleted file mode 100644 index 9d06d1b..0000000 --- a/HackerNews/Common/Utils/TransitionHandler.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// TransitionHandler.swift -// HackerNews -// -// Created by Никита Васильев on 04.03.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -protocol TransitionHandler: class { - func openModule(_ completion: @escaping (UIViewController) -> Void) -} - -extension TransitionHandler where Self: UIViewController { - func openModule(_ completion: @escaping (UIViewController) -> Void) { - completion(self) - } -} - -extension UIViewController: TransitionHandler { } diff --git a/HackerNews/Common/Utils/UITableView+.swift b/HackerNews/Common/Utils/UITableView+.swift deleted file mode 100644 index 9b60fac..0000000 --- a/HackerNews/Common/Utils/UITableView+.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// UITableView+.swift -// HackerNews -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit -extension UITableView { - - func dequeueReusableCell(forIndexPath indexPath: IndexPath) -> T { - guard let cell = self.dequeueReusableCell(withIdentifier: T.reuseIdentifier, - for: indexPath) as? T else { - fatalError("Could not dequeue cell with identifier: \(T.reuseIdentifier)") - } - return cell - } - - func dequeueReusableCell(forIndexPath indexPath: IndexPath, with model: BaseCellViewModel) -> UITableViewCell { - let cellIdentifier = String(describing: type(of: model).cellClass) - let cell = dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) - - model.setup(cell) - - return cell - } - - func viewForHeader(_: T.Type) -> T where T: NibLoadableView { - guard let header = Bundle.main.loadNibNamed(T.NibName, - owner: self, - options: nil)?.first as? T else { - fatalError("Could not loadNibNamed: \(T.NibName)") - } - return header - } - - func register(_: T.Type) { - let nib = UINib(nibName: T.NibName, bundle: nil) - self.register(nib, forCellReuseIdentifier: T.reuseIdentifier) - } -} diff --git a/HackerNews/Common/Utils/UITableViewCell+.swift b/HackerNews/Common/Utils/UITableViewCell+.swift deleted file mode 100644 index a9b3a68..0000000 --- a/HackerNews/Common/Utils/UITableViewCell+.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// UITableViewCell+.swift -// HackerNews -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit -extension UITableViewCell: ReusableView { } diff --git a/HackerNews/Common/Utils/UIViewControllerFactory.swift b/HackerNews/Common/Utils/UIViewControllerFactory.swift deleted file mode 100644 index bfa6c12..0000000 --- a/HackerNews/Common/Utils/UIViewControllerFactory.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// UIViewControllerFactory.swift -// HackerNews -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import UIKit - -extension UIStoryboard { - func instantiateViewController(type: T.Type) -> T { - guard let viewController = self.instantiateViewController(withIdentifier: T.storyboardIdentifier) as? T else { - fatalError("Couldn't instantiate view controller with identifier \(T.storyboardIdentifier)") - } - - return viewController - } - - func instantiateViewController(_ storyboardIdentifier: String) -> T { - guard let viewController = self.instantiateViewController(withIdentifier: storyboardIdentifier) as? T else { - fatalError("Couldn't instantiate view controller with identifier \(storyboardIdentifier)") - } - - return viewController - } -} - -protocol StoryboardIdentifiable { - static var storyboardIdentifier: String { get } -} - -extension StoryboardIdentifiable where Self: UIViewController { - static var storyboardIdentifier: String { - return String(describing: self) - } -} - -extension UIViewController: StoryboardIdentifiable { } diff --git a/HackerNews/Common/Utils/Weak.swift b/HackerNews/Common/Utils/Weak.swift deleted file mode 100644 index 5dcffaa..0000000 --- a/HackerNews/Common/Utils/Weak.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// Weak.swift -// HackerNews -// -// Created by Никита Васильев on 16.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -class Weak where T: AnyObject { - private(set) weak var value: T? - - init(_ value: T?) { - self.value = value - } -} diff --git a/HackerNews/Common/Utils/Weak/WeakObject.swift b/HackerNews/Common/Utils/Weak/WeakObject.swift deleted file mode 100644 index 91ef667..0000000 --- a/HackerNews/Common/Utils/Weak/WeakObject.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// WeakObject.swift -// HackerNews -// -// Created by Никита Васильев on 28.05.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -class WeakObject where T: AnyObject { - - // MARK: Private Properties - - /// Identifier. - private let identifier: ObjectIdentifier - - /// Wrapped object. - fileprivate(set) weak var object: T? - - // MARK: Initialization - - /// Create a new `WeakObject` instance. - /// - /// - Parameter object: Wrapped object. - init(object: T) { - self.identifier = ObjectIdentifier(object) - self.object = object - } -} - -// MARK: Equatable -extension WeakObject: Equatable { - static func == (lhs: WeakObject, rhs: WeakObject) -> Bool { - return lhs.identifier == rhs.identifier - } -} - -// MARK: Hashable -extension WeakObject: Hashable { - func hash(into hasher: inout Hasher) { - hasher.combine(identifier) - } -} diff --git a/HackerNews/Common/Utils/Weak/WeakObjectSet.swift b/HackerNews/Common/Utils/Weak/WeakObjectSet.swift deleted file mode 100644 index 3ac8bf6..0000000 --- a/HackerNews/Common/Utils/Weak/WeakObjectSet.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// WeakObjectSet.swift -// HackerNews -// -// Created by Никита Васильев on 28.05.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -final class WeakObjectSet where T: AnyObject { - - // MARK: Private Properties - - /// Constains all objects. - fileprivate(set) var objects: Set> - - // MARK: Public Properties - - /// Return all valid object that contains in set. - /// - /// - Complexity: `O(n)` - /// - /// - Returns: An array of valid objects. - var allObjects: [T] { - return objects.compactMap { $0.object } - } - - // MARK: Initialization - - /// Create a new `WeakObjectSet` instance with empty data. - init() { - self.objects = Set>([]) - } - - /// Create a new `WeakObjectSet` instance with objects. - /// - /// - Parameter objects: An array that contains objects to be appended to set. - init(objects: [T]) { - self.objects = Set>(objects.map({ WeakObject(object: $0) })) - } - - // MARK: Public Methods - - /// Returns a Boolean value that indicates whether the given element exists in the set. - /// - /// - Parameter object: An element to look for in the set. - /// - /// - Complexity: O(1) - /// - /// - Returns: true if member exists in the set; otherwise, false. - func contains(_ object: T) -> Bool { - return objects.contains(WeakObject(object: object)) - } - - /// Add object to set. - /// - /// - Parameter object: The object to be added to set. - func append(_ object: T) { - objects.formUnion([WeakObject(object: object)]) - } - - /// Add objects to set. - /// - /// - Complexity: - /// - /// - Parameter array: The array to be added to set. - func append(contentsOf array: [T]) { - array.forEach { append($0) } - } - - /// Remove object from set. - /// - /// - Complexity: O(n) - /// - /// - Parameter object: The object to be delete. - func remove(_ object: T) { - objects.remove(WeakObject(object: object)) - } -} diff --git a/HackerNews/Common/Views/Cells/LoadingCell/SkeletonCell.swift b/HackerNews/Common/Views/Cells/LoadingCell/SkeletonCell.swift deleted file mode 100644 index 741d8b8..0000000 --- a/HackerNews/Common/Views/Cells/LoadingCell/SkeletonCell.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// LoadingCell.swift -// HackerNews -// -// Created by Никита Васильев on 23.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import Skeleton - -class SkeletonCell: UITableViewCell { - // MARK: IBOutlets - @IBOutlet private var gradientViews: [GradientContainerView]! - - // MARK: Layout - override func layoutSubviews() { - super.layoutSubviews() - - gradientViews.forEach { - $0.backgroundColor = .clear - $0.gradientLayer.cornerRadius = Metrics.cornerRadius - } - } - - // MARK: Public Methods - - /// Apply theme to cell. - /// - /// - Parameter theme: A `Theme` value that contains the current application theme. - func apply(theme: Theme) { - theme.postCell.apply(to: self) - gradientViews.forEach { theme.skeleton.apply(to: $0) } - } -} - -// MARK: GradientsOwner -extension SkeletonCell: GradientsOwner { - var gradientLayers: [CAGradientLayer] { - return gradientViews.map { $0.gradientLayer } - } -} - -// MARK: Constants -extension SkeletonCell { - private enum Metrics { - static let cornerRadius: CGFloat = 8.0 - } -} diff --git a/HackerNews/Common/Views/Cells/LoadingCell/SkeletonCell.xib b/HackerNews/Common/Views/Cells/LoadingCell/SkeletonCell.xib deleted file mode 100644 index 6e028ec..0000000 --- a/HackerNews/Common/Views/Cells/LoadingCell/SkeletonCell.xib +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNews/Common/Views/HNImageView.swift b/HackerNews/Common/Views/HNImageView.swift deleted file mode 100644 index 18bcb3c..0000000 --- a/HackerNews/Common/Views/HNImageView.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// HNImageView.swift -// HackerNews -// -// Created by Никита Васильев on 20.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import Kingfisher - -final class HNImageView: UIView { - - // MARK: Private Properties - private var downloadTask: DownloadTask? - private var imageView: UIImageView = UIImageView() - - // MARK: Public Properties - - /// <#Description#> - var placeholderImage: UIImage? = nil { - didSet { - setPlaceholder() - } - } - - /// <#Description#> - var cornerRadius: CGFloat = 8.0 { - didSet { - layer.cornerRadius = cornerRadius - } - } - - // MARK: Intitialization - override init(frame: CGRect) { - super.init(frame: frame) - setup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - - // MARK: Public Methods - - /// <#Description#> - /// - /// - Parameter url: <#url description#> - /// - /// - Returns: <#description#> - func setImage(from url: URL?) { - guard let url = url, let thumbnailURL = URL(string: Constants.Links.imageExtractorURL + (url.absoluteString)) else { - return - } - - let newSize = 60 - let thumbnailSize = CGFloat(newSize) * UIScreen.main.scale - let thumbnailCGSize = CGSize(width: thumbnailSize, height: thumbnailSize) - let imageSizeProcessor = ResizingImageProcessor(referenceSize: thumbnailCGSize, - mode: .aspectFill) - let options: KingfisherOptionsInfo = [ - .processor(imageSizeProcessor) - ] - - let resource = ImageResource(downloadURL: thumbnailURL) - - downloadTask = KingfisherManager.shared.retrieveImage(with: resource, options: options) { result in - switch result { - case .success(let imageResult): - DispatchQueue.main.async { - self.contentMode = .center - self.imageView.image = imageResult.image - } - default: break - } - } - } - - /// <#Description#> - func cancel() { - downloadTask?.cancel() - } - - // MARK: Private Methods - - /// <#Description#> - private func setup() { - addSubview(imageView) - imageView.translatesAutoresizingMaskIntoConstraints = false - - NSLayoutConstraint.activate([ - imageView.topAnchor.constraint(equalTo: topAnchor), - imageView.leadingAnchor.constraint(equalTo: leadingAnchor), - imageView.trailingAnchor.constraint(equalTo: trailingAnchor), - imageView.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - - layer.cornerRadius = cornerRadius - clipsToBounds = true - } - - private func setPlaceholder() { - DispatchQueue.main.async { - self.contentMode = .center - self.imageView.image = self.placeholderImage - } - } -} diff --git a/HackerNews/Common/Views/LoadingFooterView/LoadingFooterView.swift b/HackerNews/Common/Views/LoadingFooterView/LoadingFooterView.swift deleted file mode 100644 index 36d2942..0000000 --- a/HackerNews/Common/Views/LoadingFooterView/LoadingFooterView.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// LoadingFooterView.swift -// HackerNews -// -// Created by Никита Васильев on 26.04.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -final class LoadingFooterView: UIView { - - // MARK: Private Properties - private var activityIndicatorView: UIActivityIndicatorView = UIActivityIndicatorView() - - // MARK: Public Properties - var isAnimating: Bool = false { - didSet { - isAnimating ? activityIndicatorView.stopAnimating() : activityIndicatorView.startAnimating() - } - } - - // MARK: Initialization - override init(frame: CGRect) { - super.init(frame: frame) - configure() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - configure() - } - - // MARK: Private Methods - private func configure() { - activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false - addSubview(activityIndicatorView) - - NSLayoutConstraint.activate([ - centerXAnchor.constraint(equalTo: activityIndicatorView.centerXAnchor), - centerYAnchor.constraint(equalTo: activityIndicatorView.centerYAnchor) - ]) - } -} diff --git a/HackerNews/Hacker News.entitlements b/HackerNews/Hacker News.entitlements deleted file mode 100644 index ee95ab7..0000000 --- a/HackerNews/Hacker News.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/HackerNews/Resources/Assets.xcassets/FilterIcon.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 67% rename from HackerNews/Resources/Assets.xcassets/FilterIcon.imageset/Contents.json rename to HackerNews/Resources/Assets.xcassets/AccentColor.colorset/Contents.json index 6c36a60..eb87897 100644 --- a/HackerNews/Resources/Assets.xcassets/FilterIcon.imageset/Contents.json +++ b/HackerNews/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,7 +1,6 @@ { - "images" : [ + "colors" : [ { - "filename" : "filter_icon.pdf", "idiom" : "universal" } ], diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 4e4cb5b..a657e33 100644 --- a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,111 +1,9 @@ { "images" : [ { - "filename" : "icon_20pt@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon_20pt@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "icon_29pt@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon_29pt@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "icon_40pt@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon_40pt@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "icon_60pt@2x.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "icon_60pt@3x.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "icon_20pt.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "icon_20pt@2x-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon_29pt.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "icon_29pt@2x-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon_40pt.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "icon_40pt@2x-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon_76pt.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "filename" : "icon_76pt@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "icon_83.5@2x.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "Icon.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "filename" : "icon.png", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png deleted file mode 100644 index f1c9c8a..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/Icon.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 0000000..b173c58 Binary files /dev/null and b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png deleted file mode 100644 index f54d121..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png deleted file mode 100644 index 855c8e8..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png deleted file mode 100644 index 855c8e8..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png deleted file mode 100644 index adaa6cc..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png deleted file mode 100644 index 7411229..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png deleted file mode 100644 index 67c7880..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png deleted file mode 100644 index 67c7880..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png deleted file mode 100644 index 409e371..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png deleted file mode 100644 index 855c8e8..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png deleted file mode 100644 index 1f38b52..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png deleted file mode 100644 index 1f38b52..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png deleted file mode 100644 index 7e0f65c..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png deleted file mode 100644 index 7e0f65c..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png deleted file mode 100644 index c961695..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png deleted file mode 100644 index 9a62669..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png deleted file mode 100644 index 10d7467..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png b/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png deleted file mode 100644 index 32aaa62..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29.png b/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29.png deleted file mode 100644 index 5e37ff0..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29@2x.png b/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29@2x.png deleted file mode 100644 index 9f16cc5..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29@3x.png b/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29@3x.png deleted file mode 100644 index 13608ce..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/29x29@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/Contents.json deleted file mode 100644 index 6819bbc..0000000 --- a/HackerNews/Resources/Assets.xcassets/Cells/Comments.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "filename" : "29x29.png", - "scale" : "1x" - }, - { - "idiom" : "iphone", - "filename" : "29x29@2x.png", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "filename" : "29x29@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/Cells/Contents.json b/HackerNews/Resources/Assets.xcassets/Cells/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/HackerNews/Resources/Assets.xcassets/Cells/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/Cells/PointsIcon.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/Cells/PointsIcon.imageset/Contents.json deleted file mode 100644 index cc94767..0000000 --- a/HackerNews/Resources/Assets.xcassets/Cells/PointsIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "points_icon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/HackerNews/Resources/Assets.xcassets/Cells/PointsIcon.imageset/points_icon.pdf b/HackerNews/Resources/Assets.xcassets/Cells/PointsIcon.imageset/points_icon.pdf deleted file mode 100644 index 9ef0714..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Cells/PointsIcon.imageset/points_icon.pdf and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/ConnectionErrorIcon.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/ConnectionErrorIcon.imageset/Contents.json deleted file mode 100644 index 59e4e46..0000000 --- a/HackerNews/Resources/Assets.xcassets/ConnectionErrorIcon.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "connection_error_icon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNews/Resources/Assets.xcassets/ConnectionErrorIcon.imageset/connection_error_icon.pdf b/HackerNews/Resources/Assets.xcassets/ConnectionErrorIcon.imageset/connection_error_icon.pdf deleted file mode 100644 index e747be3..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/ConnectionErrorIcon.imageset/connection_error_icon.pdf and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Contents.json b/HackerNews/Resources/Assets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/HackerNews/Resources/Assets.xcassets/Contents.json +++ b/HackerNews/Resources/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/HackerNews/Resources/Assets.xcassets/FilterIcon.imageset/filter_icon.pdf b/HackerNews/Resources/Assets.xcassets/FilterIcon.imageset/filter_icon.pdf deleted file mode 100644 index 62dd502..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/FilterIcon.imageset/filter_icon.pdf and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Placeholders/Placeholder.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/Placeholders/Placeholder.imageset/Contents.json deleted file mode 100644 index f0dd476..0000000 --- a/HackerNews/Resources/Assets.xcassets/Placeholders/Placeholder.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "placeholder_icon.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNews/Resources/Assets.xcassets/Placeholders/Placeholder.imageset/placeholder_icon.pdf b/HackerNews/Resources/Assets.xcassets/Placeholders/Placeholder.imageset/placeholder_icon.pdf deleted file mode 100644 index 8099718..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Placeholders/Placeholder.imageset/placeholder_icon.pdf and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Settings/Contents.json b/HackerNews/Resources/Assets.xcassets/Settings/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/HackerNews/Resources/Assets.xcassets/Settings/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/Settings/Help.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/Settings/Help.imageset/Contents.json deleted file mode 100644 index cbf9bac..0000000 --- a/HackerNews/Resources/Assets.xcassets/Settings/Help.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "help_icon.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/Settings/Help.imageset/help_icon.pdf b/HackerNews/Resources/Assets.xcassets/Settings/Help.imageset/help_icon.pdf deleted file mode 100644 index 3f768b1..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Settings/Help.imageset/help_icon.pdf and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Settings/Rate.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/Settings/Rate.imageset/Contents.json deleted file mode 100644 index 5f1892b..0000000 --- a/HackerNews/Resources/Assets.xcassets/Settings/Rate.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "rate_icon.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/Settings/Rate.imageset/rate_icon.pdf b/HackerNews/Resources/Assets.xcassets/Settings/Rate.imageset/rate_icon.pdf deleted file mode 100644 index 61bf0ad..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Settings/Rate.imageset/rate_icon.pdf and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/Settings/ThemeIcon.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/Settings/ThemeIcon.imageset/Contents.json deleted file mode 100644 index 6240bff..0000000 --- a/HackerNews/Resources/Assets.xcassets/Settings/ThemeIcon.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "pallete.pdf" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - }, - "properties" : { - "template-rendering-intent" : "template" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/Settings/ThemeIcon.imageset/pallete.pdf b/HackerNews/Resources/Assets.xcassets/Settings/ThemeIcon.imageset/pallete.pdf deleted file mode 100644 index 0484207..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/Settings/ThemeIcon.imageset/pallete.pdf and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25.png b/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25.png deleted file mode 100644 index 5dc5249..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25@2x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25@2x.png deleted file mode 100644 index 67ee6e8..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25@3x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25@3x.png deleted file mode 100644 index df6df68..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/25x25@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/Contents.json deleted file mode 100644 index c984430..0000000 --- a/HackerNews/Resources/Assets.xcassets/TabBar/Ask.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "25x25.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "25x25@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "25x25@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Contents.json b/HackerNews/Resources/Assets.xcassets/TabBar/Contents.json deleted file mode 100644 index da4a164..0000000 --- a/HackerNews/Resources/Assets.xcassets/TabBar/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25.png b/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25.png deleted file mode 100644 index e8282a8..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25@2x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25@2x.png deleted file mode 100644 index 515bec6..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25@3x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25@3x.png deleted file mode 100644 index 485dc07..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/25x25@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/Contents.json deleted file mode 100644 index 91cbd30..0000000 --- a/HackerNews/Resources/Assets.xcassets/TabBar/Earth.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "filename" : "25x25.png", - "scale" : "1x" - }, - { - "idiom" : "iphone", - "filename" : "25x25@2x.png", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "filename" : "25x25@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25.png b/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25.png deleted file mode 100644 index bd983de..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25@2x.png b/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25@2x.png deleted file mode 100644 index 34872f8..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25@3x.png b/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25@3x.png deleted file mode 100644 index 553b09c..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/25x25@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/Contents.json deleted file mode 100644 index 72b8e2e..0000000 --- a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "settings_icon.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "settings_icon@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "settings_icon@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon.png b/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon.png deleted file mode 100644 index 000f69e..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon@2x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon@2x.png deleted file mode 100644 index 6be7c12..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon@3x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon@3x.png deleted file mode 100644 index b9c4506..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Settings.imageset/settings_icon@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25.png b/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25.png deleted file mode 100644 index 747aa38..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25@2x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25@2x.png deleted file mode 100644 index ae09961..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25@2x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25@3x.png b/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25@3x.png deleted file mode 100644 index dcb82c6..0000000 Binary files a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/25x25@3x.png and /dev/null differ diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/Contents.json b/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/Contents.json deleted file mode 100644 index c984430..0000000 --- a/HackerNews/Resources/Assets.xcassets/TabBar/Show.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "25x25.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "25x25@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "25x25@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/HackerNews/Resources/Configurations/Base.xcconfig b/HackerNews/Resources/Configurations/Base.xcconfig new file mode 100644 index 0000000..d60a362 --- /dev/null +++ b/HackerNews/Resources/Configurations/Base.xcconfig @@ -0,0 +1,4 @@ +HTTPS = https:/ + +BASE_BUNDLE_ID = com.nikitavasilev.hackernews +DEVELOPMENT_TEAM = ZY8BJSR866 \ No newline at end of file diff --git a/HackerNews/Resources/Configurations/Beta.xcconfig b/HackerNews/Resources/Configurations/Beta.xcconfig new file mode 100644 index 0000000..55e93fa --- /dev/null +++ b/HackerNews/Resources/Configurations/Beta.xcconfig @@ -0,0 +1,4 @@ +#include "Base.xcconfig" + +ENVIRONMENT = debug +APP_GROUP = group.$(BASE_BUNDLE_ID).beta \ No newline at end of file diff --git a/HackerNews/Resources/Configurations/Debug.xcconfig b/HackerNews/Resources/Configurations/Debug.xcconfig new file mode 100644 index 0000000..9927715 --- /dev/null +++ b/HackerNews/Resources/Configurations/Debug.xcconfig @@ -0,0 +1,4 @@ +#include "Base.xcconfig" + +ENVIRONMENT = debug +APP_GROUP = group.$(BASE_BUNDLE_ID).debug \ No newline at end of file diff --git a/HackerNews/Resources/Configurations/Release.xcconfig b/HackerNews/Resources/Configurations/Release.xcconfig new file mode 100644 index 0000000..1139aa5 --- /dev/null +++ b/HackerNews/Resources/Configurations/Release.xcconfig @@ -0,0 +1,4 @@ +#include "Base.xcconfig" + +ENVIRONMENT = production +APP_GROUP = $(BASE_BUNDLE_ID) \ No newline at end of file diff --git a/HackerNews/Resources/Fonts/Poppins-Bold.ttf b/HackerNews/Resources/Fonts/Poppins-Bold.ttf deleted file mode 100644 index 44313ca..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-Bold.ttf and /dev/null differ diff --git a/HackerNews/Resources/Fonts/Poppins-ExtraBold.ttf b/HackerNews/Resources/Fonts/Poppins-ExtraBold.ttf deleted file mode 100644 index 88d0f1e..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-ExtraBold.ttf and /dev/null differ diff --git a/HackerNews/Resources/Fonts/Poppins-ExtraLight.ttf b/HackerNews/Resources/Fonts/Poppins-ExtraLight.ttf deleted file mode 100644 index 4620a42..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-ExtraLight.ttf and /dev/null differ diff --git a/HackerNews/Resources/Fonts/Poppins-Italic.ttf b/HackerNews/Resources/Fonts/Poppins-Italic.ttf deleted file mode 100644 index 8efebbf..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-Italic.ttf and /dev/null differ diff --git a/HackerNews/Resources/Fonts/Poppins-Light.ttf b/HackerNews/Resources/Fonts/Poppins-Light.ttf deleted file mode 100644 index 8a6ac68..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-Light.ttf and /dev/null differ diff --git a/HackerNews/Resources/Fonts/Poppins-Medium.ttf b/HackerNews/Resources/Fonts/Poppins-Medium.ttf deleted file mode 100644 index 5b46f19..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-Medium.ttf and /dev/null differ diff --git a/HackerNews/Resources/Fonts/Poppins-Regular.ttf b/HackerNews/Resources/Fonts/Poppins-Regular.ttf deleted file mode 100644 index 246a861..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-Regular.ttf and /dev/null differ diff --git a/HackerNews/Resources/Fonts/Poppins-SemiBold.ttf b/HackerNews/Resources/Fonts/Poppins-SemiBold.ttf deleted file mode 100644 index 3bbad2a..0000000 Binary files a/HackerNews/Resources/Fonts/Poppins-SemiBold.ttf and /dev/null differ diff --git a/HackerNews/Resources/Info.plist b/HackerNews/Resources/Info.plist index 58c40ab..df36b4c 100644 --- a/HackerNews/Resources/Info.plist +++ b/HackerNews/Resources/Info.plist @@ -2,14 +2,8 @@ - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) - CFBundleIcons - - CFBundleIcons~ipad - CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -20,8 +14,19 @@ APPL CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + + CFBundleVersion - 2365 + 2 + ITSAppUsesNonExemptEncryption + LSRequiresIPhoneOS NSAppTransportSecurity @@ -29,37 +34,21 @@ NSAllowsArbitraryLoads - UIAppFonts - - Poppins-Bold.ttf - Poppins-ExtraBold.ttf - Poppins-Light.ttf - Poppins-Italic.ttf - Poppins-ExtraLight.ttf - Poppins-Medium.ttf - Poppins-Regular.ttf - Poppins-SemiBold.ttf - - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - + UILaunchScreens + + UILaunchScreen + + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationLandscapeLeft UISupportedInterfaceOrientations~ipad UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown - UIViewControllerBasedStatusBarAppearance - diff --git a/HackerNews/Resources/Strings/en.lproj/Localizable.strings b/HackerNews/Resources/Strings/en.lproj/Localizable.strings deleted file mode 100644 index babcb65..0000000 --- a/HackerNews/Resources/Strings/en.lproj/Localizable.strings +++ /dev/null @@ -1,39 +0,0 @@ -"Theme" = "Theme"; - -"Themes" = "Themes"; - -"Light" = "Light"; - -"Dark" = "Dark"; - -"Settings" = "Settings"; - -"News" = "News"; - -"New" = "New"; - -"Top" = "Top"; - -"Best" = "Best"; - -"Stories" = "Stories"; - -"Ask" = "Ask"; - -"Show" = "Show"; - -"Appearance" = "Appearance"; - -"Comments" = "Comments"; - -"No comments" = "No comments"; - -"settings.themes.header" = "Themes"; - -"settings.help.header" = "Help"; - -"settings.themes.title" = "Themes"; - -"settings.help.title" = "Help"; - -"settings.help.rate" = "Rate Us"; diff --git a/HackerNews/Resources/Strings/ru.lproj/Localizable.strings b/HackerNews/Resources/Strings/ru.lproj/Localizable.strings deleted file mode 100644 index 997f8cb..0000000 --- a/HackerNews/Resources/Strings/ru.lproj/Localizable.strings +++ /dev/null @@ -1,39 +0,0 @@ -"Theme" = "Тема"; - -"Themes" = "Темы"; - -"Light" = "Светлая"; - -"Dark" = "Темная"; - -"Settings" = "Настройки"; - -"News" = "Новости"; - -"New" = "Новое"; - -"Top" = "Популярное"; - -"Best" = "Лучшее"; - -"Stories" = "Истории"; - -"Appearance" = "Оформление"; - -"Ask" = "Вопросы"; - -"Show" = "Демонстрация"; - -"Comments" = "Комментарии"; - -"No comments" = "Нет комментариев"; - -"settings.themes.header" = "Темы"; - -"settings.help.header" = "Помощь"; - -"settings.themes.title" = "Темы"; - -"settings.help.title" = "Помощь"; - -"settings.help.rate" = "Оценить приложение"; diff --git a/HackerNewsTests/.swiftlint.yml b/HackerNewsTests/.swiftlint.yml deleted file mode 100644 index 5b3eade..0000000 --- a/HackerNewsTests/.swiftlint.yml +++ /dev/null @@ -1,41 +0,0 @@ -opt_in_rules: - - private_outlet - - force_unwrapping - - strong_iboutlet - - private_action - - contains_over_first_not_nil - - discouraged_object_literal - - empty_count - - empty_string - - identical_operands - - - unneeded_parentheses_in_closure_argument - - let_var_whitespace - - yoda_condition - - closure_spacing - - collection_alignment - - operator_usage_whitespace - - closure_end_indentation - - file_name_no_space - - unowned_variable_capture - - contains_over_filter_count - - contains_over_filter_is_empty - - contains_over_range_nil_comparison - - empty_collection_literal -disabled_rules: - - trailing_whitespace - - type_name - - function_body_length - -# Rule Config -line_length: 150 -identifier_name: - min_length: 1 - max_length: - warning: 40 - error: 60 -type_name: - min_length: 3 - max_length: - warning: 80 - error: 100 diff --git a/HackerNewsTests/Extensions/Data+.swift b/HackerNewsTests/Extensions/Data+.swift deleted file mode 100644 index d85eca2..0000000 --- a/HackerNewsTests/Extensions/Data+.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Data+.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -extension Data { - func json(deletingKeyPaths keyPaths: String...) throws -> Data { - let decoded = try JSONSerialization.jsonObject(with: self, options: .mutableContainers) as AnyObject - - for keyPath in keyPaths { - decoded.setValue(nil, forKeyPath: keyPath) - } - - return try JSONSerialization.data(withJSONObject: decoded) - } -} diff --git a/HackerNewsTests/Extensions/Utils.swift b/HackerNewsTests/Extensions/Utils.swift deleted file mode 100644 index 90532d7..0000000 --- a/HackerNewsTests/Extensions/Utils.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Utils.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest - -func assertThrowsKeyNotFound(_ expectedKey: String, decoding: T.Type, from data: Data, - file: StaticString = #file, line: UInt = #line) { - XCTAssertThrowsError(try JSONDecoder().decode(decoding, from: data), file: file, line: line) { error in - if case .keyNotFound(let key, _)? = error as? DecodingError { - XCTAssertEqual(expectedKey, key.stringValue, "Expected missing key \(key.stringValue) to equal \(expectedKey).", - file: file, line: line) - } else { - XCTFail("Expected '.keyNotFound(\(expectedKey))' but got \(error)", file: file, line: line) - } - } -} diff --git a/HackerNewsTests/HackerNewsTests.swift b/HackerNewsTests/HackerNewsTests.swift new file mode 100644 index 0000000..5c99f2e --- /dev/null +++ b/HackerNewsTests/HackerNewsTests.swift @@ -0,0 +1,4 @@ +// +// Nikita Vasilev +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// diff --git a/HackerNewsTests/Info.plist b/HackerNewsTests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/HackerNewsTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/HackerNewsTests/Mocks/HNServiceMock.swift b/HackerNewsTests/Mocks/HNServiceMock.swift deleted file mode 100644 index a9978e7..0000000 --- a/HackerNewsTests/Mocks/HNServiceMock.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// HNServiceMock.swift -// HackerNewsTests -// -// Created by Никита Васильев on 07.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import HNService - -@testable import HackerNews - -final class HNServiceMock: HNServiceProtocol { - var fetchIdsCalled: Bool = false - var loadPostsCalled: Bool = false - var loadCommentsCalled: Bool = false - var cancelAllTasksCalled: Bool = false - - var type: StoryType? - var fetchIdsCompletion: (([Int]) -> Void)? - var fetchIdsFail: ((Error) -> Void)? - - var loadPostsCompletion: (([PostModel]) -> Void)? - var loadPostsFail: ((Error) -> Void)? - var postsIds: [Int]? - - var loadCommentsId: Int? - var loadCommentsCompletion: ((CommentModel) -> Void)? - var loadCommentsFail: ((Error) -> Void)? - - func fetchIds(for type: StoryType, completion: @escaping ([Int]) -> Void, fail: @escaping (Error) -> Void) { - fetchIdsCalled = true - fetchIdsCompletion = completion - fetchIdsFail = fail - } - - func loadPosts(with ids: [Int], completion: @escaping ([PostModel]) -> Void, fail: @escaping (Error) -> Void) { - postsIds = ids - loadPostsCalled = true - loadPostsCompletion = completion - loadPostsFail = fail - } - - func loadComments(with id: Int, completion: @escaping (CommentModel) -> Void, fail: @escaping (Error) -> Void) { - loadCommentsCalled = true - loadCommentsId = id - loadCommentsCompletion = completion - loadCommentsFail = fail - } - - func cancelAllTasks() { - cancelAllTasksCalled = true - } -} diff --git a/HackerNewsTests/Mocks/MockContainer.swift b/HackerNewsTests/Mocks/MockContainer.swift deleted file mode 100644 index c6101fd..0000000 --- a/HackerNewsTests/Mocks/MockContainer.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// MockContainer.swift -// HackerNewsTests -// -// Created by Никита Васильев on 05.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import Swinject - -@testable import HackerNews - -final class MockContainer { - - // MARK: Public Properties - let container: Container - let assembler: Assembler - - // MARK: Initialization - init() { - container = ServiceLocatorConfigurator().container - assembler = Assembler(container: container) - - assembler.apply(assemblies: [ - RootModuleAssembly(), - MainTabBarModuleAssembly(), - SettingsModuleAssembly(), - StoriesModuleAssembly(), - ThemeModuleAssembly(), - ServiceLocatorAssembly(), - CommentsModuleAssembly() - ]) - } -} diff --git a/HackerNewsTests/Mocks/MockThemeManager.swift b/HackerNewsTests/Mocks/MockThemeManager.swift deleted file mode 100644 index 3b3341b..0000000 --- a/HackerNewsTests/Mocks/MockThemeManager.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// MockThemeManager.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -@testable import HackerNews - -final class MockThemeManager: ThemeManagerProtocol { - var theme: Theme = .light - - var themes: [Theme] = ThemeManager.shared.themes - - var addedObserver: ThemeObserver? - var removedObserver: ThemeObserver? - - func addObserver(_ observer: ThemeObserver) { - addedObserver = observer - } - - func removeObserver(_ observer: ThemeObserver) { - removedObserver = observer - } -} diff --git a/HackerNewsTests/Mocks/TestData.swift b/HackerNewsTests/Mocks/TestData.swift deleted file mode 100644 index d063bc4..0000000 --- a/HackerNewsTests/Mocks/TestData.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// TestData.swift -// HackerNewsTests -// -// Created by Никита Васильев on 05.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import HNService - -@testable import HackerNews - -enum TestData: String { - case storyJSON = """ - { - \"id\": 8863, - \"by\": \"name\", - \"descendants\": 71, - \"kids\": [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940 ], - \"score\": 111, - \"time\": 1175714200, - \"title\": \"My YC app: Dropbox - Throw away your USB drive\", - \"url\": \"http://www.getdropbox.com/u/2/screencast.html\" - } - """ - - case storyWithoutIdJSON = """ - { - \"by\": \"name\", - \"descendants\": 71, - \"kids\": [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940 ], - \"score\": 111, - \"time\": 1175714200, - \"title\": \"My YC app: Dropbox - Throw away your USB drive\", - \"url\": \"http://www.getdropbox.com/u/2/screencast.html\" - } - """ - - case commentWithoutJSON = """ - { - \"by\" : \"norvig\", - \"kids\" : [ 2922097, 2922429, 2924562, 2922709, 2922573, 2922140, 2922141 ], - \"parent\" : 2921506, - \"text\" : \"Aw shucks, guys ... you make me blush with your compliments.", - \"time\" : 1314211127 - } - """ - - case commentJSON = """ - { - \"by\" : \"norvig\", - \"id\" : 2921983, - \"kids\" : [ 2922097, 2922429, 2924562, 2922709, 2922573, 2922140, 2922141 ], - \"parent\" : 2921506, - \"text\" : \"Aw shucks, guys ... you make me blush with your compliments.", - \"time\" : 1314211127, - \"type\" : \"comment\" - } - """ - - var data: Data { - return Data(self.rawValue.utf8) - } -} - -extension TestData { - // swiftlint:disable force_try - static var post: PostModel { - return try! JSONDecoder().decode(PostModel.self, from: TestData.storyJSON.data) - } - - static var comment: CommentModel { - return try! JSONDecoder().decode(CommentModel.self, from: TestData.commentJSON.data) - } - // swiftlint:enable force_try -} diff --git a/HackerNewsTests/Mocks/UnitTestError.swift b/HackerNewsTests/Mocks/UnitTestError.swift deleted file mode 100644 index c3108ed..0000000 --- a/HackerNewsTests/Mocks/UnitTestError.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// UnitTestError.swift -// HackerNewsTests -// -// Created by Никита Васильев on 07.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct UnitTestError: Error { } diff --git a/HackerNewsTests/PerformanceTests/WeakObjectSetPerformanceTests.swift b/HackerNewsTests/PerformanceTests/WeakObjectSetPerformanceTests.swift deleted file mode 100644 index b12e43f..0000000 --- a/HackerNewsTests/PerformanceTests/WeakObjectSetPerformanceTests.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// WeakObjectSetPerformanceTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest - -@testable import HackerNews - -final class WeakObjectSetPerformanceTests: XCTestCase { - func testPermormanceContainsElement() { - let set = fillSet(count: 100_000) - measure { _ = set.contains(56732 as AnyObject) } - } -} - -// MARK: Helpers -extension WeakObjectSetPerformanceTests { - func fillSet(count: Int) -> WeakObjectSet { - let set = WeakObjectSet() - - for i in 0.. Void)? = nil) { - presentedController = viewControllerToPresent - if let completion = completion { - completion() - } - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Comments/View/CommentsViewTests.swift b/HackerNewsTests/UnitTests/Modules/Comments/View/CommentsViewTests.swift deleted file mode 100644 index 1efe602..0000000 --- a/HackerNewsTests/UnitTests/Modules/Comments/View/CommentsViewTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// CommentsViewTests.swift -// HackerNews -// -// Created by Nikita Vasilev on 24/04/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Quick -import Nimble - -@testable import HackerNews - -class CommentsViewTests: QuickSpec { - override func spec() { - - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Main/Assembly/MainTabBarAssemblyTests.swift b/HackerNewsTests/UnitTests/Modules/Main/Assembly/MainTabBarAssemblyTests.swift deleted file mode 100644 index f501f34..0000000 --- a/HackerNewsTests/UnitTests/Modules/Main/Assembly/MainTabBarAssemblyTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// MainTabBarAssemblyTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble -import Swinject - -@testable import HackerNews - -class MainTabBarAssemblySpec: QuickSpec { - override func spec() { - let container = MockContainer().container - - let viewController = container.resolve(MainTabBarViewController.self) - let presenter = viewController?.output as? MainTabBarPresenter - - describe("Checking module creation") { - it("Must be AskViewController") { - expect(viewController).toNot(beNil()) - expect(viewController).to(beAKindOf(MainTabBarViewController.self)) - } - - it("Must contains correct output") { - expect(viewController?.output).toNot(beNil()) - expect(viewController?.output).to(beIdenticalTo(presenter)) - } - - it("Must contains view and theme manager") { - expect(presenter?.view).toNot(beNil()) - expect(presenter?.view).to(beIdenticalTo(viewController)) - expect(presenter?.themeManager).toNot(beNil()) - expect(presenter?.themeManager).to(beAKindOf(ThemeManager.self)) - } - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Main/Configurator/MainTabBarConfuguratorTests.swift b/HackerNewsTests/UnitTests/Modules/Main/Configurator/MainTabBarConfuguratorTests.swift deleted file mode 100644 index 856530e..0000000 --- a/HackerNewsTests/UnitTests/Modules/Main/Configurator/MainTabBarConfuguratorTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// MainTabBarConfuguratorTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble -import Swinject - -@testable import HackerNews - -final class MainTabBarConfuguratorSpec: QuickSpec { - override func spec() { - let parentAssembler = MockContainer().assembler - let viewController = MainTabBarConfigurator(parentAssembler: parentAssembler).configure() - - describe("Check module configuration") { - it("rootViewController should be equal to MainTabBarViewController") { - expect(viewController).notTo(beNil()) - expect(viewController).to(beAKindOf(MainTabBarViewController.self)) - } - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Main/Presenter/MainTabBarPresenterTests.swift b/HackerNewsTests/UnitTests/Modules/Main/Presenter/MainTabBarPresenterTests.swift deleted file mode 100644 index 8f92217..0000000 --- a/HackerNewsTests/UnitTests/Modules/Main/Presenter/MainTabBarPresenterTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// MainTabBarPresenterTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble - -@testable import HackerNews - -final class MainTabBarPresenterSpec: QuickSpec { - override func spec() { - let themeManager = MockThemeManager() - let view = MainTabBarViewMock() - let presenter = MainTabBarPresenter() - - presenter.view = view - presenter.themeManager = themeManager - - describe("Checking view is ready") { - it("setup view") { - presenter.viewIsReady() - expect(themeManager.addedObserver === presenter).to(equal(true)) - } - } - } -} - -// MARK: Mocks -extension MainTabBarPresenterSpec { - final class MainTabBarViewMock: UISplitViewController, MainTabBarViewInput { - var theme: Theme? - - func update(theme: Theme) { - self.theme = theme - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Main/View/MainTabBarViewTests.swift b/HackerNewsTests/UnitTests/Modules/Main/View/MainTabBarViewTests.swift deleted file mode 100644 index 6850e78..0000000 --- a/HackerNewsTests/UnitTests/Modules/Main/View/MainTabBarViewTests.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// MainTabBarViewTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble - -@testable import HackerNews - -class MainTabBarViewSpec: QuickSpec { - override func spec() { - let output = MainTabBarPresenterMock() - let view = MainTabBarViewController(theme: .light, output: output) - - describe("Checking view configuration") { - it("view initializes properties") { - view.viewDidLoad() - expect(output.viewIsReadyIsCalled).to(equal(true)) - expect(view.theme).to(equal(.light)) - } - } - } -} - -// MARK: Mocks -extension MainTabBarViewSpec { - final class MainTabBarPresenterMock: MainTabBarViewOutput { - var viewIsReadyIsCalled: Bool = false - - func viewIsReady() { - viewIsReadyIsCalled = true - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Root/Assembly/RootAssemblyTests.swift b/HackerNewsTests/UnitTests/Modules/Root/Assembly/RootAssemblyTests.swift deleted file mode 100644 index 548713d..0000000 --- a/HackerNewsTests/UnitTests/Modules/Root/Assembly/RootAssemblyTests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// RootAssemblyTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble -import Swinject - -@testable import HackerNews - -class RootAssemblySpec: QuickSpec { - override func spec() { - let container = MockContainer().container - - let mainTabBarViewController = MainTabBarConfigurator(parentAssembler: MockContainer().assembler).configure() - let viewController = container.resolve(RootSplitViewController.self, argument: mainTabBarViewController) - let presenter = viewController?.output as? RootPresenter - - describe("Checking module creation") { - it("Must be AskViewController") { - expect(viewController).toNot(beNil()) - expect(viewController).to(beAKindOf(RootSplitViewController.self)) - } - - it("Must contains correct output") { - expect(viewController?.output).toNot(beNil()) - expect(viewController?.output).to(beIdenticalTo(presenter)) - } - - it("Must contains view and theme manager") { - expect(presenter?.view).toNot(beNil()) - expect(presenter?.view).to(beIdenticalTo(viewController)) - expect(presenter?.themeManager).toNot(beNil()) - expect(presenter?.themeManager).to(beAKindOf(ThemeManager.self)) - } - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Root/Configurator/RootConfiguratorTests.swift b/HackerNewsTests/UnitTests/Modules/Root/Configurator/RootConfiguratorTests.swift deleted file mode 100644 index 76115af..0000000 --- a/HackerNewsTests/UnitTests/Modules/Root/Configurator/RootConfiguratorTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// RootConfiguratorTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble -import Swinject - -@testable import HackerNews - -class RootConfiguratorSpec: QuickSpec { - override func spec() { - let parentAssembler = MockContainer().assembler - let configurator = RootConfigurator(parentAssembler: parentAssembler) - let window = UIWindow() - - configurator.installIntoWindow(window) - - describe("Check module configuration") { - it("rootViewController should be equal to RootSplitViewController") { - expect(window.rootViewController).notTo(beNil()) - expect(window.rootViewController).to(beAKindOf(RootSplitViewController.self)) - } - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Root/Presenter/RootPresenterTests.swift b/HackerNewsTests/UnitTests/Modules/Root/Presenter/RootPresenterTests.swift deleted file mode 100644 index 3de5b9d..0000000 --- a/HackerNewsTests/UnitTests/Modules/Root/Presenter/RootPresenterTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// RootPresenterTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble - -@testable import HackerNews - -final class RootPresenterSpec: QuickSpec { - override func spec() { - let themeManager = MockThemeManager() - let view = MockRootViewMock() - let presenter = RootPresenter(view: view, themeManager: themeManager) - - describe("Checking view is ready") { - it("setup view") { - presenter.viewIsReady() - expect(view.theme).to(equal(themeManager.theme)) - expect(themeManager.addedObserver === presenter).to(equal(true)) - } - } - } -} - -// MARK: Mocks -extension RootPresenterSpec { - final class MockRootViewMock: UISplitViewController, RootViewInput { - var theme: Theme? - - func setupInitialState(theme: Theme) { - self.theme = theme - } - - func update(theme: Theme) { - self.theme = theme - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Root/View/RootViewTests.swift b/HackerNewsTests/UnitTests/Modules/Root/View/RootViewTests.swift deleted file mode 100644 index 4852419..0000000 --- a/HackerNewsTests/UnitTests/Modules/Root/View/RootViewTests.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// RootViewTests.swift -// HackerNewsTests -// -// Created by Никита Васильев on 06.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Quick -import Nimble - -@testable import HackerNews - -class RootViewSpec: QuickSpec { - override func spec() { - let view = RootSplitViewController() - let output = RootViewPresenterMock() - - view.output = output - - describe("Checking view configuration") { - it("view initializes properties") { - view.viewDidLoad() - expect(output.viewIsReadyIsCalled).to(equal(true)) - expect(view.preferredDisplayMode).to(equal(.allVisible)) - expect(view.delegate).toNot(beNil()) - expect(view.delegate === view).to(equal(true)) - expect(view.preferredStatusBarStyle).to(equal(.default)) - } - } - } -} - -// MARK: Mocks -extension RootViewSpec { - final class RootViewPresenterMock: RootViewOutput { - var viewIsReadyIsCalled: Bool = false - - func viewIsReady() { - viewIsReadyIsCalled = true - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Settings/Assembly/SettingsAssemblyTests.swift b/HackerNewsTests/UnitTests/Modules/Settings/Assembly/SettingsAssemblyTests.swift deleted file mode 100644 index 296d722..0000000 --- a/HackerNewsTests/UnitTests/Modules/Settings/Assembly/SettingsAssemblyTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// SettingsAssemblyTests.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Quick -import Nimble -import Swinject - -@testable import HackerNews - -class SettingsModuleAssemblyTests: QuickSpec { - override func spec() { - let container = MockContainer().container - - let viewController = container.resolve(SettingsViewController.self) - let presenter = viewController?.output as? SettingsPresenter - let router = presenter?.router as? SettingsRouter - let interactor = presenter?.interactor as? SettingsInteractor - - describe("Checking module creation") { - it("Must be SettingsViewController") { - expect(viewController).toNot(beNil()) - expect(viewController).to(beAKindOf(SettingsViewController.self)) - } - - it("Must contains correct output") { - expect(viewController?.output).toNot(beNil()) - expect(viewController?.output).to(beIdenticalTo(presenter)) - } - - it("Must contains view, interactor and router") { - expect(presenter?.view).toNot(beNil()) - expect(presenter?.view).to(beIdenticalTo(viewController)) - expect(presenter?.router).toNot(beNil()) - expect(presenter?.router).to(beIdenticalTo(router)) - expect(presenter?.interactor).toNot(beNil()) - expect(presenter?.interactor).to(beIdenticalTo(interactor)) - expect(presenter?.themeManager).toNot(beNil()) - expect(presenter?.themeManager).to(beAKindOf(ThemeManager.self)) - } - - it("Must contains output") { - expect(interactor?.output).toNot(beNil()) - expect(interactor?.output).to(beIdenticalTo(presenter)) - } - - it("Must contains correct transitionHandler") { - expect(router?.transitionHandler).toNot(beNil()) - expect(router?.transitionHandler).to(beIdenticalTo(viewController)) - expect(router?.themeConfigurator).toNot(beNil()) - expect(router?.themeConfigurator).to(beAKindOf(ThemeConfigurator.self)) - } - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Settings/Configurator/SettingsConfiguratorTests.swift b/HackerNewsTests/UnitTests/Modules/Settings/Configurator/SettingsConfiguratorTests.swift deleted file mode 100644 index 1a622dd..0000000 --- a/HackerNewsTests/UnitTests/Modules/Settings/Configurator/SettingsConfiguratorTests.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// SettingsConfiguratorTests.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Quick -import Nimble -import Swinject - -@testable import HackerNews - -class SettingsConfiguratorTests: QuickSpec { - // MARK: Tests - override func spec() { - let assembler = MockContainer().assembler - let configurator = SettingsConfigurator(parentAssembler: assembler) - let navigationController = configurator.configureViewController() as? UINavigationController - let viewController = navigationController?.viewControllers.first - - describe("Check module configuration") { - it("Module input shouldn't be nil") { - expect(viewController).notTo(beNil()) - expect(viewController).to(beAKindOf(SettingsViewController.self)) - } - } - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Settings/Interactor/SettingsInteractorTests.swift b/HackerNewsTests/UnitTests/Modules/Settings/Interactor/SettingsInteractorTests.swift deleted file mode 100644 index 88bcda7..0000000 --- a/HackerNewsTests/UnitTests/Modules/Settings/Interactor/SettingsInteractorTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// SettingsInteractorTests.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Quick -import Nimble - -@testable import HackerNews - -final class SettingsInteractorSpec: QuickSpec { - override func spec() { - - } -} - -// MARK: Mocks -extension SettingsInteractorSpec { - final class MockPresenter: SettingsInteractorOutput { - - } -} diff --git a/HackerNewsTests/UnitTests/Modules/Settings/Presenter/SettingsPresenterTests.swift b/HackerNewsTests/UnitTests/Modules/Settings/Presenter/SettingsPresenterTests.swift deleted file mode 100644 index f0731aa..0000000 --- a/HackerNewsTests/UnitTests/Modules/Settings/Presenter/SettingsPresenterTests.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// SettingsPresenterTests.swift -// HackerNews -// -// Created by Nikita Vasilev on 18/03/2020. -// Copyright © 2020 Nikita Vasilev. All rights reserved. -// - -import Quick -import Nimble - -@testable import HackerNews - -final class SettingsPresenterSpec: QuickSpec { - override func spec() { - var view: MockView! - var router: MockRouter! - var interactor: MockInteractor! - var presenter: SettingsPresenter! - var themeManager: MockThemeManager! - - beforeEach { - view = MockView() - presenter = SettingsPresenter() - router = MockRouter() - interactor = MockInteractor() - themeManager = MockThemeManager() - - presenter.view = view - presenter.interactor = interactor - presenter.router = router - presenter.themeManager = themeManager - - view.output = presenter - } - - describe("vies is ready") { - beforeEach { - presenter.viewIsReady() - } - - it("set navigation title") { - expect(view.navigationTitle).to(equal(Locale.title.localized())) - } - - it("set theme") { - expect(view.theme).to(equal(themeManager.theme)) - } - - it("subscribe to theme notification") { - expect(themeManager.addedObserver === presenter).to(beTrue()) - } - } - - describe("settings sections") { - it("count") { - expect(presenter.getNumberOfSections()).to(equal(2)) - } - - it("title should not be empty") { - for section in 0..() - - beforeEach { - objectSet.append(1 as AnyObject) - objectSet.append(2 as AnyObject) - } - - describe("empty set") { - it("should contains some objects") { - expect(objectSet.contains(1 as AnyObject)).to(equal(true)) - expect(objectSet.contains(2 as AnyObject)).to(equal(true)) - expect(objectSet.objects.count).to(equal(2)) - } - - it("remove objects") { - objectSet.remove(1 as AnyObject) - expect(objectSet.contains(1 as AnyObject)).to(equal(false)) - expect(objectSet.objects.count).to(equal(1)) - } - - it("appending sequence to set") { - objectSet.append(contentsOf: [3 as AnyObject]) - expect(objectSet.contains(3 as AnyObject)).to(equal(true)) - } - } - - describe("") { - let setWithObjects = WeakObjectSet(objects: [1 as AnyObject]) - - it("should contains some objects") { - expect(setWithObjects.contains(1 as AnyObject)).to(equal(true)) - expect(setWithObjects.objects.count).to(equal(1)) - } - } - } -} diff --git a/HackerNewsTodayExtension/Info.plist b/HackerNewsTodayExtension/Info.plist deleted file mode 100644 index 9794d85..0000000 --- a/HackerNewsTodayExtension/Info.plist +++ /dev/null @@ -1,31 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Hacker News - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSExtension - - NSExtensionMainStoryboard - MainInterface - NSExtensionPointIdentifier - com.apple.widget-extension - - - diff --git a/HackerNewsTodayExtension/View/Base.lproj/MainInterface.storyboard b/HackerNewsTodayExtension/View/Base.lproj/MainInterface.storyboard deleted file mode 100644 index a29cd1e..0000000 --- a/HackerNewsTodayExtension/View/Base.lproj/MainInterface.storyboard +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNewsTodayExtension/View/TopStoriesViewController.swift b/HackerNewsTodayExtension/View/TopStoriesViewController.swift deleted file mode 100644 index eb880b8..0000000 --- a/HackerNewsTodayExtension/View/TopStoriesViewController.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// TopStoriesViewController.swift -// HackerNewsTodayExtension -// -// Created by Никита Васильев on 17.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import NotificationCenter -import HNService - -final class TopStoriesViewController: UIViewController { - - // MARK: IBOutlets - @IBOutlet private var tableView: UITableView! - - // MARK: Private Properties - private var service = HNService() - private var posts: [PostModel] = [] - - override func viewDidLoad() { - super.viewDidLoad() - self.extensionContext?.widgetLargestAvailableDisplayMode = .expanded - confiure() - } - - // MARK: Private Methods - private func confiure() { - tableView.register(UINib(nibName: TopStoryTableViewCell.cellIdentifier, bundle: nil), - forCellReuseIdentifier: TopStoryTableViewCell.cellIdentifier) - tableView.isScrollEnabled = false - } - - private func openLink(from urlString: String) { - guard let url = URL(string: urlString) else { return } - extensionContext?.open(url, completionHandler: nil) - } -} - -// MARK: NCWidgetProviding -extension TopStoriesViewController: NCWidgetProviding { - func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) { - fetchTopStoriesIds(completionHandler: completionHandler) - } - - func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { - if activeDisplayMode == .compact { - self.preferredContentSize = maxSize - } else if activeDisplayMode == .expanded { - self.preferredContentSize = CGSize(width: maxSize.width, height: Metrics.expandedHeight) - } - } -} - -// MARK: Loading Data -extension TopStoriesViewController { - private func fetchTopStoriesIds(completionHandler: @escaping (NCUpdateResult) -> Void) { - service.fetchIds(for: .top, completion: { [weak self] ids in - self?.fetchTopStories(from: Array(ids[0.. Void) { - service.loadPosts(with: ids, completion: { [weak self] posts in - self?.posts = posts - self?.tableView.reloadData() - completionHandler(.newData) - }, fail: { _ in - completionHandler(.failed) - }) - } -} - -// MARK: UITableViewDelegate -extension TopStoriesViewController: UITableViewDelegate { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let url = posts[indexPath.row].url { - openLink(from: url) - } - - tableView.deselectRow(at: indexPath, animated: true) - } -} - -// MARK: UITableViewDataSource -extension TopStoriesViewController: UITableViewDataSource { - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: TopStoryTableViewCell.cellIdentifier, - for: indexPath) as? TopStoryTableViewCell else { return UITableViewCell() } - let post = posts[indexPath.row] - cell.setup(model: post) - return cell - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return posts.count - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return Metrics.cellHeight - } -} - -// MARK: Metrics -extension TopStoriesViewController { - private enum Metrics { - static let cellHeight: CGFloat = 56.0 - static let expandedHeight: CGFloat = 448.0 - static let defaultPostCount: Int = 8 - } -} diff --git a/HackerNewsTodayExtension/View/Views/RateView/RateView.swift b/HackerNewsTodayExtension/View/Views/RateView/RateView.swift deleted file mode 100644 index b7c0d66..0000000 --- a/HackerNewsTodayExtension/View/Views/RateView/RateView.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// RateView.swift -// HackerNewsTodayExtension -// -// Created by Никита Васильев on 17.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit - -final class RateView: UIView { - - // MARK: Public Properties - - /// A `String` value that contains the rate. - var rate: String = "" { - didSet { - rateLabel.text = rate - } - } - - /// An `UIColor` value that contains the circle color. - var circleColor: UIColor = .systemOrange { - didSet { - setNeedsDisplay() - } - } - - // MARK: Private Properties - private lazy var rateLabel: UILabel = { - let label = UILabel() - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.5 - label.font = label.font.withSize(13) - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - // MARK: Initialization - override init(frame: CGRect) { - super.init(frame: frame) - configure() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - configure() - } - - // MARK: Override - override func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { return } - - context.setLineWidth(1.0) - circleColor.set() - - let center = CGPoint(x: rect.midX, y: rect.midY) - let radius = (frame.size.width - 10) / 2.0 - - context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true) - - context.strokePath() - } - - // MARK: Private Methods - private func configure() { - backgroundColor = .clear - - addSubview(rateLabel) - - NSLayoutConstraint.activate([ - rateLabel.centerXAnchor.constraint(equalTo: centerXAnchor), - rateLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - rateLabel.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 8.0), - trailingAnchor.constraint(greaterThanOrEqualTo: rateLabel.trailingAnchor, constant: 8.0) - ]) - } -} diff --git a/HackerNewsTodayExtension/View/Views/TopStoryTableViewCell/TopStoryTableViewCell.swift b/HackerNewsTodayExtension/View/Views/TopStoryTableViewCell/TopStoryTableViewCell.swift deleted file mode 100644 index 317fcca..0000000 --- a/HackerNewsTodayExtension/View/Views/TopStoryTableViewCell/TopStoryTableViewCell.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// TodayTableViewCell.swift -// HackerNewsTodayExtension -// -// Created by Никита Васильев on 17.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import UIKit -import struct HNService.PostModel - -final class TopStoryTableViewCell: UITableViewCell { - - // MARK: IBOutlets - @IBOutlet private var titleLabel: UILabel! - @IBOutlet private var urlLabel: UILabel! - @IBOutlet private var rateView: RateView! - - // MARK: Public Methods - func setup(model: PostModel) { - titleLabel.text = model.title - urlLabel.text = getHost(from: model.url) - rateView.rate = "\(model.score ?? 0)" - } -} - -extension TopStoryTableViewCell { - private func getHost(from urlString: String?) -> String? { - guard let urlString = urlString else { return nil } - return URL(string: urlString)?.host - } -} - -// MARK: Cell Identifier -extension TopStoryTableViewCell { - static var cellIdentifier: String { - return "TopStoryTableViewCell" - } -} diff --git a/HackerNewsTodayExtension/View/Views/TopStoryTableViewCell/TopStoryTableViewCell.xib b/HackerNewsTodayExtension/View/Views/TopStoryTableViewCell/TopStoryTableViewCell.xib deleted file mode 100644 index ae433ef..0000000 --- a/HackerNewsTodayExtension/View/Views/TopStoryTableViewCell/TopStoryTableViewCell.xib +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/HackerNewsUITests/HackerNewsUITests.swift b/HackerNewsUITests/HackerNewsUITests.swift deleted file mode 100644 index 0477970..0000000 --- a/HackerNewsUITests/HackerNewsUITests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// HackerNewsUITests.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 24/08/2018. -// Copyright © 2018 Никита Васильев. All rights reserved. -// - -import XCTest - -class HackerNewsUITests: XCTestCase { - - override func setUp() { - super.setUp() - continueAfterFailure = false - App.shared.launch() - } - - override func tearDown() { - super.tearDown() - } - - func testStoriesScreenLoad() { - waitForExistence(StoriesScreen.navigationBar) - XCTAssertTrue(StoriesScreen.navigationBar.exists) - } -} diff --git a/HackerNewsUITests/Helpers/App.swift b/HackerNewsUITests/Helpers/App.swift deleted file mode 100644 index 12b4e8a..0000000 --- a/HackerNewsUITests/Helpers/App.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// App.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 09.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest - -final class App: XCUIApplication { - public static let shared = App() -} diff --git a/HackerNewsUITests/Helpers/UITest+Helprers.swift b/HackerNewsUITests/Helpers/UITest+Helprers.swift deleted file mode 100644 index d682823..0000000 --- a/HackerNewsUITests/Helpers/UITest+Helprers.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// UITest+Helprers.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 09.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest - -private let timeout: TimeInterval = 10.0 - -func waitForExistence(_ element: XCUIElement, file: StaticString = #file, line: UInt = #line) { - if !element.waitForExistence(timeout: timeout) { - XCTFail("Timed out waiting for element: \(element) to appear", file: file, line: line) - } -} diff --git a/HackerNewsUITests/Info.plist b/HackerNewsUITests/Info.plist deleted file mode 100644 index 6c40a6c..0000000 --- a/HackerNewsUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/HackerNewsUITests/SettingsUITests.swift b/HackerNewsUITests/SettingsUITests.swift deleted file mode 100644 index 84172bf..0000000 --- a/HackerNewsUITests/SettingsUITests.swift +++ /dev/null @@ -1,93 +0,0 @@ -// -// SettingsUITests.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 09.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import XCTest - -final class SettingsUITests: XCTestCase { - override func setUp() { - super.setUp() - continueAfterFailure = false - App.shared.launch() - } - - override func tearDown() { - super.tearDown() - } - - func testThatCellExistence() { - let settingsButton = SettingScreen.tabBarButton - settingsButton.tap() - - let firstCell = SettingScreen.themeCell - let existencePredicate = NSPredicate(format: "exists == 1") - let expectationEval = expectation(for: existencePredicate, evaluatedWith: firstCell, handler: nil) - let mobWaiter = XCTWaiter.wait(for: [expectationEval], timeout: 10.0) - XCTAssert(XCTWaiter.Result.completed == mobWaiter, "Test Case Failed.") - } - - func testThatThemeCellSelection() { - let settingsButton = SettingScreen.tabBarButton - settingsButton.tap() - - let firstCell = SettingScreen.themeCell - let predicate = NSPredicate(format: "isHittable == true") - let expectationEval = expectation(for: predicate, evaluatedWith: firstCell, handler: nil) - let waiter = XCTWaiter.wait(for: [expectationEval], timeout: 10.0) - XCTAssert(XCTWaiter.Result.completed == waiter, "Test Case Failed.") - firstCell.tap() - } - - func testThatTitleLabelAndInfoLabelExistanceOnCell() { - let settingsButton = SettingScreen.tabBarButton - settingsButton.tap() - - let firstCell = SettingScreen.themeCell - - XCTAssertTrue(firstCell.exists, "Table view cell not exist.") - - let titleLabel = firstCell.staticTexts["titleLabel"] - XCTAssertTrue(titleLabel.exists, "Title label not exist.") - - let infoLabel = firstCell.staticTexts["infoLabel"] - XCTAssertTrue(infoLabel.exists, "Info label not exist.") - } - - func testThatThemeImageViewExistance() { - let settingsButton = SettingScreen.tabBarButton - settingsButton.tap() - - let themeImageView = SettingScreen.themeImageView - XCTAssertTrue(themeImageView.exists, "Image view not exist.") - } - - func testThatThemeChangedToLight() { - let settingsButton = SettingScreen.tabBarButton - settingsButton.tap() - - let themeCell = SettingScreen.themeCell - themeCell.tap() - - let darkCell = ThemesScreen.lightThemeCell - darkCell.tap() - - XCTAssertEqual(themeCell.staticTexts["infoLabel"].label, "Light") - } - - func testThatThemeChangedToDark() { - let settingsButton = SettingScreen.tabBarButton - settingsButton.tap() - - let themeCell = SettingScreen.themeCell - themeCell.tap() - - let darkCell = ThemesScreen.darkThemeCell - darkCell.tap() - - XCTAssertEqual(themeCell.staticTexts["infoLabel"].label, "Dark") - } -} diff --git a/HackerNewsUITests/UITestFactory/Screens/SettingScreen.swift b/HackerNewsUITests/UITestFactory/Screens/SettingScreen.swift deleted file mode 100644 index 6bf72ca..0000000 --- a/HackerNewsUITests/UITestFactory/Screens/SettingScreen.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// SettingScreen.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 09.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct SettingScreen { - static let tabBarButton = UITestFactory.app.tabBars.buttons["settingsTabBarItem"] - static let tableView = UITestFactory.app.tables["settingsTableView"] - static let themeCell = SettingScreen.tableView.cells.element(matching: .cell, identifier: "sTCV_0_0") - static let themeImageView = UITestFactory.app.otherElements.containing(.image, identifier: "ThemeIcon").firstMatch -} diff --git a/HackerNewsUITests/UITestFactory/Screens/StoryScreen.swift b/HackerNewsUITests/UITestFactory/Screens/StoryScreen.swift deleted file mode 100644 index 6f219c2..0000000 --- a/HackerNewsUITests/UITestFactory/Screens/StoryScreen.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// StoryScreen.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 10.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct StoriesScreen { - static let navigationBar = UITestFactory.app.navigationBars["storiesNavigationBar"] -} diff --git a/HackerNewsUITests/UITestFactory/Screens/ThemesScreen.swift b/HackerNewsUITests/UITestFactory/Screens/ThemesScreen.swift deleted file mode 100644 index 787e825..0000000 --- a/HackerNewsUITests/UITestFactory/Screens/ThemesScreen.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// ThemesScreen.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 09.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -struct ThemesScreen { - static let tableView = UITestFactory.app.tables["themesTableView"] - static let lightThemeCell = ThemesScreen.tableView.cells.element(matching: .cell, identifier: "themeCell_0_0") - static let darkThemeCell = ThemesScreen.tableView.cells.element(matching: .cell, identifier: "themeCell_0_1") -} diff --git a/HackerNewsUITests/UITestFactory/UITestFactory.swift b/HackerNewsUITests/UITestFactory/UITestFactory.swift deleted file mode 100644 index 7da1c6d..0000000 --- a/HackerNewsUITests/UITestFactory/UITestFactory.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// UITestfactory.swift -// HackerNewsUITests -// -// Created by Никита Васильев on 09.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation -import XCTest - -struct UITestFactory { - static let app = XCUIApplication() -} diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png deleted file mode 100644 index d9a3e83..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/100.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/1024.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/1024.png deleted file mode 100644 index 2480889..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/1024.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/172.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/172.png deleted file mode 100644 index 6ed5f46..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/172.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/196.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/196.png deleted file mode 100644 index ced8634..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/196.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/216.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/216.png deleted file mode 100644 index 8b8da40..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/216.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/48.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/48.png deleted file mode 100644 index f65b4d7..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/48.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/55.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/55.png deleted file mode 100644 index 3179249..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/55.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/58.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index 0d85dea..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/58.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/80.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index 71556bc..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/80.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/87.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/87.png deleted file mode 100644 index a2e96da..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/87.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/88.png b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/88.png deleted file mode 100644 index f0bae13..0000000 Binary files a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/88.png and /dev/null differ diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json b/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 57bed75..0000000 --- a/HackerNewsWatch WatchKit App/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "images" : [ - { - "filename" : "48.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "24x24", - "subtype" : "38mm" - }, - { - "filename" : "55.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "27.5x27.5", - "subtype" : "42mm" - }, - { - "filename" : "58.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "87.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "80.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "40x40", - "subtype" : "38mm" - }, - { - "filename" : "88.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "44x44", - "subtype" : "40mm" - }, - { - "filename" : "100.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "50x50", - "subtype" : "44mm" - }, - { - "filename" : "172.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "86x86", - "subtype" : "38mm" - }, - { - "filename" : "196.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "98x98", - "subtype" : "42mm" - }, - { - "filename" : "216.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "108x108", - "subtype" : "44mm" - }, - { - "filename" : "1024.png", - "idiom" : "watch-marketing", - "scale" : "1x", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit App/Base.lproj/Interface.storyboard b/HackerNewsWatch WatchKit App/Base.lproj/Interface.storyboard deleted file mode 100644 index c59dd48..0000000 --- a/HackerNewsWatch WatchKit App/Base.lproj/Interface.storyboard +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
-
- -
-
-
diff --git a/HackerNewsWatch WatchKit App/Info.plist b/HackerNewsWatch WatchKit App/Info.plist deleted file mode 100644 index 524d690..0000000 --- a/HackerNewsWatch WatchKit App/Info.plist +++ /dev/null @@ -1,31 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Hacker News - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - - WKWatchKitApp - - - diff --git a/HackerNewsWatch WatchKit Extension/.swiftlint.yml b/HackerNewsWatch WatchKit Extension/.swiftlint.yml deleted file mode 100644 index 92b6207..0000000 --- a/HackerNewsWatch WatchKit Extension/.swiftlint.yml +++ /dev/null @@ -1,45 +0,0 @@ -opt_in_rules: - - private_outlet - - force_unwrapping - - strong_iboutlet - - private_action - - contains_over_first_not_nil - - discouraged_object_literal - - empty_count - - empty_string - - identical_operands - - - unneeded_parentheses_in_closure_argument - - let_var_whitespace - - yoda_condition - - closure_spacing - - collection_alignment - - operator_usage_whitespace - - closure_end_indentation - - file_name_no_space - - unowned_variable_capture - - contains_over_filter_count - - contains_over_filter_is_empty - - contains_over_range_nil_comparison - - empty_collection_literal -disabled_rules: - - trailing_whitespace - - type_name - - force_cast - -# Rule Config -line_length: 150 -identifier_name: - min_length: 1 - max_length: - warning: 40 - error: 60 -type_name: - min_length: 3 - max_length: - warning: 80 - error: 100 - -excluded: - - Pods - - R.generated.swift diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json deleted file mode 100644 index df73a6b..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "assets" : [ - { - "filename" : "Circular.imageset", - "idiom" : "watch", - "role" : "circular" - }, - { - "filename" : "Extra Large.imageset", - "idiom" : "watch", - "role" : "extra-large" - }, - { - "filename" : "Graphic Bezel.imageset", - "idiom" : "watch", - "role" : "graphic-bezel" - }, - { - "filename" : "Graphic Circular.imageset", - "idiom" : "watch", - "role" : "graphic-circular" - }, - { - "filename" : "Graphic Corner.imageset", - "idiom" : "watch", - "role" : "graphic-corner" - }, - { - "filename" : "Graphic Large Rectangular.imageset", - "idiom" : "watch", - "role" : "graphic-large-rectangular" - }, - { - "filename" : "Modular.imageset", - "idiom" : "watch", - "role" : "modular" - }, - { - "filename" : "Utilitarian.imageset", - "idiom" : "watch", - "role" : "utilitarian" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json b/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json deleted file mode 100644 index ed7de25..0000000 --- a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "images" : [ - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : "<=145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">161" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">145" - }, - { - "idiom" : "watch", - "scale" : "2x", - "screen-width" : ">183" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/HackerNewsWatch WatchKit Extension/Common/Cells/CommentCell.swift b/HackerNewsWatch WatchKit Extension/Common/Cells/CommentCell.swift deleted file mode 100644 index 41a078f..0000000 --- a/HackerNewsWatch WatchKit Extension/Common/Cells/CommentCell.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// CommentCell.swift -// HackerNewsWatch WatchKit Extension -// -// Created by Никита Васильев on 21.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import WatchKit - -final class CommentCell: NSObject { - - // MARK: IBOutlets - @IBOutlet private var usernameLabel: WKInterfaceLabel! - @IBOutlet private var commentLabel: WKInterfaceLabel! - - // MARK: Public Methods - func setup(username: String?, text: String) { - usernameLabel.setText(username) - commentLabel.setText(text) - } -} diff --git a/HackerNewsWatch WatchKit Extension/Common/Cells/PostCell.swift b/HackerNewsWatch WatchKit Extension/Common/Cells/PostCell.swift deleted file mode 100644 index a4b1ec2..0000000 --- a/HackerNewsWatch WatchKit Extension/Common/Cells/PostCell.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PostCell.swift -// HackerNewsWatch WatchKit Extension -// -// Created by Никита Васильев on 20.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import WatchKit -import struct HNService.PostModel - -final class PostCell: NSObject { - - // MARK: IBOutlets - @IBOutlet private var titleLabel: WKInterfaceLabel! - @IBOutlet private var domainLabel: WKInterfaceLabel! - @IBOutlet private var rateLabel: WKInterfaceLabel! - - // MARK: Public Methods - func setup(post: PostModel) { - titleLabel.setText(post.title) - domainLabel.setText(getHost(from: post.url)) - rateLabel.setText("\(post.score ?? 0)") - } -} - -extension PostCell { - private func getHost(from urlString: String?) -> String? { - guard let urlString = urlString else { return nil } - return URL(string: urlString)?.host - } -} diff --git a/HackerNewsWatch WatchKit Extension/Common/Extension/String+Decode.swift b/HackerNewsWatch WatchKit Extension/Common/Extension/String+Decode.swift deleted file mode 100644 index 13a1555..0000000 --- a/HackerNewsWatch WatchKit Extension/Common/Extension/String+Decode.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// String+Decode.swift -// HackerNewsWatch WatchKit Extension -// -// Created by Никита Васильев on 21.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -extension String { - var htmlDecoded: String { - let decoded = try? NSAttributedString(data: Data(utf8), options: [ - .documentType: NSAttributedString.DocumentType.html, - .characterEncoding: String.Encoding.utf8.rawValue - ], documentAttributes: nil).string - - return decoded?.trimmingCharacters(in: .whitespacesAndNewlines) ?? self - } -} diff --git a/HackerNewsWatch WatchKit Extension/Common/Extension/String+Localized.swift b/HackerNewsWatch WatchKit Extension/Common/Extension/String+Localized.swift deleted file mode 100644 index abeb453..0000000 --- a/HackerNewsWatch WatchKit Extension/Common/Extension/String+Localized.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// String+Localized.swift -// HackerNewsWatch WatchKit Extension -// -// Created by Никита Васильев on 21.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import Foundation - -extension String { - func localized() -> String { - return NSLocalizedString(self, comment: "") - } - - func localizedFormat(arguments: CVarArg...) -> String { - return String(format: localized(), arguments: arguments) - } -} diff --git a/HackerNewsWatch WatchKit Extension/Controllers/CommentsInterfaceController.swift b/HackerNewsWatch WatchKit Extension/Controllers/CommentsInterfaceController.swift deleted file mode 100644 index f1b39d8..0000000 --- a/HackerNewsWatch WatchKit Extension/Controllers/CommentsInterfaceController.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// CommentsInterfaceController.swift -// HackerNewsWatch WatchKit Extension -// -// Created by Никита Васильев on 21.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import WatchKit -import HNService - -final class CommentsInterfaceController: WKInterfaceController { - - // MARK: IBOutlets - @IBOutlet private var loadingLabel: WKInterfaceLabel! - @IBOutlet private var tableView: WKInterfaceTable! - - // MARK: Private Properties - private let service = HNService() - private var ids: [Int] = [] - private var comments: [CommentModel] = [] - private var commentTexts = [String]() - private var loadingCommentId: Int? - - // MARK: Life Cycle - override func awake(withContext context: Any?) { - super.awake(withContext: context) - setTitle(Locale.title.localized()) - ids = context as! [Int] - tableView.setHidden(true) - if ids.isEmpty { - loadingLabel.setText(Locale.noComments.localized()) - } else { - fetchComments() - loadingLabel.setText(Locale.loading.localized()) - } - - } - - // MARK: Private Methods - private func fetchComments() { - guard let id = ids.last, loadingCommentId == nil else { - loadingLabel.setHidden(true) - tableView.setHidden(false) - return - } - ids.removeLast() - self.loadingCommentId = id - self.fetchComment(for: id) - } - - private func prepareComments(from comment: CommentModel) { - if comment.deleted { - loadingCommentId = nil - fetchComments() - return - } - - var commentObject = comment - commentObject.level = 0 - - var sortedComments = [comment] - - self.getComments(from: (commentObject.comments, 1), to: &sortedComments) - - var row = self.comments.count - - sortedComments.forEach { model in - if model.deleted { - return - } - - if let text = model.text { - self.commentTexts.append(text.htmlDecoded) - } - - self.comments.append(model) - - row += 1 - } - - self.loadingCommentId = nil - - self.tableView.setNumberOfRows(comments.count, withRowType: "CommentCell") - - for (index, comment) in comments.enumerated() { - let controller = tableView.rowController(at: index) as! CommentCell - controller.setup(username: comment.by, text: commentTexts[index]) - } - - fetchComments() - } - - private func getComments(from tuple: (graph: [CommentModel], level: Int), to comments: inout [CommentModel]) { - for var comment in tuple.graph { - comment.level = tuple.level - comments.append(comment) - getComments(from: (comment.comments, tuple.level + 1), to: &comments) - } - } - - private func fetchComment(for id: Int) { - service.loadComments(with: id, completion: { [weak self] model in - self?.prepareComments(from: model) - }, fail: { [weak self] error in - self?.loadingLabel.setText(error.localizedDescription) - }) - } -} - -// MARK: Locale -extension CommentsInterfaceController { - private enum Locale { - static let title: String = "Comments" - static let loading: String = "Loading..." - static let noComments: String = "No comments" - } -} diff --git a/HackerNewsWatch WatchKit Extension/Controllers/TopStoriesInterfaceController.swift b/HackerNewsWatch WatchKit Extension/Controllers/TopStoriesInterfaceController.swift deleted file mode 100644 index 065a9e7..0000000 --- a/HackerNewsWatch WatchKit Extension/Controllers/TopStoriesInterfaceController.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// TopStoriesInterfaceController.swift -// HackerNewsWatch WatchKit Extension -// -// Created by Никита Васильев on 20.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import WatchKit -import HNService - -final class TopStoriesInterfaceController: WKInterfaceController { - - // MARK: IBOutlets - @IBOutlet private var loadingLabel: WKInterfaceLabel! - @IBOutlet private var tableView: WKInterfaceTable! - - // MARK: Private Properties - private let service = HNService() - private var posts: [PostModel] = [] - - // MARK: Life Cycle - override func awake(withContext context: Any?) { - super.awake(withContext: context) - setTitle(Locale.title.localized()) - loadingLabel.setText(Locale.loading.localized()) - fetchIds() - } - - override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) { - self.pushController(withName: "CommentsInterfaceController", context: posts[rowIndex].kids) - } - - // MARK: Private Methods - private func fetchIds() { - service.fetchIds(for: .top, completion: { [weak self] ids in - self?.fetchPosts(with: ids) - }, fail: { [weak self] error in - self?.loadingLabel.setText(error.localizedDescription) - }) - } - - private func fetchPosts(with ids: [Int]) { - service.loadPosts(with: ids, completion: { [weak self] posts in - self?.posts = posts - self?.configureTableView(posts) - self?.loadingLabel.setHidden(true) - }, fail: { [weak self] error in - self?.loadingLabel.setText(error.localizedDescription) - }) - } - - private func configureTableView(_ posts: [PostModel]) { - tableView.setNumberOfRows(posts.count, withRowType: "PostCell") - - for (index, post) in posts.enumerated() { - let controller = tableView.rowController(at: index) as! PostCell - controller.setup(post: post) - } - } -} - -// MARK: Locale -extension TopStoriesInterfaceController { - private enum Locale { - static let title: String = "Top Stories" - static let loading: String = "Loading..." - } -} diff --git a/HackerNewsWatch WatchKit Extension/ExtensionDelegate.swift b/HackerNewsWatch WatchKit Extension/ExtensionDelegate.swift deleted file mode 100644 index 6fb4553..0000000 --- a/HackerNewsWatch WatchKit Extension/ExtensionDelegate.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// ExtensionDelegate.swift -// HackerNewsWatch WatchKit Extension -// -// Created by Никита Васильев on 19.06.2020. -// Copyright © 2020 Никита Васильев. All rights reserved. -// - -import WatchKit - -class ExtensionDelegate: NSObject, WKExtensionDelegate { - - func applicationDidFinishLaunching() { - // Perform any final initialization of your application. - } - - func applicationDidBecomeActive() { - - } - - func applicationWillResignActive() { - - } - - func handle(_ backgroundTasks: Set) { - for task in backgroundTasks { - // Use a switch statement to check the task type - switch task { - case let backgroundTask as WKApplicationRefreshBackgroundTask: - // Be sure to complete the background task once you’re done. - backgroundTask.setTaskCompletedWithSnapshot(false) - case let snapshotTask as WKSnapshotRefreshBackgroundTask: - // Snapshot tasks have a unique completion call, make sure to set your expiration date - snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil) - case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask: - // Be sure to complete the connectivity task once you’re done. - connectivityTask.setTaskCompletedWithSnapshot(false) - case let urlSessionTask as WKURLSessionRefreshBackgroundTask: - // Be sure to complete the URL session task once you’re done. - urlSessionTask.setTaskCompletedWithSnapshot(false) - case let relevantShortcutTask as WKRelevantShortcutRefreshBackgroundTask: - // Be sure to complete the relevant-shortcut task once you're done. - relevantShortcutTask.setTaskCompletedWithSnapshot(false) - case let intentDidRunTask as WKIntentDidRunRefreshBackgroundTask: - // Be sure to complete the intent-did-run task once you're done. - intentDidRunTask.setTaskCompletedWithSnapshot(false) - default: - // make sure to complete unhandled task types - task.setTaskCompletedWithSnapshot(false) - } - } - } - -} diff --git a/HackerNewsWatch WatchKit Extension/Info.plist b/HackerNewsWatch WatchKit Extension/Info.plist deleted file mode 100644 index d4a5fc7..0000000 --- a/HackerNewsWatch WatchKit Extension/Info.plist +++ /dev/null @@ -1,38 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - HackerNews - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - NSExtension - - NSExtensionAttributes - - WKAppBundleIdentifier - com.nikitavasilev.HackerNewsWatch.watchkitapp - - NSExtensionPointIdentifier - com.apple.watchkit - - WKExtensionDelegateClassName - $(PRODUCT_MODULE_NAME).ExtensionDelegate - WKWatchOnly - - - diff --git a/HackerNewsWatch WatchKit Extension/Resources/Strings/en.lproj/Localizable.strings b/HackerNewsWatch WatchKit Extension/Resources/Strings/en.lproj/Localizable.strings deleted file mode 100644 index 76d28f8..0000000 --- a/HackerNewsWatch WatchKit Extension/Resources/Strings/en.lproj/Localizable.strings +++ /dev/null @@ -1,4 +0,0 @@ -"Comments" = "Comments"; -"Loading..." = "Loading..."; -"No comments" = "No comments"; -"Top Stories" = "Top Stories"; diff --git a/HackerNewsWatch WatchKit Extension/Resources/Strings/ru.lproj/Localizable.strings b/HackerNewsWatch WatchKit Extension/Resources/Strings/ru.lproj/Localizable.strings deleted file mode 100644 index c7c4bac..0000000 --- a/HackerNewsWatch WatchKit Extension/Resources/Strings/ru.lproj/Localizable.strings +++ /dev/null @@ -1,4 +0,0 @@ -"Comments" = "Комментарии"; -"Loading..." = "Загрузка..."; -"No comments" = "Нет комментариев"; -"Top Stories" = "Топ историй"; diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f56a3f6..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Nikita Vasilev - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c1cd606 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +CHILD_MAKEFILES_DIRS = $(sort $(dir $(wildcard Modules/*/*/Makefile))) + +all: bootstrap swiftgen + +bootstrap: hook scripts + mint bootstrap + +hook: + ln -sf ../../hooks/pre-commit .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + +mint: + mint bootstrap + +lint: + mint run swiftlint + +fmt: + mint run swiftformat Sources Tests + +setup_build_tools: + sh scripts/setup_build_tools.sh + +swiftgen: + @for d in $(CHILD_MAKEFILES_DIRS); do ( cd $$d && make swiftgen; ); done + +.PHONY: all bootstrap hook mint lint fmt setup_build_tools \ No newline at end of file diff --git a/Mintfile b/Mintfile new file mode 100644 index 0000000..3c6da00 --- /dev/null +++ b/Mintfile @@ -0,0 +1,2 @@ +nicklockwood/SwiftFormat@0.54.0 +realm/SwiftLint@0.55.1 \ No newline at end of file diff --git a/Modules/Common/AppUtils/.gitignore b/Modules/Common/AppUtils/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Modules/Common/AppUtils/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/Common/AppUtils/Package.swift b/Modules/Common/AppUtils/Package.swift new file mode 100644 index 0000000..db5499b --- /dev/null +++ b/Modules/Common/AppUtils/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable prefixed_toplevel_constant + +import PackageDescription + +let package = Package( + name: "AppUtils", + products: [ + .library(name: "AppUtils", targets: ["AppUtils"]), + ], + dependencies: [ + .package(url: "https://github.com/AliSoftware/Dip.git", .upToNextMajor(from: "7.1.1")), + ], + targets: [ + .target(name: "AppUtils", dependencies: [ + .product(name: "Dip", package: "Dip"), + ]), + .testTarget(name: "AppUtilsTests", dependencies: ["AppUtils"]), + ] +) diff --git a/Modules/Common/AppUtils/README.md b/Modules/Common/AppUtils/README.md new file mode 100644 index 0000000..c7814ff --- /dev/null +++ b/Modules/Common/AppUtils/README.md @@ -0,0 +1,3 @@ +# AppUtils + +A description of this package. diff --git a/Modules/Common/AppUtils/Sources/AppUtils/Classes/Core/IDateFormatter.swift b/Modules/Common/AppUtils/Sources/AppUtils/Classes/Core/IDateFormatter.swift new file mode 100644 index 0000000..f689cdc --- /dev/null +++ b/Modules/Common/AppUtils/Sources/AppUtils/Classes/Core/IDateFormatter.swift @@ -0,0 +1,16 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +// MARK: - IDateFormatter + +public protocol IDateFormatter { + func string(from date: Date) -> String +} + +// MARK: - DateFormatter + IDateFormatter + +extension DateFormatter: IDateFormatter {} diff --git a/Modules/Common/AppUtils/Sources/AppUtils/Classes/DI/AppAssembly.swift b/Modules/Common/AppUtils/Sources/AppUtils/Classes/DI/AppAssembly.swift new file mode 100644 index 0000000..6afb46e --- /dev/null +++ b/Modules/Common/AppUtils/Sources/AppUtils/Classes/DI/AppAssembly.swift @@ -0,0 +1,44 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Dip +import Foundation + +open class AppAssembly { + // MARK: Properties + + private static let container = Locator.shared + + // MARK: Initialization + + public init() { + Dip.logLevel = .None + } + + // MARK: Methods + + public func resolve( + _ type: T.Type = T.self, + scope: ComponentScope = .shared, + tag: DependencyTagConvertible? = nil, + factory: @escaping () -> T + ) -> T { + if let object = try? Locator.shared.container.resolve(type, tag: tag) as? T { return object } + + Locator.shared.container.register(scope, type: type, tag: tag) { _ in factory() } + + return resolve(tag: tag) + } + + // MARK: Private + + private func resolve(tag: DependencyTagConvertible?) -> T { + do { + return try Locator.shared.container.resolve(tag: tag) as T + } catch { + fatalError("[AppAssembly] The instance with type \(String(describing: T.self)) wasn't found in the container.") + } + } +} diff --git a/Modules/Common/AppUtils/Sources/AppUtils/Classes/DI/Locator.swift b/Modules/Common/AppUtils/Sources/AppUtils/Classes/DI/Locator.swift new file mode 100644 index 0000000..185ce05 --- /dev/null +++ b/Modules/Common/AppUtils/Sources/AppUtils/Classes/DI/Locator.swift @@ -0,0 +1,39 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Dip +import Foundation + +// MARK: - IDefinition + +public protocol IDefinition { + func implements
(type a: A.Type) -> IDefinition +} + +// MARK: - Locator + +public final class Locator { + let container = DependencyContainer() + + public static let shared = Locator() + + public func resolve() -> T { + do { + return try container.resolve() + } catch { + fatalError("Object with type \(String(describing: T.self)) must be register in the container") + } + } + + public func register( + _ scope: ComponentScope = .shared, + type: T.Type = T.self, + tag: DependencyTagConvertible? = nil, + types _: [Any.Type] = [], + factory: @escaping (()) throws -> T + ) { + container.register(scope, type: type, tag: tag, factory: factory) + } +} diff --git a/Modules/Common/AppUtils/Sources/AppUtils/Classes/Helpers/DateFormatter+.swift b/Modules/Common/AppUtils/Sources/AppUtils/Classes/Helpers/DateFormatter+.swift new file mode 100644 index 0000000..c813712 --- /dev/null +++ b/Modules/Common/AppUtils/Sources/AppUtils/Classes/Helpers/DateFormatter+.swift @@ -0,0 +1,14 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +public extension DateFormatter { + static var EEEEMMMd: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE, MMM d" + return formatter + }() +} diff --git a/Modules/Common/AppUtils/Tests/AppUtilsTests/AppUtilsTests.swift b/Modules/Common/AppUtils/Tests/AppUtilsTests/AppUtilsTests.swift new file mode 100644 index 0000000..bac746b --- /dev/null +++ b/Modules/Common/AppUtils/Tests/AppUtilsTests/AppUtilsTests.swift @@ -0,0 +1,9 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +@testable import AppUtils +import XCTest + +final class AppUtilsTests: XCTestCase {} diff --git a/Modules/Common/DesignKit/.gitignore b/Modules/Common/DesignKit/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Modules/Common/DesignKit/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/Common/DesignKit/Makefile b/Modules/Common/DesignKit/Makefile new file mode 100644 index 0000000..76c5b88 --- /dev/null +++ b/Modules/Common/DesignKit/Makefile @@ -0,0 +1,3 @@ +## swiftgen: Trigger code generation from assets with swiftgen tool +swiftgen: + swiftgen \ No newline at end of file diff --git a/Modules/Common/DesignKit/Package.swift b/Modules/Common/DesignKit/Package.swift new file mode 100644 index 0000000..48d5a91 --- /dev/null +++ b/Modules/Common/DesignKit/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable all + +import PackageDescription + +let package = Package( + name: "DesignKit", + platforms: [.iOS(.v16)], + products: [ + .library(name: "DesignKit", targets: ["DesignKit"]), + ], + dependencies: [], + targets: [ + .target( + name: "DesignKit", + dependencies: [], + resources: [ + .process("Resources"), + ] + ), + .testTarget(name: "DesignKitTests", dependencies: ["DesignKit"]), + ] +) diff --git a/Modules/Common/DesignKit/README.md b/Modules/Common/DesignKit/README.md new file mode 100644 index 0000000..58780b9 --- /dev/null +++ b/Modules/Common/DesignKit/README.md @@ -0,0 +1,3 @@ +# DesignKit + +A description of this package. diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Classes/Design/Core/CGFloat+.swift b/Modules/Common/DesignKit/Sources/DesignKit/Classes/Design/Core/CGFloat+.swift new file mode 100644 index 0000000..e6c908b --- /dev/null +++ b/Modules/Common/DesignKit/Sources/DesignKit/Classes/Design/Core/CGFloat+.swift @@ -0,0 +1,17 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +public extension CGFloat { + /// 12px + static let size12 = 12.0 + /// 13px + static let size13 = 13.0 + /// 15px + static let size15 = 15.0 + /// 17px + static let size17 = 17.0 +} diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Classes/Design/Generated/.gitkeep b/Modules/Common/DesignKit/Sources/DesignKit/Classes/Design/Generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/HackerNewsWatch WatchKit App/Assets.xcassets/Contents.json b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/Contents.json similarity index 100% rename from HackerNewsWatch WatchKit App/Assets.xcassets/Contents.json rename to Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/Contents.json diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/dynamic_gray.colorset/Contents.json b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/dynamic_gray.colorset/Contents.json new file mode 100644 index 0000000..848e014 --- /dev/null +++ b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/dynamic_gray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "235", + "green" : "235", + "red" : "235" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "57", + "green" : "57", + "red" : "57" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/dynamic_light_gray.colorset/Contents.json b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/dynamic_light_gray.colorset/Contents.json new file mode 100644 index 0000000..a5c27a5 --- /dev/null +++ b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Colors.xcassets/dynamic_light_gray.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "14", + "green" : "14", + "red" : "14" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "203", + "green" : "203", + "red" : "203" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Black.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Black.ttf new file mode 100644 index 0000000..7af9fb4 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Black.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-BlackItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-BlackItalic.ttf new file mode 100644 index 0000000..c608366 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-BlackItalic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Bold.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Bold.ttf new file mode 100644 index 0000000..0927b81 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Bold.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-BoldItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-BoldItalic.ttf new file mode 100644 index 0000000..02f5784 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-BoldItalic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraBold.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraBold.ttf new file mode 100644 index 0000000..e33afd4 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraBold.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraBoldItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraBoldItalic.ttf new file mode 100644 index 0000000..92fc301 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraBoldItalic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraLight.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraLight.ttf new file mode 100644 index 0000000..8aa56c1 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraLight.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraLightItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraLightItalic.ttf new file mode 100644 index 0000000..98c10f1 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ExtraLightItalic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Italic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Italic.ttf new file mode 100644 index 0000000..cff3ceb Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Italic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Light.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Light.ttf new file mode 100644 index 0000000..fd787a8 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Light.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-LightItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-LightItalic.ttf new file mode 100644 index 0000000..6a2c9d4 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-LightItalic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Medium.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Medium.ttf new file mode 100644 index 0000000..4012225 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Medium.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-MediumItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-MediumItalic.ttf new file mode 100644 index 0000000..84b2539 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-MediumItalic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Regular.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Regular.ttf new file mode 100644 index 0000000..f4a266d Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Regular.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-SemiBold.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-SemiBold.ttf new file mode 100644 index 0000000..189ce9d Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-SemiBold.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-SemiBoldItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-SemiBoldItalic.ttf new file mode 100644 index 0000000..4c59d86 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-SemiBoldItalic.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Thin.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Thin.ttf new file mode 100644 index 0000000..7d085bb Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-Thin.ttf differ diff --git a/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ThinItalic.ttf b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ThinItalic.ttf new file mode 100644 index 0000000..6fbfad1 Binary files /dev/null and b/Modules/Common/DesignKit/Sources/DesignKit/Resources/Fonts/Montserrat-ThinItalic.ttf differ diff --git a/Modules/Common/DesignKit/Tests/DesignKitTests/DesignKitTests.swift b/Modules/Common/DesignKit/Tests/DesignKitTests/DesignKitTests.swift new file mode 100644 index 0000000..a5e4e1c --- /dev/null +++ b/Modules/Common/DesignKit/Tests/DesignKitTests/DesignKitTests.swift @@ -0,0 +1,16 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +@testable import DesignKit +import XCTest + +final class DesignKitTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(DesignKit().text, "Hello, World!") + } +} diff --git a/Modules/Common/DesignKit/swiftgen.yml b/Modules/Common/DesignKit/swiftgen.yml new file mode 100644 index 0000000..405a288 --- /dev/null +++ b/Modules/Common/DesignKit/swiftgen.yml @@ -0,0 +1,16 @@ +input_dir: Sources/DesignKit/Resources +output_dir: Sources/DesignKit/Classes/Design/Generated +xcassets: + inputs: Colors.xcassets + outputs: + templateName: swift5 + output: Colors.swift + params: + publicAccess: true +fonts: + inputs: Fonts + outputs: + templateName: swift4 + output: Fonts.swift + params: + publicAccess: 1 diff --git a/Modules/Common/HackerNewsLocalization/.gitignore b/Modules/Common/HackerNewsLocalization/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Modules/Common/HackerNewsLocalization/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/Common/HackerNewsLocalization/Makefile b/Modules/Common/HackerNewsLocalization/Makefile new file mode 100644 index 0000000..76c5b88 --- /dev/null +++ b/Modules/Common/HackerNewsLocalization/Makefile @@ -0,0 +1,3 @@ +## swiftgen: Trigger code generation from assets with swiftgen tool +swiftgen: + swiftgen \ No newline at end of file diff --git a/Modules/Common/HackerNewsLocalization/Package.swift b/Modules/Common/HackerNewsLocalization/Package.swift new file mode 100644 index 0000000..6031d74 --- /dev/null +++ b/Modules/Common/HackerNewsLocalization/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable all + +import PackageDescription + +let package = Package( + name: "HackerNewsLocalization", + defaultLocalization: "en", + platforms: [ + .iOS(.v16), + ], + products: [ + .library(name: "HackerNewsLocalization", targets: ["HackerNewsLocalization"]), + ], + dependencies: [], + targets: [ + .target(name: "HackerNewsLocalization", dependencies: []), + ] +) diff --git a/Modules/Common/HackerNewsLocalization/README.md b/Modules/Common/HackerNewsLocalization/README.md new file mode 100644 index 0000000..f4bb1e2 --- /dev/null +++ b/Modules/Common/HackerNewsLocalization/README.md @@ -0,0 +1,3 @@ +# HackerNewsLocalization + +A description of this package. diff --git a/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/Classes/.gitkeep b/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/Classes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/HackerNewsLocalization.swift b/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/HackerNewsLocalization.swift new file mode 100644 index 0000000..872a631 --- /dev/null +++ b/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/HackerNewsLocalization.swift @@ -0,0 +1,10 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +public struct HackerNewsLocalization { + public private(set) var text = "Hello, World!" + + public init() {} +} diff --git a/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/Resources/en.lproj/Localizable.strings b/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/Resources/en.lproj/Localizable.strings new file mode 100644 index 0000000..fc7c54f --- /dev/null +++ b/Modules/Common/HackerNewsLocalization/Sources/HackerNewsLocalization/Resources/en.lproj/Localizable.strings @@ -0,0 +1,23 @@ +"tab-bar.item.home" = "Home"; +"tab-bar.item.search" = "Search"; +"tab-bar.item.settings" = "Settings"; +"news.article.comments" = "%d comments"; +"settings.menu.send_feedback" = "Send Feedback"; +"settings.menu.rate_us" = "Rate Us"; +"settings.menu.github" = "GitHub"; +"settings.navbar.title" = "Settings"; +"comment.expand-branch" = "Expand the branch (%@ Replies)"; +"comment.user.unknown" = "Unknown"; +"post-details.navigation-bar.title" = "Comments"; +"replies.navigation-bar.title" = "Replies"; +"sidebar.items.settings" = "Settings"; +"sidebar.groups.feeds.title" = "Feeds"; +"common.actions.cancel" = "Cancel"; +"common.actions.close" = "Close"; +"settings.author" = "By %@"; +"post-type.new" = "New"; +"post-type.best" = "Best"; +"post-type.top" = "Top"; +"post-type.ask" = "Ask"; +"post-type.show" = "Show"; +"post-type.jobs" = "Jobs"; diff --git a/Modules/Common/HackerNewsLocalization/swiftgen.yml b/Modules/Common/HackerNewsLocalization/swiftgen.yml new file mode 100644 index 0000000..701c4d8 --- /dev/null +++ b/Modules/Common/HackerNewsLocalization/swiftgen.yml @@ -0,0 +1,10 @@ +input_dir: Sources/HackerNewsLocalization/Resources +output_dir: Sources/HackerNewsLocalization/Classes +strings: + inputs: + - en.lproj/Localizable.strings + outputs: + templateName: structured-swift5 + output: Strings.swift + params: + publicAccess: true diff --git a/Modules/Common/UIExtensions/.gitignore b/Modules/Common/UIExtensions/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Modules/Common/UIExtensions/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/Common/UIExtensions/Package.swift b/Modules/Common/UIExtensions/Package.swift new file mode 100644 index 0000000..6285fbf --- /dev/null +++ b/Modules/Common/UIExtensions/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable prefixed_toplevel_constant + +import PackageDescription + +let package = Package( + name: "UIExtensions", + platforms: [.iOS(.v13)], + products: [ + .library(name: "UIExtensions", targets: ["UIExtensions"]), + ], + dependencies: [], + targets: [ + .target(name: "UIExtensions", dependencies: []), + .testTarget(name: "UIExtensionsTests", dependencies: ["UIExtensions"]), + ] +) diff --git a/Modules/Common/UIExtensions/README.md b/Modules/Common/UIExtensions/README.md new file mode 100644 index 0000000..a80c5b2 --- /dev/null +++ b/Modules/Common/UIExtensions/README.md @@ -0,0 +1,3 @@ +# UIExtensions + +A description of this package. diff --git a/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/UIFont+SUI.swift b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/UIFont+SUI.swift new file mode 100644 index 0000000..be94901 --- /dev/null +++ b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/UIFont+SUI.swift @@ -0,0 +1,12 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +public extension UIFont { + var sui: SwiftUI.Font { + SwiftUI.Font(self) + } +} diff --git a/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/UIWindow+MotionEnded.swift b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/UIWindow+MotionEnded.swift new file mode 100644 index 0000000..51fbeb9 --- /dev/null +++ b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/UIWindow+MotionEnded.swift @@ -0,0 +1,14 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import UIKit + +extension UIWindow { + override open func motionEnded(_ motion: UIEvent.EventSubtype, with _: UIEvent?) { + if motion == .motionShake { + NotificationCenter.default.post(name: UIDevice.deviceDidShakeNotification, object: nil) + } + } +} diff --git a/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/View/View+EraseToAnyView.swift b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/View/View+EraseToAnyView.swift new file mode 100644 index 0000000..dc2d4bd --- /dev/null +++ b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/View/View+EraseToAnyView.swift @@ -0,0 +1,12 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +public extension View { + func eraseToAnyView() -> AnyView { + AnyView(self) + } +} diff --git a/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/View/View+Shake.swift b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/View/View+Shake.swift new file mode 100644 index 0000000..54a885b --- /dev/null +++ b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Extensions/View/View+Shake.swift @@ -0,0 +1,12 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +public extension View { + func onShake(perform action: @escaping () -> Void) -> some View { + modifier(DeviceShakeViewModifier(action: action)) + } +} diff --git a/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Helpers/NSAttributedString+HTML.swift b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Helpers/NSAttributedString+HTML.swift new file mode 100644 index 0000000..172a438 --- /dev/null +++ b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Helpers/NSAttributedString+HTML.swift @@ -0,0 +1,102 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import UIKit + +// swiftlint:disable function_body_length force_unwrapping non_optional_string_data_conversion +public extension NSAttributedString { + static func html(withBody body: String) -> NSAttributedString { + // Match the HTML `lang` attribute to current localisation used by the app (aka Bundle.main). + let bundle = Bundle.main + let lang = bundle.preferredLocalizations.first + ?? bundle.developmentLocalization + ?? "en" + + return (try? NSAttributedString( + data: """ + + + + + + + + + \(body) + + + + """.data(using: .utf8)!, + options: [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: NSUTF8StringEncoding, + ], + documentAttributes: nil + )) ?? NSAttributedString(string: body) + } +} + +// swiftlint:enable function_body_length force_unwrapping non_optional_string_data_conversion + +// MARK: Converting UIColors into CSS friendly color hex string + +private extension UIColor { + var hex: String { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + getRed(&red, green: &green, blue: &blue, alpha: &alpha) + + return String( + format: "#%02lX%02lX%02lX%02lX", + lroundf(Float(red * 255)), + lroundf(Float(green * 255)), + lroundf(Float(blue * 255)), + lroundf(Float(alpha * 255)) + ) + } +} + +// extension NSAttributedString { +// func trimmedAttributedString() -> NSAttributedString { +// let nonNewlines = CharacterSet.whitespacesAndNewlines.inverted +// +// // Find first non-whitespace character and new line character +// let startRange = string.rangeOfCharacter(from: nonNewlines) +// +// // Find last non-whitespace character and new line character. +// let endRange = string.rangeOfCharacter(from: nonNewlines, options: .backwards) +// guard let startLocation = startRange?.lowerBound, let endLocation = endRange?.lowerBound else { +// return self +// } +// // Getting range out of locations. This trim out leading and trailing whitespaces and new line characters. +// let range = NSRange(startLocation...endLocation, in: string) +// return attributedSubstring(from: range) +// } +// } diff --git a/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Helpers/UIDevice+Notification.swift b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Helpers/UIDevice+Notification.swift new file mode 100644 index 0000000..7627496 --- /dev/null +++ b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/Helpers/UIDevice+Notification.swift @@ -0,0 +1,10 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import UIKit + +extension UIDevice { + static let deviceDidShakeNotification = Notification.Name(rawValue: "deviceDidShakeNotification") +} diff --git a/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/ViewModifiers/DeviceShakeViewModifier.swift b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/ViewModifiers/DeviceShakeViewModifier.swift new file mode 100644 index 0000000..6e9ea1a --- /dev/null +++ b/Modules/Common/UIExtensions/Sources/UIExtensions/Classes/ViewModifiers/DeviceShakeViewModifier.swift @@ -0,0 +1,18 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +struct DeviceShakeViewModifier: ViewModifier { + let action: () -> Void + + func body(content: Content) -> some View { + content + .onAppear() + .onReceive(NotificationCenter.default.publisher(for: UIDevice.deviceDidShakeNotification)) { _ in + action() + } + } +} diff --git a/Modules/Common/UIExtensions/Tests/UIExtensionsTests/UIExtensionsTests.swift b/Modules/Common/UIExtensions/Tests/UIExtensionsTests/UIExtensionsTests.swift new file mode 100644 index 0000000..8f76c44 --- /dev/null +++ b/Modules/Common/UIExtensions/Tests/UIExtensionsTests/UIExtensionsTests.swift @@ -0,0 +1,16 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +@testable import UIExtensions +import XCTest + +final class UIExtensionsTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(UIExtensions().text, "Hello, World!") + } +} diff --git a/Modules/Features/Home/.gitignore b/Modules/Features/Home/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/Modules/Features/Home/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/Features/Home/Package.swift b/Modules/Features/Home/Package.swift new file mode 100644 index 0000000..12873d8 --- /dev/null +++ b/Modules/Features/Home/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable prefixed_toplevel_constant + +import PackageDescription + +let package = Package( + name: "Home", + platforms: [.iOS(.v17)], + products: [ + .library(name: "Home", targets: ["Home"]), + .library(name: "HomeInterfaces", targets: ["HomeInterfaces"]), + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.5.5")), + .package(url: "https://github.com/space-code/network-layer.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/onevcat/Kingfisher.git", .upToNextMajor(from: "7.10.1")), + .package(url: "https://github.com/space-code/skeleton-ui.git", .upToNextMajor(from: "1.0.3")), + .package(url: "https://github.com/space-code/blade.git", .upToNextMajor(from: "1.1.0")), + .package(path: "../../Common/AppUtils"), + .package(path: "../../Common/UIExtensions"), + .package(path: "../../Common/HackerNewsLocalization"), + .package(path: "../../Common/DesignKit"), + .package(path: "../../Features/Settings"), + ], + targets: [ + .target( + name: "Home", + dependencies: [ + "HomeInterfaces", + .product(name: "Kingfisher", package: "Kingfisher"), + .product(name: "AppUtils", package: "AppUtils"), + .product(name: "NetworkLayer", package: "network-layer"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "UIExtensions", package: "UIExtensions"), + .product(name: "HackerNewsLocalization", package: "HackerNewsLocalization"), + .product(name: "DesignKit", package: "DesignKit"), + .product(name: "SkeletonUI", package: "skeleton-ui"), + .product(name: "Blade", package: "blade"), + .product(name: "BladeTCA", package: "blade"), + .product(name: "SettingsInterfaces", package: "Settings"), + ] + ), + .target(name: "HomeInterfaces", dependencies: []), + .testTarget(name: "HomeTests", dependencies: ["Home"]), + ] +) diff --git a/Modules/Features/Home/README.md b/Modules/Features/Home/README.md new file mode 100644 index 0000000..8aacf77 --- /dev/null +++ b/Modules/Features/Home/README.md @@ -0,0 +1,3 @@ +# Home + +A description of this package. diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+CommentsPager.swift b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+CommentsPager.swift new file mode 100644 index 0000000..f18a1de --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+CommentsPager.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture + +extension DependencyValues { + var commentsPager: ICommentsPager { + get { self[CommentsPagerKey.self] } + set { self[CommentsPagerKey.self] = newValue } + } +} + +// MARK: - CommentsPagerKey + +private enum CommentsPagerKey: DependencyKey { + static var liveValue: ICommentsPager = Locator.shared.resolve() +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostDetailViewModelFactory.swift b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostDetailViewModelFactory.swift new file mode 100644 index 0000000..d72c14b --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostDetailViewModelFactory.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import Foundation + +// MARK: - PostDetailViewModelFactoryKey + +private enum PostDetailViewModelFactoryKey: DependencyKey { + static var liveValue: IPostDetailViewModelFactory = PostDetailViewModelFactory(dateTimeFormatter: RelativeDateTimeFormatter()) +} + +extension DependencyValues { + var postDetailViewModelFactory: IPostDetailViewModelFactory { + get { self[PostDetailViewModelFactoryKey.self] } + set { self[PostDetailViewModelFactoryKey.self] = newValue } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsPager.swift b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsPager.swift new file mode 100644 index 0000000..f6a4b67 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsPager.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture + +extension DependencyValues { + var postsPager: PostsPager { + get { self[PostsPagerKey.self] } + set { self[PostsPagerKey.self] = newValue } + } +} + +// MARK: - PostsPagerKey + +private enum PostsPagerKey: DependencyKey { + static var liveValue: PostsPager = Locator.shared.resolve() +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsService.swift b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsService.swift new file mode 100644 index 0000000..9bd1ed9 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsService.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture + +extension DependencyValues { + var postsService: IPostsService { + get { self[PostsServiceKey.self] } + set { self[PostsServiceKey.self] = newValue } + } +} + +// MARK: - PostsServiceKey + +private enum PostsServiceKey: DependencyKey { + static var liveValue: IPostsService = Locator.shared.resolve() +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsViewModelFactory.swift b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsViewModelFactory.swift new file mode 100644 index 0000000..0f8ea48 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+PostsViewModelFactory.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import Foundation + +extension DependencyValues { + var postsViewModelFactory: IPostViewModelFactory { + get { self[PostViewModelFactoryKey.self] } + set { self[PostViewModelFactoryKey.self] = newValue } + } +} + +// MARK: - PostViewModelFactoryKey + +private enum PostViewModelFactoryKey: DependencyKey { + static var liveValue: IPostViewModelFactory = PostViewModelFactory(dateTimeFormatter: RelativeDateTimeFormatter()) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+RepliesService.swift b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+RepliesService.swift new file mode 100644 index 0000000..ca073ef --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+RepliesService.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture + +extension DependencyValues { + var repliesService: IRepliesService { + get { self[RepliesServiceKey.self] } + set { self[RepliesServiceKey.self] = newValue } + } +} + +// MARK: - RepliesServiceKey + +private enum RepliesServiceKey: DependencyKey { + static var liveValue: IRepliesService = Locator.shared.resolve() +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+RepliesViewModelFactory.swift b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+RepliesViewModelFactory.swift new file mode 100644 index 0000000..e13b45e --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/DI/DependencyValues/DependencyValues+RepliesViewModelFactory.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import Foundation + +extension DependencyValues { + var repliesViewModelFactory: IRepliesViewModelFactory { + get { self[RepliesViewModelFactoryKey.self] } + set { self[RepliesViewModelFactoryKey.self] = newValue } + } +} + +// MARK: - RepliesViewModelFactoryKey + +private enum RepliesViewModelFactoryKey: DependencyKey { + static var liveValue: IRepliesViewModelFactory = RepliesViewModelFactory(dateTimeFormatter: RelativeDateTimeFormatter()) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Providers/AppNameProvider/AppNameProvider.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Providers/AppNameProvider/AppNameProvider.swift new file mode 100644 index 0000000..f9210b8 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Providers/AppNameProvider/AppNameProvider.swift @@ -0,0 +1,32 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +// MARK: - AppNameProvider + +final class AppNameProvider: IAppNameProvider { + // MARK: Properties + + private let bundle: Bundle + + // MARK: Initialization + + init(bundle: Bundle) { + self.bundle = bundle + } + + // MARK: IAppNameProvider + + var applicationName: String { + bundle.infoDictionary?[.bundleNameKey] as? String ?? "" + } +} + +// MARK: - Constants + +private extension String { + static let bundleNameKey = "CFBundleName" +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Providers/AppNameProvider/IAppNameProvider.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Providers/AppNameProvider/IAppNameProvider.swift new file mode 100644 index 0000000..bf8f7de --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Providers/AppNameProvider/IAppNameProvider.swift @@ -0,0 +1,10 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +protocol IAppNameProvider { + var applicationName: String { get } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Providers/PageLoaderProvider/PageLoaderProvider.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Providers/PageLoaderProvider/PageLoaderProvider.swift new file mode 100644 index 0000000..6710113 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Providers/PageLoaderProvider/PageLoaderProvider.swift @@ -0,0 +1,36 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade + +// MARK: - PageLoaderProvider + +actor PageLoaderProvider { + // MARK: Properties + + /// The service responsible for loading pages of data. + private let paginatorService: any IOffsetPageLoader + + // MARK: Initialization + + init(paginatorService: any IOffsetPageLoader) { + self.paginatorService = paginatorService + } + + // MARK: Private + + /// Loads a specific page of data and updates the internal state. + private func loadPage(limit: Int, offset: Int) async throws -> Page { + try await paginatorService.loadPage(request: OffsetPaginationRequest(limit: limit, offset: offset)) + } +} + +// MARK: IOffsetPageLoader + +extension PageLoaderProvider: IOffsetPageLoader { + func loadPage(request: OffsetPaginationRequest) async throws -> Page { + try await loadPage(limit: request.limit, offset: request.offset) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/CommentsService.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/CommentsService.swift new file mode 100644 index 0000000..169efa1 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/CommentsService.swift @@ -0,0 +1,37 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation +import NetworkLayer +import NetworkLayerInterfaces + +// MARK: - CommentsService + +final class CommentsService { + // MARK: Private + + private let requestProcessor: IRequestProcessor + + // MARK: Initialization + + init(requestProcessor: IRequestProcessor) { + self.requestProcessor = requestProcessor + } + + // MARK: Private + + private func _loadComment(id: Int) async throws -> Comment { + let request = CommentRequest(id: id) + return try await requestProcessor.send(request, strategy: nil, delegate: nil, configure: nil).data + } +} + +// MARK: ICommentsService + +extension CommentsService: ICommentsService { + func loadComment(id: Int) async throws -> Comment { + try await _loadComment(id: id) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/ICommentsService.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/ICommentsService.swift new file mode 100644 index 0000000..c6a5b20 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/ICommentsService.swift @@ -0,0 +1,10 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +protocol ICommentsService { + func loadComment(id: Int) async throws -> Comment +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/Requests/CommentRequest.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/Requests/CommentRequest.swift new file mode 100644 index 0000000..a103351 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/Requests/CommentRequest.swift @@ -0,0 +1,24 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +final class CommentRequest: BaseRequest { + // MARK: Properties + + private let id: Int + + // MARK: Initialization + + init(id: Int) { + self.id = id + } + + // MARK: BaseRequest + + override var path: String { + "item/\(id).json" + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/Responses/Comment.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/Responses/Comment.swift new file mode 100644 index 0000000..6ca8c5f --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/CommentsService/Responses/Comment.swift @@ -0,0 +1,33 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +struct Comment: Decodable, Equatable { + let id: Int + let author: String? + let text: String? + let time: Int + let kids: [Int] + +// var comments: [Comment] = [] + + enum CodingKeys: CodingKey { + case id + case by + case text + case time + case kids + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(Int.self, forKey: .id) + author = try container.decodeIfPresent(String.self, forKey: .by) + text = try container.decodeIfPresent(String.self, forKey: .text) + time = try container.decode(Int.self, forKey: .time) + kids = try container.decodeIfPresent([Int].self, forKey: .kids) ?? [] + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/IPostsService.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/IPostsService.swift new file mode 100644 index 0000000..ecc2c88 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/IPostsService.swift @@ -0,0 +1,11 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Foundation + +protocol IPostsService { + func loadIDs(for type: PostType) async throws -> [Int] + func loadPosts(with ids: [Int]) async throws -> [Post] +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/PostsService.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/PostsService.swift new file mode 100644 index 0000000..51a65b1 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/PostsService.swift @@ -0,0 +1,50 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +// MARK: - PostsService + +final class PostsService { + // MARK: Private + + private let requestProcessor: IRequestProcessor + + // MARK: Initialization + + init(requestProcessor: IRequestProcessor) { + self.requestProcessor = requestProcessor + } + + // MARK: Private + + private func loadPost(withID id: Int) async throws -> Post { + let request = PostRequest(id: id) + return try await requestProcessor.send(request, strategy: nil, delegate: nil, configure: nil).data + } +} + +// MARK: IPostsService + +extension PostsService: IPostsService { + func loadIDs(for type: PostType) async throws -> [Int] { + let request = PostIdentifiersRequest(postType: type) + return try await requestProcessor.send(request, strategy: nil, delegate: nil, configure: nil).data + } + + func loadPosts(with ids: [Int]) async throws -> [Post] { + try await withThrowingTaskGroup(of: Post.self, returning: [Post].self, body: { taskGroup in + for id in ids { + taskGroup.addTask { try await self.loadPost(withID: id) } + } + + let posts = try await taskGroup.reduce(into: [Post]()) { $0.append($1) } + .sorted(by: { $0.time > $1.time }) + + return posts + }) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/BaseRequest.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/BaseRequest.swift new file mode 100644 index 0000000..42be42b --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/BaseRequest.swift @@ -0,0 +1,19 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Foundation +import NetworkLayerInterfaces + +class BaseRequest: IRequest { + var path: String { "" } + + var httpMethod: NetworkLayerInterfaces.HTTPMethod { + .get + } + + var domainName: String { + "https://hacker-news.firebaseio.com/v0" + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/PostIdentifiersRequest.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/PostIdentifiersRequest.swift new file mode 100644 index 0000000..97fd34c --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/PostIdentifiersRequest.swift @@ -0,0 +1,37 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +final class PostIdentifiersRequest: BaseRequest { + // MARK: Properties + + private let postType: PostType + + // MARK: Initialization + + init(postType: PostType) { + self.postType = postType + } + + // MARK: IRequest + + override var path: String { + switch postType { + case .new: + return "newstories.json" + case .best: + return "beststories.json" + case .top: + return "topstories.json" + case .ask: + return "askstories.json" + case .show: + return "showstories.json" + case .jobs: + return "jobstories.json" + } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/PostRequest.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/PostRequest.swift new file mode 100644 index 0000000..def952a --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Requests/PostRequest.swift @@ -0,0 +1,24 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +final class PostRequest: BaseRequest { + // MARK: Properties + + private let id: Int + + // MARK: Initialization + + init(id: Int) { + self.id = id + } + + // MARK: BaseRequest + + override var path: String { + "item/\(id).json" + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Responses/Post.swift b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Responses/Post.swift new file mode 100644 index 0000000..8fb0024 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/Core/Services/PostsService/Responses/Post.swift @@ -0,0 +1,52 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Foundation + +struct Post: Decodable, Equatable, Identifiable { + /// The item's unique id. + let id: Int + + /// The title of the story, poll or job. + let title: String? + + /// The story's score, or the votes for a pollopt. + let score: Int? + + /// The username of the item's author. + let author: String? + + /// The URL of the story. + let url: String? + + /// The ids of the item's comments, in ranked display order. + var kids: [Int] + + /// Creation date of the item, in Unix Time. + var time: Int + + private enum CodingKeys: String, CodingKey { + case id + case title + case score + case by + case url + case kids + case time + } + + // MARK: Initialization + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(Int.self, forKey: .id) + title = try? container.decode(String.self, forKey: .title) + score = try? container.decode(Int.self, forKey: .score) + author = try? container.decode(String.self, forKey: .by) + url = try? container.decode(String.self, forKey: .url) + kids = (try? container.decode([Int].self, forKey: .kids)) ?? [] + time = (try? container.decode(Int.self, forKey: .time)) ?? .zero + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/DI/PresentationAssembly/HomePresentationAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/DI/PresentationAssembly/HomePresentationAssembly.swift new file mode 100644 index 0000000..c7bb739 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/DI/PresentationAssembly/HomePresentationAssembly.swift @@ -0,0 +1,57 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Foundation +import SettingsInterfaces + +// MARK: - IHomePresentationAssembly + +protocol IHomePresentationAssembly { + var newsAssembly: IPostsAssembly { get } +} + +// MARK: - HomePresentationAssembly + +final class HomePresentationAssembly: IHomePresentationAssembly { + // MARK: Properties + + private let settingsAssembly: ISettingsPublicAssembly + private let servicesAssembly: IHomeServicesAssembly + + // MARK: Initialization + + init(servicesAssembly: IHomeServicesAssembly, settingsAssembly: ISettingsPublicAssembly) { + self.servicesAssembly = servicesAssembly + self.settingsAssembly = settingsAssembly + } + + // MARK: IHomePresentationAssembly + + var newsAssembly: IPostsAssembly { + PostsAssembly( + postsService: servicesAssembly.postsService, + appNameProvider: servicesAssembly.appNameProvider, + postDetailsAssembly: postDetailsAssembly, + settingsAssembly: settingsAssembly + ) + } + + // MARK: Private + + private var postDetailsAssembly: IPostDetailAssembly { + PostDetailAssembly( + commentsService: servicesAssembly.commentsService, + postsService: servicesAssembly.postsService, + repliesAssembly: repliesAssembly + ) + } + + private var repliesAssembly: IRepliesAssembly { + RepliesAssembly( + commentsService: servicesAssembly.commentsService, + postsService: servicesAssembly.postsService + ) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/DI/PublicAssembly/HomePublicAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/DI/PublicAssembly/HomePublicAssembly.swift new file mode 100644 index 0000000..dbcbeb7 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/DI/PublicAssembly/HomePublicAssembly.swift @@ -0,0 +1,33 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import HomeInterfaces +import NetworkLayerInterfaces +import SettingsInterfaces +import SwiftUI +import UIExtensions + +public final class HomePublicAssembly: IHomePublicAssembly { + // MARK: Properties + + private let presentationAssembly: IHomePresentationAssembly + private let servicesAssembly: IHomeServicesAssembly + + // MARK: Initialization + + public init(requestProcessor: IRequestProcessor, settingsAssembly: ISettingsPublicAssembly) { + servicesAssembly = HomeServicesAssembly(requestProcessor: requestProcessor) + presentationAssembly = HomePresentationAssembly( + servicesAssembly: servicesAssembly, + settingsAssembly: settingsAssembly + ) + } + + // MARK: IHomePublicAssembly + + public func assemble() -> AnyView { + presentationAssembly.newsAssembly.assemble().eraseToAnyView() + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/DI/ServicesAssembly/HomeServicesAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/DI/ServicesAssembly/HomeServicesAssembly.swift new file mode 100644 index 0000000..f19e6d6 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/DI/ServicesAssembly/HomeServicesAssembly.swift @@ -0,0 +1,50 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import Foundation +import NetworkLayerInterfaces + +// MARK: - IHomeServicesAssembly + +protocol IHomeServicesAssembly { + var postsService: IPostsService { get } + var commentsService: ICommentsService { get } + var appNameProvider: IAppNameProvider { get } +} + +// MARK: - HomeServicesAssembly + +final class HomeServicesAssembly: AppAssembly, IHomeServicesAssembly { + // MARK: Properties + + private let requestProcessor: IRequestProcessor + + // MARK: Initialization + + init(requestProcessor: IRequestProcessor) { + self.requestProcessor = requestProcessor + } + + // MARK: IHomeServicesAssembly + + var postsService: IPostsService { + resolve(IPostsService.self) { + PostsService(requestProcessor: self.requestProcessor) + } + } + + var appNameProvider: IAppNameProvider { + resolve(IAppNameProvider.self) { + AppNameProvider(bundle: .main) + } + } + + var commentsService: ICommentsService { + resolve(ICommentsService.self) { + CommentsService(requestProcessor: self.requestProcessor) + } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/Helpers/Bootstrappable/BootstrappableAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/Helpers/Bootstrappable/BootstrappableAssembly.swift new file mode 100644 index 0000000..8d40d26 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/Helpers/Bootstrappable/BootstrappableAssembly.swift @@ -0,0 +1,18 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +class BootstrappableAssembly: IBootstrappable { + // MARK: Initialization + + init() { + bootstrap() + } + + // MARK: IBootstrappable + + func bootstrap() {} +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/Helpers/Bootstrappable/IBootstrappable.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/Helpers/Bootstrappable/IBootstrappable.swift new file mode 100644 index 0000000..8482cac --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/Helpers/Bootstrappable/IBootstrappable.swift @@ -0,0 +1,10 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +protocol IBootstrappable { + func bootstrap() +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/ArticleView/ArticleView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/ArticleView/ArticleView.swift new file mode 100644 index 0000000..5c8316c --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/ArticleView/ArticleView.swift @@ -0,0 +1,142 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import DesignKit +import HackerNewsLocalization +import SwiftUI + +// MARK: - ArticleView + +struct ArticleView: View { + // MARK: Properties + + private let viewModel: ViewModel + + // MARK: Initialization + + init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + // MARK: View + + var body: some View { + VStack(alignment: .leading) { + headerView + + titleView + .padding(.vertical, 4.0) + + Divider() + + footerView + } + .fixedSize(horizontal: false, vertical: true) + } + + // MARK: Private + + private var headerView: some View { + HStack(alignment: .center) { + imageView + + viewModel.link.map { + Text($0) + .font(FontFamily.Montserrat.medium.font(size: .size12).sui) + .foregroundColor(Color.orange) + .lineLimit(1) + } + } + } + + private var titleView: some View { + Text(viewModel.title) + .font(FontFamily.Montserrat.semiBold.font(size: .size17).sui) + .lineLimit(.lineLimit) + } + + private var imageView: some View { + viewModel.imageURL.map { + ImageView(url: $0) + .frame(width: .headerImageSize, height: .headerImageSize) + .background(Color.white) + .clipShape(Circle()) + } + } + + private var footerView: some View { + HStack { + IndicatorView(viewModel: IndicatorView.ViewModel(imageName: .arrowUp, text: viewModel.rating)) + Divider() + IndicatorView(viewModel: IndicatorView.ViewModel(imageName: .person, text: viewModel.author)) + Divider() + IndicatorView(viewModel: IndicatorView.ViewModel(imageName: .comments, text: "\(viewModel.numberOfComments)")) + Divider() + IndicatorView(viewModel: IndicatorView.ViewModel(imageName: .calendar, text: viewModel.date)) + } + } +} + +// MARK: ArticleView.ViewModel + +extension ArticleView { + struct ViewModel: Equatable, Identifiable { + let id = UUID() + let articleID: Int + let title: String + let author: String + let link: String? + let rating: String + let numberOfComments: Int + let date: String + let imageURL: URL? + let url: URL? + } +} + +// MARK: - Constants + +private extension Int { + static let lineLimit = 2 +} + +// MARK: Constants + +private extension CGFloat { + static let headerSpacing = 4.0 + static let headerImageSize = 12.0 +} + +private extension String { + static let arrowUp = "arrow.up.circle.fill" + static let person = "person.circle.fill" + static let calendar = "calendar.circle.fill" + static let comments = "line.3.horizontal.circle.fill" + static let dot = "•" +} + +// MARK: - Preview + +#if DEBUG + + private var viewModel = ArticleView.ViewModel( + articleID: .zero, + title: "Proton Mail Rewrites Your Emails", + author: "jamesik", + link: "x64.sh", + rating: "64", + numberOfComments: 30, + date: "22 hours ago", + imageURL: nil, + url: nil + ) + + struct ArticleView_Previews: PreviewProvider { + static var previews: some View { + ArticleView(viewModel: viewModel) + } + } + +#endif diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/CommentView/CommentHeaderView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/CommentView/CommentHeaderView.swift new file mode 100644 index 0000000..e2034de --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/CommentView/CommentHeaderView.swift @@ -0,0 +1,40 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +// MARK: - CommentHeaderView + +struct CommentHeaderView: View { + private let viewModel: ViewModel + + init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + var body: some View { + HStack(alignment: .center) { + IndicatorView(viewModel: .init(imageName: .person, text: viewModel.username)) + + Divider() + + IndicatorView(viewModel: .init(imageName: .calendar, text: viewModel.date)) + } + } +} + +// MARK: CommentHeaderView.ViewModel + +extension CommentHeaderView { + struct ViewModel { + let username: String + let date: String + } +} + +private extension String { + static let person = "person.circle.fill" + static let calendar = "calendar.circle.fill" +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/CommentView/CommentView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/CommentView/CommentView.swift new file mode 100644 index 0000000..965e7a7 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/CommentView/CommentView.swift @@ -0,0 +1,64 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI +import UIExtensions + +// MARK: - CommentView + +struct CommentView: View { + // MARK: Properties + + let viewModel: ViewModel + + // MARK: View + + var body: some View { + VStack(alignment: .leading) { + CommentHeaderView(viewModel: .init(username: viewModel.username, date: viewModel.date)) + + Divider() + + contentView + } + .fixedSize(horizontal: false, vertical: true) + } + + private var contentView: some View { + VStack(alignment: .leading) { + Text(AttributedString(.html(withBody: viewModel.text))) + } + } +} + +// MARK: CommentView.ViewModel + +extension CommentView { + struct ViewModel: Equatable, Identifiable { + let id = UUID() + let username: String + let date: String + let text: String + } +} + +// MARK: Constants + +private extension CGFloat { + static let headerSpacing = 4.0 + static let headerImageSize = 12.0 +} + +// MARK: Preview + +// #Preview { +// CommentView( +// viewModel: CommentView.ViewModel( +// username: "nsvasilev", +// date: "22 hours ago", +// text: "Comment message" +// ) +// ) +// } diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/ImageView/ImageView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/ImageView/ImageView.swift new file mode 100644 index 0000000..92e0bce --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/ImageView/ImageView.swift @@ -0,0 +1,28 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Kingfisher +import SwiftUI + +// MARK: - ImageView + +struct ImageView: View { + // MARK: Properties + + private let url: URL + + // MARK: Initialization + + init(url: URL) { + self.url = url + } + + // MARK: - View + + var body: some View { + KFImage.url(url) + .resizable() + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/IndicatorView/IndicatorView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/IndicatorView/IndicatorView.swift new file mode 100644 index 0000000..6f74b18 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/IndicatorView/IndicatorView.swift @@ -0,0 +1,51 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +// MARK: - IndicatorView + +struct IndicatorView: View { + // MARK: Properties + + private let viewModel: ViewModel + + // MARK: Initialization + + init(viewModel: ViewModel) { + self.viewModel = viewModel + } + + // MARK: View + + var body: some View { + HStack(spacing: .headerSpacing) { + Image(systemName: viewModel.imageName) + .resizable() + .frame(width: .headerImageSize, height: .headerImageSize) + .foregroundStyle(Color(uiColor: UIColor.secondaryLabel)) + Text(viewModel.text) + .lineLimit(1) + .font(.caption) + .foregroundStyle(Color(uiColor: UIColor.secondaryLabel)) + } + } +} + +// MARK: IndicatorView.ViewModel + +extension IndicatorView { + struct ViewModel { + let imageName: String + let text: String + } +} + +// MARK: Constants + +private extension CGFloat { + static let headerSpacing = 4.0 + static let headerImageSize = 12.0 +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SafariView/SafariView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SafariView/SafariView.swift new file mode 100644 index 0000000..9746374 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SafariView/SafariView.swift @@ -0,0 +1,36 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SafariServices +import SwiftUI + +struct SafariView: UIViewControllerRepresentable { + let url: URL + let onDismiss: () -> Void + + class Coordinator: NSObject, SFSafariViewControllerDelegate { + let parent: SafariView + + init(parent: SafariView) { + self.parent = parent + } + + func safariViewControllerDidFinish(_: SFSafariViewController) { + parent.onDismiss() + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + func makeUIViewController(context: Context) -> SFSafariViewController { + let safariViewController = SFSafariViewController(url: url) + safariViewController.delegate = context.coordinator + return safariViewController + } + + func updateUIViewController(_: SFSafariViewController, context _: Context) {} +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SegmentControlView/SegmentControlItemView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SegmentControlView/SegmentControlItemView.swift new file mode 100644 index 0000000..7432b98 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SegmentControlView/SegmentControlItemView.swift @@ -0,0 +1,48 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +// MARK: - SegmentControlItemView + +struct SegmentControlItemView: View { + // MARK: Properties + + let id: ID + let action: (() -> Void)? + @Binding var selection: ID + @ViewBuilder var content: () -> ContentView + @ViewBuilder var background: () -> BackgroundView + + // MARK: View + + var body: some View { + Button( + action: { + withAnimation { + selection = id + action?() + } + }, label: { + content() + } + ) + .clipShape(background()) + } +} + +#if DEBUG + struct SegmentControlItemView_Previews: PreviewProvider { + static var previews: some View { + SegmentControlItemView( + id: PostType.new, + action: nil, + selection: .constant(.new), + content: { Text(PostType.new.title) }, + background: { RoundedRectangle(cornerRadius: 20) } + ) + } + } +#endif diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SegmentControlView/SegmentControlView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SegmentControlView/SegmentControlView.swift new file mode 100644 index 0000000..bde8011 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/SharedComponents/UIComponents/SegmentControlView/SegmentControlView.swift @@ -0,0 +1,55 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +// MARK: - SegmentControlView + +struct SegmentControlView: View { + // MARK: Properties + + let segments: [ID] + @Binding var selection: ID + @ViewBuilder var content: (ID) -> ContentView + @ViewBuilder var background: () -> BackgroundView + + // MARK: View + + var body: some View { + ScrollViewReader { value in + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(Array(segments.enumerated()), id: \.offset) { index, segment in + SegmentControlItemView( + id: segment, + action: { value.scrollTo(index) }, + selection: $selection, + content: { content(segment) }, + background: { background() } + ) + .id(index) + .fixedSize() + } + } + .padding(.horizontal, 20.0) + } + } + } +} + +// MARK: - Preview + +#if DEBUG + struct SegmentControlView_Previews: PreviewProvider { + static var previews: some View { + SegmentControlView( + segments: PostType.allCases, + selection: .constant(.new), + content: { Text($0.title) }, + background: { RoundedRectangle(cornerRadius: 20) } + ) + } + } +#endif diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/CommentsPager.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/CommentsPager.swift new file mode 100644 index 0000000..15df5e6 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/CommentsPager.swift @@ -0,0 +1,25 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import Foundation + +actor CommentsPager: ICommentsPager { + // MARK: Properties + + private let paginator: CommentsPaginatorService + + // MARK: Initialization + + init(paginator: CommentsPaginatorService) { + self.paginator = paginator + } + + // MARK: Public + + func load(request: OffsetPaginationRequest, behaviour: LoadBehaviour, postID: Int) async throws -> Page { + try await paginator.loadPage(request: request, behaviour: behaviour, postID: postID) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/CommentsPaginatorService.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/CommentsPaginatorService.swift new file mode 100644 index 0000000..f9eb433 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/CommentsPaginatorService.swift @@ -0,0 +1,71 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import Foundation + +// MARK: - CommentsPaginatorService + +actor CommentsPaginatorService { + // MARK: Properties + + private let commentsService: ICommentsService + private let postsService: IPostsService + + private var ids: [Int] = [] + + // MARK: Initialization + + init(commentsService: ICommentsService, postsService: IPostsService) { + self.commentsService = commentsService + self.postsService = postsService + } + + // MARK: IOffsetPageLoader + + func loadPage(request: OffsetPaginationRequest, behaviour: LoadBehaviour, postID: Int) async throws -> Page { + ids = try await loadCommentsIDs(postID: postID, behaviour: behaviour) + let comments = try await loadComments(ids: Array(ids[safe: request.offset ... request.limit + request.offset - 1])) + + return Page(items: comments, hasMoreData: comments.count < ids.count) + } + + // MARK: Private + + private func loadCommentsIDs(postID: Int, behaviour: LoadBehaviour) async throws -> [Int] { + var ids: [Int] = [] + + switch behaviour { + case .reload: + let post = try await postsService.loadPosts(with: [postID]).first + ids = post?.kids ?? [] + case .useCache: + if !ids.isEmpty { return ids } + return try await loadCommentsIDs(postID: postID, behaviour: .reload) + } + + return ids + } + + private func loadComments(ids: [Int]) async throws -> [Comment] { + try await ids.asyncMap { id in + try await self.commentsService.loadComment(id: id) + } + } +} + +extension Sequence { + func asyncMap( + _ transform: (Element) async throws -> T + ) async rethrows -> [T] { + var values = [T]() + + for element in self { + try await values.append(transform(element)) + } + + return values + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/ICommentsPager.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/ICommentsPager.swift new file mode 100644 index 0000000..5f0fae3 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/ICommentsPager.swift @@ -0,0 +1,19 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import Foundation + +// MARK: - ICommentsPager + +protocol ICommentsPager { + func load(request: OffsetPaginationRequest, behaviour: LoadBehaviour, postID: Int) async throws -> Page +} + +extension ICommentsPager { + func load(request: OffsetPaginationRequest, postID: Int) async throws -> Page { + try await load(request: request, behaviour: .useCache, postID: postID) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/LoadBehaviour.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/LoadBehaviour.swift new file mode 100644 index 0000000..c94c241 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/CommentsPager/LoadBehaviour.swift @@ -0,0 +1,11 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +enum LoadBehaviour { + case reload + case useCache +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailAssembly.swift new file mode 100644 index 0000000..c598bf7 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailAssembly.swift @@ -0,0 +1,51 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture +import SwiftUI + +// MARK: - IPostDetailAssembly + +protocol IPostDetailAssembly { + func assemble(store: StoreOf) -> AnyView +} + +// MARK: - PostDetailAssembly + +final class PostDetailAssembly: BootstrappableAssembly, IPostDetailAssembly { + // MARK: Properties + + private let commentsService: ICommentsService + private let postsService: IPostsService + private let repliesAssembly: IRepliesAssembly + + // MARK: Initialization + + init(commentsService: ICommentsService, postsService: IPostsService, repliesAssembly: IRepliesAssembly) { + self.commentsService = commentsService + self.postsService = postsService + self.repliesAssembly = repliesAssembly + } + + // MARK: IPostDetailAssembly + + func assemble(store: StoreOf) -> AnyView { + let view = PostDetailView(store: store, repliesAssembly: repliesAssembly) + return view.eraseToAnyView() + } + + // MARK: BootstrappableAssembly + + override func bootstrap() { + Locator.shared.register(type: ICommentsPager.self) { + let paginator = CommentsPaginatorService( + commentsService: self.commentsService, + postsService: self.postsService + ) + return CommentsPager(paginator: paginator) + } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailFeature.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailFeature.swift new file mode 100644 index 0000000..d4a9ee4 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailFeature.swift @@ -0,0 +1,98 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import BladeTCA +import ComposableArchitecture +import Foundation + +// MARK: - PostDetailFeature + +@Reducer +struct PostDetailFeature { + // MARK: Types + + struct State: Equatable { + var viewModel: ArticleView.ViewModel + var paginator: PaginatorState + var isSafariViewPresented: Bool = false + var safariURL: URL? + var hasComments: Bool = true + + @PresentationState var replies: RepliesFeature.State? + } + + enum Action { + case presentSafariView(URL?) + case dismissSafariView + case refresh + case postLoaded(post: Post?) + case replyButtonTapped(commentID: Int) + case child(PaginatorAction) + case replies(PresentationAction) + case close + } + + // MARK: Dependencies + + @Dependency(\.postsService) var postsService + @Dependency(\.commentsPager) var pager + @Dependency(\.postDetailViewModelFactory) var viewModelFactory + @Dependency(\.postsViewModelFactory) var postsViewModelFactory + + // MARK: Reducer + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .refresh: + return .run { [state] send in + let post = try await postsService.loadPosts(with: [state.viewModel.articleID]) + return await send(.postLoaded(post: post.first)) + } + case let .presentSafariView(url): + state.safariURL = url + state.isSafariViewPresented = true + return .none + case .dismissSafariView: + state.isSafariViewPresented = false + state.safariURL = nil + return .none + case let .postLoaded(post): + if let post { + state.viewModel = postsViewModelFactory.makeViewModel(from: post) + state.hasComments = !post.kids.isEmpty + } + return .send(.child(.requestPage(.initial)), animation: .default) + case .child: + return .none + case .replies: + return .none + case let .replyButtonTapped(commentID): + state.replies = RepliesFeature.State(commentID: commentID, replies: []) + return .none + case .close: + return .none + } + } + .paginator( + state: \PostDetailFeature.State.paginator, + action: /PostDetailFeature.Action.child, + loadPage: { request, state in + try await pager.load(request: request, postID: state.viewModel.articleID) + .compactMap { viewModelFactory.makeViewModel(from: $0) } + } + ) + .ifLet(\.$replies, action: \.replies) { + RepliesFeature() + } + } +} + +// MARK: - Constants + +private extension OffsetPaginationRequest { + static let initial = OffsetPaginationRequest(limit: 5, offset: .zero) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailView.swift new file mode 100644 index 0000000..9594e15 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailView.swift @@ -0,0 +1,180 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import BladeTCA +import ComposableArchitecture +import HackerNewsLocalization +import SkeletonUI +import SwiftUI + +// MARK: - PostDetailView + +struct PostDetailView: View { + // MARK: Properties + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + @State private var isLoading = false + + private let repliesAssembly: IRepliesAssembly + + let store: StoreOf + + // MARK: Initialization + + init(store: StoreOf, repliesAssembly: IRepliesAssembly) { + self.repliesAssembly = repliesAssembly + self.store = store + } + + // MARK: View + + var body: some View { + contentView + .listStyle(.insetGrouped) + .listRowSpacing(.inset) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(L10n.PostDetails.NavigationBar.title) + .navigationDestination(store: store.scope(state: \.$replies, action: \.replies)) { store in + repliesAssembly.assemble(store: store) + } + .refreshable { await refresh() } + .onAppear { store.send(.refresh) } + } + + // MARK: Private + + private var contentView: some View { + WithViewStore(store, observe: { $0 }) { store in + toolbar(with: store) { + if store.hasComments { + commentsView(with: store) + } else { + emptyView(with: store) + } + } + } + } + + private func toolbar( + with store: ViewStore, + @ViewBuilder content: () -> some View + ) -> some View { + content() + .toolbar { + if horizontalSizeClass != .compact { + ToolbarItem(placement: .navigationBarLeading) { + Button(L10n.Common.Actions.close) { + store.send(.close) + } + .tint(.orange) + } + } + } + } + + private func commentsView(with store: ViewStore) -> some View { + PaginatorView( + store: self.store.scope(state: \.paginator, action: \.child), + content: { state, handler in + SkeletonView( + data: state, + quantity: .quantity, + configuration: .configuration, + builder: { comment, index in + if index == .zero { + articleView(with: store) + } + + comment.map { comment in + handler(comment) + } + }, + skeletonBuilder: { index in + reductedView(index: index) + } + ) + }, + rowContent: { item -> AnyView in + ShortCommentView( + viewModel: item, + action: { + store.send(.replyButtonTapped(commentID: item.id)) + } + ) + .eraseToAnyView() + } + ) + } + + private func emptyView(with store: ViewStore) -> some View { + List { + articleView(with: store) + } + } + + private func articleView(with store: ViewStore) -> some View { + ArticleView(viewModel: store.viewModel) + .onTapGesture { store.send(.presentSafariView(store.viewModel.url)) } + .fullScreenCover( + isPresented: store.binding( + get: \.isSafariViewPresented, + send: PostDetailFeature.Action.dismissSafariView + ) + ) { + if let url = store.safariURL { + SafariView(url: url, onDismiss: { + store.send(.dismissSafariView) + }) + } + } + } + + private func reductedView(index: Int) -> some View { + VStack { + if index == 0 { + HStack { + RoundedRectangle(cornerRadius: .cornerRadius) + RoundedRectangle(cornerRadius: .cornerRadius) + } + } else { + RoundedRectangle(cornerRadius: .cornerRadius) + } + } + } + + @MainActor + private func refresh() async { + defer { isLoading = false } + isLoading = true + await store.send(.refresh).finish() + } +} + +// MARK: - Constants + +private extension Int { + static let quantity = 20 + static let numberOfLines = 4 +} + +private extension CGFloat { + static let cornerRadius = 8.0 + static let inset = 8.0 +} + +private extension SkeletonConfiguration { + static let configuration = SkeletonConfiguration( + numberOfLines: .numberOfLines, + scales: [0.25, 1.0, 0.8], + insets: .init(top: .inset, leading: .zero, bottom: .inset, trailing: .zero), + gradient: Gradient(stops: [ + .init(color: Color(uiColor: .gray).opacity(0.3), location: 0.8), + .init(color: Color(uiColor: .gray).opacity(0.5), location: 0.9), + .init(color: Color(uiColor: .gray).opacity(0.3), location: 1.0), + ]) + ) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailViewModelFactory.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailViewModelFactory.swift new file mode 100644 index 0000000..729b354 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/PostDetailViewModelFactory.swift @@ -0,0 +1,63 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation +import HackerNewsLocalization + +// MARK: - IPostDetailViewModelFactory + +protocol IPostDetailViewModelFactory { + func makeViewModel(from comment: Comment) -> ShortCommentView.ViewModel? +} + +// MARK: - PostDetailViewModelFactory + +struct PostDetailViewModelFactory: IPostDetailViewModelFactory { + // MARK: Properties + + private let dateTimeFormatter: RelativeDateTimeFormatter + + // MARK: Initialization + + init(dateTimeFormatter: RelativeDateTimeFormatter) { + self.dateTimeFormatter = dateTimeFormatter + } + + // MARK: IPostDetailViewModelFactory + + func makeViewModel(from comment: Comment) -> ShortCommentView.ViewModel? { + guard let commentVM: CommentView.ViewModel = makeViewModel(from: comment) else { return nil } + + return ShortCommentView.ViewModel( + id: comment.id, + comment: commentVM, + answers: comment.kids.count == .zero ? nil : L10n.Comment.expandBranch(comment.kids.count) + ) + } + + // MARK: Private + + private func makeViewModel(from comment: Comment) -> CommentView.ViewModel? { + guard let text = comment.text, !comment.text.isNilOrEmpty else { + return nil + } + return CommentView.ViewModel( + username: comment.author ?? L10n.Comment.User.unknown, + date: dateTimeFormatter.localizedString( + for: Date(timeIntervalSince1970: TimeInterval(comment.time)), + relativeTo: Date() + ), + text: text + ) + } +} + +extension RelativeDateTimeFormatter { + static let standard: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .full + return formatter + }() +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/Views/ShortCommentView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/Views/ShortCommentView.swift new file mode 100644 index 0000000..64684bd --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/Views/ShortCommentView.swift @@ -0,0 +1,89 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +// MARK: - ShortCommentView + +struct ShortCommentView: View { + // MARK: Properties + + private let viewModel: ViewModel + private let action: () -> Void + + // MARK: Initialization + + init(viewModel: ViewModel, action: @escaping () -> Void) { + self.viewModel = viewModel + self.action = action + } + + // MARK: View + + var body: some View { + VStack(alignment: .center) { + CommentView(viewModel: viewModel.comment) + .frame(maxWidth: .infinity) + + if viewModel.answers != nil { + Divider() + } + + answersButton + .padding(.top, 4.0) + .buttonStyle(RepliesButtonStyle()) + } +// .padding() +// .background( +// RoundedRectangle(cornerRadius: .cornerRadius) +// .foregroundStyle(Color(uiColor: UIColor.secondarySystemBackground)) +// ) + } + + // MARK: Private + + private var answersButton: some View { + viewModel.answers.map { answer in + Button(action: { + action() + }, label: { + Text(answer) + }) + } + } +} + +// MARK: ShortCommentView.ViewModel + +extension ShortCommentView { + struct ViewModel: Equatable, Identifiable { + let id: Int + let comment: CommentView.ViewModel + let answers: String? + } +} + +// MARK: - Constants + +private extension CGFloat { + static let cornerRadius = 16.0 +} + +// MARK: Preview + +// #Preview { +// ShortCommentView( +// viewModel: ShortCommentView.ViewModel( +// id: 1, +// comment: CommentView.ViewModel( +// username: "nsvasilev", +// date: "22 hours ago", +// text: "Comment text" +// ), +// answers: "8 Replies" +// ), +// action: {} +// ) +// } diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/Views/Styles/RepliesButtonStyle.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/Views/Styles/RepliesButtonStyle.swift new file mode 100644 index 0000000..5bec46e --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/PostDetail/Views/Styles/RepliesButtonStyle.swift @@ -0,0 +1,15 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +struct RepliesButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + configuration.label + .foregroundStyle(configuration.isPressed ? Color.orange.opacity(0.8) : Color.orange) + .font(.footnote) + .fontWeight(.bold) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Model/Page+Map.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Model/Page+Map.swift new file mode 100644 index 0000000..06dbd37 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Model/Page+Map.swift @@ -0,0 +1,26 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade + +extension Page { + func map(_ closure: @escaping (T) throws -> U) rethrows -> Page { + let items = try items.map { try closure($0) } + let page = Page( + items: items, + hasMoreData: hasMoreData + ) + return page + } + + func compactMap(_ closure: @escaping (T) throws -> U?) rethrows -> Page { + let items = try items.compactMap { try closure($0) } + let page = Page( + items: items, + hasMoreData: hasMoreData + ) + return page + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Model/PostType.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Model/PostType.swift new file mode 100644 index 0000000..ef92ce1 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Model/PostType.swift @@ -0,0 +1,52 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation +import HackerNewsLocalization + +enum PostType: Int, CaseIterable, Identifiable { + case top + case new + case best + case ask + case show + case jobs + + var title: String { + switch self { + case .new: + return L10n.PostType.new + case .best: + return L10n.PostType.best + case .top: + return L10n.PostType.top + case .ask: + return L10n.PostType.ask + case .show: + return L10n.PostType.show + case .jobs: + return L10n.PostType.jobs + } + } + + var systemName: String { + switch self { + case .new: + return "sun.min.fill" + case .best: + return "star.fill" + case .top: + return "square.stack.3d.up.fill" + case .ask: + return "brain.head.profile.fill" + case .show: + return "eyes.inverse" + case .jobs: + return "suitcase.fill" + } + } + + var id: Self { self } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostViewModelFactory.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostViewModelFactory.swift new file mode 100644 index 0000000..c38718c --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostViewModelFactory.swift @@ -0,0 +1,75 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation +import HackerNewsLocalization + +// MARK: - IPostViewModelFactory + +protocol IPostViewModelFactory { + func makeViewModel(from post: Post) -> ArticleView.ViewModel +} + +// MARK: - PostViewModelFactory + +final class PostViewModelFactory: IPostViewModelFactory { + // MARK: Properties + + private let dateTimeFormatter: RelativeDateTimeFormatter + + // MARK: Initialization + + init(dateTimeFormatter: RelativeDateTimeFormatter) { + self.dateTimeFormatter = dateTimeFormatter + } + + // MARK: IPostViewModelFactory + + func makeViewModel(from post: Post) -> ArticleView.ViewModel { + ArticleView.ViewModel( + articleID: post.id, + title: post.title ?? "", + author: post.author ?? L10n.Comment.User.unknown, + link: makeLink(post.url), + rating: String(post.score ?? 0), + numberOfComments: post.kids.count, + date: dateTimeFormatter.localizedString( + for: Date( + timeIntervalSince1970: TimeInterval(post.time) + ), + relativeTo: Date() + ), + imageURL: makeImageURL(post.url), + url: makeURL(post.url) + ) + } + + // MARK: Private + + private func makeLink(_ link: String?) -> String? { + guard let link, let url = URL(string: link) else { return nil } + return url.host() + } + + private func makeImageURL(_ urlString: String?) -> URL? { + guard let urlString, let url = URL( + string: .extractURL + urlString + ) else { return nil } + return url + } + + private func makeURL(_ urlString: String?) -> URL? { + guard let urlString, let url = URL(string: urlString) else { + return nil + } + return url + } +} + +// MARK: - Constants + +private extension String { + static let extractURL = "http://www.google.com/s2/favicons?sz=64&domain=" +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsAssembly.swift new file mode 100644 index 0000000..3c3f080 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsAssembly.swift @@ -0,0 +1,90 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import Blade +import BladeTCA +import ComposableArchitecture +import SettingsInterfaces +import SwiftUI + +// MARK: - IPostsAssembly + +protocol IPostsAssembly { + func assemble() -> AnyView +} + +// MARK: - PostsAssembly + +final class PostsAssembly: BootstrappableAssembly, IPostsAssembly { + // MARK: Properties + + private let postsService: IPostsService + private let appNameProvider: IAppNameProvider + private let postDetailsAssembly: IPostDetailAssembly + private let settingsAssembly: ISettingsPublicAssembly + + private lazy var store: StoreOf = Store( + initialState: PostsViewStore.State( + selectedItem: .new, + paginator: PaginatorState(items: [], position: .zero) + ) + ) { + PostsViewStore() + } + + // MARK: Initialization + + init( + postsService: IPostsService, + appNameProvider: IAppNameProvider, + postDetailsAssembly: IPostDetailAssembly, + settingsAssembly: ISettingsPublicAssembly + ) { + self.postsService = postsService + self.appNameProvider = appNameProvider + self.postDetailsAssembly = postDetailsAssembly + self.settingsAssembly = settingsAssembly + super.init() + } + + // MARK: Override + + override func bootstrap() { + Locator.shared.register(.singleton) { + let paginators = Dictionary(uniqueKeysWithValues: PostType.allCases.map { + let paginatorService = PostsPaginatorService(postsService: self.postsService, postType: $0) + let paginator = PageLoaderProvider(paginatorService: paginatorService) + return ($0, paginator) + }) + + return PostsPager(paginators: paginators) + } + } + + // MARK: Public + + func assemble() -> AnyView { + PostsView( + store: store, + navigationTitleAssembly: navigationTitleAssembly, + postDetailsAssembly: postDetailsAssembly, + settingsAssembly: settingsAssembly + ).eraseToAnyView() + } + + // MARK: Private + + private var viewModelFactory: IPostViewModelFactory { + PostViewModelFactory(dateTimeFormatter: RelativeDateTimeFormatter()) + } + + private var navigationTitleAssembly: INavigationTitleAssembly { + NavigationTitleAssembly( + appNameProvider: appNameProvider, + dateTimeFormatter: RelativeDateTimeFormatter() + ) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsPager/PostsPager.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsPager/PostsPager.swift new file mode 100644 index 0000000..07381b7 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsPager/PostsPager.swift @@ -0,0 +1,28 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import Foundation +import NetworkLayerInterfaces + +actor PostsPager { + // MARK: Properties + + private let paginators: [PostType: any IOffsetPageLoader] + + // MARK: Initialization + + init(paginators: [PostType: any IOffsetPageLoader]) { + self.paginators = paginators + } + + // MARK: Public + + func load(request: OffsetPaginationRequest, postType: PostType) async throws -> Page { + // FIXME: Remove force unwrapping + // swiftlint:disable:next force_unwrapping + try await paginators[postType]!.loadPage(request: request) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsPager/PostsPaginatorService.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsPager/PostsPaginatorService.swift new file mode 100644 index 0000000..3a827a3 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsPager/PostsPaginatorService.swift @@ -0,0 +1,68 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import Foundation + +// MARK: - PostsPaginatorService + +actor PostsPaginatorService: IOffsetPageLoader { + // MARK: Properties + + private let postsService: IPostsService + private let postType: PostType + + private var ids: [PostType: [Int]] = [:] + + // MARK: Initialization + + init(postsService: IPostsService, postType: PostType) { + self.postsService = postsService + self.postType = postType + } + + // MARK: IPaginatorService + + // FIXME: Implement a force update when refreshing articles + func loadPage(request: OffsetPaginationRequest) async throws -> Page { + let ids = try await prefetchIDs(for: postType) + let posts = try await postsService.loadPosts( + with: Array(ids[safe: request.offset ... request.limit + request.offset - 1]) + ) + + let offset = request.limit + request.offset + + return Page( + items: posts, + hasMoreData: offset < ids.count + ) + } + + // MARK: Private + + private func prefetchIDs(for postType: PostType) async throws -> [Int] { + if let ids = ids[postType], !ids.isEmpty { return ids } + + let ids = try await postsService.loadIDs(for: postType) + self.ids[postType] = ids + return ids + } +} + +// MARK: Private + +// FIXME: Make a public extension +extension Array { + subscript(safe range: ClosedRange) -> [Element] { + let minIndex = Swift.max(range.lowerBound, 0) + let maxIndex = Swift.min(range.upperBound, count - 1) + + guard minIndex <= maxIndex else { + return [] + } + + return Array(self[minIndex ... maxIndex]) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsView.swift new file mode 100644 index 0000000..6d5f991 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsView.swift @@ -0,0 +1,161 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import DesignKit +import SettingsInterfaces +import SwiftUI + +// MARK: - PostsView + +struct PostsView: View { + // MARK: Properties + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + @State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn + @State private var isLoading = false + @State private var isPresented = false + + private let store: StoreOf + private let navigationTitleAssembly: INavigationTitleAssembly + private let postDetailsAssembly: IPostDetailAssembly + private let settingsAssembly: ISettingsPublicAssembly + + // MARK: Initialization + + init( + store: StoreOf, + navigationTitleAssembly: INavigationTitleAssembly, + postDetailsAssembly: IPostDetailAssembly, + settingsAssembly: ISettingsPublicAssembly + ) { + self.store = store + self.navigationTitleAssembly = navigationTitleAssembly + self.postDetailsAssembly = postDetailsAssembly + self.settingsAssembly = settingsAssembly + } + + // MARK: View + + var body: some View { + VStack { + if horizontalSizeClass == .compact { + compactView + } else { + splitView + } + } + .onAppear { + Task { + await refresh() + } + } + } + + // MARK: Private + + private var postListView: some View { + PostListView(store: store) + .scrollDisabled(isLoading) + .listStyle(.insetGrouped) + .listRowSpacing(.listRowSpacing) + .refreshable { + await refresh() + } + } + + private var compactView: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + NavigationStack { + VStack(spacing: .compactSpacing) { + segmentedControlView(viewStore: viewStore) + postListView + .navigationDestination(store: store.scope(state: \.$postDetail, action: \.postDetail)) { store in + postDetailsAssembly.assemble(store: store) + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { toolbarView } + } + } + } + + private var splitView: some View { + NavigationSplitView( + columnVisibility: $columnVisibility, + sidebar: { + PostSidebarView(store: store, settingsAssembly: settingsAssembly) + .toolbar { toolbarView } + }, content: { + WithViewStore(store, observe: { $0 }) { viewStore in + postListView + .navigationTitle(viewStore.selectedItem.title) + } + }, detail: { + NavigationStack { + IfLetStore(store.scope(state: \.$postDetail, action: \.postDetail)) { store in + postDetailsAssembly.assemble(store: store) + } + } + } + ) + .listStyle(.sidebar) + .navigationSplitViewStyle(.balanced) + .tint(.orange) + } + + private var toolbarView: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { + navigationTitleAssembly.assemble() + .padding(.bottom, 8.0) + } + } + + private func segmentedControlView( + viewStore: ViewStore + ) -> some View { + SegmentControlView( + segments: PostType.allCases, + selection: viewStore.binding( + get: \.selectedItem, + send: { .binding($0) } + ), + content: { segment in + HStack(alignment: .center, spacing: .spacing) { + Image(systemName: segment.systemName) + Text(segment.title) + .font(FontFamily.Montserrat.semiBold.font(size: .size17).sui) + } + .padding(.insets) + .foregroundColor(segment == viewStore.selectedItem ? Color(uiColor: .white) : Asset.dynamicLightGray.swiftUIColor) + .background(segment == viewStore.selectedItem ? Color(uiColor: .systemOrange) : Asset.dynamicGray.swiftUIColor) + }, + background: { + RoundedRectangle(cornerRadius: .cornerRadius, style: .continuous) + } + ) + } + + @MainActor + private func refresh() async { + defer { isLoading = false } + isLoading = true + await store.send(.refresh).finish() + } +} + +// MARK: - Constants + +private extension CGFloat { + static let cornerRadius = 20.0 + static let spacing = 4.0 + static let listRowSpacing = 8.0 + static let compactSpacing = 8.0 +} + +private extension EdgeInsets { + static let insets = EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsViewStore.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsViewStore.swift new file mode 100644 index 0000000..46bbe42 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/PostsViewStore.swift @@ -0,0 +1,89 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import Blade +import BladeTCA +import ComposableArchitecture +import Foundation + +// MARK: - PostsViewStore + +@Reducer +struct PostsViewStore { + // MARK: Types + + struct State: Equatable { + @BindingState var selectedItem: PostType + var selectedPostID: ArticleView.ViewModel.ID? + @PresentationState var postDetail: PostDetailFeature.State? + + var paginator: PaginatorState + } + + enum Action { + case refresh + case binding(PostType?) + + case selectItem(ArticleView.ViewModel) + case postDetail(PresentationAction) + case child(PaginatorAction) + } + + // MARK: Properties + + @Dependency(\.postsViewModelFactory) var viewModelFactory + @Dependency(\.postsPager) var pager + + // MARK: Reducer + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .refresh: + return .send(.child(.requestPage(.initial)), animation: .default) + case let .binding(postType): + guard let postType, postType != state.selectedItem else { + return .none + } + state.paginator.items = [] + state.selectedItem = postType + state.postDetail = nil + return .send(.refresh) + case let .selectItem(viewModel): + state.selectedPostID = viewModel.id + state.postDetail = .init( + viewModel: viewModel, + paginator: .init(items: [], position: .zero) + ) + return .none + case .child: + return .none + case .postDetail(.dismiss), .postDetail(.presented(.close)): + state.selectedPostID = nil + state.postDetail = nil + return .none + case .postDetail(.presented): + return .none + } + } + .paginator( + state: \PostsViewStore.State.paginator, + action: /PostsViewStore.Action.child, + loadPage: { request, state in + try await pager.load(request: request, postType: state.selectedItem) + .map { viewModelFactory.makeViewModel(from: $0) } + } + ) + .ifLet(\.$postDetail, action: \.postDetail) { + PostDetailFeature() + } + } +} + +// MARK: - Constants + +private extension OffsetPaginationRequest { + static let initial = OffsetPaginationRequest(limit: 20, offset: .zero) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleAssembly.swift new file mode 100644 index 0000000..f76daa1 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleAssembly.swift @@ -0,0 +1,45 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture +import SwiftUI + +// MARK: - INavigationTitleAssembly + +protocol INavigationTitleAssembly { + func assemble() -> AnyView +} + +// MARK: - NavigationTitleAssembly + +final class NavigationTitleAssembly: INavigationTitleAssembly { + // MARK: Properties + + private let dateTimeFormatter: RelativeDateTimeFormatter + private let appNameProvider: IAppNameProvider + + private lazy var store: StoreOf = Store( + initialState: NavigationTitleViewStore.State(date: "", appName: "") + ) { + NavigationTitleViewStore( + appNameProvider: self.appNameProvider, + dateFormatter: DateFormatter.EEEEMMMd + ) + } + + // MARK: Initialization + + init(appNameProvider: IAppNameProvider, dateTimeFormatter: RelativeDateTimeFormatter) { + self.appNameProvider = appNameProvider + self.dateTimeFormatter = dateTimeFormatter + } + + // MARK: INavigationTitleAssembly + + func assemble() -> AnyView { + NavigationTitleView(store: store).eraseToAnyView() + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleView.swift new file mode 100644 index 0000000..8809993 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleView.swift @@ -0,0 +1,37 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import DesignKit +import SwiftUI +import UIExtensions + +struct NavigationTitleView: View { + // MARK: Properties + + private let store: StoreOf + + // MARK: Initialization + + init(store: StoreOf) { + self.store = store + } + + // MARK: View + + var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + VStack(alignment: .leading) { + Text(viewStore.date) + .font(FontFamily.Montserrat.semiBold.font(size: .size13).sui) + Text(viewStore.appName) + .font(FontFamily.Montserrat.bold.font(size: .size17).sui) + } + } + .onAppear { + store.send(.appear) + } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleViewStore.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleViewStore.swift new file mode 100644 index 0000000..e927a30 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/NavigationTitleView/NavigationTitleViewStore.swift @@ -0,0 +1,44 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture +import Foundation + +struct NavigationTitleViewStore: Reducer { + // MARK: Types + + struct State: Equatable { + var date: String + var appName: String + } + + enum Action { + case appear + } + + // MARK: Properties + + private let appNameProvider: IAppNameProvider + private let dateFormatter: IDateFormatter + + // MARK: Initialization + + init(appNameProvider: IAppNameProvider, dateFormatter: IDateFormatter) { + self.appNameProvider = appNameProvider + self.dateFormatter = dateFormatter + } + + // MARK: Reducer + + func reduce(into state: inout State, action: Action) -> Effect { + switch action { + case .appear: + state.date = dateFormatter.string(from: Date()) + state.appName = appNameProvider.applicationName + return .none + } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/PostListView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/PostListView.swift new file mode 100644 index 0000000..874cd08 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/PostListView.swift @@ -0,0 +1,104 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import BladeTCA +import ComposableArchitecture +import SkeletonUI +import SwiftUI + +// MARK: - PostListView + +struct PostListView: View { + // MARK: Properties + + @State private var selectedID: ArticleView.ViewModel.ID? + + private let store: StoreOf + + // MARK: Initialization + + init(store: StoreOf) { + self.store = store + } + + // MARK: View + + var body: some View { + PaginatorView( + store: store.scope(state: \.paginator, action: \.child), + content: { state, handler in + ScrollViewReader { reader in + SkeletonView( + data: state, + quantity: .quantity, + configuration: .configuration, + builder: { article, index in + WithViewStore(store, observe: { $0 }) { store in + article.map { article in + handler(article) + .id(index) + .onTapGesture { store.send(.selectItem(article)) } + } + } + }, skeletonBuilder: { index in + reductedView(index: index) + } + ) + .onChange(of: state.isEmpty) { _, _ in + withAnimation { + reader.scrollTo(Int.zero) + } + } + } + }, + rowContent: { item -> ArticleView in + ArticleView(viewModel: item) + } + ) + } + + // MARK: Private + + private func reductedView(index: Int) -> some View { + VStack { + if index == 3 { + HStack { + RoundedRectangle(cornerRadius: .cornerRadius) + RoundedRectangle(cornerRadius: .cornerRadius) + RoundedRectangle(cornerRadius: .cornerRadius) + RoundedRectangle(cornerRadius: .cornerRadius) + } + } else { + RoundedRectangle(cornerRadius: .cornerRadius) + } + } + } +} + +// MARK: - Constants + +private extension Int { + static let quantity = 20 + static let numberOfLines = 4 +} + +private extension CGFloat { + static let cornerRadius = 8.0 + static let inset = 8.0 +} + +private extension SkeletonConfiguration { + static let configuration = SkeletonConfiguration( + numberOfLines: .numberOfLines, + scales: [0.5, 1.0, 0.3, 1], + insets: .init(top: .inset, leading: .zero, bottom: .inset, trailing: .zero), + gradient: Gradient(stops: [ + .init(color: Color(uiColor: .gray).opacity(0.3), location: 0.8), + .init(color: Color(uiColor: .gray).opacity(0.5), location: 0.9), + .init(color: Color(uiColor: .gray).opacity(0.3), location: 1.0), + ]) + ) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/PostSidebarView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/PostSidebarView.swift new file mode 100644 index 0000000..8bc9d93 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Posts/Views/PostSidebarView.swift @@ -0,0 +1,92 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import HackerNewsLocalization +import SettingsInterfaces +import SwiftUI + +// MARK: - PostSidebarView + +struct PostSidebarView: View { + // MARK: Properties + + private let settingsAssembly: ISettingsPublicAssembly + private let store: StoreOf + + @State private var feedExpanded = true + @State private var isSettingsPresented = false + + // MARK: Initialization + + init(store: StoreOf, settingsAssembly: ISettingsPublicAssembly) { + self.store = store + self.settingsAssembly = settingsAssembly + } + + // MARK: View + + var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + VStack { + sidebarView(viewStore: viewStore) + settingsButton + } + .sheet(isPresented: $isSettingsPresented, content: { + settingsAssembly.assemble() + }) + } + } + + // MARK: Private + + private var settingsButton: some View { + Button( + action: { + isSettingsPresented = true + }, label: { + HStack { + Image(systemName: .gear) + Text(L10n.Sidebar.Items.settings) + } + } + ) + .tint(.orange) + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.small) + } + + private func sidebarView(viewStore: ViewStore) -> some View { + List( + selection: viewStore.binding( + get: { $0.selectedItem }, + send: { .binding($0?.id) } + ) + ) { + DisclosureGroup( + isExpanded: $feedExpanded, + content: { + ForEach(PostType.allCases) { postType in + Label(postType.title, systemImage: postType.systemName) + } + }, + label: { + Text(L10n.Sidebar.Groups.Feeds.title) + .font(.headline) + } + ) + } + .listStyle(.sidebar) + .tint(.orange) + .scrollContentBackground(.hidden) + } +} + +// MARK: - Constants + +private extension String { + static let gear = "gear" +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesAssembly.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesAssembly.swift new file mode 100644 index 0000000..26d8de2 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesAssembly.swift @@ -0,0 +1,45 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import AppUtils +import ComposableArchitecture +import SwiftUI + +// MARK: - IRepliesAssembly + +protocol IRepliesAssembly { + func assemble(store: StoreOf) -> AnyView +} + +// MARK: - RepliesAssembly + +final class RepliesAssembly: BootstrappableAssembly, IRepliesAssembly { + // MARK: Properties + + private let commentsService: ICommentsService + private let postsService: IPostsService + + // MARK: Initialization + + init(commentsService: ICommentsService, postsService: IPostsService) { + self.commentsService = commentsService + self.postsService = postsService + } + + // MARK: IRepliesAssembly + + func assemble(store: StoreOf) -> AnyView { + let view = RepliesView(store: store) + return view.eraseToAnyView() + } + + // MARK: IBootstrappable + + override func bootstrap() { + Locator.shared.register(type: IRepliesService.self) { + RepliesService(commentsService: self.commentsService) + } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesFeature.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesFeature.swift new file mode 100644 index 0000000..f05a2ec --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesFeature.swift @@ -0,0 +1,44 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture + +@Reducer +struct RepliesFeature { + // MARK: Types + + struct State: Equatable { + let commentID: Int + var replies: [RepliesCommentView.ViewModel] + } + + enum Action { + case refresh + case replies([RepliesCommentView.ViewModel]) + } + + // MARK: Properties + + @Dependency(\.repliesService) var repliesService + @Dependency(\.repliesViewModelFactory) var viewModelFactory + + // MARK: Reducer + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .refresh: + return .run { [state] send in + let comments = try await repliesService.loadComments(for: state.commentID) + let viewModels = viewModelFactory.makeViewModel(from: comments) + return await send(.replies(viewModels)) + } + case let .replies(viewModel): + state.replies = viewModel + return .none + } + } + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/IRepliesService.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/IRepliesService.swift new file mode 100644 index 0000000..25d6889 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/IRepliesService.swift @@ -0,0 +1,11 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Blade +import Foundation + +protocol IRepliesService { + func loadComments(for commentID: Int) async throws -> ReplyComment +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/RepliesService.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/RepliesService.swift new file mode 100644 index 0000000..a632de4 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/RepliesService.swift @@ -0,0 +1,44 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +// MARK: - RepliesService + +final class RepliesService { + // MARK: Properties + + private let commentsService: ICommentsService + + // MARK: Initialization + + init(commentsService: ICommentsService) { + self.commentsService = commentsService + } +} + +// MARK: IRepliesService + +extension RepliesService: IRepliesService { + func loadComments(for id: Int) async throws -> ReplyComment { + let comment = try await commentsService.loadComment(id: id) + + if comment.kids.isEmpty { + return ReplyComment(comment: comment) + } + + let subComments = try await withThrowingTaskGroup(of: ReplyComment.self, returning: [ReplyComment].self) { taskGroup in + for id in comment.kids { + taskGroup.addTask { try await self.loadComments(for: id) } + } + + let comments = try await taskGroup.reduce(into: [ReplyComment]()) { $0.append($1) } + .sorted(by: { $0.comment.time < $1.comment.time }) + return comments + } + + return ReplyComment(comment: comment, replies: subComments) + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/ReplyComment.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/ReplyComment.swift new file mode 100644 index 0000000..4b2062b --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesService/ReplyComment.swift @@ -0,0 +1,11 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +struct ReplyComment { + var comment: Comment + var replies: [ReplyComment] = [] +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesView.swift new file mode 100644 index 0000000..69f6fb3 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesView.swift @@ -0,0 +1,98 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import BladeTCA +import ComposableArchitecture +import HackerNewsLocalization +import SkeletonUI +import SwiftUI + +// MARK: - RepliesView + +struct RepliesView: View { + // MARK: Properties + + let store: StoreOf + + // MARK: Initialization + + init(store: StoreOf) { + self.store = store + } + + // MARK: View + + var body: some View { + WithViewStore(store, observe: { $0 }) { store in + NavigationStack { + contentView(with: store) + .listStyle(.insetGrouped) + .listRowSpacing(.listRowSpacing) + .navigationTitle(L10n.Replies.NavigationBar.title) + .toolbarTitleDisplayMode(.inline) + } + .onAppear { + store.send(.refresh) + } + } + } + + // MARK: Private + + private func contentView(with store: ViewStore) -> some View { + SkeletonView( + data: store.replies, + quantity: .quantity, + configuration: .configuration, + builder: { reply, _ in + reply.map { reply in + RepliesCommentView(viewModel: reply) + } + }, + skeletonBuilder: { index in + reductedView(index: index) + } + ) + } + + private func reductedView(index: Int) -> some View { + VStack { + if index == .zero { + HStack { + RoundedRectangle(cornerRadius: .cornerRadius) + RoundedRectangle(cornerRadius: .cornerRadius) + } + } else { + RoundedRectangle(cornerRadius: .cornerRadius) + } + } + } +} + +// MARK: - Constants + +private extension Int { + static let quantity = 20 + static let numberOfLines = 4 +} + +private extension CGFloat { + static let cornerRadius = 8.0 + static let inset = 8.0 + static let listRowSpacing = 8.0 +} + +private extension SkeletonConfiguration { + static let configuration = SkeletonConfiguration( + numberOfLines: .numberOfLines, + scales: [0.25, 1.0, 0.8], + insets: .init(top: .inset, leading: .zero, bottom: .inset, trailing: .zero), + gradient: Gradient(stops: [ + .init(color: Color(uiColor: .gray).opacity(0.3), location: 0.8), + .init(color: Color(uiColor: .gray).opacity(0.5), location: 0.9), + .init(color: Color(uiColor: .gray).opacity(0.3), location: 1.0), + ]) + ) +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesViewModelFactory.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesViewModelFactory.swift new file mode 100644 index 0000000..3a9e9e3 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/RepliesViewModelFactory.swift @@ -0,0 +1,68 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +// MARK: - IRepliesViewModelFactory + +protocol IRepliesViewModelFactory { + func makeViewModel(from reply: ReplyComment) -> [RepliesCommentView.ViewModel] +} + +// MARK: - RepliesViewModelFactory + +final class RepliesViewModelFactory: IRepliesViewModelFactory { + // MARK: Properties + + private let dateTimeFormatter: RelativeDateTimeFormatter + + // MARK: Initialization + + init(dateTimeFormatter: RelativeDateTimeFormatter) { + self.dateTimeFormatter = dateTimeFormatter + } + + // MARK: IRepliesViewModelFactory + + func makeViewModel(from reply: ReplyComment) -> [RepliesCommentView.ViewModel] { + makeViewModel(from: reply, level: .zero) + } + + // MARK: Private + + private func makeViewModel(from reply: ReplyComment, level: Int) -> [RepliesCommentView.ViewModel] { + let comment = makeViewModel(from: reply.comment) + + var comments: [RepliesCommentView.ViewModel] = [RepliesCommentView.ViewModel(comment: comment, level: level)] + + if reply.replies.isEmpty { + return comments + } + + for reply in reply.replies where !reply.comment.text.isNilOrEmpty { + let replies = makeViewModel(from: reply, level: level + 1) + comments.append(contentsOf: replies) + } + + return comments + } + + private func makeViewModel(from comment: Comment) -> CommentView.ViewModel { + CommentView.ViewModel( + username: comment.author ?? "", + date: dateTimeFormatter.localizedString( + for: Date(timeIntervalSince1970: TimeInterval(comment.time)), + relativeTo: Date() + ), + text: comment.text ?? "" + ) + } +} + +extension String? { + var isNilOrEmpty: Bool { + self == nil || self?.isEmpty == true + } +} diff --git a/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/Views/RepliesCommentView.swift b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/Views/RepliesCommentView.swift new file mode 100644 index 0000000..5a1bad5 --- /dev/null +++ b/Modules/Features/Home/Sources/Home/Classes/UI/Presentation/UserStories/Replies/Views/RepliesCommentView.swift @@ -0,0 +1,71 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +// MARK: - RepliesCommentView + +struct RepliesCommentView: View { + // MARK: Properties + + let viewModel: ViewModel + + // MARK: View + + var body: some View { + contentView + } + + // MARK: Private + + private var commentView: some View { + VStack(alignment: .leading) { + Text(AttributedString(.html(withBody: viewModel.comment.text))) + } + } + + private var verticalLine: some View { + RoundedRectangle(cornerRadius: 2.0) + .frame(maxHeight: .infinity) + .frame(width: 2.0) + .foregroundStyle(.orange) + } + + private var contentView: some View { + VStack(alignment: .leading) { + CommentHeaderView(viewModel: .init(username: viewModel.comment.username, date: viewModel.comment.date)) + + Divider() + + HStack { + if viewModel.level > 0 { + verticalLine + .padding(.vertical, 4.0) + } + + commentView + } + .padding(.leading, .spacing * CGFloat(viewModel.level)) + } + .fixedSize(horizontal: false, vertical: true) + } +} + +// MARK: RepliesCommentView.ViewModel + +extension RepliesCommentView { + struct ViewModel: Identifiable, Equatable { + var id: UUID { comment.id } + let comment: CommentView.ViewModel + let level: Int + } +} + +// MARK: - Constants + +private extension CGFloat { + static let cornerRadius = 16.0 + static let spacing = 8.0 +} diff --git a/Modules/Features/Home/Sources/HomeInterfaces/Classes/DI/IHomePublicAssembly.swift b/Modules/Features/Home/Sources/HomeInterfaces/Classes/DI/IHomePublicAssembly.swift new file mode 100644 index 0000000..6254eb4 --- /dev/null +++ b/Modules/Features/Home/Sources/HomeInterfaces/Classes/DI/IHomePublicAssembly.swift @@ -0,0 +1,10 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +public protocol IHomePublicAssembly { + func assemble() -> AnyView +} diff --git a/Modules/Features/Home/Tests/HomeTests/HomeTests.swift b/Modules/Features/Home/Tests/HomeTests/HomeTests.swift new file mode 100644 index 0000000..2ef80a3 --- /dev/null +++ b/Modules/Features/Home/Tests/HomeTests/HomeTests.swift @@ -0,0 +1,9 @@ +// +// HackerNews +// Copyright © 2023 Nikita Vasilev. All rights reserved. +// + +@testable import Home +import XCTest + +final class HomeTests: XCTestCase {} diff --git a/Modules/Features/Settings/.gitignore b/Modules/Features/Settings/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/Modules/Features/Settings/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Modules/Features/Settings/Makefile b/Modules/Features/Settings/Makefile new file mode 100644 index 0000000..76c5b88 --- /dev/null +++ b/Modules/Features/Settings/Makefile @@ -0,0 +1,3 @@ +## swiftgen: Trigger code generation from assets with swiftgen tool +swiftgen: + swiftgen \ No newline at end of file diff --git a/Modules/Features/Settings/Package.swift b/Modules/Features/Settings/Package.swift new file mode 100644 index 0000000..cc25c09 --- /dev/null +++ b/Modules/Features/Settings/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable prefixed_toplevel_constant + +import PackageDescription + +let package = Package( + name: "Settings", + platforms: [.iOS(.v17)], + products: [ + .library(name: "Settings", targets: ["Settings"]), + .library(name: "SettingsInterfaces", targets: ["SettingsInterfaces"]), + ], + dependencies: [ + .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", .upToNextMajor(from: "1.5.5")), + .package(path: "../../Common/HackerNewsLocalization"), + .package(path: "../../Common/DesignKit"), + .package(path: "../../Common/UIExtensions"), + ], + targets: [ + .target( + name: "Settings", + dependencies: [ + "SettingsInterfaces", + .product(name: "UIExtensions", package: "UIExtensions"), + .product(name: "DesignKit", package: "DesignKit"), + .product(name: "HackerNewsLocalization", package: "HackerNewsLocalization"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ] + ), + .target(name: "SettingsInterfaces"), + .testTarget(name: "SettingsTests", dependencies: ["Settings"]), + ] +) diff --git a/Modules/Features/Settings/Sources/Settings/Classes/DI/PresentationAssembly/SettingsPresentationAssembly.swift b/Modules/Features/Settings/Sources/Settings/Classes/DI/PresentationAssembly/SettingsPresentationAssembly.swift new file mode 100644 index 0000000..e9088de --- /dev/null +++ b/Modules/Features/Settings/Sources/Settings/Classes/DI/PresentationAssembly/SettingsPresentationAssembly.swift @@ -0,0 +1,20 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import Foundation + +// MARK: - ISettingsPresentationAssembly + +protocol ISettingsPresentationAssembly { + var rootSettingsAssembly: IRootSettingsAssembly { get } +} + +// MARK: - SettingsPresentationAssembly + +final class SettingsPresentationAssembly: ISettingsPresentationAssembly { + var rootSettingsAssembly: any IRootSettingsAssembly { + RootSettingsAssembly() + } +} diff --git a/Modules/Features/Settings/Sources/Settings/Classes/DI/PublicAssembly/SettingsPublicAssembly.swift b/Modules/Features/Settings/Sources/Settings/Classes/DI/PublicAssembly/SettingsPublicAssembly.swift new file mode 100644 index 0000000..e7e0618 --- /dev/null +++ b/Modules/Features/Settings/Sources/Settings/Classes/DI/PublicAssembly/SettingsPublicAssembly.swift @@ -0,0 +1,19 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SettingsInterfaces +import SwiftUI + +public final class SettingsPublicAssembly: ISettingsPublicAssembly { + private let presentationAssembly: ISettingsPresentationAssembly + + public init() { + presentationAssembly = SettingsPresentationAssembly() + } + + public func assemble() -> AnyView { + presentationAssembly.rootSettingsAssembly.assemble() + } +} diff --git a/Modules/Features/Settings/Sources/Settings/Classes/Generated/.gitkeep b/Modules/Features/Settings/Sources/Settings/Classes/Generated/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/SharedComponents/UIComponents/MailView/MailView.swift b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/SharedComponents/UIComponents/MailView/MailView.swift new file mode 100644 index 0000000..ccc99e7 --- /dev/null +++ b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/SharedComponents/UIComponents/MailView/MailView.swift @@ -0,0 +1,42 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import MessageUI +import SwiftUI + +struct MailView: UIViewControllerRepresentable { + @Binding var isPresented: Bool + + class Coordinator: NSObject, MFMailComposeViewControllerDelegate { + @Binding var isPresented: Bool + + init(isPresented: Binding) { + _isPresented = isPresented + } + + func mailComposeController( + _ controller: MFMailComposeViewController, + didFinishWith _: MFMailComposeResult, + error _: Error? + ) { + isPresented = false + controller.dismiss(animated: true) + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(isPresented: $isPresented) + } + + func makeUIViewController(context: Context) -> MFMailComposeViewController { + let vc = MFMailComposeViewController() + vc.setSubject("Feedback") + vc.setToRecipients(["your_email@example.com"]) + vc.mailComposeDelegate = context.coordinator + return vc + } + + func updateUIViewController(_: MFMailComposeViewController, context _: Context) {} +} diff --git a/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsAssembly.swift b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsAssembly.swift new file mode 100644 index 0000000..7a2bcd1 --- /dev/null +++ b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsAssembly.swift @@ -0,0 +1,27 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import SwiftUI + +// MARK: - IRootSettingsAssembly + +protocol IRootSettingsAssembly { + func assemble() -> AnyView +} + +// MARK: - RootSettingsAssembly + +final class RootSettingsAssembly: IRootSettingsAssembly { + private lazy var store: StoreOf = Store( + initialState: RootSettingsFeature.State(isEmailSheetPresented: false) + ) { + RootSettingsFeature() + } + + func assemble() -> AnyView { + AnyView(RootSettingsView(store: store)) + } +} diff --git a/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsFeature.swift b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsFeature.swift new file mode 100644 index 0000000..4f2435e --- /dev/null +++ b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsFeature.swift @@ -0,0 +1,56 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import UIKit + +// MARK: - RootSettingsFeature + +@Reducer +struct RootSettingsFeature { + struct State: Equatable { + var isEmailSheetPresented: Bool = false + } + + enum Action: Equatable { + case sendFeedback + case rateUs + case openGitHub + case setEmailSheetPresented(Bool) + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .sendFeedback: + state.isEmailSheetPresented = true + return .none + + case .rateUs: + if let url = URL(string: .rateUs) { + UIApplication.shared.open(url) + } + return .none + + case .openGitHub: + if let url = URL(string: .githubURL) { + UIApplication.shared.open(url) + } + return .none + + case let .setEmailSheetPresented(isPresented): + state.isEmailSheetPresented = isPresented + return .none + } + } + } +} + +// MARK: - Constants + +private extension String { + static let githubURL = "https://github.com/nik3212/HackerNews" + static let rateUs = "https://apps.apple.com/app/idYOUR_APP_ID?action=write-review" +} diff --git a/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsView.swift b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsView.swift new file mode 100644 index 0000000..62f4d18 --- /dev/null +++ b/Modules/Features/Settings/Sources/Settings/Classes/UI/Presentation/UserStories/RootSettings/RootSettingsView.swift @@ -0,0 +1,103 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import ComposableArchitecture +import DesignKit +import HackerNewsLocalization +import SwiftUI +import UIExtensions + +// MARK: - RootSettingsView + +struct RootSettingsView: View { + // MARK: Properties + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @Environment(\.presentationMode) private var presentationMode + + let store: StoreOf + + // MARK: Initialization + + init(store: StoreOf) { + self.store = store + } + + // MARK: View + + var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + NavigationView { + contentView(viewStore) + .navigationTitle(L10n.Settings.Navbar.title) + .toolbar { + if horizontalSizeClass != .compact { + ToolbarItem(placement: .navigationBarLeading) { + Button(L10n.Common.Actions.cancel) { + presentationMode.wrappedValue.dismiss() + } + .tint(.orange) + } + } + } + } + .listStyle(.insetGrouped) + .tint(Color(uiColor: .label)) + } + } + + private var headerView: some View { + HStack { + Asset.Settings.headerIcon.swiftUIImage + + VStack { + Text(String.appName) + .font(FontFamily.Montserrat.semiBold.font(size: .size17).sui) + Text(L10n.Settings.author(String.author)) + .font(FontFamily.Montserrat.semiBold.font(size: .size13).sui) + } + } + } + + private func contentView(_ viewStore: ViewStore) -> some View { + List { + headerView + + Button(action: { + viewStore.send(.sendFeedback) + }) { + NavigationLink(L10n.Settings.Menu.sendFeedback, destination: EmptyView()) + } + .sheet(isPresented: viewStore.binding( + get: \.isEmailSheetPresented, + send: RootSettingsFeature.Action.setEmailSheetPresented + )) { + MailView(isPresented: viewStore.binding( + get: \.isEmailSheetPresented, + send: RootSettingsFeature.Action.setEmailSheetPresented + )) + } + + Button(action: { + viewStore.send(.rateUs) + }) { + NavigationLink(L10n.Settings.Menu.rateUs, destination: EmptyView()) + } + + Button(action: { + viewStore.send(.openGitHub) + }) { + NavigationLink(L10n.Settings.Menu.github, destination: EmptyView()) + } + } + } +} + +// MARK: - Constants + +private extension String { + static let author = "Nikita Vasilev" + static let appName = "HackerNews" +} diff --git a/HackerNewsWatch WatchKit Extension/Assets.xcassets/Contents.json b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Contents.json similarity index 100% rename from HackerNewsWatch WatchKit Extension/Assets.xcassets/Contents.json rename to Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Contents.json diff --git a/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/Contents.json b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/Contents.json b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/Contents.json similarity index 61% rename from HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/Contents.json rename to Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/Contents.json index c984430..b903e45 100644 --- a/HackerNews/Resources/Assets.xcassets/TabBar/News.imageset/Contents.json +++ b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/Contents.json @@ -2,22 +2,21 @@ "images" : [ { "idiom" : "universal", - "filename" : "25x25.png", "scale" : "1x" }, { + "filename" : "app_icon-60@2x.png", "idiom" : "universal", - "filename" : "25x25@2x.png", "scale" : "2x" }, { + "filename" : "app_icon-60@3x.png", "idiom" : "universal", - "filename" : "25x25@3x.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/app_icon-60@2x.png b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/app_icon-60@2x.png new file mode 100644 index 0000000..cc06bae Binary files /dev/null and b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/app_icon-60@2x.png differ diff --git a/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/app_icon-60@3x.png b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/app_icon-60@3x.png new file mode 100644 index 0000000..b87beb1 Binary files /dev/null and b/Modules/Features/Settings/Sources/Settings/Resources/Assets.xcassets/Settings/header-icon.imageset/app_icon-60@3x.png differ diff --git a/Modules/Features/Settings/Sources/SettingsInterfaces/Classes/DI/ISettingsPublicAssembly.swift b/Modules/Features/Settings/Sources/SettingsInterfaces/Classes/DI/ISettingsPublicAssembly.swift new file mode 100644 index 0000000..55de4ff --- /dev/null +++ b/Modules/Features/Settings/Sources/SettingsInterfaces/Classes/DI/ISettingsPublicAssembly.swift @@ -0,0 +1,10 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +import SwiftUI + +public protocol ISettingsPublicAssembly { + func assemble() -> AnyView +} diff --git a/Modules/Features/Settings/Tests/SettingsTests/SettingsTests.swift b/Modules/Features/Settings/Tests/SettingsTests/SettingsTests.swift new file mode 100644 index 0000000..c15a2f3 --- /dev/null +++ b/Modules/Features/Settings/Tests/SettingsTests/SettingsTests.swift @@ -0,0 +1,9 @@ +// +// HackerNews +// Copyright © 2024 Nikita Vasilev. All rights reserved. +// + +@testable import Settings +import XCTest + +final class SettingsTests: XCTestCase {} diff --git a/Modules/Features/Settings/swiftgen.yml b/Modules/Features/Settings/swiftgen.yml new file mode 100644 index 0000000..ce206ad --- /dev/null +++ b/Modules/Features/Settings/swiftgen.yml @@ -0,0 +1,9 @@ +input_dir: Sources/Settings/Resources +output_dir: Sources/Settings/Classes/Generated +xcassets: + inputs: Assets.xcassets + outputs: + templateName: swift5 + output: Assets.swift + params: + publicAccess: true diff --git a/Podfile b/Podfile deleted file mode 100644 index a313434..0000000 --- a/Podfile +++ /dev/null @@ -1,74 +0,0 @@ -source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '10.0' -use_frameworks! - -workspace 'HackerNews' - -xcodeproj 'HackerNews.xcodeproj' -xcodeproj 'Features/NetworkManager/NetworkManager.xcodeproj' - -def analytics_pods - pod 'Firebase/Crashlytics' -end - -def di_pods - pod 'Swinject' -end - -def app_pods - pod 'R.swift' - pod 'Kingfisher' - pod "Skeleton" - pod 'EmptyDataSet-Swift', '~> 5.0.0' -end - -def other_pods - pod 'SwiftLint' - pod 'Sourcery' -end - -def test_pods - pod 'Nimble' - pod 'Quick' - pod 'iOSSnapshotTestCase' -end - -def test_ui_pods - pod 'iOSSnapshotTestCase' -end - -target :HackerNews do - xcodeproj 'HackerNews.xcodeproj' - app_pods - di_pods - other_pods - analytics_pods -end - -target :HackerNewsTests do - xcodeproj 'HackerNews.xcodeproj' - test_pods - di_pods - analytics_pods -end - -target :HackerNewsUITests do - xcodeproj 'HackerNews.xcodeproj' - test_ui_pods -end - -target :NetworkManager do - xcodeproj 'Features/NetworkManager/NetworkManager.xcodeproj' -end - -target :NetworkManagerTests do - xcodeproj 'Features/NetworkManager/NetworkManager.xcodeproj' -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['SWIFT_VERSION'] = '5.0' - end - end -end diff --git a/R.generated.swift b/R.generated.swift deleted file mode 100644 index dafb41e..0000000 --- a/R.generated.swift +++ /dev/null @@ -1,1132 +0,0 @@ -// -// This is a generated file, do not edit! -// Generated by R.swift, see https://github.com/mac-cain13/R.swift -// - -import Foundation -import Rswift -import UIKit - -/// This `R` struct is generated and contains references to static resources. -struct R: Rswift.Validatable { - fileprivate static let applicationLocale = hostingBundle.preferredLocalizations.first.flatMap { Locale(identifier: $0) } ?? Locale.current - fileprivate static let hostingBundle = Bundle(for: R.Class.self) - - /// Find first language and bundle for which the table exists - fileprivate static func localeBundle(tableName: String, preferredLanguages: [String]) -> (Foundation.Locale, Foundation.Bundle)? { - // Filter preferredLanguages to localizations, use first locale - var languages = preferredLanguages - .map { Locale(identifier: $0) } - .prefix(1) - .flatMap { locale -> [String] in - if hostingBundle.localizations.contains(locale.identifier) { - if let language = locale.languageCode, hostingBundle.localizations.contains(language) { - return [locale.identifier, language] - } else { - return [locale.identifier] - } - } else if let language = locale.languageCode, hostingBundle.localizations.contains(language) { - return [language] - } else { - return [] - } - } - - // If there's no languages, use development language as backstop - if languages.isEmpty { - if let developmentLocalization = hostingBundle.developmentLocalization { - languages = [developmentLocalization] - } - } else { - // Insert Base as second item (between locale identifier and languageCode) - languages.insert("Base", at: 1) - - // Add development language as backstop - if let developmentLocalization = hostingBundle.developmentLocalization { - languages.append(developmentLocalization) - } - } - - // Find first language for which table exists - // Note: key might not exist in chosen language (in that case, key will be shown) - for language in languages { - if let lproj = hostingBundle.url(forResource: language, withExtension: "lproj"), - let lbundle = Bundle(url: lproj) - { - let strings = lbundle.url(forResource: tableName, withExtension: "strings") - let stringsdict = lbundle.url(forResource: tableName, withExtension: "stringsdict") - - if strings != nil || stringsdict != nil { - return (Locale(identifier: language), lbundle) - } - } - } - - // If table is available in main bundle, don't look for localized resources - let strings = hostingBundle.url(forResource: tableName, withExtension: "strings", subdirectory: nil, localization: nil) - let stringsdict = hostingBundle.url(forResource: tableName, withExtension: "stringsdict", subdirectory: nil, localization: nil) - - if strings != nil || stringsdict != nil { - return (applicationLocale, hostingBundle) - } - - // If table is not found for requested languages, key will be shown - return nil - } - - /// Load string from Info.plist file - fileprivate static func infoPlistString(path: [String], key: String) -> String? { - var dict = hostingBundle.infoDictionary - for step in path { - guard let obj = dict?[step] as? [String: Any] else { return nil } - dict = obj - } - return dict?[key] as? String - } - - static func validate() throws { - try font.validate() - try intern.validate() - } - - #if os(iOS) || os(tvOS) - /// This `R.storyboard` struct is generated, and contains static references to 4 storyboards. - struct storyboard { - /// Storyboard `Comments`. - static let comments = _R.storyboard.comments() - /// Storyboard `LaunchScreen`. - static let launchScreen = _R.storyboard.launchScreen() - /// Storyboard `Settings`. - static let settings = _R.storyboard.settings() - /// Storyboard `Theme`. - static let theme = _R.storyboard.theme() - - #if os(iOS) || os(tvOS) - /// `UIStoryboard(name: "Comments", bundle: ...)` - static func comments(_: Void = ()) -> UIKit.UIStoryboard { - return UIKit.UIStoryboard(resource: R.storyboard.comments) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIStoryboard(name: "LaunchScreen", bundle: ...)` - static func launchScreen(_: Void = ()) -> UIKit.UIStoryboard { - return UIKit.UIStoryboard(resource: R.storyboard.launchScreen) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIStoryboard(name: "Settings", bundle: ...)` - static func settings(_: Void = ()) -> UIKit.UIStoryboard { - return UIKit.UIStoryboard(resource: R.storyboard.settings) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIStoryboard(name: "Theme", bundle: ...)` - static func theme(_: Void = ()) -> UIKit.UIStoryboard { - return UIKit.UIStoryboard(resource: R.storyboard.theme) - } - #endif - - fileprivate init() {} - } - #endif - - /// This `R.entitlements` struct is generated, and contains static references to 2 properties. - struct entitlements { - static let comAppleSecurityAppSandbox = true - static let comAppleSecurityNetworkClient = true - - fileprivate init() {} - } - - /// This `R.file` struct is generated, and contains static references to 9 files. - struct file { - /// Resource file `GoogleService-Info.plist`. - static let googleServiceInfoPlist = Rswift.FileResource(bundle: R.hostingBundle, name: "GoogleService-Info", pathExtension: "plist") - /// Resource file `Poppins-Bold.ttf`. - static let poppinsBoldTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-Bold", pathExtension: "ttf") - /// Resource file `Poppins-ExtraBold.ttf`. - static let poppinsExtraBoldTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-ExtraBold", pathExtension: "ttf") - /// Resource file `Poppins-ExtraLight.ttf`. - static let poppinsExtraLightTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-ExtraLight", pathExtension: "ttf") - /// Resource file `Poppins-Italic.ttf`. - static let poppinsItalicTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-Italic", pathExtension: "ttf") - /// Resource file `Poppins-Light.ttf`. - static let poppinsLightTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-Light", pathExtension: "ttf") - /// Resource file `Poppins-Medium.ttf`. - static let poppinsMediumTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-Medium", pathExtension: "ttf") - /// Resource file `Poppins-Regular.ttf`. - static let poppinsRegularTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-Regular", pathExtension: "ttf") - /// Resource file `Poppins-SemiBold.ttf`. - static let poppinsSemiBoldTtf = Rswift.FileResource(bundle: R.hostingBundle, name: "Poppins-SemiBold", pathExtension: "ttf") - - /// `bundle.url(forResource: "GoogleService-Info", withExtension: "plist")` - static func googleServiceInfoPlist(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.googleServiceInfoPlist - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-Bold", withExtension: "ttf")` - static func poppinsBoldTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsBoldTtf - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-ExtraBold", withExtension: "ttf")` - static func poppinsExtraBoldTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsExtraBoldTtf - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-ExtraLight", withExtension: "ttf")` - static func poppinsExtraLightTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsExtraLightTtf - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-Italic", withExtension: "ttf")` - static func poppinsItalicTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsItalicTtf - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-Light", withExtension: "ttf")` - static func poppinsLightTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsLightTtf - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-Medium", withExtension: "ttf")` - static func poppinsMediumTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsMediumTtf - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-Regular", withExtension: "ttf")` - static func poppinsRegularTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsRegularTtf - return fileResource.bundle.url(forResource: fileResource) - } - - /// `bundle.url(forResource: "Poppins-SemiBold", withExtension: "ttf")` - static func poppinsSemiBoldTtf(_: Void = ()) -> Foundation.URL? { - let fileResource = R.file.poppinsSemiBoldTtf - return fileResource.bundle.url(forResource: fileResource) - } - - fileprivate init() {} - } - - /// This `R.font` struct is generated, and contains static references to 8 fonts. - struct font: Rswift.Validatable { - /// Font `Poppins-Bold`. - static let poppinsBold = Rswift.FontResource(fontName: "Poppins-Bold") - /// Font `Poppins-ExtraBold`. - static let poppinsExtraBold = Rswift.FontResource(fontName: "Poppins-ExtraBold") - /// Font `Poppins-ExtraLight`. - static let poppinsExtraLight = Rswift.FontResource(fontName: "Poppins-ExtraLight") - /// Font `Poppins-Italic`. - static let poppinsItalic = Rswift.FontResource(fontName: "Poppins-Italic") - /// Font `Poppins-Light`. - static let poppinsLight = Rswift.FontResource(fontName: "Poppins-Light") - /// Font `Poppins-Medium`. - static let poppinsMedium = Rswift.FontResource(fontName: "Poppins-Medium") - /// Font `Poppins-Regular`. - static let poppinsRegular = Rswift.FontResource(fontName: "Poppins-Regular") - /// Font `Poppins-SemiBold`. - static let poppinsSemiBold = Rswift.FontResource(fontName: "Poppins-SemiBold") - - /// `UIFont(name: "Poppins-Bold", size: ...)` - static func poppinsBold(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsBold, size: size) - } - - /// `UIFont(name: "Poppins-ExtraBold", size: ...)` - static func poppinsExtraBold(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsExtraBold, size: size) - } - - /// `UIFont(name: "Poppins-ExtraLight", size: ...)` - static func poppinsExtraLight(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsExtraLight, size: size) - } - - /// `UIFont(name: "Poppins-Italic", size: ...)` - static func poppinsItalic(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsItalic, size: size) - } - - /// `UIFont(name: "Poppins-Light", size: ...)` - static func poppinsLight(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsLight, size: size) - } - - /// `UIFont(name: "Poppins-Medium", size: ...)` - static func poppinsMedium(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsMedium, size: size) - } - - /// `UIFont(name: "Poppins-Regular", size: ...)` - static func poppinsRegular(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsRegular, size: size) - } - - /// `UIFont(name: "Poppins-SemiBold", size: ...)` - static func poppinsSemiBold(size: CGFloat) -> UIKit.UIFont? { - return UIKit.UIFont(resource: poppinsSemiBold, size: size) - } - - static func validate() throws { - if R.font.poppinsBold(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-Bold' could not be loaded, is 'Poppins-Bold.ttf' added to the UIAppFonts array in this targets Info.plist?") } - if R.font.poppinsExtraBold(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-ExtraBold' could not be loaded, is 'Poppins-ExtraBold.ttf' added to the UIAppFonts array in this targets Info.plist?") } - if R.font.poppinsExtraLight(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-ExtraLight' could not be loaded, is 'Poppins-ExtraLight.ttf' added to the UIAppFonts array in this targets Info.plist?") } - if R.font.poppinsItalic(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-Italic' could not be loaded, is 'Poppins-Italic.ttf' added to the UIAppFonts array in this targets Info.plist?") } - if R.font.poppinsLight(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-Light' could not be loaded, is 'Poppins-Light.ttf' added to the UIAppFonts array in this targets Info.plist?") } - if R.font.poppinsMedium(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-Medium' could not be loaded, is 'Poppins-Medium.ttf' added to the UIAppFonts array in this targets Info.plist?") } - if R.font.poppinsRegular(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-Regular' could not be loaded, is 'Poppins-Regular.ttf' added to the UIAppFonts array in this targets Info.plist?") } - if R.font.poppinsSemiBold(size: 42) == nil { throw Rswift.ValidationError(description:"[R.swift] Font 'Poppins-SemiBold' could not be loaded, is 'Poppins-SemiBold.ttf' added to the UIAppFonts array in this targets Info.plist?") } - } - - fileprivate init() {} - } - - /// This `R.id` struct is generated, and contains static references to accessibility identifiers. - struct id { - struct postTableViewCell { - /// Accessibility identifier `PostCell`. - static let postCell: String = "PostCell" - - fileprivate init() {} - } - - fileprivate init() {} - } - - /// This `R.image` struct is generated, and contains static references to 13 images. - struct image { - /// Image `Ask`. - static let ask = Rswift.ImageResource(bundle: R.hostingBundle, name: "Ask") - /// Image `Comments`. - static let comments = Rswift.ImageResource(bundle: R.hostingBundle, name: "Comments") - /// Image `ConnectionErrorIcon`. - static let connectionErrorIcon = Rswift.ImageResource(bundle: R.hostingBundle, name: "ConnectionErrorIcon") - /// Image `Earth`. - static let earth = Rswift.ImageResource(bundle: R.hostingBundle, name: "Earth") - /// Image `FilterIcon`. - static let filterIcon = Rswift.ImageResource(bundle: R.hostingBundle, name: "FilterIcon") - /// Image `Help`. - static let help = Rswift.ImageResource(bundle: R.hostingBundle, name: "Help") - /// Image `News`. - static let news = Rswift.ImageResource(bundle: R.hostingBundle, name: "News") - /// Image `Placeholder`. - static let placeholder = Rswift.ImageResource(bundle: R.hostingBundle, name: "Placeholder") - /// Image `PointsIcon`. - static let pointsIcon = Rswift.ImageResource(bundle: R.hostingBundle, name: "PointsIcon") - /// Image `Rate`. - static let rate = Rswift.ImageResource(bundle: R.hostingBundle, name: "Rate") - /// Image `Settings`. - static let settings = Rswift.ImageResource(bundle: R.hostingBundle, name: "Settings") - /// Image `Show`. - static let show = Rswift.ImageResource(bundle: R.hostingBundle, name: "Show") - /// Image `ThemeIcon`. - static let themeIcon = Rswift.ImageResource(bundle: R.hostingBundle, name: "ThemeIcon") - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Ask", bundle: ..., traitCollection: ...)` - static func ask(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.ask, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Comments", bundle: ..., traitCollection: ...)` - static func comments(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.comments, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "ConnectionErrorIcon", bundle: ..., traitCollection: ...)` - static func connectionErrorIcon(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.connectionErrorIcon, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Earth", bundle: ..., traitCollection: ...)` - static func earth(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.earth, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "FilterIcon", bundle: ..., traitCollection: ...)` - static func filterIcon(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.filterIcon, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Help", bundle: ..., traitCollection: ...)` - static func help(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.help, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "News", bundle: ..., traitCollection: ...)` - static func news(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.news, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Placeholder", bundle: ..., traitCollection: ...)` - static func placeholder(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.placeholder, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "PointsIcon", bundle: ..., traitCollection: ...)` - static func pointsIcon(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.pointsIcon, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Rate", bundle: ..., traitCollection: ...)` - static func rate(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.rate, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Settings", bundle: ..., traitCollection: ...)` - static func settings(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.settings, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "Show", bundle: ..., traitCollection: ...)` - static func show(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.show, compatibleWith: traitCollection) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UIImage(named: "ThemeIcon", bundle: ..., traitCollection: ...)` - static func themeIcon(compatibleWith traitCollection: UIKit.UITraitCollection? = nil) -> UIKit.UIImage? { - return UIKit.UIImage(resource: R.image.themeIcon, compatibleWith: traitCollection) - } - #endif - - fileprivate init() {} - } - - /// This `R.nib` struct is generated, and contains static references to 5 nibs. - struct nib { - /// Nib `CommentCell`. - static let commentCell = _R.nib._CommentCell() - /// Nib `PostTableViewCell`. - static let postTableViewCell = _R.nib._PostTableViewCell() - /// Nib `SettingsTableViewCell`. - static let settingsTableViewCell = _R.nib._SettingsTableViewCell() - /// Nib `SkeletonCell`. - static let skeletonCell = _R.nib._SkeletonCell() - /// Nib `ThemeSelectableTableViewCell`. - static let themeSelectableTableViewCell = _R.nib._ThemeSelectableTableViewCell() - - #if os(iOS) || os(tvOS) - /// `UINib(name: "CommentCell", in: bundle)` - @available(*, deprecated, message: "Use UINib(resource: R.nib.commentCell) instead") - static func commentCell(_: Void = ()) -> UIKit.UINib { - return UIKit.UINib(resource: R.nib.commentCell) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UINib(name: "PostTableViewCell", in: bundle)` - @available(*, deprecated, message: "Use UINib(resource: R.nib.postTableViewCell) instead") - static func postTableViewCell(_: Void = ()) -> UIKit.UINib { - return UIKit.UINib(resource: R.nib.postTableViewCell) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UINib(name: "SettingsTableViewCell", in: bundle)` - @available(*, deprecated, message: "Use UINib(resource: R.nib.settingsTableViewCell) instead") - static func settingsTableViewCell(_: Void = ()) -> UIKit.UINib { - return UIKit.UINib(resource: R.nib.settingsTableViewCell) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UINib(name: "SkeletonCell", in: bundle)` - @available(*, deprecated, message: "Use UINib(resource: R.nib.skeletonCell) instead") - static func skeletonCell(_: Void = ()) -> UIKit.UINib { - return UIKit.UINib(resource: R.nib.skeletonCell) - } - #endif - - #if os(iOS) || os(tvOS) - /// `UINib(name: "ThemeSelectableTableViewCell", in: bundle)` - @available(*, deprecated, message: "Use UINib(resource: R.nib.themeSelectableTableViewCell) instead") - static func themeSelectableTableViewCell(_: Void = ()) -> UIKit.UINib { - return UIKit.UINib(resource: R.nib.themeSelectableTableViewCell) - } - #endif - - static func commentCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> CommentCell? { - return R.nib.commentCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? CommentCell - } - - static func postTableViewCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> PostTableViewCell? { - return R.nib.postTableViewCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? PostTableViewCell - } - - static func settingsTableViewCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> SettingsTableViewCell? { - return R.nib.settingsTableViewCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? SettingsTableViewCell - } - - static func skeletonCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> SkeletonCell? { - return R.nib.skeletonCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? SkeletonCell - } - - static func themeSelectableTableViewCell(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> ThemeSelectableTableViewCell? { - return R.nib.themeSelectableTableViewCell.instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? ThemeSelectableTableViewCell - } - - fileprivate init() {} - } - - /// This `R.reuseIdentifier` struct is generated, and contains static references to 5 reuse identifiers. - struct reuseIdentifier { - /// Reuse identifier `CommentCell`. - static let commentCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "CommentCell") - /// Reuse identifier `PostTableViewCell`. - static let postTableViewCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "PostTableViewCell") - /// Reuse identifier `SettingsTableViewCell`. - static let settingsTableViewCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "SettingsTableViewCell") - /// Reuse identifier `SkeletonCell`. - static let skeletonCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "SkeletonCell") - /// Reuse identifier `ThemeSelectableTableViewCell`. - static let themeSelectableTableViewCell: Rswift.ReuseIdentifier = Rswift.ReuseIdentifier(identifier: "ThemeSelectableTableViewCell") - - fileprivate init() {} - } - - /// This `R.string` struct is generated, and contains static references to 2 localization tables. - struct string { - /// This `R.string.launchScreen` struct is generated, and contains static references to 1 localization keys. - struct launchScreen { - /// ru translation: Hacker News - /// - /// Locales: ru - static let zeoby6PText = Rswift.StringResource(key: "0ZE-OB-y6P.text", tableName: "LaunchScreen", bundle: R.hostingBundle, locales: ["ru"], comment: nil) - - /// ru translation: Hacker News - /// - /// Locales: ru - static func zeoby6PText(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("0ZE-OB-y6P.text", tableName: "LaunchScreen", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "LaunchScreen", preferredLanguages: preferredLanguages) else { - return "0ZE-OB-y6P.text" - } - - return NSLocalizedString("0ZE-OB-y6P.text", tableName: "LaunchScreen", bundle: bundle, comment: "") - } - - fileprivate init() {} - } - - /// This `R.string.localizable` struct is generated, and contains static references to 20 localization keys. - struct localizable { - /// en translation: Appearance - /// - /// Locales: ru, en - static let appearance = Rswift.StringResource(key: "Appearance", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Ask - /// - /// Locales: ru, en - static let ask = Rswift.StringResource(key: "Ask", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Best - /// - /// Locales: ru, en - static let best = Rswift.StringResource(key: "Best", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Comments - /// - /// Locales: ru, en - static let comments = Rswift.StringResource(key: "Comments", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Dark - /// - /// Locales: ru, en - static let dark = Rswift.StringResource(key: "Dark", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Help - /// - /// Locales: ru, en - static let settingsHelpHeader = Rswift.StringResource(key: "settings.help.header", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Help - /// - /// Locales: ru, en - static let settingsHelpTitle = Rswift.StringResource(key: "settings.help.title", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Light - /// - /// Locales: ru, en - static let light = Rswift.StringResource(key: "Light", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: New - /// - /// Locales: ru, en - static let new = Rswift.StringResource(key: "New", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: News - /// - /// Locales: ru, en - static let news = Rswift.StringResource(key: "News", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: No comments - /// - /// Locales: ru, en - static let noComments = Rswift.StringResource(key: "No comments", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Rate Us - /// - /// Locales: ru, en - static let settingsHelpRate = Rswift.StringResource(key: "settings.help.rate", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Settings - /// - /// Locales: ru, en - static let settings = Rswift.StringResource(key: "Settings", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Show - /// - /// Locales: ru, en - static let show = Rswift.StringResource(key: "Show", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Stories - /// - /// Locales: ru, en - static let stories = Rswift.StringResource(key: "Stories", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Theme - /// - /// Locales: ru, en - static let theme = Rswift.StringResource(key: "Theme", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Themes - /// - /// Locales: ru, en - static let settingsThemesHeader = Rswift.StringResource(key: "settings.themes.header", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Themes - /// - /// Locales: ru, en - static let settingsThemesTitle = Rswift.StringResource(key: "settings.themes.title", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Themes - /// - /// Locales: ru, en - static let themes = Rswift.StringResource(key: "Themes", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - /// en translation: Top - /// - /// Locales: ru, en - static let top = Rswift.StringResource(key: "Top", tableName: "Localizable", bundle: R.hostingBundle, locales: ["ru", "en"], comment: nil) - - /// en translation: Appearance - /// - /// Locales: ru, en - static func appearance(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Appearance", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Appearance" - } - - return NSLocalizedString("Appearance", bundle: bundle, comment: "") - } - - /// en translation: Ask - /// - /// Locales: ru, en - static func ask(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Ask", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Ask" - } - - return NSLocalizedString("Ask", bundle: bundle, comment: "") - } - - /// en translation: Best - /// - /// Locales: ru, en - static func best(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Best", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Best" - } - - return NSLocalizedString("Best", bundle: bundle, comment: "") - } - - /// en translation: Comments - /// - /// Locales: ru, en - static func comments(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Comments", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Comments" - } - - return NSLocalizedString("Comments", bundle: bundle, comment: "") - } - - /// en translation: Dark - /// - /// Locales: ru, en - static func dark(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Dark", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Dark" - } - - return NSLocalizedString("Dark", bundle: bundle, comment: "") - } - - /// en translation: Help - /// - /// Locales: ru, en - static func settingsHelpHeader(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("settings.help.header", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "settings.help.header" - } - - return NSLocalizedString("settings.help.header", bundle: bundle, comment: "") - } - - /// en translation: Help - /// - /// Locales: ru, en - static func settingsHelpTitle(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("settings.help.title", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "settings.help.title" - } - - return NSLocalizedString("settings.help.title", bundle: bundle, comment: "") - } - - /// en translation: Light - /// - /// Locales: ru, en - static func light(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Light", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Light" - } - - return NSLocalizedString("Light", bundle: bundle, comment: "") - } - - /// en translation: New - /// - /// Locales: ru, en - static func new(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("New", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "New" - } - - return NSLocalizedString("New", bundle: bundle, comment: "") - } - - /// en translation: News - /// - /// Locales: ru, en - static func news(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("News", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "News" - } - - return NSLocalizedString("News", bundle: bundle, comment: "") - } - - /// en translation: No comments - /// - /// Locales: ru, en - static func noComments(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("No comments", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "No comments" - } - - return NSLocalizedString("No comments", bundle: bundle, comment: "") - } - - /// en translation: Rate Us - /// - /// Locales: ru, en - static func settingsHelpRate(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("settings.help.rate", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "settings.help.rate" - } - - return NSLocalizedString("settings.help.rate", bundle: bundle, comment: "") - } - - /// en translation: Settings - /// - /// Locales: ru, en - static func settings(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Settings", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Settings" - } - - return NSLocalizedString("Settings", bundle: bundle, comment: "") - } - - /// en translation: Show - /// - /// Locales: ru, en - static func show(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Show", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Show" - } - - return NSLocalizedString("Show", bundle: bundle, comment: "") - } - - /// en translation: Stories - /// - /// Locales: ru, en - static func stories(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Stories", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Stories" - } - - return NSLocalizedString("Stories", bundle: bundle, comment: "") - } - - /// en translation: Theme - /// - /// Locales: ru, en - static func theme(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Theme", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Theme" - } - - return NSLocalizedString("Theme", bundle: bundle, comment: "") - } - - /// en translation: Themes - /// - /// Locales: ru, en - static func settingsThemesHeader(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("settings.themes.header", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "settings.themes.header" - } - - return NSLocalizedString("settings.themes.header", bundle: bundle, comment: "") - } - - /// en translation: Themes - /// - /// Locales: ru, en - static func settingsThemesTitle(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("settings.themes.title", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "settings.themes.title" - } - - return NSLocalizedString("settings.themes.title", bundle: bundle, comment: "") - } - - /// en translation: Themes - /// - /// Locales: ru, en - static func themes(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Themes", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Themes" - } - - return NSLocalizedString("Themes", bundle: bundle, comment: "") - } - - /// en translation: Top - /// - /// Locales: ru, en - static func top(preferredLanguages: [String]? = nil) -> String { - guard let preferredLanguages = preferredLanguages else { - return NSLocalizedString("Top", bundle: hostingBundle, comment: "") - } - - guard let (_, bundle) = localeBundle(tableName: "Localizable", preferredLanguages: preferredLanguages) else { - return "Top" - } - - return NSLocalizedString("Top", bundle: bundle, comment: "") - } - - fileprivate init() {} - } - - fileprivate init() {} - } - - fileprivate struct intern: Rswift.Validatable { - fileprivate static func validate() throws { - try _R.validate() - } - - fileprivate init() {} - } - - fileprivate class Class {} - - fileprivate init() {} -} - -struct _R: Rswift.Validatable { - static func validate() throws { - #if os(iOS) || os(tvOS) - try storyboard.validate() - #endif - } - - #if os(iOS) || os(tvOS) - struct nib { - struct _CommentCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType { - typealias ReusableType = CommentCell - - let bundle = R.hostingBundle - let identifier = "CommentCell" - let name = "CommentCell" - - func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> CommentCell? { - return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? CommentCell - } - - fileprivate init() {} - } - - struct _PostTableViewCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType { - typealias ReusableType = PostTableViewCell - - let bundle = R.hostingBundle - let identifier = "PostTableViewCell" - let name = "PostTableViewCell" - - func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> PostTableViewCell? { - return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? PostTableViewCell - } - - fileprivate init() {} - } - - struct _SettingsTableViewCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType { - typealias ReusableType = SettingsTableViewCell - - let bundle = R.hostingBundle - let identifier = "SettingsTableViewCell" - let name = "SettingsTableViewCell" - - func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> SettingsTableViewCell? { - return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? SettingsTableViewCell - } - - fileprivate init() {} - } - - struct _SkeletonCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType { - typealias ReusableType = SkeletonCell - - let bundle = R.hostingBundle - let identifier = "SkeletonCell" - let name = "SkeletonCell" - - func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> SkeletonCell? { - return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? SkeletonCell - } - - fileprivate init() {} - } - - struct _ThemeSelectableTableViewCell: Rswift.NibResourceType, Rswift.ReuseIdentifierType { - typealias ReusableType = ThemeSelectableTableViewCell - - let bundle = R.hostingBundle - let identifier = "ThemeSelectableTableViewCell" - let name = "ThemeSelectableTableViewCell" - - func firstView(owner ownerOrNil: AnyObject?, options optionsOrNil: [UINib.OptionsKey : Any]? = nil) -> ThemeSelectableTableViewCell? { - return instantiate(withOwner: ownerOrNil, options: optionsOrNil)[0] as? ThemeSelectableTableViewCell - } - - fileprivate init() {} - } - - fileprivate init() {} - } - #endif - - #if os(iOS) || os(tvOS) - struct storyboard: Rswift.Validatable { - static func validate() throws { - #if os(iOS) || os(tvOS) - try comments.validate() - #endif - #if os(iOS) || os(tvOS) - try launchScreen.validate() - #endif - #if os(iOS) || os(tvOS) - try settings.validate() - #endif - #if os(iOS) || os(tvOS) - try theme.validate() - #endif - } - - #if os(iOS) || os(tvOS) - struct comments: Rswift.StoryboardResourceType, Rswift.Validatable { - let bundle = R.hostingBundle - let commentsViewController = StoryboardViewControllerResource(identifier: "CommentsViewController") - let name = "Comments" - - func commentsViewController(_: Void = ()) -> CommentsViewController? { - return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: commentsViewController) - } - - static func validate() throws { - if #available(iOS 11.0, tvOS 11.0, *) { - } - if _R.storyboard.comments().commentsViewController() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'commentsViewController' could not be loaded from storyboard 'Comments' as 'CommentsViewController'.") } - } - - fileprivate init() {} - } - #endif - - #if os(iOS) || os(tvOS) - struct launchScreen: Rswift.StoryboardResourceWithInitialControllerType, Rswift.Validatable { - typealias InitialController = UIKit.UIViewController - - let bundle = R.hostingBundle - let name = "LaunchScreen" - - static func validate() throws { - if #available(iOS 11.0, tvOS 11.0, *) { - } - } - - fileprivate init() {} - } - #endif - - #if os(iOS) || os(tvOS) - struct settings: Rswift.StoryboardResourceType, Rswift.Validatable { - let bundle = R.hostingBundle - let name = "Settings" - let settingsViewController = StoryboardViewControllerResource(identifier: "SettingsViewController") - - func settingsViewController(_: Void = ()) -> SettingsViewController? { - return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: settingsViewController) - } - - static func validate() throws { - if #available(iOS 11.0, tvOS 11.0, *) { - } - if _R.storyboard.settings().settingsViewController() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'settingsViewController' could not be loaded from storyboard 'Settings' as 'SettingsViewController'.") } - } - - fileprivate init() {} - } - #endif - - #if os(iOS) || os(tvOS) - struct theme: Rswift.StoryboardResourceType, Rswift.Validatable { - let bundle = R.hostingBundle - let name = "Theme" - let themeViewController = StoryboardViewControllerResource(identifier: "ThemeViewController") - - func themeViewController(_: Void = ()) -> ThemeViewController? { - return UIKit.UIStoryboard(resource: self).instantiateViewController(withResource: themeViewController) - } - - static func validate() throws { - if #available(iOS 11.0, tvOS 11.0, *) { - } - if _R.storyboard.theme().themeViewController() == nil { throw Rswift.ValidationError(description:"[R.swift] ViewController with identifier 'themeViewController' could not be loaded from storyboard 'Theme' as 'ThemeViewController'.") } - } - - fileprivate init() {} - } - #endif - - fileprivate init() {} - } - #endif - - fileprivate init() {} -} diff --git a/README.md b/README.md index 781534c..fe452f0 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,71 @@ -# HackerNews +

HackerNews

-![](https://user-images.githubusercontent.com/17319991/87887377-477dfe80-ca2d-11ea-8422-b4e2dd52ff46.png) +
+ +![HackerNews: A HackerNews Reader](https://github.com/user-attachments/assets/8a5ba7ae-1e1d-47de-80db-10ed725c5e5e)

## Description +HackerNews is an app for readering Hacker News, built using TCA architecture. -A Hacker News reader iOS application. +- [Features](#features) +- [Requirements](#requirements) +- [Usage](#usage) +- [Contributing](#contributing) +- [Communication](#communication) +- [Have a Question](#have-a-question) +- [Author](#author) +- [License](#license) ## Features -* View "top", "newest", "best", "ask" and "show" posts from Hacker News +* View "Top," "Newest," "Best," "Ask," and "Show" posts from Hacker News * Read posts using the `SFSafariViewController` component -* View comments -* Today extension -* VIPER architecture -* Supports dark theme -* Thumbnails generated for posts * Full iPad multitasking support -* Uses the official [Firebase-based Hacker News API](https://github.com/HackerNews/API) +* Utilizes the official [Firebase-based Hacker News API](https://github.com/HackerNews/API) ## Usage -1) Download the repository - +1) Download the repository: ``` $ git clone https://github.com/nik3212/HackerNews $ cd HackerNews ``` -2) Install the required dependencies - +2) Bootstrap the development environment: ``` -$ pod install +make bootstrap ``` -3) Open the workspace project in Xcode - +3) Open the project in Xcode: ``` -$ open HackerNews.xcworkspace +$ open HackerNews.xcodeproj ``` -4) Compile and run the app in your simulator +4) Compile and run the app in your simulator. +## Communication +- If you **found a bug**, open an issue. +- If you **have a feature request**, open an issue. +- If you **want to contribute**, submit a pull request. # Requirements - -- Xcode 11.5+ -- iOS 10+ -- Swift 5.2+ +- Xcode 15+ +- iOS 17+ +- Swift 5.9+ ## Contributing - Please feel free to help out with this project! If you see something that could be made better or want a new feature, open up an issue or send a Pull Request! -## About - -Hacker News is an open source project by [Nikita Vasilev](mailto:nv3212@gmail.com) licensed under the [MIT license](LICENSE). - -## Credits - -I use several open source projects in Hacker News, in no particular order: +## Have a Question? +Contact us via [issues on GitHub](https://github.com/nik3212/HackerNews/issues). -* [CocoaPods](https://github.com/CocoaPods/CocoaPods) -* [Firebase](https://github.com/firebase/firebase-ios-sdk) -* [SwiftLint](https://github.com/realm/SwiftLint) -* [Sourcery](https://github.com/krzysztofzablocki/Sourcery) -* [Swinject](https://github.com/Swinject/Swinject) -* [Kingfisher](https://github.com/onevcat/Kingfisher) -* [Skeleton](https://github.com/gonzalonunez/Skeleton) -* [EmptyDataSet-Swift](https://github.com/Xiaoye220/EmptyDataSet-Swift) -* [Nimble](https://github.com/Quick/Nimble) -* [Quick](https://github.com/Quick/Quick) -* [iOSSnapshotTestCase](https://github.com/uber/ios-snapshot-test-case) -* [R.swift](https://github.com/mac-cain13/R.swift) +## Author +Nikita Vasilev, nv3212@gmail.com ## License diff --git a/Rambafile b/Rambafile deleted file mode 100644 index 53f7261..0000000 --- a/Rambafile +++ /dev/null @@ -1,34 +0,0 @@ -### Headers settings -company: Nikita Vasilev - -### Xcode project settings -project_name: HackerNews -xcodeproj_path: HackerNews.xcodeproj - -### Code generation settings section -# The main project target name -project_target: HackerNews - -# The file path for new modules -project_file_path: HackerNews/Classes/PresentationLayer - -# The Xcode group path to new modules -project_group_path: HackerNews/Classes/PresentationLayer - -### Tests generation settings section -# The tests target name -test_target: HackerNewsTests - -# The file path for new tests -test_file_path: HackerNewsTests - -# The Xcode group path to new tests -test_group_path: HackerNewsTests - -### Templates - -catalogs: -- 'https://github.com/nik3212/GenerambaTemplates' - -templates: -- {name: nv_viper} diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_11_0_1_2_0_11_0_1_375x667@2x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_11_0_1_2_0_11_0_1_375x667@2x.png deleted file mode 100644 index e28c972..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_11_0_1_2_0_11_0_1_375x667@2x.png and /dev/null differ diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_2_0_13_5_375x667@2x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_2_0_13_5_375x667@2x.png deleted file mode 100644 index 62782ba..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_2_0_13_5_375x667@2x.png and /dev/null differ diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_3_0_13_5_414x736@3x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_3_0_13_5_414x736@3x.png deleted file mode 100644 index 40edff5..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_3_0_13_5_414x736@3x.png and /dev/null differ diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_3_0_13_5_414x896@3x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_3_0_13_5_414x896@3x.png deleted file mode 100644 index 3870a4f..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayDarkTheme_13_5_3_0_13_5_414x896@3x.png and /dev/null differ diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_11_0_1_2_0_11_0_1_375x667@2x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_11_0_1_2_0_11_0_1_375x667@2x.png deleted file mode 100644 index 19c9a60..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_11_0_1_2_0_11_0_1_375x667@2x.png and /dev/null differ diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_2_0_13_5_375x667@2x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_2_0_13_5_375x667@2x.png deleted file mode 100644 index b1e2feb..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_2_0_13_5_375x667@2x.png and /dev/null differ diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_3_0_13_5_414x736@3x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_3_0_13_5_414x736@3x.png deleted file mode 100644 index 152692c..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_3_0_13_5_414x736@3x.png and /dev/null differ diff --git a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_3_0_13_5_414x896@3x.png b/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_3_0_13_5_414x896@3x.png deleted file mode 100644 index 0fd94a4..0000000 Binary files a/Snapshots/ReferenceImages_64/HackerNewsTests.SettingsShapshotTests/testThatViewControllerDisplayLightTheme_13_5_3_0_13_5_414x896@3x.png and /dev/null differ diff --git a/Sourcery/Scripts/SourceryGeneratorWatch.command b/Sourcery/Scripts/SourceryGeneratorWatch.command deleted file mode 100755 index bdee040..0000000 --- a/Sourcery/Scripts/SourceryGeneratorWatch.command +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -### Make sure Sourcery is installed, if not ‘brew install sourcery’ ### - -cd "$(dirname "$0")" -cd .. -sourcery --config ../.sourcery.yml --watch diff --git a/Sourcery/Templates/ItemRepresentable.stencil b/Sourcery/Templates/ItemRepresentable.stencil deleted file mode 100644 index bd80545..0000000 --- a/Sourcery/Templates/ItemRepresentable.stencil +++ /dev/null @@ -1,18 +0,0 @@ -{% for type in types.all|annotated:"ItemRepresentable" %} -// sourcery:inline:auto:{{ type.name }}.ItemRepresentable -extension {{ type.name }}: Decodable { - private enum CodingKeys: String, CodingKey { - {% for variable in type.storedVariables %} - case {{ variable.name }} = "{{ variable.name }}" - {% endfor %} - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - {% for variable in type.storedVariables %} - {{ variable.name }} = try container.decode({{ variable.unwrappedTypeName }}.self, forKey: .{{ variable.name }}) - {% endfor %} - } -} -// sourcery:end -{% endfor %} diff --git a/Templates/nv_viper/Code/Assembly/assembly.swift.liquid b/Templates/nv_viper/Code/Assembly/assembly.swift.liquid deleted file mode 100644 index 4ecc013..0000000 --- a/Templates/nv_viper/Code/Assembly/assembly.swift.liquid +++ /dev/null @@ -1,35 +0,0 @@ -{% include 'header' %} - -import Swinject - -final class {{ prefix }}{{ module_info.name }}ModuleAssembly: Assembly { - func assemble(container: Container) { - container.register({{ prefix }}{{ module_info.name }}Interactor.self) { (_, presenter: {{ prefix }}{{ module_info.name }}Presenter) in - let interactor = {{ prefix }}{{ module_info.name }}Interactor() - interactor.output = presenter - return interactor - } - - container.register({{ prefix }}{{ module_info.name }}Router.self) { (_, viewController: {{ prefix }}{{ module_info.name }}ViewController) in - let router = {{ prefix }}{{ module_info.name }}Router() - router.transitionHandler = viewController - return router - } - - container.register({{ prefix }}{{ module_info.name }}ModuleInput.self) { (resolver, viewController: {{ prefix }}{{ module_info.name }}ViewController) in - let presenter = {{ prefix }}{{ module_info.name }}Presenter() - - presenter.view = viewController - presenter.interactor = resolver.resolve({{ prefix }}{{ module_info.name }}Interactor.self, argument: presenter) - presenter.router = resolver.resolve({{ prefix }}{{ module_info.name }}Router.self, argument: viewController) - - return presenter - } - - container.register({{ prefix }}{{ module_info.name }}ViewController.self) { (resolver) in - let viewController = ViewControllerFactory.instantiate(.{{ prefix }}{{ module_info.name }}) as {{ prefix }}{{ module_info.name }}ViewController - viewController.output = resolver.resolve({{ prefix }}{{ module_info.name }}ModuleInput.self, argument: viewController) as? {{ prefix }}{{ module_info.name }}Presenter - return viewController - } - } -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Configurator/configurator.swift.liquid b/Templates/nv_viper/Code/Configurator/configurator.swift.liquid deleted file mode 100644 index 8bc8424..0000000 --- a/Templates/nv_viper/Code/Configurator/configurator.swift.liquid +++ /dev/null @@ -1,12 +0,0 @@ -{% include 'header' %} - -final class {{ prefix }}{{ module_info.name }}Configurator { - private var viewController: {{ prefix }}{{ module_info.name }}ViewController = { - return ApplicationAssembly.shared.assembler.resolver.resolve({{ prefix }}{{ module_info.name }}ViewController.self)! - }() - - func configure() -> {{ prefix }}{{ module_info.name }}ModuleInput? { - let moduleInput = ApplicationAssembly.shared.assembler.resolver.resolve({{ prefix }}{{ module_info.name }}ModuleInput.self, argument: viewController) - return moduleInput - } -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Configurator/module_input.swift.liquid b/Templates/nv_viper/Code/Configurator/module_input.swift.liquid deleted file mode 100644 index a4281b1..0000000 --- a/Templates/nv_viper/Code/Configurator/module_input.swift.liquid +++ /dev/null @@ -1,7 +0,0 @@ -{% include 'header' %} - -import UIKit - -protocol {{ prefix }}{{ module_info.name }}ModuleInput: class { - -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Interactor/interactor.swift.liquid b/Templates/nv_viper/Code/Interactor/interactor.swift.liquid deleted file mode 100644 index b033198..0000000 --- a/Templates/nv_viper/Code/Interactor/interactor.swift.liquid +++ /dev/null @@ -1,12 +0,0 @@ -{% include 'header' %} - -import Foundation - -class {{ prefix }}{{ module_info.name }}Interactor { - weak var output: {{ prefix }}{{ module_info.name }}Presenter! -} - -// MARK: {{ prefix }}{{ module_info.name }}InteractorInput -extension {{ prefix }}{{ module_info.name }}Interactor: {{ prefix }}{{ module_info.name }}InteractorInput { - -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Interactor/interactor_input.swift.liquid b/Templates/nv_viper/Code/Interactor/interactor_input.swift.liquid deleted file mode 100644 index 459d14b..0000000 --- a/Templates/nv_viper/Code/Interactor/interactor_input.swift.liquid +++ /dev/null @@ -1,5 +0,0 @@ -{% include 'header' %} - -protocol {{ prefix }}{{ module_info.name }}InteractorInput { - -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Interactor/interactor_output.swift.liquid b/Templates/nv_viper/Code/Interactor/interactor_output.swift.liquid deleted file mode 100644 index 4bbc93c..0000000 --- a/Templates/nv_viper/Code/Interactor/interactor_output.swift.liquid +++ /dev/null @@ -1,5 +0,0 @@ -{% include 'header' %} - -protocol {{ prefix }}{{ module_info.name }}InteractorOutput: class { - -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Presenter/presenter.swift.liquid b/Templates/nv_viper/Code/Presenter/presenter.swift.liquid deleted file mode 100644 index b3c0fac..0000000 --- a/Templates/nv_viper/Code/Presenter/presenter.swift.liquid +++ /dev/null @@ -1,26 +0,0 @@ -{% include 'header' %} - -import UIKit - -class {{ prefix }}{{ module_info.name }}Presenter { - weak var view: {{ prefix }}{{ module_info.name }}ViewInput! - var interactor: {{ prefix }}{{ module_info.name }}InteractorInput! - var router: {{ prefix }}{{ module_info.name }}RouterInput! -} - -// MARK: {{ prefix }}{{ module_info.name }}ViewOutput -extension {{ prefix }}{{ module_info.name }}Presenter: {{ prefix }}{{ module_info.name }}ViewOutput { - func viewIsReady() { - view.setupInitialState() - } -} - -// MARK: {{ prefix }}{{ module_info.name }}InteractorOutput -extension {{ prefix }}{{ module_info.name }}Presenter: {{ prefix }}{{ module_info.name }}InteractorOutput { - -} - -// MARK: {{ prefix }}{{ module_info.name }}ModuleInput -extension {{ prefix }}{{ module_info.name }}Presenter: {{ prefix }}{{ module_info.name }}ModuleInput { - -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Router/router.swift.liquid b/Templates/nv_viper/Code/Router/router.swift.liquid deleted file mode 100644 index 7f65cb9..0000000 --- a/Templates/nv_viper/Code/Router/router.swift.liquid +++ /dev/null @@ -1,12 +0,0 @@ -{% include 'header' %} - -import Foundation - -class {{ prefix }}{{ module_info.name }}Router { - weak var transitionHandler: TransitionHandler? -} - -// MARK: {{ prefix }}{{ module_info.name }}RouterInput -extension {{ prefix }}{{ module_info.name }}Router: {{ prefix }}{{ module_info.name }}RouterInput { - -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/Router/router_input.swift.liquid b/Templates/nv_viper/Code/Router/router_input.swift.liquid deleted file mode 100644 index 5773fe5..0000000 --- a/Templates/nv_viper/Code/Router/router_input.swift.liquid +++ /dev/null @@ -1,5 +0,0 @@ -{% include 'header' %} - -protocol {{ prefix }}{{ module_info.name }}RouterInput { - -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/View/view_input.swift.liquid b/Templates/nv_viper/Code/View/view_input.swift.liquid deleted file mode 100644 index d527475..0000000 --- a/Templates/nv_viper/Code/View/view_input.swift.liquid +++ /dev/null @@ -1,5 +0,0 @@ -{% include 'header' %} - -protocol {{ prefix }}{{ module_info.name }}ViewInput: class, Presentable { - func setupInitialState() -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/View/view_output.swift.liquid b/Templates/nv_viper/Code/View/view_output.swift.liquid deleted file mode 100644 index 36431fd..0000000 --- a/Templates/nv_viper/Code/View/view_output.swift.liquid +++ /dev/null @@ -1,5 +0,0 @@ -{% include 'header' %} - -protocol {{ prefix }}{{ module_info.name }}ViewOutput: class { - func viewIsReady() -} \ No newline at end of file diff --git a/Templates/nv_viper/Code/View/viewcontroller.storyboard.liquid b/Templates/nv_viper/Code/View/viewcontroller.storyboard.liquid deleted file mode 100644 index a59bf82..0000000 --- a/Templates/nv_viper/Code/View/viewcontroller.storyboard.liquid +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Templates/nv_viper/Code/View/viewcontroller.swift.liquid b/Templates/nv_viper/Code/View/viewcontroller.swift.liquid deleted file mode 100644 index df3fbfb..0000000 --- a/Templates/nv_viper/Code/View/viewcontroller.swift.liquid +++ /dev/null @@ -1,19 +0,0 @@ -{% include 'header' %} - -import UIKit - -class {{ prefix }}{{ module_info.name }}ViewController: UIViewController { - var output: {{ prefix }}{{ module_info.name }}ViewOutput! - - override func viewDidLoad() { - super.viewDidLoad() - - output.viewIsReady() - } -} - -extension {{ prefix }}{{ module_info.name }}ViewController: {{ prefix }}{{ module_info.name }}ViewInput { - func setupInitialState() { - - } -} \ No newline at end of file diff --git a/Templates/nv_viper/Tests/Assembly/assembly_tests.swift.liquid b/Templates/nv_viper/Tests/Assembly/assembly_tests.swift.liquid deleted file mode 100644 index 547a164..0000000 --- a/Templates/nv_viper/Tests/Assembly/assembly_tests.swift.liquid +++ /dev/null @@ -1,52 +0,0 @@ -{% include 'header' %} - -import Quick -import Nimble -import Swinject - -@testable import {{ module_info.project_name }} - -class {{ prefix }}{{ module_info.name }}ModuleAssemblyTests: QuickSpec { - let container = Container() - - override func spec() { - let assembly = {{ module_info.name }}ModuleAssembly() - assembly.assemble(container: container) - - let viewController = container.resolve({{ module_info.name }}ViewController.self) - let presenter = viewController?.output as? {{ module_info.name }}Presenter - let router = presenter?.router as? {{ module_info.name }}Router - let interactor = presenter?.interactor as? {{ module_info.name }}Interactor - - describe("Checking module creation") { - it("Must be {{ module_info.name }}ViewController") { - expect(viewController).toNot(beNil()) - expect(viewController).to(beAKindOf({{ module_info.name }}ViewController.self)) - } - - it("Must contains correct output") { - expect(viewController?.output).toNot(beNil()) - expect(viewController?.output).to(beIdenticalTo(presenter)) - } - - it("Must contains view, interactor and router") { - expect(presenter?.view).toNot(beNil()) - expect(presenter?.view).to(beIdenticalTo(viewController)) - expect(presenter?.router).toNot(beNil()) - expect(presenter?.router).to(beIdenticalTo(router)) - expect(presenter?.interactor).toNot(beNil()) - expect(presenter?.interactor).to(beIdenticalTo(interactor)) - } - - it("Must contains output") { - expect(interactor?.output).toNot(beNil()) - expect(interactor?.output).to(beIdenticalTo(presenter)) - } - - it("Must contains correct transitionHandler") { - expect(router?.transitionHandler).toNot(beNil()) - expect(router?.transitionHandler).to(beIdenticalTo(viewController)) - } - } - } -} \ No newline at end of file diff --git a/Templates/nv_viper/Tests/Configurator/configurator_tests.swift.liquid b/Templates/nv_viper/Tests/Configurator/configurator_tests.swift.liquid deleted file mode 100644 index cd059a6..0000000 --- a/Templates/nv_viper/Tests/Configurator/configurator_tests.swift.liquid +++ /dev/null @@ -1,19 +0,0 @@ -{% include 'header' %} - -import Quick -import Nimble - -@testable import {{ module_info.project_name }} - -class {{ prefix }}{{ module_info.name }}ConfiguratorTests: QuickSpec { - override func spec() { - let module = {{ module_info.name }}Configurator().configure() - - describe("Check module configuration") { - it("Module input shouldn't be nil") { - expect(module).notTo(beNil()) - expect(module).to(beAKindOf({{ module_info.name }}ModuleInput.self)) - } - } - } -} diff --git a/Templates/nv_viper/Tests/Interactor/interactor_tests.swift.liquid b/Templates/nv_viper/Tests/Interactor/interactor_tests.swift.liquid deleted file mode 100644 index efe38fc..0000000 --- a/Templates/nv_viper/Tests/Interactor/interactor_tests.swift.liquid +++ /dev/null @@ -1,16 +0,0 @@ -{% include 'header' %} - -import Quick -import Nimble - -@testable import {{ module_info.project_name }} - -class {{ prefix }}{{ module_info.name }}InteractorTests: QuickSpec { - override func spec() { - - } - - class MockPresenter: {{ module_info.name }}InteractorOutput { - - } -} diff --git a/Templates/nv_viper/Tests/Presenter/presenter_tests.swift.liquid b/Templates/nv_viper/Tests/Presenter/presenter_tests.swift.liquid deleted file mode 100644 index 1ce14b4..0000000 --- a/Templates/nv_viper/Tests/Presenter/presenter_tests.swift.liquid +++ /dev/null @@ -1,26 +0,0 @@ -{% include 'header' %} - -import Quick -import Nimble - -@testable import {{ module_info.project_name }} - -class {{ prefix }}{{ module_info.name }}PresenterTests: QuickSpec { - override func spec() { - - } - - class MockInteractor: {{ module_info.name }}InteractorInput { - - } - - class MockRouter: {{ module_info.name }}RouterInput { - - } - - class MockView: UIViewController, {{ module_info.name }}ViewInput { - func setupInitialState() { - - } - } -} diff --git a/Templates/nv_viper/Tests/Router/router_tests.swift.liquid b/Templates/nv_viper/Tests/Router/router_tests.swift.liquid deleted file mode 100644 index d6ff020..0000000 --- a/Templates/nv_viper/Tests/Router/router_tests.swift.liquid +++ /dev/null @@ -1,12 +0,0 @@ -{% include 'header' %} - -import Quick -import Nimble - -@testable import {{ module_info.project_name }} - -class {{ prefix }}{{ module_info.name }}RouterTests: QuickSpec { - override func spec() { - - } -} diff --git a/Templates/nv_viper/Tests/View/view_tests.swift.liquid b/Templates/nv_viper/Tests/View/view_tests.swift.liquid deleted file mode 100644 index 4fa8874..0000000 --- a/Templates/nv_viper/Tests/View/view_tests.swift.liquid +++ /dev/null @@ -1,12 +0,0 @@ -{% include 'header' %} - -import Quick -import Nimble - -@testable import {{ module_info.project_name }} - -class {{ prefix }}{{ module_info.name }}ViewTests: QuickSpec { - override func spec() { - - } -} diff --git a/Templates/nv_viper/nv_viper.rambaspec b/Templates/nv_viper/nv_viper.rambaspec deleted file mode 100644 index 4dc5e41..0000000 --- a/Templates/nv_viper/nv_viper.rambaspec +++ /dev/null @@ -1,61 +0,0 @@ -# Template information section -name: nv_viper -summary: Swift + Viper + Storyboard -author: Nikita Vasilev -version: 1.0.0 -license: MIT - -# The declarations for code files - -code_files: -# View layer -- {name: View/ViewInput.swift, path: Code/View/view_input.swift.liquid} -- {name: View/ViewOutput.swift, path: Code/View/view_output.swift.liquid} -- {name: View/ViewController.swift, path: Code/View/viewcontroller.swift.liquid} -- {name: View/Module.storyboard, path: Code/View/viewcontroller.storyboard.liquid} - -# Presenter layer -- {name: Presenter/Presenter.swift, path: Code/Presenter/presenter.swift.liquid} - -# Interactor layer -- {name: Interactor/InteractorInput.swift, path: Code/Interactor/interactor_input.swift.liquid} -- {name: Interactor/InteractorOutput.swift, path: Code/Interactor/interactor_output.swift.liquid} -- {name: Interactor/Interactor.swift, path: Code/Interactor/interactor.swift.liquid} - -# Router layer -- {name: Router/RouterInput.swift, path: Code/Router/router_input.swift.liquid} -- {name: Router/Router.swift, path: Code/Router/router.swift.liquid} - -# Assembly -- {name: Assembly/Assembly.swift, path: Code/Assembly/assembly.swift.liquid} - -# Configurator -- {name: Configurator/Configurator.swift, path: Code/Configurator/configurator.swift.liquid} -- {name: Configurator/ModuleInput.swift, path: Code/Configurator/module_input.swift.liquid} - -# The declarations for test files -test_files: - -# Assemblys tests -- {name: Assembly/AssemblyTests.swift, path: Tests/Assembly/assembly_tests.swift.liquid} - -# View tests -- {name: View/ViewTests.swift, path: Tests/View/view_tests.swift.liquid} - -# Interactor tests -- {name: Interactor/InteractorTests.swift, path: Tests/Interactor/interactor_tests.swift.liquid} - -# Presenter tests -- {name: Presenter/PresenterTests.swift, path: Tests/Presenter/presenter_tests.swift.liquid} - -# Router tests -- {name: Router/RouterTests.swift, path: Tests/Router/router_tests.swift.liquid} - -# Configurator tests -- {name: Configurator/ConfiguratorTests.swift, path: Tests/Configurator/configurator_tests.swift.liquid} - -# Template dependencies -dependencies: -- Swinject -- Quick -- Nimble \ No newline at end of file diff --git a/Templates/nv_viper/snippets/header.liquid b/Templates/nv_viper/snippets/header.liquid deleted file mode 100644 index e71965b..0000000 --- a/Templates/nv_viper/snippets/header.liquid +++ /dev/null @@ -1,7 +0,0 @@ -// -// {{ module_info.file_name }} -// {{ module_info.project_name }} -// -// Created by {{ developer.name }} on {{ date }}. -// Copyright © {{ year }} {{ developer.company }}. All rights reserved. -// \ No newline at end of file diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..83d3b0e --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,11 @@ +team_id "A8WE5LL2GU" +itc_team_name "Nikita Vasilev" +itc_team_id "118933826" + +for_platform :ios do + app_identifier "com.nikitavasilev.HackerNews" + + for_lane :beta do + app_identifier "com.nikitavasilev.HackerNews.beta" + end +end \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..65f64d1 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,175 @@ +#!usr/bin/ruby + +fastlane_version '2.0' + +default_platform :ios + +api_key = "fastlane/api-key.json" +xcodeproj = "HackerNews.xcodeproj" + +before_all do + ENV['FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT'] = '120' + build_number = increment_build_number + version_number = get_version_number(xcodeproj: xcodeproj) +end + +after_all do |lane| + # Discard all uncommitted changes(the new app icon images) + clean_icons +end + +platform :ios do + ## General + + desc "Generate new certificates" + lane :generate_new_certificates do + sync_code_signing( + type: "development", + app_identifier: ['com.nikitavasilev.HackerNews.beta', 'com.nikitavasilev.HackerNews.debug', 'com.nikitavasilev.HackerNews'], + force_for_new_devices: true, + readonly: false + ) + sync_code_signing( + type: "appstore", + app_identifier: ['com.nikitavasilev.HackerNews.beta', 'com.nikitavasilev.HackerNews.debug', 'com.nikitavasilev.HackerNews'], + force_for_new_devices: true, + readonly: false + ) + end + + desc "Regester devices on apple portal" + lane :register do + register_devices( + devices_file: "./fastlane/devices.txt" + ) + match( + type: "development", + force_for_new_devices: true, + ) + end + + ## Builing apps + + desc "Create a HackerNews Beta build for TestFlight" + lane :beta do + overrideParams = { + scheme: "Debug", + export_method: "app-store" + } + + add_badge_to_icon(release: "beta") + + testflight_build({overrideParams: overrideParams}) + end + + desc "Create a HackerNews Production build for TestFlight" + lane :production do + overrideParams = { + scheme: "Release", + export_method: "app-store" + } + + testflight_build({overrideParams: overrideParams}) + end + + ## Tests + + desc "Run Unit Tests" + lane :test do |options| + scan( + scheme: "Debug", + cloned_source_packages_path: ".cache/spm", + clean: true, + device: "iPhone 14 Pro", + reset_simulator: true, + reinstall_app: true, + skip_detect_devices: true, + parallel_testing: false, + code_coverage: true, + use_system_scm: true, + result_bundle: true, + output_directory: "./build/test", + output_files: "hackernews.unit.test.html,hackernews.unit.test.report.junit.xml", + xcargs: "-skipMacroValidation", + ) + + slather( + cobertura_xml: true, + output_directory: "./build/test", + proj: "HackerNews.xcodeproj", + scheme: "Debug", + binary_basename: "HackerNews", + ) + end + + # Private Methods + + desc "Upload build to TestFlight" + private_lane :testflight_build do |options| + overrideParams = options[:overrideParams] + defaultParams = gym_params() + gym(defaultParams.merge!(overrideParams)) + + changelog = changelog_from_git_commits( + between: ["dev", "HEAD"], + pretty: "- (%ae) %s",# Optional, lets you provide a custom format to apply to each commit when generating the changelog text + date_format: "short",# Optional, lets you provide an additional date format to dates within the pretty-formatted string + match_lightweight_tag: false, # Optional, lets you ignore lightweight (non-annotated) tags when searching for the last tag + merge_commit_filtering: "exclude_merges" # Optional, lets you filter out merge commits + ) + + pilot( + api_key_path: api_key, + skip_submission: true, + skip_waiting_for_build_processing: false, + changelog: changelog, + groups: "Internal" + ) + + send_message_to_slack( + message: "🎉 The new version released on TestFlight", + environment: overrideParams[:scheme] + ) + end + + desc "Returns the parameters that should be used in any fastlane build" + lane :gym_params do + { + project: xcodeproj, + sdk: "iphoneos", + clean: true, + output_directory: "build" + } + end + + private_lane :add_badge_to_icon do |options| + version_number = lane_context[SharedValues::VERSION_NUMBER] + build_number = lane_context[SharedValues::BUILD_NUMBER] + if options[:release] == "beta" + add_badge( + shield: "#{version_number}-#{build_number}-orange", + no_badge: false, + ) + end + end + + private_lane :clean_icons do + # Regardless of git flags, always want to forcefully reset icon changes + reset_git_repo(files: ["HackerNews/Resources/Assets.xcassets/AppIcon*"], force: true) + end + + private_lane :send_message_to_slack do |options| + version_number = lane_context[SharedValues::VERSION_NUMBER] + build_number = lane_context[SharedValues::BUILD_NUMBER] + + slack( + message: options[:message], + payload: { + "Build Date" => Time.new.to_s, + "Environment": options[:environment], + "Version": version_number, + "Build Number": build_number + } + ) + end + end \ No newline at end of file diff --git a/fastlane/Matchfile b/fastlane/Matchfile new file mode 100644 index 0000000..6079def --- /dev/null +++ b/fastlane/Matchfile @@ -0,0 +1,3 @@ +git_url "git@github.com:nik3212/certs-ios.git" +clone_branch_directly true +app_identifier(["com.nikitavasilev.HackerNews", "com.nikitavasilev.HackerNews.beta", "com.nikitavasilev.HackerNews.debug"]) \ No newline at end of file diff --git a/fastlane/Pluginfile b/fastlane/Pluginfile new file mode 100644 index 0000000..f0a1373 --- /dev/null +++ b/fastlane/Pluginfile @@ -0,0 +1,5 @@ +# Autogenerated by fastlane +# +# Ensure this file is checked in to source control! + +gem 'fastlane-plugin-badge' \ No newline at end of file diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..bf8d785 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,72 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios generate_new_certificates + +```sh +[bundle exec] fastlane ios generate_new_certificates +``` + +Generate new certificates + +### ios register + +```sh +[bundle exec] fastlane ios register +``` + +Regester devices on apple portal + +### ios beta + +```sh +[bundle exec] fastlane ios beta +``` + +Create a HackerNews Beta build for TestFlight + +### ios production + +```sh +[bundle exec] fastlane ios production +``` + +Create a HackerNews Production build for TestFlight + +### ios test + +```sh +[bundle exec] fastlane ios test +``` + +Run Unit Tests + +### ios gym_params + +```sh +[bundle exec] fastlane ios gym_params +``` + +Returns the parameters that should be used in any fastlane build + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/report.xml b/fastlane/report.xml new file mode 100644 index 0000000..06f8315 --- /dev/null +++ b/fastlane/report.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..689c031 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,40 @@ +#!/bin/bash +git diff --diff-filter=d --staged --name-only | grep -e '\.swift$' | while read line; do + if [[ $line == *"/Generated"* ]]; then + echo "IGNORING GENERATED FILE: " "$line"; + else + mint run swiftformat swiftformat "${line}"; + git add "$line"; + fi +done + +LINT=$(which mint) +if [[ -e "${LINT}" ]]; then + # Export files in SCRIPT_INPUT_FILE_$count to lint against later + count=0 + while IFS= read -r file_path; do + export SCRIPT_INPUT_FILE_$count="$file_path" + count=$((count + 1)) + done < <(git diff --name-only --cached --diff-filter=d | grep ".swift$") + export SCRIPT_INPUT_FILE_COUNT=$count + + if [ "$count" -eq 0 ]; then + echo "No files to lint!" + exit 0 + fi + + echo "Found $count lintable files! Linting now.." + mint run swiftlint --use-script-input-files --strict --config .swiftlint.yml + RESULT=$? # swiftline exit value is number of errors + + if [ $RESULT -eq 0 ]; then + echo "🎉 Well done. No violation." + fi + exit $RESULT +else + echo "⚠️ WARNING: SwiftLint not found" + echo "⚠️ You might want to edit .git/hooks/pre-commit to locate your swiftlint" + exit 0 +fi + +xcodegen \ No newline at end of file diff --git a/project.yml b/project.yml new file mode 100644 index 0000000..b86ac97 --- /dev/null +++ b/project.yml @@ -0,0 +1,163 @@ +name: HackerNews +options: + developmentLanguage: en + createIntermediateGroups: true + deploymentTarget: + iOS: 17.0 + xcodeVersion: 15.2 +configs: + Debug: debug + Beta: beta + Release: release +configFiles: + Debug: ./HackerNews/Resources/Configurations/Debug.xcconfig + Beta: ./HackerNews/Resources/Configurations/Beta.xcconfig + Release: ./HackerNews/Resources/Configurations/Release.xcconfig +settings: + base: + SWIFT_VERSION: "5.7" + configs: + Debug: + DEVELOPMENT_TEAM: A8WE5LL2GU + OTHER_SWIFT_FLAGS: -D DEBUG + SWIFT_OPTIMIZATION_LEVEL: -Onone + GCC_OPTIMIZATION_LEVEL: 1 + SWIFT_COMPILATION_MODE: "incremental" + Beta: + DEVELOPMENT_TEAM: A8WE5LL2GU + OTHER_SWIFT_FLAGS: -D DEBUG + SWIFT_OPTIMIZATION_LEVEL: -O + GCC_OPTIMIZATION_LEVEL: 1 + SWIFT_COMPILATION_MODE: "incremental" + Release: + DEVELOPMENT_TEAM: A8WE5LL2GU + OTHER_SWIFT_FLAGS: -D RELEASE + OTHER_LDFLAGS: -Objc + SWIFT_COMPILATION_MODE: wholemodule +packages: + # Common + + HackerNewsLocalization: + path: Modules/Common/HackerNewsLocalization + UIExtensions: + path: Modules/Common/UIExtensions + AppUtils: + path: Modules/Common/AppUtils + DesignKit: + path: Modules/Common/DesignKit + + # Feature + + Home: + path: Modules/Features/Home + Settings: + path: Modules/Features/Settings + + # External + + ComposableArchitecture: + url: https://github.com/pointfreeco/swift-composable-architecture.git + from: 1.13.1 + SwiftCollections: + url: https://github.com/apple/swift-collections.git + from: 1.0.5 + Pulse: + url: https://github.com/kean/Pulse.git + from: 4.0.5 + +attributes: + ORGANIZATIONNAME: Nikita Vasilev +schemes: + Debug: + build: + targets: + HackerNews: all + run: + config: Debug + test: + gatherCoverageData: true + targets: + - HackerNewsTests + - package: Home/HomeTests + - package: Settings/SettingsTests + coverageTargets: + - HackerNews + - package: Home/Home + - package: Settings/Settings + Release: + build: + targets: + HackerNews: all + run: + config: Release + Beta: + build: + targets: + HackerNews: all +targets: + HackerNews: + type: application + platform: iOS + dependencies: + - package: HackerNewsLocalization + - package: ComposableArchitecture + - package: SwiftCollections + product: OrderedCollections + - package: Home + - package: Settings + - package: UIExtensions + - package: DesignKit + - package: Pulse + product: PulseUI + sources: + - path: HackerNews + settings: + base: + MARKETING_VERSION: 3.0.0 + CURRENT_PROJECT_VERSION: 1 + TARGETED_DEVICE_FAMILY: "1,2" + configs: + Beta: + PRODUCT_NAME: HackerNews + PRODUCT_BUNDLE_IDENTIFIER: com.nikitavasilev.HackerNews.beta + CODE_SIGN_IDENTITY: "iPhone Developer" + PROVISIONING_PROFILE_SPECIFIER: match Development com.nikitavasilev.HackerNews.beta + Debug: + PRODUCT_NAME: HackerNews + PRODUCT_BUNDLE_IDENTIFIER: com.nikitavasilev.HackerNews.debug + CODE_SIGN_IDENTITY: "iPhone Developer" + PROVISIONING_PROFILE_SPECIFIER: match Development com.nikitavasilev.HackerNews.debug + Release: + PRODUCT_NAME: HackerNews + PRODUCT_BUNDLE_IDENTIFIER: com.nikitavasilev.HackerNews + CODE_SIGN_IDENTITY: "iPhone Distribution" + PROVISIONING_PROFILE_SPECIFIER: match AppStore com.nikitavasilev.HackerNews + prebuildScripts: + - script: | + make swiftgen + name: SwiftGen + - script: | + if [[ "${CONFIGURATION}" == "Debug" || "${CONFIGURATION}" == "Beta" ]]; then + export PATH="$PATH:/opt/homebrew/bin" + + echo "[Swiftlint] Run" + + if which mint >/dev/null; then + xcrun --sdk macosx make lint + + echo "[Swiftlint] Complete" + else + echo "[Swiftlint] Bootstrap mint" + fi + fi + name: SwiftLint + HackerNewsTests: + type: bundle.unit-test + platform: iOS + settings: + GENERATE_INFOPLIST_FILE: YES + BUNDLE_LOADER: $(BUILT_PRODUCTS_DIR)/HackerNews.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/HackerNews + sources: + - HackerNewsTests + dependencies: + - target: HackerNews diff --git a/scripts/setup_build_tools.sh b/scripts/setup_build_tools.sh new file mode 100644 index 0000000..d7b5d21 --- /dev/null +++ b/scripts/setup_build_tools.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +which -s xcodegen +if [[ $? != 0 ]] ; then + # Install xcodegen + echo "Installing xcodegen." + brew install xcodegen +fi + +which -s swiftgen +if [[ $? != 0 ]] ; then + # Install swiftgen + echo "Installing swiftgen." + brew install swiftgen +fi \ No newline at end of file

+Licence +CI +