├── .gitignore ├── .ruby-version ├── .swiftlint.yml ├── Gemfile ├── Gemfile.lock ├── Podfile ├── Podfile.lock ├── TableCollectionFeatures.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── TableCollectionFeatures.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── TableCollectionFeatures ├── AppDelegate.swift ├── Base.lproj │ └── LaunchScreen.storyboard ├── BaseClasses │ ├── BaseNavigationController.swift │ ├── BaseViewController.swift │ └── BaseViewModel.swift ├── Constants.swift ├── Extensions │ └── Observable+Extensions.swift ├── Flows │ ├── AppCoordinator.swift │ └── Main │ │ ├── View │ │ ├── ExpandableCell.swift │ │ ├── JumpAvoidingFlowLayout.swift │ │ └── MainViewController.swift │ │ └── ViewModel │ │ └── MainViewModel.swift ├── Helpers │ ├── ActivityTracker.swift │ └── ErrorTracker.swift └── Resources │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ └── arrow_down.imageset │ │ ├── Contents.json │ │ └── Icon.pdf │ └── Info.plist └── cleanup.rb /.gitignore: -------------------------------------------------------------------------------- 1 | #Gitignore from https://github.com/github/gitignore/blob/master/Swift.gitignore 2 | .DS_Store 3 | .Trashes 4 | *.swp 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | ## AppCode 9 | *.idea 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | # Swift Package Manager 33 | # 34 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 35 | # Packages/ 36 | # Package.pins 37 | # Package.resolved 38 | .build/ 39 | # CocoaPods 40 | # 41 | # We recommend against adding the Pods directory to your .gitignore. However 42 | # you should judge for yourself, the pros and cons are mentioned at: 43 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 44 | # 45 | Pods/ 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | Carthage/Build 51 | # fastlane 52 | # 53 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 54 | # screenshots whenever they are needed. 55 | # For more information about the recommended setup visit: 56 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 57 | fastlane/report.xml 58 | fastlane/Preview.html 59 | fastlane/screenshots 60 | fastlane/test_output 61 | node_modules 62 | 63 | sourcetree-hook.json -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.0 -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: 2 | - Carthage 3 | - Pods 4 | opt_in_rules: 5 | - anyobject_protocol 6 | - array_init 7 | - attributes 8 | - closure_end_indentation 9 | - closure_spacing 10 | - collection_alignment 11 | - conditional_returns_on_newline 12 | - contains_over_first_not_nil 13 | - convenience_type 14 | - discouraged_object_literal 15 | - empty_count 16 | - empty_string 17 | - explicit_init 18 | - extension_access_modifier 19 | - fallthrough 20 | - fatal_error_message 21 | - file_types_order 22 | - first_where 23 | - identical_operands 24 | - implicit_return 25 | - last_where 26 | - legacy_random 27 | - let_var_whitespace 28 | - lower_acl_than_parent 29 | - modifier_order 30 | - multiline_arguments 31 | - multiline_arguments_brackets 32 | - multiline_function_chains 33 | - multiline_literal_brackets 34 | - multiline_parameters 35 | - operator_usage_whitespace 36 | - overridden_super_call 37 | - override_in_extension 38 | - pattern_matching_keywords 39 | - private_action 40 | - private_outlet 41 | - redundant_type_annotation 42 | - sorted_first_last 43 | - unavailable_function 44 | - unneeded_parentheses_in_closure_argument 45 | - unused_enumerated 46 | - unused_import 47 | - unused_private_declaration 48 | - vertical_parameter_alignment_on_call 49 | - yoda_condition 50 | 51 | disabled_rules: 52 | - trailing_newline 53 | - trailing_whitespace 54 | - todo # включить перед первым релизом! 55 | - type_body_length 56 | - file_length 57 | - function_body_length 58 | - cyclomatic_complexity 59 | 60 | identifier_name: 61 | severity: warning 62 | min_length: 2 # only warning 63 | 64 | class_delegate_protocol: 65 | severity: error 66 | 67 | deployment_target: 68 | iOS_deployment_target: 11 69 | 70 | multiline_arguments: 71 | first_argument_location: next_line 72 | 73 | attributes: 74 | always_on_same_line: ["@IBAction", "@NSManaged", "@objc"] 75 | 76 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | gem "fastlane" 6 | gem 'cocoapods' 7 | gem 'httparty' 8 | 9 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 10 | eval_gemfile(plugins_path) if File.exist?(plugins_path) -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.3) 5 | activesupport (5.2.6) 6 | concurrent-ruby (~> 1.0, >= 1.0.2) 7 | i18n (>= 0.7, < 2) 8 | minitest (~> 5.1) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.5) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | artifactory (3.0.15) 16 | atomos (0.1.3) 17 | aws-eventstream (1.1.1) 18 | aws-partitions (1.472.0) 19 | aws-sdk-core (3.115.0) 20 | aws-eventstream (~> 1, >= 1.0.2) 21 | aws-partitions (~> 1, >= 1.239.0) 22 | aws-sigv4 (~> 1.1) 23 | jmespath (~> 1.0) 24 | aws-sdk-kms (1.44.0) 25 | aws-sdk-core (~> 3, >= 3.112.0) 26 | aws-sigv4 (~> 1.1) 27 | aws-sdk-s3 (1.96.1) 28 | aws-sdk-core (~> 3, >= 3.112.0) 29 | aws-sdk-kms (~> 1) 30 | aws-sigv4 (~> 1.1) 31 | aws-sigv4 (1.2.3) 32 | aws-eventstream (~> 1, >= 1.0.2) 33 | babosa (1.0.4) 34 | claide (1.0.3) 35 | cocoapods (1.10.1) 36 | addressable (~> 2.6) 37 | claide (>= 1.0.2, < 2.0) 38 | cocoapods-core (= 1.10.1) 39 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 40 | cocoapods-downloader (>= 1.4.0, < 2.0) 41 | cocoapods-plugins (>= 1.0.0, < 2.0) 42 | cocoapods-search (>= 1.0.0, < 2.0) 43 | cocoapods-trunk (>= 1.4.0, < 2.0) 44 | cocoapods-try (>= 1.1.0, < 2.0) 45 | colored2 (~> 3.1) 46 | escape (~> 0.0.4) 47 | fourflusher (>= 2.3.0, < 3.0) 48 | gh_inspector (~> 1.0) 49 | molinillo (~> 0.6.6) 50 | nap (~> 1.0) 51 | ruby-macho (~> 1.4) 52 | xcodeproj (>= 1.19.0, < 2.0) 53 | cocoapods-core (1.10.1) 54 | activesupport (> 5.0, < 6) 55 | addressable (~> 2.6) 56 | algoliasearch (~> 1.0) 57 | concurrent-ruby (~> 1.1) 58 | fuzzy_match (~> 2.0.4) 59 | nap (~> 1.0) 60 | netrc (~> 0.11) 61 | public_suffix 62 | typhoeus (~> 1.0) 63 | cocoapods-deintegrate (1.0.4) 64 | cocoapods-downloader (1.4.0) 65 | cocoapods-plugins (1.0.0) 66 | nap 67 | cocoapods-search (1.0.0) 68 | cocoapods-trunk (1.5.0) 69 | nap (>= 0.8, < 2.0) 70 | netrc (~> 0.11) 71 | cocoapods-try (1.2.0) 72 | colored (1.2) 73 | colored2 (3.1.2) 74 | commander (4.6.0) 75 | highline (~> 2.0.0) 76 | concurrent-ruby (1.1.9) 77 | declarative (0.0.20) 78 | digest-crc (0.6.3) 79 | rake (>= 12.0.0, < 14.0.0) 80 | domain_name (0.5.20190701) 81 | unf (>= 0.0.5, < 1.0.0) 82 | dotenv (2.7.6) 83 | emoji_regex (3.2.2) 84 | escape (0.0.4) 85 | ethon (0.14.0) 86 | ffi (>= 1.15.0) 87 | excon (0.82.0) 88 | faraday (1.4.3) 89 | faraday-em_http (~> 1.0) 90 | faraday-em_synchrony (~> 1.0) 91 | faraday-excon (~> 1.1) 92 | faraday-net_http (~> 1.0) 93 | faraday-net_http_persistent (~> 1.1) 94 | multipart-post (>= 1.2, < 3) 95 | ruby2_keywords (>= 0.0.4) 96 | faraday-cookie_jar (0.0.7) 97 | faraday (>= 0.8.0) 98 | http-cookie (~> 1.0.0) 99 | faraday-em_http (1.0.0) 100 | faraday-em_synchrony (1.0.0) 101 | faraday-excon (1.1.0) 102 | faraday-net_http (1.0.1) 103 | faraday-net_http_persistent (1.1.0) 104 | faraday_middleware (1.0.0) 105 | faraday (~> 1.0) 106 | fastimage (2.2.4) 107 | fastlane (2.186.0) 108 | CFPropertyList (>= 2.3, < 4.0.0) 109 | addressable (>= 2.3, < 3.0.0) 110 | artifactory (~> 3.0) 111 | aws-sdk-s3 (~> 1.0) 112 | babosa (>= 1.0.3, < 2.0.0) 113 | bundler (>= 1.12.0, < 3.0.0) 114 | colored 115 | commander (~> 4.6) 116 | dotenv (>= 2.1.1, < 3.0.0) 117 | emoji_regex (>= 0.1, < 4.0) 118 | excon (>= 0.71.0, < 1.0.0) 119 | faraday (~> 1.0) 120 | faraday-cookie_jar (~> 0.0.6) 121 | faraday_middleware (~> 1.0) 122 | fastimage (>= 2.1.0, < 3.0.0) 123 | gh_inspector (>= 1.1.2, < 2.0.0) 124 | google-apis-androidpublisher_v3 (~> 0.1) 125 | google-apis-playcustomapp_v1 (~> 0.1) 126 | google-cloud-storage (~> 1.31) 127 | highline (~> 2.0) 128 | json (< 3.0.0) 129 | jwt (>= 2.1.0, < 3) 130 | mini_magick (>= 4.9.4, < 5.0.0) 131 | multipart-post (~> 2.0.0) 132 | naturally (~> 2.2) 133 | plist (>= 3.1.0, < 4.0.0) 134 | rubyzip (>= 2.0.0, < 3.0.0) 135 | security (= 0.1.3) 136 | simctl (~> 1.6.3) 137 | terminal-notifier (>= 2.0.0, < 3.0.0) 138 | terminal-table (>= 1.4.5, < 2.0.0) 139 | tty-screen (>= 0.6.3, < 1.0.0) 140 | tty-spinner (>= 0.8.0, < 1.0.0) 141 | word_wrap (~> 1.0.0) 142 | xcodeproj (>= 1.13.0, < 2.0.0) 143 | xcpretty (~> 0.3.0) 144 | xcpretty-travis-formatter (>= 0.0.3) 145 | fastlane-plugin-firebase_app_distribution (0.2.9) 146 | ffi (1.15.3) 147 | fourflusher (2.3.1) 148 | fuzzy_match (2.0.4) 149 | gh_inspector (1.1.3) 150 | google-apis-androidpublisher_v3 (0.7.0) 151 | google-apis-core (>= 0.3, < 2.a) 152 | google-apis-core (0.3.0) 153 | addressable (~> 2.5, >= 2.5.1) 154 | googleauth (~> 0.14) 155 | httpclient (>= 2.8.1, < 3.0) 156 | mini_mime (~> 1.0) 157 | representable (~> 3.0) 158 | retriable (>= 2.0, < 4.0) 159 | rexml 160 | signet (~> 0.14) 161 | webrick 162 | google-apis-iamcredentials_v1 (0.5.0) 163 | google-apis-core (>= 0.3, < 2.a) 164 | google-apis-playcustomapp_v1 (0.4.0) 165 | google-apis-core (>= 0.3, < 2.a) 166 | google-apis-storage_v1 (0.5.0) 167 | google-apis-core (>= 0.3, < 2.a) 168 | google-cloud-core (1.6.0) 169 | google-cloud-env (~> 1.0) 170 | google-cloud-errors (~> 1.0) 171 | google-cloud-env (1.5.0) 172 | faraday (>= 0.17.3, < 2.0) 173 | google-cloud-errors (1.1.0) 174 | google-cloud-storage (1.32.0) 175 | addressable (~> 2.5) 176 | digest-crc (~> 0.4) 177 | google-apis-iamcredentials_v1 (~> 0.1) 178 | google-apis-storage_v1 (~> 0.1) 179 | google-cloud-core (~> 1.6) 180 | googleauth (>= 0.16.2, < 2.a) 181 | mini_mime (~> 1.0) 182 | googleauth (0.16.2) 183 | faraday (>= 0.17.3, < 2.0) 184 | jwt (>= 1.4, < 3.0) 185 | memoist (~> 0.16) 186 | multi_json (~> 1.11) 187 | os (>= 0.9, < 2.0) 188 | signet (~> 0.14) 189 | highline (2.0.3) 190 | http-cookie (1.0.4) 191 | domain_name (~> 0.5) 192 | httparty (0.18.1) 193 | mime-types (~> 3.0) 194 | multi_xml (>= 0.5.2) 195 | httpclient (2.8.3) 196 | i18n (1.8.10) 197 | concurrent-ruby (~> 1.0) 198 | jmespath (1.4.0) 199 | json (2.5.1) 200 | jwt (2.2.3) 201 | memoist (0.16.2) 202 | mime-types (3.3.1) 203 | mime-types-data (~> 3.2015) 204 | mime-types-data (3.2021.0225) 205 | mini_magick (4.11.0) 206 | mini_mime (1.1.0) 207 | minitest (5.14.4) 208 | molinillo (0.6.6) 209 | multi_json (1.15.0) 210 | multi_xml (0.6.0) 211 | multipart-post (2.0.0) 212 | nanaimo (0.3.0) 213 | nap (1.1.0) 214 | naturally (2.2.1) 215 | netrc (0.11.0) 216 | os (1.1.1) 217 | plist (3.6.0) 218 | public_suffix (4.0.6) 219 | rake (13.0.3) 220 | representable (3.1.1) 221 | declarative (< 0.1.0) 222 | trailblazer-option (>= 0.1.1, < 0.2.0) 223 | uber (< 0.2.0) 224 | retriable (3.1.2) 225 | rexml (3.2.5) 226 | rouge (2.0.7) 227 | ruby-macho (1.4.0) 228 | ruby2_keywords (0.0.4) 229 | rubyzip (2.3.0) 230 | security (0.1.3) 231 | signet (0.15.0) 232 | addressable (~> 2.3) 233 | faraday (>= 0.17.3, < 2.0) 234 | jwt (>= 1.5, < 3.0) 235 | multi_json (~> 1.10) 236 | simctl (1.6.8) 237 | CFPropertyList 238 | naturally 239 | terminal-notifier (2.0.0) 240 | terminal-table (1.8.0) 241 | unicode-display_width (~> 1.1, >= 1.1.1) 242 | thread_safe (0.3.6) 243 | trailblazer-option (0.1.1) 244 | tty-cursor (0.7.1) 245 | tty-screen (0.8.1) 246 | tty-spinner (0.9.3) 247 | tty-cursor (~> 0.7) 248 | typhoeus (1.4.0) 249 | ethon (>= 0.9.0) 250 | tzinfo (1.2.9) 251 | thread_safe (~> 0.1) 252 | uber (0.1.0) 253 | unf (0.1.4) 254 | unf_ext 255 | unf_ext (0.0.7.7) 256 | unicode-display_width (1.7.0) 257 | webrick (1.7.0) 258 | word_wrap (1.0.0) 259 | xcodeproj (1.19.0) 260 | CFPropertyList (>= 2.3.3, < 4.0) 261 | atomos (~> 0.1.3) 262 | claide (>= 1.0.2, < 2.0) 263 | colored2 (~> 3.1) 264 | nanaimo (~> 0.3.0) 265 | xcpretty (0.3.0) 266 | rouge (~> 2.0.7) 267 | xcpretty-travis-formatter (1.0.1) 268 | xcpretty (~> 0.2, >= 0.0.7) 269 | 270 | PLATFORMS 271 | x86_64-darwin-19 272 | 273 | DEPENDENCIES 274 | cocoapods 275 | fastlane 276 | fastlane-plugin-firebase_app_distribution 277 | httparty 278 | 279 | BUNDLED WITH 280 | 2.2.7 281 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11.0' 2 | inhibit_all_warnings! 3 | 4 | source 'https://github.com/cocoapods/specs.git' 5 | 6 | target 'TableCollectionFeatures' do 7 | 8 | use_frameworks! 9 | 10 | # Networking 11 | pod 'Moya/RxSwift' 12 | 13 | # Reactive 14 | pod 'RxSwift' 15 | pod 'RxCocoa' 16 | 17 | # Utilities 18 | pod 'SwiftLint' 19 | pod 'SwifterSwift' 20 | 21 | # Routing 22 | pod 'XCoordinator' 23 | pod 'XCoordinator/RxSwift' 24 | 25 | # Layout 26 | pod 'SnapKit' 27 | 28 | end 29 | 30 | post_install do |installer| 31 | installer.pods_project.targets.each do |target| 32 | target.build_configurations.each do |config| 33 | config.build_settings['LD_NO_PIE'] = 'NO' 34 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' 35 | 36 | if config.name == 'Release' 37 | config.build_settings['SWIFT_COMPILATION_MODE'] = 'wholemodule' 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.4.3) 3 | - Moya/Core (14.0.0): 4 | - Alamofire (~> 5.0) 5 | - Moya/RxSwift (14.0.0): 6 | - Moya/Core 7 | - RxSwift (~> 5.0) 8 | - RxCocoa (5.1.1): 9 | - RxRelay (~> 5) 10 | - RxSwift (~> 5) 11 | - RxRelay (5.1.1): 12 | - RxSwift (~> 5) 13 | - RxSwift (5.1.2) 14 | - SnapKit (5.0.1) 15 | - SwifterSwift (5.2.0): 16 | - SwifterSwift/AppKit (= 5.2.0) 17 | - SwifterSwift/CoreAnimation (= 5.2.0) 18 | - SwifterSwift/CoreGraphics (= 5.2.0) 19 | - SwifterSwift/CoreLocation (= 5.2.0) 20 | - SwifterSwift/Dispatch (= 5.2.0) 21 | - SwifterSwift/Foundation (= 5.2.0) 22 | - SwifterSwift/MapKit (= 5.2.0) 23 | - SwifterSwift/SceneKit (= 5.2.0) 24 | - SwifterSwift/SpriteKit (= 5.2.0) 25 | - SwifterSwift/StoreKit (= 5.2.0) 26 | - SwifterSwift/SwiftStdlib (= 5.2.0) 27 | - SwifterSwift/UIKit (= 5.2.0) 28 | - SwifterSwift/AppKit (5.2.0) 29 | - SwifterSwift/CoreAnimation (5.2.0) 30 | - SwifterSwift/CoreGraphics (5.2.0) 31 | - SwifterSwift/CoreLocation (5.2.0) 32 | - SwifterSwift/Dispatch (5.2.0) 33 | - SwifterSwift/Foundation (5.2.0) 34 | - SwifterSwift/MapKit (5.2.0) 35 | - SwifterSwift/SceneKit (5.2.0) 36 | - SwifterSwift/SpriteKit (5.2.0) 37 | - SwifterSwift/StoreKit (5.2.0) 38 | - SwifterSwift/SwiftStdlib (5.2.0) 39 | - SwifterSwift/UIKit (5.2.0) 40 | - SwiftLint (0.43.1) 41 | - XCoordinator (2.0.7): 42 | - XCoordinator/Core (= 2.0.7) 43 | - XCoordinator/Core (2.0.7) 44 | - XCoordinator/RxSwift (2.0.7): 45 | - RxSwift (~> 5.0) 46 | - XCoordinator/Core 47 | 48 | DEPENDENCIES: 49 | - Moya/RxSwift 50 | - RxCocoa 51 | - RxSwift 52 | - SnapKit 53 | - SwifterSwift 54 | - SwiftLint 55 | - XCoordinator 56 | - XCoordinator/RxSwift 57 | 58 | SPEC REPOS: 59 | https://github.com/cocoapods/specs.git: 60 | - Alamofire 61 | - Moya 62 | - RxCocoa 63 | - RxRelay 64 | - RxSwift 65 | - SnapKit 66 | - SwifterSwift 67 | - SwiftLint 68 | - XCoordinator 69 | 70 | SPEC CHECKSUMS: 71 | Alamofire: e447a2774a40c996748296fa2c55112fdbbc42f9 72 | Moya: 5b45dacb75adb009f97fde91c204c1e565d31916 73 | RxCocoa: 32065309a38d29b5b0db858819b5bf9ef038b601 74 | RxRelay: d77f7d771495f43c556cbc43eebd1bb54d01e8e9 75 | RxSwift: 1e2e1f9570186967f617e49128ed26ccf1bafc8e 76 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 77 | SwifterSwift: 334181863c416882d97b7a60c05054d9e4d799e2 78 | SwiftLint: 99f82d07b837b942dd563c668de129a03fc3fb52 79 | XCoordinator: 9ee654fedef1ffede33d5c11677708a555c00542 80 | 81 | PODFILE CHECKSUM: 7227325f01ecb326885979ad8e5b938079480fd7 82 | 83 | COCOAPODS: 1.10.1 84 | -------------------------------------------------------------------------------- /TableCollectionFeatures.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 503D035D268A23E40091684D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D035C268A23E40091684D /* AppDelegate.swift */; }; 11 | 503D0362268A23E40091684D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 503D0361268A23E40091684D /* Assets.xcassets */; }; 12 | 503D0366268A23E40091684D /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D0365268A23E40091684D /* AppCoordinator.swift */; }; 13 | 503D036A268A23E40091684D /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D0369268A23E40091684D /* MainViewController.swift */; }; 14 | 503D036F268A23E40091684D /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D036E268A23E40091684D /* MainViewModel.swift */; }; 15 | 503D0372268A23E40091684D /* ErrorTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D0371268A23E40091684D /* ErrorTracker.swift */; }; 16 | 503D0374268A23E40091684D /* ActivityTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D0373268A23E40091684D /* ActivityTracker.swift */; }; 17 | 503D0377268A23E40091684D /* Observable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D0376268A23E40091684D /* Observable+Extensions.swift */; }; 18 | 503D037A268A23E40091684D /* BaseNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D0379268A23E40091684D /* BaseNavigationController.swift */; }; 19 | 503D037C268A23E40091684D /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D037B268A23E40091684D /* BaseViewController.swift */; }; 20 | 503D037E268A23E40091684D /* BaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D037D268A23E40091684D /* BaseViewModel.swift */; }; 21 | 503D0380268A23E40091684D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D037F268A23E40091684D /* Constants.swift */; }; 22 | 503D038C268A23E40091684D /* (null) in Resources */ = {isa = PBXBuildFile; }; 23 | 503D038F268A23E40091684D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 503D038D268A23E40091684D /* LaunchScreen.storyboard */; }; 24 | 503D03BA268A30A90091684D /* ExpandableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D03B9268A30A90091684D /* ExpandableCell.swift */; }; 25 | 503D03BE268A68340091684D /* JumpAvoidingFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503D03BD268A68340091684D /* JumpAvoidingFlowLayout.swift */; }; 26 | 525F3F84EE299E8C131E8A49 /* Pods_TableCollectionFeatures.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A3E44697D72AC637C927D3F6 /* Pods_TableCollectionFeatures.framework */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 503D0359268A23E40091684D /* TableCollectionFeatures.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TableCollectionFeatures.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31 | 503D035C268A23E40091684D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 32 | 503D0361268A23E40091684D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 33 | 503D0363268A23E40091684D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | 503D0365268A23E40091684D /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 35 | 503D0369268A23E40091684D /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 36 | 503D036E268A23E40091684D /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; 37 | 503D0371268A23E40091684D /* ErrorTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorTracker.swift; sourceTree = ""; }; 38 | 503D0373268A23E40091684D /* ActivityTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTracker.swift; sourceTree = ""; }; 39 | 503D0376268A23E40091684D /* Observable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Extensions.swift"; sourceTree = ""; }; 40 | 503D0379268A23E40091684D /* BaseNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseNavigationController.swift; sourceTree = ""; }; 41 | 503D037B268A23E40091684D /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; 42 | 503D037D268A23E40091684D /* BaseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewModel.swift; sourceTree = ""; }; 43 | 503D037F268A23E40091684D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 44 | 503D038E268A23E40091684D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 45 | 503D03B9268A30A90091684D /* ExpandableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpandableCell.swift; sourceTree = ""; }; 46 | 503D03BD268A68340091684D /* JumpAvoidingFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpAvoidingFlowLayout.swift; sourceTree = ""; }; 47 | 8C79D3589280E071350F1F99 /* Pods-TableCollectionFeatures.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TableCollectionFeatures.debug.xcconfig"; path = "Target Support Files/Pods-TableCollectionFeatures/Pods-TableCollectionFeatures.debug.xcconfig"; sourceTree = ""; }; 48 | A3E44697D72AC637C927D3F6 /* Pods_TableCollectionFeatures.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TableCollectionFeatures.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | F876FA20827FD30D94EDB6CD /* Pods-TableCollectionFeatures.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TableCollectionFeatures.release.xcconfig"; path = "Target Support Files/Pods-TableCollectionFeatures/Pods-TableCollectionFeatures.release.xcconfig"; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 503D0355268A23E40091684D /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | 525F3F84EE299E8C131E8A49 /* Pods_TableCollectionFeatures.framework in Frameworks */, 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | /* End PBXFrameworksBuildPhase section */ 62 | 63 | /* Begin PBXGroup section */ 64 | 23E5B954710500A1B2522637 /* Frameworks */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | A3E44697D72AC637C927D3F6 /* Pods_TableCollectionFeatures.framework */, 68 | ); 69 | name = Frameworks; 70 | sourceTree = ""; 71 | }; 72 | 503D034F268A23E40091684D = { 73 | isa = PBXGroup; 74 | children = ( 75 | 503D035B268A23E40091684D /* TableCollectionFeatures */, 76 | 503D035A268A23E40091684D /* Products */, 77 | A2A109A9487CDB9E663F8E3B /* Pods */, 78 | 23E5B954710500A1B2522637 /* Frameworks */, 79 | ); 80 | sourceTree = ""; 81 | }; 82 | 503D035A268A23E40091684D /* Products */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 503D0359268A23E40091684D /* TableCollectionFeatures.app */, 86 | ); 87 | name = Products; 88 | sourceTree = ""; 89 | }; 90 | 503D035B268A23E40091684D /* TableCollectionFeatures */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 503D035C268A23E40091684D /* AppDelegate.swift */, 94 | 503D037F268A23E40091684D /* Constants.swift */, 95 | 503D038D268A23E40091684D /* LaunchScreen.storyboard */, 96 | 503D0360268A23E40091684D /* Resources */, 97 | 503D0364268A23E40091684D /* Flows */, 98 | 503D0370268A23E40091684D /* Helpers */, 99 | 503D0375268A23E40091684D /* Extensions */, 100 | 503D0378268A23E40091684D /* BaseClasses */, 101 | ); 102 | path = TableCollectionFeatures; 103 | sourceTree = ""; 104 | }; 105 | 503D0360268A23E40091684D /* Resources */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 503D0361268A23E40091684D /* Assets.xcassets */, 109 | 503D0363268A23E40091684D /* Info.plist */, 110 | ); 111 | path = Resources; 112 | sourceTree = ""; 113 | }; 114 | 503D0364268A23E40091684D /* Flows */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 503D0365268A23E40091684D /* AppCoordinator.swift */, 118 | 503D0367268A23E40091684D /* Main */, 119 | ); 120 | path = Flows; 121 | sourceTree = ""; 122 | }; 123 | 503D0367268A23E40091684D /* Main */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | 503D0368268A23E40091684D /* View */, 127 | 503D036D268A23E40091684D /* ViewModel */, 128 | ); 129 | path = Main; 130 | sourceTree = ""; 131 | }; 132 | 503D0368268A23E40091684D /* View */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 503D03B9268A30A90091684D /* ExpandableCell.swift */, 136 | 503D0369268A23E40091684D /* MainViewController.swift */, 137 | 503D03BD268A68340091684D /* JumpAvoidingFlowLayout.swift */, 138 | ); 139 | path = View; 140 | sourceTree = ""; 141 | }; 142 | 503D036D268A23E40091684D /* ViewModel */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 503D036E268A23E40091684D /* MainViewModel.swift */, 146 | ); 147 | path = ViewModel; 148 | sourceTree = ""; 149 | }; 150 | 503D0370268A23E40091684D /* Helpers */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 503D0371268A23E40091684D /* ErrorTracker.swift */, 154 | 503D0373268A23E40091684D /* ActivityTracker.swift */, 155 | ); 156 | path = Helpers; 157 | sourceTree = ""; 158 | }; 159 | 503D0375268A23E40091684D /* Extensions */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 503D0376268A23E40091684D /* Observable+Extensions.swift */, 163 | ); 164 | path = Extensions; 165 | sourceTree = ""; 166 | }; 167 | 503D0378268A23E40091684D /* BaseClasses */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 503D0379268A23E40091684D /* BaseNavigationController.swift */, 171 | 503D037B268A23E40091684D /* BaseViewController.swift */, 172 | 503D037D268A23E40091684D /* BaseViewModel.swift */, 173 | ); 174 | path = BaseClasses; 175 | sourceTree = ""; 176 | }; 177 | A2A109A9487CDB9E663F8E3B /* Pods */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 8C79D3589280E071350F1F99 /* Pods-TableCollectionFeatures.debug.xcconfig */, 181 | F876FA20827FD30D94EDB6CD /* Pods-TableCollectionFeatures.release.xcconfig */, 182 | ); 183 | path = Pods; 184 | sourceTree = ""; 185 | }; 186 | /* End PBXGroup section */ 187 | 188 | /* Begin PBXNativeTarget section */ 189 | 503D0358268A23E40091684D /* TableCollectionFeatures */ = { 190 | isa = PBXNativeTarget; 191 | buildConfigurationList = 503D0393268A23E40091684D /* Build configuration list for PBXNativeTarget "TableCollectionFeatures" */; 192 | buildPhases = ( 193 | A4BD03FD11AA7B77BEE0DB4B /* [CP] Check Pods Manifest.lock */, 194 | 503D0354268A23E40091684D /* Sources */, 195 | 503D0355268A23E40091684D /* Frameworks */, 196 | 503D0356268A23E40091684D /* Resources */, 197 | 503D0357268A23E40091684D /* SwiftLint */, 198 | E23C6DF4E8EE1F08E18E37D3 /* [CP] Embed Pods Frameworks */, 199 | ); 200 | buildRules = ( 201 | ); 202 | dependencies = ( 203 | ); 204 | name = TableCollectionFeatures; 205 | productName = TableCollectionFeatures; 206 | productReference = 503D0359268A23E40091684D /* TableCollectionFeatures.app */; 207 | productType = "com.apple.product-type.application"; 208 | }; 209 | /* End PBXNativeTarget section */ 210 | 211 | /* Begin PBXProject section */ 212 | 503D0350268A23E40091684D /* Project object */ = { 213 | isa = PBXProject; 214 | attributes = { 215 | LastSwiftUpdateCheck = 1210; 216 | LastUpgradeCheck = 1210; 217 | TargetAttributes = { 218 | 503D0358268A23E40091684D = { 219 | CreatedOnToolsVersion = 12.1; 220 | }; 221 | }; 222 | }; 223 | buildConfigurationList = 503D0353268A23E40091684D /* Build configuration list for PBXProject "TableCollectionFeatures" */; 224 | compatibilityVersion = "Xcode 9.3"; 225 | developmentRegion = en; 226 | hasScannedForEncodings = 0; 227 | knownRegions = ( 228 | en, 229 | Base, 230 | ); 231 | mainGroup = 503D034F268A23E40091684D; 232 | productRefGroup = 503D035A268A23E40091684D /* Products */; 233 | projectDirPath = ""; 234 | projectRoot = ""; 235 | targets = ( 236 | 503D0358268A23E40091684D /* TableCollectionFeatures */, 237 | ); 238 | }; 239 | /* End PBXProject section */ 240 | 241 | /* Begin PBXResourcesBuildPhase section */ 242 | 503D0356268A23E40091684D /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | 503D0362268A23E40091684D /* Assets.xcassets in Resources */, 247 | 503D038F268A23E40091684D /* LaunchScreen.storyboard in Resources */, 248 | 503D038C268A23E40091684D /* (null) in Resources */, 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXResourcesBuildPhase section */ 253 | 254 | /* Begin PBXShellScriptBuildPhase section */ 255 | 503D0357268A23E40091684D /* SwiftLint */ = { 256 | isa = PBXShellScriptBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | ); 260 | inputFileListPaths = ( 261 | ); 262 | inputPaths = ( 263 | ); 264 | name = SwiftLint; 265 | outputFileListPaths = ( 266 | ); 267 | outputPaths = ( 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | shellPath = /bin/sh; 271 | shellScript = "${SRCROOT}/Pods/SwiftLint/swiftlint --config \"${SRCROOT}/.swiftlint.yml\""; 272 | }; 273 | A4BD03FD11AA7B77BEE0DB4B /* [CP] Check Pods Manifest.lock */ = { 274 | isa = PBXShellScriptBuildPhase; 275 | buildActionMask = 2147483647; 276 | files = ( 277 | ); 278 | inputFileListPaths = ( 279 | ); 280 | inputPaths = ( 281 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 282 | "${PODS_ROOT}/Manifest.lock", 283 | ); 284 | name = "[CP] Check Pods Manifest.lock"; 285 | outputFileListPaths = ( 286 | ); 287 | outputPaths = ( 288 | "$(DERIVED_FILE_DIR)/Pods-TableCollectionFeatures-checkManifestLockResult.txt", 289 | ); 290 | runOnlyForDeploymentPostprocessing = 0; 291 | shellPath = /bin/sh; 292 | 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"; 293 | showEnvVarsInLog = 0; 294 | }; 295 | E23C6DF4E8EE1F08E18E37D3 /* [CP] Embed Pods Frameworks */ = { 296 | isa = PBXShellScriptBuildPhase; 297 | buildActionMask = 2147483647; 298 | files = ( 299 | ); 300 | inputFileListPaths = ( 301 | "${PODS_ROOT}/Target Support Files/Pods-TableCollectionFeatures/Pods-TableCollectionFeatures-frameworks-${CONFIGURATION}-input-files.xcfilelist", 302 | ); 303 | name = "[CP] Embed Pods Frameworks"; 304 | outputFileListPaths = ( 305 | "${PODS_ROOT}/Target Support Files/Pods-TableCollectionFeatures/Pods-TableCollectionFeatures-frameworks-${CONFIGURATION}-output-files.xcfilelist", 306 | ); 307 | runOnlyForDeploymentPostprocessing = 0; 308 | shellPath = /bin/sh; 309 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-TableCollectionFeatures/Pods-TableCollectionFeatures-frameworks.sh\"\n"; 310 | showEnvVarsInLog = 0; 311 | }; 312 | /* End PBXShellScriptBuildPhase section */ 313 | 314 | /* Begin PBXSourcesBuildPhase section */ 315 | 503D0354268A23E40091684D /* Sources */ = { 316 | isa = PBXSourcesBuildPhase; 317 | buildActionMask = 2147483647; 318 | files = ( 319 | 503D03BE268A68340091684D /* JumpAvoidingFlowLayout.swift in Sources */, 320 | 503D037A268A23E40091684D /* BaseNavigationController.swift in Sources */, 321 | 503D0380268A23E40091684D /* Constants.swift in Sources */, 322 | 503D037E268A23E40091684D /* BaseViewModel.swift in Sources */, 323 | 503D036A268A23E40091684D /* MainViewController.swift in Sources */, 324 | 503D0366268A23E40091684D /* AppCoordinator.swift in Sources */, 325 | 503D035D268A23E40091684D /* AppDelegate.swift in Sources */, 326 | 503D036F268A23E40091684D /* MainViewModel.swift in Sources */, 327 | 503D037C268A23E40091684D /* BaseViewController.swift in Sources */, 328 | 503D0377268A23E40091684D /* Observable+Extensions.swift in Sources */, 329 | 503D0372268A23E40091684D /* ErrorTracker.swift in Sources */, 330 | 503D0374268A23E40091684D /* ActivityTracker.swift in Sources */, 331 | 503D03BA268A30A90091684D /* ExpandableCell.swift in Sources */, 332 | ); 333 | runOnlyForDeploymentPostprocessing = 0; 334 | }; 335 | /* End PBXSourcesBuildPhase section */ 336 | 337 | /* Begin PBXVariantGroup section */ 338 | 503D038D268A23E40091684D /* LaunchScreen.storyboard */ = { 339 | isa = PBXVariantGroup; 340 | children = ( 341 | 503D038E268A23E40091684D /* Base */, 342 | ); 343 | name = LaunchScreen.storyboard; 344 | sourceTree = ""; 345 | }; 346 | /* End PBXVariantGroup section */ 347 | 348 | /* Begin XCBuildConfiguration section */ 349 | 503D0391268A23E40091684D /* Debug */ = { 350 | isa = XCBuildConfiguration; 351 | buildSettings = { 352 | ALWAYS_SEARCH_USER_PATHS = NO; 353 | CLANG_ANALYZER_NONNULL = YES; 354 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 355 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 356 | CLANG_CXX_LIBRARY = "libc++"; 357 | CLANG_ENABLE_MODULES = YES; 358 | CLANG_ENABLE_OBJC_ARC = YES; 359 | CLANG_ENABLE_OBJC_WEAK = YES; 360 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 361 | CLANG_WARN_BOOL_CONVERSION = YES; 362 | CLANG_WARN_COMMA = YES; 363 | CLANG_WARN_CONSTANT_CONVERSION = YES; 364 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 365 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 366 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 367 | CLANG_WARN_EMPTY_BODY = YES; 368 | CLANG_WARN_ENUM_CONVERSION = YES; 369 | CLANG_WARN_INFINITE_RECURSION = YES; 370 | CLANG_WARN_INT_CONVERSION = YES; 371 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 372 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 373 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 374 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 375 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 376 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 377 | CLANG_WARN_STRICT_PROTOTYPES = YES; 378 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 379 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 380 | CLANG_WARN_UNREACHABLE_CODE = YES; 381 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 382 | COPY_PHASE_STRIP = NO; 383 | DEBUG_INFORMATION_FORMAT = dwarf; 384 | ENABLE_STRICT_OBJC_MSGSEND = YES; 385 | ENABLE_TESTABILITY = YES; 386 | GCC_C_LANGUAGE_STANDARD = gnu11; 387 | GCC_DYNAMIC_NO_PIC = NO; 388 | GCC_NO_COMMON_BLOCKS = YES; 389 | GCC_OPTIMIZATION_LEVEL = 0; 390 | GCC_PREPROCESSOR_DEFINITIONS = ( 391 | "DEBUG=1", 392 | "$(inherited)", 393 | ); 394 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 395 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 396 | GCC_WARN_UNDECLARED_SELECTOR = YES; 397 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 398 | GCC_WARN_UNUSED_FUNCTION = YES; 399 | GCC_WARN_UNUSED_VARIABLE = YES; 400 | INFOPLIST_FILE = TableCollectionFeatures/Resources/Info.plist; 401 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 402 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 403 | MTL_FAST_MATH = YES; 404 | ONLY_ACTIVE_ARCH = YES; 405 | SDKROOT = iphoneos; 406 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 407 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 408 | TARGETED_DEVICE_FAMILY = 1; 409 | }; 410 | name = Debug; 411 | }; 412 | 503D0392268A23E40091684D /* Release */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | ALWAYS_SEARCH_USER_PATHS = NO; 416 | CLANG_ANALYZER_NONNULL = YES; 417 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 418 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 419 | CLANG_CXX_LIBRARY = "libc++"; 420 | CLANG_ENABLE_MODULES = YES; 421 | CLANG_ENABLE_OBJC_ARC = YES; 422 | CLANG_ENABLE_OBJC_WEAK = YES; 423 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 424 | CLANG_WARN_BOOL_CONVERSION = YES; 425 | CLANG_WARN_COMMA = YES; 426 | CLANG_WARN_CONSTANT_CONVERSION = YES; 427 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 428 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 429 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 430 | CLANG_WARN_EMPTY_BODY = YES; 431 | CLANG_WARN_ENUM_CONVERSION = YES; 432 | CLANG_WARN_INFINITE_RECURSION = YES; 433 | CLANG_WARN_INT_CONVERSION = YES; 434 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 435 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 436 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 437 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 438 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 439 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 440 | CLANG_WARN_STRICT_PROTOTYPES = YES; 441 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 442 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 443 | CLANG_WARN_UNREACHABLE_CODE = YES; 444 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 445 | COPY_PHASE_STRIP = NO; 446 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 447 | ENABLE_NS_ASSERTIONS = NO; 448 | ENABLE_STRICT_OBJC_MSGSEND = YES; 449 | GCC_C_LANGUAGE_STANDARD = gnu11; 450 | GCC_NO_COMMON_BLOCKS = YES; 451 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 452 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 453 | GCC_WARN_UNDECLARED_SELECTOR = YES; 454 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 455 | GCC_WARN_UNUSED_FUNCTION = YES; 456 | GCC_WARN_UNUSED_VARIABLE = YES; 457 | INFOPLIST_FILE = TableCollectionFeatures/Resources/Info.plist; 458 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 459 | MTL_ENABLE_DEBUG_INFO = NO; 460 | MTL_FAST_MATH = YES; 461 | SDKROOT = iphoneos; 462 | SWIFT_COMPILATION_MODE = wholemodule; 463 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 464 | TARGETED_DEVICE_FAMILY = 1; 465 | VALIDATE_PRODUCT = YES; 466 | }; 467 | name = Release; 468 | }; 469 | 503D0394268A23E40091684D /* Debug */ = { 470 | isa = XCBuildConfiguration; 471 | baseConfigurationReference = 8C79D3589280E071350F1F99 /* Pods-TableCollectionFeatures.debug.xcconfig */; 472 | buildSettings = { 473 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 474 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 475 | CODE_SIGN_IDENTITY = "iPhone Developer"; 476 | CODE_SIGN_STYLE = Automatic; 477 | DEVELOPER_ACCOUNT = "deploy.madbrains@gmail.com"; 478 | DEVELOPMENT_TEAM = CDVQN43CHN; 479 | INFOPLIST_FILE = TableCollectionFeatures/Resources/Info.plist; 480 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 481 | LD_RUNPATH_SEARCH_PATHS = ( 482 | "$(inherited)", 483 | "@executable_path/Frameworks", 484 | ); 485 | MATCH_PROV_PROFILE_TYPES = ""; 486 | PRODUCT_BUNDLE_IDENTIFIER = ru.madbrains.TableCollectionFeatures; 487 | PRODUCT_NAME = "$(TARGET_NAME)"; 488 | PROVISIONING_PROFILE_SPECIFIER = ""; 489 | SWIFT_VERSION = 5.0; 490 | TARGETED_DEVICE_FAMILY = 1; 491 | }; 492 | name = Debug; 493 | }; 494 | 503D0395268A23E40091684D /* Release */ = { 495 | isa = XCBuildConfiguration; 496 | baseConfigurationReference = F876FA20827FD30D94EDB6CD /* Pods-TableCollectionFeatures.release.xcconfig */; 497 | buildSettings = { 498 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 499 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 500 | CODE_SIGN_IDENTITY = "iPhone Distribution"; 501 | CODE_SIGN_STYLE = Manual; 502 | DEVELOPER_ACCOUNT = "deploy.madbrains@gmail.com"; 503 | DEVELOPMENT_TEAM = CDVQN43CHN; 504 | INFOPLIST_FILE = TableCollectionFeatures/Resources/Info.plist; 505 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 506 | LD_RUNPATH_SEARCH_PATHS = ( 507 | "$(inherited)", 508 | "@executable_path/Frameworks", 509 | ); 510 | MATCH_PROV_PROFILE_TYPES = "adhoc,appstore"; 511 | PRODUCT_BUNDLE_IDENTIFIER = ru.madbrains.TableCollectionFeatures; 512 | PRODUCT_NAME = "$(TARGET_NAME)"; 513 | PROVISIONING_PROFILE_SPECIFIER = "match AdHoc $(PRODUCT_BUNDLE_IDENTIFIER)"; 514 | SWIFT_VERSION = 5.0; 515 | TARGETED_DEVICE_FAMILY = 1; 516 | }; 517 | name = Release; 518 | }; 519 | /* End XCBuildConfiguration section */ 520 | 521 | /* Begin XCConfigurationList section */ 522 | 503D0353268A23E40091684D /* Build configuration list for PBXProject "TableCollectionFeatures" */ = { 523 | isa = XCConfigurationList; 524 | buildConfigurations = ( 525 | 503D0391268A23E40091684D /* Debug */, 526 | 503D0392268A23E40091684D /* Release */, 527 | ); 528 | defaultConfigurationIsVisible = 0; 529 | defaultConfigurationName = Release; 530 | }; 531 | 503D0393268A23E40091684D /* Build configuration list for PBXNativeTarget "TableCollectionFeatures" */ = { 532 | isa = XCConfigurationList; 533 | buildConfigurations = ( 534 | 503D0394268A23E40091684D /* Debug */, 535 | 503D0395268A23E40091684D /* Release */, 536 | ); 537 | defaultConfigurationIsVisible = 0; 538 | defaultConfigurationName = Release; 539 | }; 540 | /* End XCConfigurationList section */ 541 | }; 542 | rootObject = 503D0350268A23E40091684D /* Project object */; 543 | } 544 | -------------------------------------------------------------------------------- /TableCollectionFeatures.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TableCollectionFeatures.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TableCollectionFeatures.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /TableCollectionFeatures.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TableCollectionFeatures/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import UIKit 9 | import XCoordinator 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | private lazy var mainWindow = UIWindow() 15 | 16 | private let router = AppCoordinator().strongRouter 17 | 18 | func application( 19 | _ application: UIApplication, 20 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 21 | ) -> Bool { 22 | router.setRoot(for: mainWindow) 23 | 24 | return true 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /TableCollectionFeatures/BaseClasses/BaseNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseNavigationController.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class BaseNavigationController: UINavigationController { 11 | 12 | override var childForStatusBarStyle: UIViewController? { 13 | topViewController 14 | } 15 | 16 | override var childForStatusBarHidden: UIViewController? { 17 | topViewController 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /TableCollectionFeatures/BaseClasses/BaseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewController.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RxCocoa 11 | 12 | class BaseViewController: UIViewController { 13 | 14 | let disposeBag = DisposeBag() 15 | 16 | var shouldHideNavigationBar = false 17 | 18 | override func viewWillAppear(_ animated: Bool) { 19 | super.viewWillAppear(animated) 20 | navigationController?.setNavigationBarHidden(shouldHideNavigationBar, animated: true) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /TableCollectionFeatures/BaseClasses/BaseViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseViewModel.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import RxSwift 9 | import RxCocoa 10 | 11 | class BaseViewModel { 12 | 13 | let disposeBag = DisposeBag() 14 | 15 | } 16 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | enum Constants { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Extensions/Observable+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Observable+Extensions.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import RxCocoa 11 | 12 | public protocol OptionalType { 13 | 14 | associatedtype Wrapped 15 | 16 | var optional: Wrapped? { get } 17 | 18 | } 19 | 20 | extension Optional: OptionalType { 21 | 22 | public var optional: Wrapped? { self } 23 | 24 | } 25 | 26 | extension ObservableType where Element == Bool { 27 | 28 | func not() -> Observable { 29 | self.map(!) 30 | } 31 | 32 | } 33 | 34 | extension SharedSequenceConvertibleType { 35 | 36 | func mapToVoid() -> SharedSequence { 37 | map { _ in } 38 | } 39 | 40 | } 41 | 42 | extension SharedSequenceConvertibleType where Element == Bool { 43 | 44 | func not() -> SharedSequence { 45 | self.map(!) 46 | } 47 | 48 | func isTrue() -> SharedSequence { 49 | flatMap { isTrue in 50 | guard isTrue else { 51 | return SharedSequence.empty() 52 | } 53 | return SharedSequence.just(true) 54 | } 55 | } 56 | 57 | func filterFalse() -> SharedSequence { 58 | filter { !$0 } 59 | } 60 | 61 | } 62 | 63 | extension SharedSequenceConvertibleType where Element: OptionalType { 64 | 65 | func ignoreNil() -> SharedSequence { 66 | flatMap { value in 67 | value.optional.map { .just($0) } ?? .empty() 68 | } 69 | } 70 | 71 | } 72 | 73 | extension ObservableType { 74 | 75 | func catchErrorJustComplete() -> Observable { 76 | catchError { _ in .empty() } 77 | } 78 | 79 | func asDriverOnErrorJustComplete() -> Driver { 80 | asDriver { _ in .empty() } 81 | } 82 | 83 | func mapToVoid() -> Observable { 84 | map { _ in } 85 | } 86 | 87 | } 88 | 89 | extension ObservableType where Element: OptionalType { 90 | 91 | func ignoreNil() -> Observable { 92 | flatMap { value in 93 | value.optional.map { Observable.just($0) } ?? Observable.empty() 94 | } 95 | } 96 | 97 | } 98 | 99 | extension ObservableType where Element: Collection { 100 | 101 | func ignoreEmpty() -> Observable { 102 | flatMap { array in 103 | array.isEmpty ? Observable.empty() : Observable.just(array) 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Flows/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import UIKit 9 | import XCoordinator 10 | import RxSwift 11 | import RxCocoa 12 | 13 | enum AppRoute: Route { 14 | 15 | case main 16 | 17 | } 18 | 19 | class AppCoordinator: NavigationCoordinator { 20 | 21 | init() { 22 | super.init(rootViewController: BaseNavigationController(), initialRoute: .main) 23 | 24 | rootViewController.setNavigationBarHidden(true, animated: false) 25 | } 26 | 27 | override func prepareTransition(for route: AppRoute) -> NavigationTransition { 28 | switch route { 29 | case .main: 30 | let vc = MainViewController() 31 | return .set([vc]) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Flows/Main/View/ExpandableCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpandableCell.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | class ExpandableCell: UICollectionViewCell { 12 | 13 | // MARK: Переопределяем isSelected, чтобы на каждое изменение вызывать updateAppearance() 14 | override var isSelected: Bool { 15 | didSet { 16 | updateAppearance() 17 | } 18 | } 19 | 20 | // MARK: Констрейнт для расширенного состояния 21 | private var expandedConstraint: Constraint! 22 | 23 | // MARK: Констрейнт для сжатого состояния 24 | private var collapsedConstraint: Constraint! 25 | 26 | private lazy var mainContainer = UIView() 27 | private lazy var topContainer = UIView() 28 | private lazy var bottomContainer = UIView() 29 | 30 | private lazy var arrowImageView: UIImageView = { 31 | let imageView = UIImageView(image: UIImage(named: "arrow_down")!.withRenderingMode(.alwaysTemplate)) 32 | imageView.tintColor = .black 33 | imageView.contentMode = .scaleAspectFit 34 | return imageView 35 | }() 36 | 37 | // // MARK: Работает только в случае, если нам вообще не нужна интеракция с нижним контейнером 38 | // // Нажатия по нижнему контейнеру обрабатываться вообще не будут 39 | // override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { 40 | // topContainer.point(inside: point, with: event) 41 | // } 42 | 43 | override init(frame: CGRect) { 44 | super.init(frame: frame) 45 | 46 | configureView() 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | super.init(coder: coder) 51 | 52 | configureView() 53 | } 54 | 55 | // MARK: При изменении состояния выбора ячейки переключаем констрейнты и анимируем поворот стрелки 56 | private func updateAppearance() { 57 | collapsedConstraint.isActive = !isSelected 58 | expandedConstraint.isActive = isSelected 59 | 60 | UIView.animate(withDuration: 0.3) { 61 | let upsideDown = CGAffineTransform(rotationAngle: .pi * -0.999 ) 62 | self.arrowImageView.transform = self.isSelected ? upsideDown : .identity 63 | } 64 | } 65 | 66 | private func configureView() { 67 | mainContainer.clipsToBounds = true 68 | topContainer.backgroundColor = UIColor.systemYellow 69 | bottomContainer.backgroundColor = UIColor.systemGreen 70 | 71 | // MARK: Добавление гестуры на нижний контейнер позволяет любое нажатие перенаправлять на нее 72 | // В отличие от переопределения pointInside, этот вариант не запрещает интеракцию в контейнере 73 | // Любая кнопка, находящаяся в этом контейнере, сработает, т.к. фактически лежит над гестурой 74 | let tapGesture = UITapGestureRecognizer() 75 | bottomContainer.addGestureRecognizer(tapGesture) 76 | 77 | contentView.cornerRadius = 12 78 | contentView.clipsToBounds = true 79 | 80 | layer.shadowColor = UIColor.black.cgColor 81 | layer.shadowRadius = 5 82 | layer.shadowOpacity = 0.5 83 | layer.shadowOffset = .zero 84 | 85 | makeConstraints() 86 | updateAppearance() 87 | } 88 | 89 | private func makeConstraints() { 90 | contentView.addSubview(mainContainer) 91 | 92 | mainContainer.snp.makeConstraints { make in 93 | make.edges.equalToSuperview() 94 | } 95 | 96 | mainContainer.addSubviews([topContainer, bottomContainer]) 97 | 98 | topContainer.snp.makeConstraints { make in 99 | make.top.left.right.equalToSuperview() 100 | make.height.equalTo(50) 101 | } 102 | 103 | // MARK: Констрейнт для сжатого состояния (низ ячейки совпадает с низом верхнего контейнера) 104 | topContainer.snp.prepareConstraints { make in 105 | collapsedConstraint = make.bottom.equalToSuperview().constraint 106 | collapsedConstraint.layoutConstraints.first?.priority = .defaultLow 107 | } 108 | 109 | topContainer.addSubview(arrowImageView) 110 | 111 | arrowImageView.snp.makeConstraints { make in 112 | make.height.equalTo(16) 113 | make.width.equalTo(24) 114 | make.centerY.equalToSuperview() 115 | make.right.equalToSuperview().offset(-20) 116 | } 117 | 118 | bottomContainer.snp.makeConstraints { make in 119 | make.top.equalTo(topContainer.snp.bottom) 120 | make.left.right.equalToSuperview() 121 | make.height.equalTo(100) 122 | } 123 | 124 | // MARK: Констрейнт для расширенного состояния (низ ячейки совпадает с низом нижнего контейнера) 125 | bottomContainer.snp.prepareConstraints { make in 126 | expandedConstraint = make.bottom.equalToSuperview().constraint 127 | expandedConstraint.layoutConstraints.first?.priority = .defaultLow 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Flows/Main/View/JumpAvoidingFlowLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JumpAvoidingFlowLayout.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 29.06.2021. 6 | // 7 | 8 | import UIKit 9 | 10 | class JumpAvoidingFlowLayout: UICollectionViewFlowLayout { 11 | 12 | override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { 13 | guard let collectionView = collectionView else { 14 | return proposedContentOffset 15 | } 16 | 17 | let targetX: CGFloat = { 18 | let totalWidth = collectionViewContentSize.width + collectionView.contentInset.horizontal 19 | 20 | if totalWidth > collectionView.bounds.size.width { 21 | return proposedContentOffset.x 22 | } 23 | 24 | return 0 25 | }() 26 | 27 | let targetY: CGFloat = { 28 | let totalHeight = collectionViewContentSize.height + collectionView.contentInset.vertical 29 | 30 | if totalHeight > collectionView.bounds.size.height { 31 | return proposedContentOffset.y 32 | } 33 | 34 | return 0 35 | }() 36 | 37 | return CGPoint(x: targetX, y: targetY) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Flows/Main/View/MainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewController.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RxCocoa 11 | import SnapKit 12 | import SwifterSwift 13 | 14 | class MainViewController: BaseViewController { 15 | 16 | private let sizingCell = ExpandableCell() 17 | 18 | // MARK: Важно выставить allowsMultipleSelection для множественного выбора 19 | private lazy var collectionView: UICollectionView = { 20 | let layout = JumpAvoidingFlowLayout() 21 | layout.scrollDirection = .vertical 22 | 23 | let view = UICollectionView(frame: .zero, collectionViewLayout: layout) 24 | view.showsVerticalScrollIndicator = false 25 | view.allowsMultipleSelection = true 26 | view.alwaysBounceVertical = true 27 | view.delegate = self 28 | view.dataSource = self 29 | 30 | return view 31 | }() 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | configureView() 37 | } 38 | 39 | private func configureView() { 40 | title = "Главная" 41 | view.backgroundColor = .white 42 | collectionView.backgroundColor = .white 43 | 44 | collectionView.register(cellWithClass: ExpandableCell.self) 45 | 46 | view.addSubview(collectionView) 47 | 48 | collectionView.snp.makeConstraints { make in 49 | make.edges.equalTo(view.safeAreaLayoutGuide) 50 | } 51 | } 52 | 53 | } 54 | 55 | extension MainViewController: UICollectionViewDataSource { 56 | 57 | func collectionView( 58 | _ collectionView: UICollectionView, 59 | cellForItemAt indexPath: IndexPath 60 | ) -> UICollectionViewCell { 61 | collectionView.dequeueReusableCell(withClass: ExpandableCell.self, for: indexPath) 62 | } 63 | 64 | func numberOfSections(in collectionView: UICollectionView) -> Int { 65 | 1 66 | } 67 | 68 | func collectionView( 69 | _ collectionView: UICollectionView, 70 | numberOfItemsInSection section: Int 71 | ) -> Int { 72 | 5 73 | } 74 | 75 | } 76 | 77 | extension MainViewController: UICollectionViewDelegateFlowLayout { 78 | 79 | // MARK: Динамический расчет высоты 80 | func collectionView( 81 | _ collectionView: UICollectionView, 82 | layout collectionViewLayout: UICollectionViewLayout, 83 | sizeForItemAt indexPath: IndexPath 84 | ) -> CGSize { 85 | let isSelected = collectionView.indexPathsForSelectedItems?.contains(indexPath) ?? false 86 | 87 | sizingCell.frame = CGRect( 88 | origin: .zero, 89 | size: CGSize(width: collectionView.bounds.width - 40, height: 1000) 90 | ) 91 | 92 | sizingCell.isSelected = isSelected 93 | sizingCell.setNeedsLayout() 94 | sizingCell.layoutIfNeeded() 95 | 96 | let size = sizingCell.systemLayoutSizeFitting( 97 | CGSize(width: collectionView.bounds.width - 40, height: .greatestFiniteMagnitude), 98 | withHorizontalFittingPriority: .required, 99 | verticalFittingPriority: .defaultLow 100 | ) 101 | 102 | return size 103 | } 104 | 105 | func collectionView( 106 | _ collectionView: UICollectionView, 107 | layout collectionViewLayout: UICollectionViewLayout, 108 | insetForSectionAt section: Int 109 | ) -> UIEdgeInsets { 110 | UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) 111 | } 112 | 113 | func collectionView( 114 | _ collectionView: UICollectionView, 115 | layout collectionViewLayout: UICollectionViewLayout, 116 | minimumLineSpacingForSectionAt section: Int 117 | ) -> CGFloat { 118 | 24 119 | } 120 | 121 | } 122 | 123 | extension MainViewController: UICollectionViewDelegate { 124 | 125 | // MARK: Переопределяем метод для анимированного сворачивания ячейки 126 | func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { 127 | collectionView.deselectItem(at: indexPath, animated: true) 128 | collectionView.performBatchUpdates(nil) 129 | return true 130 | } 131 | 132 | // MARK: Переопределяем метод для анимированного разворачивания ячейки 133 | func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 134 | collectionView.selectItem(at: indexPath, animated: true, scrollPosition: []) 135 | collectionView.performBatchUpdates(nil) 136 | 137 | // MARK: И скроллим так, чтобы при разворачивании ячейки ее было полностью видно 138 | DispatchQueue.main.async { 139 | guard let attributes = collectionView.collectionViewLayout.layoutAttributesForItem(at: indexPath) else { 140 | return 141 | } 142 | 143 | let desiredOffset = attributes.frame.origin.y - 20 144 | let contentHeight = collectionView.collectionViewLayout.collectionViewContentSize.height 145 | let maxPossibleOffset = contentHeight - collectionView.bounds.height 146 | let finalOffset = max(min(desiredOffset, maxPossibleOffset), 0) 147 | 148 | collectionView.setContentOffset( 149 | CGPoint(x: 0, y: finalOffset), 150 | animated: true 151 | ) 152 | 153 | // MARK: Весь этот костыль можно спокойно заменить на: 154 | // collectionView.scrollToItem(at: indexPath, at: .top, animated: true) 155 | // Но тогда не будет инсета в 20 пикселей сверху (для красоты) 156 | } 157 | 158 | return true 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Flows/Main/ViewModel/MainViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainViewModel.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import RxSwift 9 | import RxCocoa 10 | import XCoordinator 11 | 12 | class MainViewModel: BaseViewModel { 13 | 14 | func transform(input: Input) -> Output { 15 | Output() 16 | } 17 | 18 | } 19 | 20 | extension MainViewModel { 21 | 22 | struct Input { 23 | } 24 | 25 | struct Output { 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Helpers/ActivityTracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTracker.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import RxCocoa 11 | 12 | final class ActivityTracker: SharedSequenceConvertibleType { 13 | 14 | typealias Element = Bool 15 | typealias SharingStrategy = DriverSharingStrategy 16 | 17 | private let _lock = NSRecursiveLock() 18 | private let _variable = BehaviorRelay(value: false) 19 | private let _loading: SharedSequence 20 | 21 | init() { 22 | _loading = _variable.asDriver() 23 | .distinctUntilChanged() 24 | } 25 | 26 | func reset() { 27 | _lock.lock() 28 | _variable.accept(true) 29 | _lock.unlock() 30 | } 31 | 32 | func stop() { 33 | _lock.lock() 34 | _variable.accept(false) 35 | _lock.unlock() 36 | } 37 | 38 | func asSharedSequence() -> SharedSequence { 39 | _loading 40 | } 41 | 42 | private func subscribed() { 43 | _lock.lock() 44 | _variable.accept(true) 45 | _lock.unlock() 46 | } 47 | 48 | private func sendStopLoading() { 49 | _lock.lock() 50 | _variable.accept(false) 51 | _lock.unlock() 52 | } 53 | 54 | fileprivate func trackActivityOfObservable(_ source: O) -> Observable { 55 | source.asObservable() 56 | .do( 57 | onNext: { _ in 58 | self.sendStopLoading() 59 | }, 60 | onError: { _ in 61 | self.sendStopLoading() 62 | }, 63 | onCompleted: { 64 | self.sendStopLoading() 65 | }, 66 | onSubscribe: subscribed 67 | ) 68 | } 69 | 70 | } 71 | 72 | extension ObservableConvertibleType { 73 | 74 | func trackActivity(_ activityTracker: ActivityTracker) -> Observable { 75 | activityTracker.trackActivityOfObservable(self) 76 | } 77 | 78 | } 79 | 80 | extension Driver { 81 | 82 | func trackActivity(_ activityTracker: ActivityTracker) -> Driver { 83 | activityTracker.trackActivityOfObservable(self).asDriverOnErrorJustComplete() 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Helpers/ErrorTracker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorTracker.swift 3 | // TableCollectionFeatures 4 | // 5 | // Created by Alexander Khiger on 28.06.2021. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import RxCocoa 11 | 12 | final class ErrorTracker: SharedSequenceConvertibleType { 13 | 14 | typealias SharingStrategy = DriverSharingStrategy 15 | 16 | private let _subject = PublishSubject() 17 | 18 | deinit { 19 | _subject.onCompleted() 20 | } 21 | 22 | func trackError(from source: O) -> Observable { 23 | source.asObservable().do(onError: onError) 24 | } 25 | 26 | func asSharedSequence() -> SharedSequence { 27 | _subject.asObservable().asDriverOnErrorJustComplete() 28 | } 29 | 30 | func asObservable() -> Observable { 31 | _subject.asObservable() 32 | } 33 | 34 | func onError(_ error: Error) { 35 | _subject.onNext(error) 36 | } 37 | 38 | } 39 | 40 | extension ObservableConvertibleType { 41 | 42 | func trackError(_ errorTracker: ErrorTracker) -> Observable { 43 | errorTracker.trackError(from: self) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Resources/Assets.xcassets/arrow_down.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Icon.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "original" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TableCollectionFeatures/Resources/Assets.xcassets/arrow_down.imageset/Icon.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 0.000000 -1.000000 1.000000 0.000000 10.713379 11.587646 cm 14 | 0.721569 0.756863 0.800000 scn 15 | 0.293458 5.580875 m 16 | -0.096329 5.970663 -0.094204 6.604760 0.293762 6.992725 c 17 | 0.381608 7.080571 l 18 | 0.771564 7.470527 1.401193 7.473140 1.795672 7.078661 c 19 | 6.879697 1.994636 l 20 | 7.270708 1.603625 7.274176 0.973139 6.879697 0.578660 c 21 | 1.795672 -4.505364 l 22 | 1.404662 -4.896375 0.769574 -4.895239 0.381608 -4.507274 c 23 | 0.293762 -4.419429 l 24 | -0.096193 -4.029473 -0.099542 -3.400579 0.293458 -3.007579 c 25 | 4.587685 1.286648 l 26 | 0.293458 5.580875 l 27 | h 28 | f 29 | n 30 | Q 31 | 32 | endstream 33 | endobj 34 | 35 | 3 0 obj 36 | 561 37 | endobj 38 | 39 | 4 0 obj 40 | << /Annots [] 41 | /Type /Page 42 | /MediaBox [ 0.000000 0.000000 24.000000 16.000000 ] 43 | /Resources 1 0 R 44 | /Contents 2 0 R 45 | /Parent 5 0 R 46 | >> 47 | endobj 48 | 49 | 5 0 obj 50 | << /Kids [ 4 0 R ] 51 | /Count 1 52 | /Type /Pages 53 | >> 54 | endobj 55 | 56 | 6 0 obj 57 | << /Type /Catalog 58 | /Pages 5 0 R 59 | >> 60 | endobj 61 | 62 | xref 63 | 0 7 64 | 0000000000 65535 f 65 | 0000000010 00000 n 66 | 0000000034 00000 n 67 | 0000000651 00000 n 68 | 0000000673 00000 n 69 | 0000000846 00000 n 70 | 0000000920 00000 n 71 | trailer 72 | << /ID [ (some) (id) ] 73 | /Root 6 0 R 74 | /Size 7 75 | >> 76 | startxref 77 | 979 78 | %%EOF -------------------------------------------------------------------------------- /TableCollectionFeatures/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | UILaunchStoryboardName 22 | LaunchScreen 23 | LSRequiresIPhoneOS 24 | 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /cleanup.rb: -------------------------------------------------------------------------------- 1 | #import the xcodeproj ruby gem 2 | require "xcodeproj" 3 | 4 | #define the path to your .xcodeproj file 5 | project_path = "./TableCollectionFeatures.xcodeproj" 6 | 7 | #open the xcode project 8 | project = Xcodeproj::Project.open(project_path) 9 | 10 | #iterate over groups, remove Fastlane and Environment references 11 | project.groups.each do |group| 12 | group.recursive_children_groups.each do |childGroup| 13 | if childGroup.display_name == "Fastlane" || childGroup.display_name == "Environment" 14 | childGroup.remove_from_project 15 | end 16 | end 17 | end 18 | 19 | #iterate over files, remove unnecessary files 20 | project.files.each do |file| 21 | hierPath = file.hierarchy_path 22 | 23 | if hierPath == "/TableCollectionFeatures/Info.plist" 24 | file.remove_from_project 25 | File.delete("." + hierPath) if File.exist?("." + hierPath) 26 | end 27 | 28 | if hierPath == "/TableCollectionFeatures/Assets.xcassets" 29 | file.remove_from_project 30 | FileUtils.rm_rf("./TableCollectionFeatures/Assets.xcassets") 31 | end 32 | 33 | end 34 | 35 | puts "All unnecessary files and references were removed" 36 | 37 | # Save the project file 38 | project.save 39 | --------------------------------------------------------------------------------