├── .github └── workflows │ └── app.yml ├── .gitignore ├── .swiftlint.yml ├── GithubProfileWiki ├── Gemfile ├── Gemfile.lock ├── GithubProfileWiki.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ ├── GithubProfileWiki.xcscheme │ │ └── GithubProfileWikiTests.xcscheme ├── GithubProfileWiki │ ├── Model │ │ ├── Followers.swift │ │ └── User.swift │ ├── Networking │ │ ├── Base │ │ │ ├── Endpoint.swift │ │ │ ├── HTTPMethod.swift │ │ │ ├── NetworkConstants.swift │ │ │ ├── NetworkManager.swift │ │ │ └── RequestError.swift │ │ ├── Endpoints │ │ │ ├── FollowersEndpoint.swift │ │ │ └── UserEndpoint.swift │ │ └── Services │ │ │ ├── FollowersService.swift │ │ │ └── UserService.swift │ ├── ProjectFiles │ │ ├── AppDelegate.swift │ │ ├── Info.plist │ │ └── SceneDelegate.swift │ ├── Reusables │ │ ├── Constants │ │ │ └── Constants.swift │ │ ├── Extensions │ │ │ ├── Date+Extension.swift │ │ │ ├── Optional+Extension.swift │ │ │ ├── String+Extension.swift │ │ │ ├── UIImage+Extension.swift │ │ │ ├── UIView+Constraints.swift │ │ │ └── UIViewController+Extension.swift │ │ └── Views │ │ │ ├── AlertPopupView │ │ │ └── AlertPopupViewController.swift │ │ │ ├── BaseBodyLabel │ │ │ └── BaseBodyLabel.swift │ │ │ ├── BaseButton │ │ │ └── BaseUIButton.swift │ │ │ ├── BaseImageView │ │ │ └── BaseImageView.swift │ │ │ ├── BaseTextField │ │ │ └── BaseUITextField.swift │ │ │ ├── BaseTitleLabel │ │ │ └── BaseTitleLabel.swift │ │ │ ├── EmptyStateView │ │ │ └── EmptyStateView.swift │ │ │ ├── GitHubInfoView │ │ │ ├── GithubInfoViewController.swift │ │ │ └── SubViews │ │ │ │ ├── FollowerInfoViewController.swift │ │ │ │ ├── GithubItemInfoView.swift │ │ │ │ └── RepoInfoViewController.swift │ │ │ └── ProfileHeaderView │ │ │ └── ProfileHeaderViewController.swift │ ├── Screens │ │ ├── Favorites │ │ │ ├── FavoriteCell.swift │ │ │ ├── FavoritesView.swift │ │ │ ├── FavoritesViewController.swift │ │ │ └── FavoritesViewModel.swift │ │ ├── FollowersList │ │ │ ├── FollowerCell.swift │ │ │ ├── FollowersListViewController.swift │ │ │ └── FollowersListViewModel.swift │ │ ├── Profile │ │ │ ├── ProfileView.swift │ │ │ ├── ProfileViewController.swift │ │ │ └── ProfileViewModel.swift │ │ └── Search │ │ │ ├── SearchView.swift │ │ │ └── SearchViewController.swift │ ├── StylingResources │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon.png │ │ │ │ ├── icon_20pt@2x-1.png │ │ │ │ ├── icon_20pt@3x.png │ │ │ │ ├── icon_29pt.png │ │ │ │ ├── icon_29pt@2x-1.png │ │ │ │ ├── icon_29pt@3x.png │ │ │ │ ├── icon_40pt@2x-1.png │ │ │ │ ├── icon_40pt@3x.png │ │ │ │ ├── icon_60pt@2x.png │ │ │ │ ├── icon_60pt@3x.png │ │ │ │ ├── icon_76pt.png │ │ │ │ ├── icon_76pt@2x.png │ │ │ │ └── icon_83.5@2x.png │ │ │ ├── Contents.json │ │ │ ├── Icons │ │ │ │ ├── Contents.json │ │ │ │ └── github-search.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ └── github-search.png │ │ │ ├── empty-state-logo-dark.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── empty-state-logo-dark@2x.png │ │ │ │ └── empty-state-logo-dark@3x.png │ │ │ └── empty-state-logo.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── empty-state-logo@2x.png │ │ │ │ └── empty-state-logo@3x.png │ │ └── Base.lproj │ │ │ └── LaunchScreen.storyboard │ └── Utilities │ │ └── UserDefaults │ │ ├── UserDefaults+Helper.swift │ │ └── UserDefaultsManager.swift ├── GithubProfileWikiTests │ ├── DateConverterTests.swift │ ├── GithubProfileWikiTests.swift │ ├── JSONResponses │ │ ├── followers_response.json │ │ └── user_response.json │ └── Mockable.swift └── fastlane │ ├── Appfile │ ├── Fastfile │ └── README.md └── README.md /.github/workflows/app.yml: -------------------------------------------------------------------------------- 1 | name: Development Workflow 2 | 3 | on: 4 | push: 5 | branches: [main, develop] 6 | pull_request: 7 | branches: [main, develop] 8 | paths: 9 | - ".github/workflows/swiftlint.yml" 10 | - ".swiftlint.yml" 11 | - "**/*.swift" 12 | 13 | jobs: 14 | test: 15 | name: Build 16 | runs-on: macos-11 17 | defaults: 18 | run: 19 | working-directory: ./GithubProfileWiki 20 | strategy: 21 | matrix: 22 | destination: ["platform=iOS Simulator,OS=15.2,name=iPhone 12"] 23 | env: 24 | BUNDLE_GEMFILE: ${{ github.workspace }}/GithubProfileWiki/Gemfile 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@master 28 | 29 | - name: Install dependencies 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: 2.7 33 | bundler-cache: true 34 | 35 | - name: Run Unit Tests 36 | run: | 37 | bundle exec fastlane unittest 38 | env: 39 | destination: ${{ matrix.destination }} 40 | - name: Build 41 | run: | 42 | xcodebuild clean build -project GithubProfileWiki.xcodeproj -scheme GithubProfileWiki -destination "${destination}" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO ONLY_ACTIVE_ARCH=NO 43 | env: 44 | destination: ${{ matrix.destination }} 45 | - name: Run SwiftLint 46 | run: swiftlint lint --reporter github-actions-logging 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | GithubProfileWiki/fastlane/report.xml 81 | GithubProfileWiki/fastlane/Preview.html 82 | GithubProfileWiki/fastlane/screenshots/**/*.png 83 | GithubProfileWiki/fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers turned on by default to exclude from running 2 | - colon 3 | - control_statement 4 | opt_in_rules: # some rules are turned off by default, so you need to opt-in 5 | - empty_count # Find all the available rules by running: `swiftlint rules` 6 | 7 | # Alternatively, specify all rules explicitly by uncommenting this option: 8 | # whitelist_rules: # delete `disabled_rules` & `opt_in_rules` if using this 9 | # - empty_parameters 10 | # - vertical_whitespace 11 | 12 | excluded: # paths to ignore during linting. Takes precedence over `included`. 13 | - Pods 14 | 15 | analyzer_rules: # Rules run by `swiftlint analyze` (experimental) 16 | - explicit_self 17 | 18 | # configurable rules can be customized from this configuration file 19 | # binary rules can set their severity level 20 | force_cast: error # implicitly 21 | force_try: 22 | severity: warning # explicitly 23 | # rules that have both warning and error levels, can set just the warning level 24 | # implicitly 25 | line_length: 140 26 | # they can set both implicitly with an array 27 | type_body_length: 28 | # - 300 # warning 29 | - 400 # error 30 | # or they can set both explicitly 31 | file_length: 32 | # warning: 300 33 | error: 800 34 | # naming rules can set warnings/errors for min_length and max_length 35 | # additionally they can set excluded names 36 | type_name: 37 | min_length: 4 # only warning 38 | max_length: # warning and error 39 | # warning: 40 40 | error: 50 41 | excluded: iPhone # excluded via string 42 | allowed_symbols: ["_"] # these are allowed in type names 43 | identifier_name: 44 | min_length: # only min_length 45 | error: 3 # only error 46 | excluded: # excluded via string array 47 | - id 48 | - URL 49 | - to 50 | - tr 51 | - en 52 | - gr 53 | - lt 54 | - no 55 | cyclomatic_complexity: 56 | ignores_case_statements: true 57 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji, sonarqube, markdown, github-actions-logging) 58 | -------------------------------------------------------------------------------- /GithubProfileWiki/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "xcode-install" 5 | -------------------------------------------------------------------------------- /GithubProfileWiki/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.5) 5 | rexml 6 | addressable (2.8.0) 7 | public_suffix (>= 2.0.2, < 5.0) 8 | artifactory (3.0.15) 9 | atomos (0.1.3) 10 | aws-eventstream (1.2.0) 11 | aws-partitions (1.562.0) 12 | aws-sdk-core (3.127.0) 13 | aws-eventstream (~> 1, >= 1.0.2) 14 | aws-partitions (~> 1, >= 1.525.0) 15 | aws-sigv4 (~> 1.1) 16 | jmespath (~> 1.0) 17 | aws-sdk-kms (1.55.0) 18 | aws-sdk-core (~> 3, >= 3.127.0) 19 | aws-sigv4 (~> 1.1) 20 | aws-sdk-s3 (1.113.0) 21 | aws-sdk-core (~> 3, >= 3.127.0) 22 | aws-sdk-kms (~> 1) 23 | aws-sigv4 (~> 1.4) 24 | aws-sigv4 (1.4.0) 25 | aws-eventstream (~> 1, >= 1.0.2) 26 | babosa (1.0.4) 27 | claide (1.0.3) 28 | colored (1.2) 29 | colored2 (3.1.2) 30 | commander (4.6.0) 31 | highline (~> 2.0.0) 32 | declarative (0.0.20) 33 | digest-crc (0.6.4) 34 | rake (>= 12.0.0, < 14.0.0) 35 | domain_name (0.5.20190701) 36 | unf (>= 0.0.5, < 1.0.0) 37 | dotenv (2.7.6) 38 | emoji_regex (3.2.3) 39 | excon (0.91.0) 40 | faraday (1.10.0) 41 | faraday-em_http (~> 1.0) 42 | faraday-em_synchrony (~> 1.0) 43 | faraday-excon (~> 1.1) 44 | faraday-httpclient (~> 1.0) 45 | faraday-multipart (~> 1.0) 46 | faraday-net_http (~> 1.0) 47 | faraday-net_http_persistent (~> 1.0) 48 | faraday-patron (~> 1.0) 49 | faraday-rack (~> 1.0) 50 | faraday-retry (~> 1.0) 51 | ruby2_keywords (>= 0.0.4) 52 | faraday-cookie_jar (0.0.7) 53 | faraday (>= 0.8.0) 54 | http-cookie (~> 1.0.0) 55 | faraday-em_http (1.0.0) 56 | faraday-em_synchrony (1.0.0) 57 | faraday-excon (1.1.0) 58 | faraday-httpclient (1.0.1) 59 | faraday-multipart (1.0.3) 60 | multipart-post (>= 1.2, < 3) 61 | faraday-net_http (1.0.1) 62 | faraday-net_http_persistent (1.2.0) 63 | faraday-patron (1.0.0) 64 | faraday-rack (1.0.0) 65 | faraday-retry (1.0.3) 66 | faraday_middleware (1.2.0) 67 | faraday (~> 1.0) 68 | fastimage (2.2.6) 69 | fastlane (2.204.3) 70 | CFPropertyList (>= 2.3, < 4.0.0) 71 | addressable (>= 2.8, < 3.0.0) 72 | artifactory (~> 3.0) 73 | aws-sdk-s3 (~> 1.0) 74 | babosa (>= 1.0.3, < 2.0.0) 75 | bundler (>= 1.12.0, < 3.0.0) 76 | colored 77 | commander (~> 4.6) 78 | dotenv (>= 2.1.1, < 3.0.0) 79 | emoji_regex (>= 0.1, < 4.0) 80 | excon (>= 0.71.0, < 1.0.0) 81 | faraday (~> 1.0) 82 | faraday-cookie_jar (~> 0.0.6) 83 | faraday_middleware (~> 1.0) 84 | fastimage (>= 2.1.0, < 3.0.0) 85 | gh_inspector (>= 1.1.2, < 2.0.0) 86 | google-apis-androidpublisher_v3 (~> 0.3) 87 | google-apis-playcustomapp_v1 (~> 0.1) 88 | google-cloud-storage (~> 1.31) 89 | highline (~> 2.0) 90 | json (< 3.0.0) 91 | jwt (>= 2.1.0, < 3) 92 | mini_magick (>= 4.9.4, < 5.0.0) 93 | multipart-post (~> 2.0.0) 94 | naturally (~> 2.2) 95 | optparse (~> 0.1.1) 96 | plist (>= 3.1.0, < 4.0.0) 97 | rubyzip (>= 2.0.0, < 3.0.0) 98 | security (= 0.1.3) 99 | simctl (~> 1.6.3) 100 | terminal-notifier (>= 2.0.0, < 3.0.0) 101 | terminal-table (>= 1.4.5, < 2.0.0) 102 | tty-screen (>= 0.6.3, < 1.0.0) 103 | tty-spinner (>= 0.8.0, < 1.0.0) 104 | word_wrap (~> 1.0.0) 105 | xcodeproj (>= 1.13.0, < 2.0.0) 106 | xcpretty (~> 0.3.0) 107 | xcpretty-travis-formatter (>= 0.0.3) 108 | gh_inspector (1.1.3) 109 | google-apis-androidpublisher_v3 (0.16.0) 110 | google-apis-core (>= 0.4, < 2.a) 111 | google-apis-core (0.4.2) 112 | addressable (~> 2.5, >= 2.5.1) 113 | googleauth (>= 0.16.2, < 2.a) 114 | httpclient (>= 2.8.1, < 3.a) 115 | mini_mime (~> 1.0) 116 | representable (~> 3.0) 117 | retriable (>= 2.0, < 4.a) 118 | rexml 119 | webrick 120 | google-apis-iamcredentials_v1 (0.10.0) 121 | google-apis-core (>= 0.4, < 2.a) 122 | google-apis-playcustomapp_v1 (0.7.0) 123 | google-apis-core (>= 0.4, < 2.a) 124 | google-apis-storage_v1 (0.11.0) 125 | google-apis-core (>= 0.4, < 2.a) 126 | google-cloud-core (1.6.0) 127 | google-cloud-env (~> 1.0) 128 | google-cloud-errors (~> 1.0) 129 | google-cloud-env (1.5.0) 130 | faraday (>= 0.17.3, < 2.0) 131 | google-cloud-errors (1.2.0) 132 | google-cloud-storage (1.36.1) 133 | addressable (~> 2.8) 134 | digest-crc (~> 0.4) 135 | google-apis-iamcredentials_v1 (~> 0.1) 136 | google-apis-storage_v1 (~> 0.1) 137 | google-cloud-core (~> 1.6) 138 | googleauth (>= 0.16.2, < 2.a) 139 | mini_mime (~> 1.0) 140 | googleauth (1.1.2) 141 | faraday (>= 0.17.3, < 3.a) 142 | jwt (>= 1.4, < 3.0) 143 | memoist (~> 0.16) 144 | multi_json (~> 1.11) 145 | os (>= 0.9, < 2.0) 146 | signet (>= 0.16, < 2.a) 147 | highline (2.0.3) 148 | http-cookie (1.0.4) 149 | domain_name (~> 0.5) 150 | httpclient (2.8.3) 151 | jmespath (1.6.0) 152 | json (2.6.1) 153 | jwt (2.3.0) 154 | memoist (0.16.2) 155 | mini_magick (4.11.0) 156 | mini_mime (1.1.2) 157 | multi_json (1.15.0) 158 | multipart-post (2.0.0) 159 | nanaimo (0.3.0) 160 | naturally (2.2.1) 161 | optparse (0.1.1) 162 | os (1.1.4) 163 | plist (3.6.0) 164 | public_suffix (4.0.6) 165 | rake (13.0.6) 166 | representable (3.1.1) 167 | declarative (< 0.1.0) 168 | trailblazer-option (>= 0.1.1, < 0.2.0) 169 | uber (< 0.2.0) 170 | retriable (3.1.2) 171 | rexml (3.2.5) 172 | rouge (2.0.7) 173 | ruby2_keywords (0.0.5) 174 | rubyzip (2.3.2) 175 | security (0.1.3) 176 | signet (0.16.1) 177 | addressable (~> 2.8) 178 | faraday (>= 0.17.5, < 3.0) 179 | jwt (>= 1.5, < 3.0) 180 | multi_json (~> 1.10) 181 | simctl (1.6.8) 182 | CFPropertyList 183 | naturally 184 | terminal-notifier (2.0.0) 185 | terminal-table (1.8.0) 186 | unicode-display_width (~> 1.1, >= 1.1.1) 187 | trailblazer-option (0.1.2) 188 | tty-cursor (0.7.1) 189 | tty-screen (0.8.1) 190 | tty-spinner (0.9.3) 191 | tty-cursor (~> 0.7) 192 | uber (0.1.0) 193 | unf (0.1.4) 194 | unf_ext 195 | unf_ext (0.0.8) 196 | unicode-display_width (1.8.0) 197 | webrick (1.7.0) 198 | word_wrap (1.0.0) 199 | xcode-install (2.8.0) 200 | claide (>= 0.9.1, < 1.1.0) 201 | fastlane (>= 2.1.0, < 3.0.0) 202 | xcodeproj (1.21.0) 203 | CFPropertyList (>= 2.3.3, < 4.0) 204 | atomos (~> 0.1.3) 205 | claide (>= 1.0.2, < 2.0) 206 | colored2 (~> 3.1) 207 | nanaimo (~> 0.3.0) 208 | rexml (~> 3.2.4) 209 | xcpretty (0.3.0) 210 | rouge (~> 2.0.7) 211 | xcpretty-travis-formatter (1.0.1) 212 | xcpretty (~> 0.2, >= 0.0.7) 213 | 214 | PLATFORMS 215 | universal-darwin-20 216 | x86_64-darwin-19 217 | 218 | DEPENDENCIES 219 | fastlane 220 | xcode-install 221 | 222 | BUNDLED WITH 223 | 2.3.8 224 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3609175B279F609A0031FF14 /* FollowerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3609175A279F609A0031FF14 /* FollowerCell.swift */; }; 11 | 3609175E279F62550031FF14 /* BaseImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3609175D279F62550031FF14 /* BaseImageView.swift */; }; 12 | 362435AC27CE832A00DBDA35 /* UserEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 362435AB27CE832A00DBDA35 /* UserEndpoint.swift */; }; 13 | 363BF5A327ADD9F3005028C1 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 363BF5A227ADD9F3005028C1 /* Kingfisher */; }; 14 | 363F40BB27AEE9C5006C79AB /* EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40BA27AEE9C5006C79AB /* EmptyStateView.swift */; }; 15 | 363F40BE27AF174A006C79AB /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40BD27AF174A006C79AB /* ProfileViewController.swift */; }; 16 | 363F40C027AF1C2F006C79AB /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40BF27AF1C2F006C79AB /* UserService.swift */; }; 17 | 363F40C227AF1C80006C79AB /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40C127AF1C80006C79AB /* ProfileViewModel.swift */; }; 18 | 363F40C427AF1CA3006C79AB /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40C327AF1CA3006C79AB /* User.swift */; }; 19 | 363F40C727AF28F9006C79AB /* ProfileHeaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40C627AF28F9006C79AB /* ProfileHeaderViewController.swift */; }; 20 | 363F40CC27AFDF1D006C79AB /* GithubItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40CB27AFDF1D006C79AB /* GithubItemInfoView.swift */; }; 21 | 363F40CF27AFE451006C79AB /* GithubInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 363F40CE27AFE450006C79AB /* GithubInfoViewController.swift */; }; 22 | 364D4AB027B48F0600E773B6 /* RepoInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4AAF27B48F0600E773B6 /* RepoInfoViewController.swift */; }; 23 | 364D4AB227B4906D00E773B6 /* FollowerInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4AB127B4906D00E773B6 /* FollowerInfoViewController.swift */; }; 24 | 364D4AB427B496F900E773B6 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4AB327B496F900E773B6 /* String+Extension.swift */; }; 25 | 364D4AB627B497C000E773B6 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4AB527B497C000E773B6 /* Date+Extension.swift */; }; 26 | 364D4ABB27B5A9F900E773B6 /* UserDefaultsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4ABA27B5A9F900E773B6 /* UserDefaultsManager.swift */; }; 27 | 364D4ABD27B5B73400E773B6 /* UserDefaults+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4ABC27B5B73400E773B6 /* UserDefaults+Helper.swift */; }; 28 | 364D4ABF27B5B85100E773B6 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4ABE27B5B85100E773B6 /* Optional+Extension.swift */; }; 29 | 364D4AC127B5C7C900E773B6 /* FavoriteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364D4AC027B5C7C900E773B6 /* FavoriteCell.swift */; }; 30 | 364E0773279522530085E799 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364E0772279522530085E799 /* AppDelegate.swift */; }; 31 | 364E0775279522530085E799 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364E0774279522530085E799 /* SceneDelegate.swift */; }; 32 | 364E077C279522540085E799 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 364E077B279522540085E799 /* Assets.xcassets */; }; 33 | 364E077F279522540085E799 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 364E077D279522540085E799 /* LaunchScreen.storyboard */; }; 34 | 364E0787279527600085E799 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364E0786279527600085E799 /* SearchViewController.swift */; }; 35 | 364E0789279527950085E799 /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364E0788279527950085E799 /* FavoritesViewController.swift */; }; 36 | 364E078B27989BB60085E799 /* BaseUITextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364E078A27989BB60085E799 /* BaseUITextField.swift */; }; 37 | 364E078E279927BB0085E799 /* UIImage+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 364E078D279927BB0085E799 /* UIImage+Extension.swift */; }; 38 | 3651CAEE27CEF51F006D208A /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3651CAED27CEF51F006D208A /* Mockable.swift */; }; 39 | 365802E027CAE7DF00925158 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365802DF27CAE7DF00925158 /* SearchView.swift */; }; 40 | 365802EA27CB098100925158 /* FavoritesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365802E927CB098100925158 /* FavoritesViewModel.swift */; }; 41 | 365802EE27CBC8A700925158 /* FavoritesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365802ED27CBC8A700925158 /* FavoritesView.swift */; }; 42 | 365931BE279C409F00834D25 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365931BD279C409F00834D25 /* Constants.swift */; }; 43 | 365931C9279C442700834D25 /* UIView+Constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = 365931C8279C442700834D25 /* UIView+Constraints.swift */; }; 44 | 368CD922279D8CA7003FABA6 /* NetworkConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 368CD921279D8CA7003FABA6 /* NetworkConstants.swift */; }; 45 | 368CD92E279D9347003FABA6 /* FollowersService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 368CD92D279D9347003FABA6 /* FollowersService.swift */; }; 46 | 368CD931279D95F5003FABA6 /* Followers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 368CD930279D95F5003FABA6 /* Followers.swift */; }; 47 | 368CD933279D9937003FABA6 /* FollowersListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 368CD932279D9937003FABA6 /* FollowersListViewModel.swift */; }; 48 | 36AA948327BEC24400254E77 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AA948227BEC24400254E77 /* ProfileView.swift */; }; 49 | 36AD8877279C16D000B0B5D3 /* BaseTitleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD8876279C16D000B0B5D3 /* BaseTitleLabel.swift */; }; 50 | 36AD8879279C183200B0B5D3 /* BaseBodyLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD8878279C183200B0B5D3 /* BaseBodyLabel.swift */; }; 51 | 36AD887B279C198D00B0B5D3 /* AlertPopupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD887A279C198D00B0B5D3 /* AlertPopupViewController.swift */; }; 52 | 36AD887D279C25E900B0B5D3 /* UIViewController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36AD887C279C25E900B0B5D3 /* UIViewController+Extension.swift */; }; 53 | 36B88A1727CEF89A0024659B /* followers_response.json in Resources */ = {isa = PBXBuildFile; fileRef = 36B88A1627CEF89A0024659B /* followers_response.json */; }; 54 | 36B88A1927CEFA410024659B /* user_response.json in Resources */ = {isa = PBXBuildFile; fileRef = 36B88A1827CEFA410024659B /* user_response.json */; }; 55 | 36E0302D27CD919F0002DBF0 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E0302C27CD919F0002DBF0 /* Endpoint.swift */; }; 56 | 36E0302F27CD92270002DBF0 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E0302E27CD92270002DBF0 /* HTTPMethod.swift */; }; 57 | 36E0303127CD92750002DBF0 /* RequestError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E0303027CD92750002DBF0 /* RequestError.swift */; }; 58 | 36E0303327CD931F0002DBF0 /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E0303227CD931F0002DBF0 /* NetworkManager.swift */; }; 59 | 36E0303A27CD94880002DBF0 /* FollowersEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36E0303927CD94880002DBF0 /* FollowersEndpoint.swift */; }; 60 | 36F286BB2799302A006F389D /* FollowersListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F286BA2799302A006F389D /* FollowersListViewController.swift */; }; 61 | 36F5975927CD605C006B048A /* GithubProfileWikiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F5975827CD605C006B048A /* GithubProfileWikiTests.swift */; }; 62 | 36F5976027CD6594006B048A /* DateConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F5975F27CD6594006B048A /* DateConverterTests.swift */; }; 63 | 5FA7CF5E94C4D36BF5C3DBFD /* BaseUIButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA7C310A09EA8FB121B2813 /* BaseUIButton.swift */; }; 64 | /* End PBXBuildFile section */ 65 | 66 | /* Begin PBXContainerItemProxy section */ 67 | 36F5975A27CD605C006B048A /* PBXContainerItemProxy */ = { 68 | isa = PBXContainerItemProxy; 69 | containerPortal = 364E0767279522530085E799 /* Project object */; 70 | proxyType = 1; 71 | remoteGlobalIDString = 364E076E279522530085E799; 72 | remoteInfo = GithubProfileWiki; 73 | }; 74 | /* End PBXContainerItemProxy section */ 75 | 76 | /* Begin PBXFileReference section */ 77 | 3609175A279F609A0031FF14 /* FollowerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerCell.swift; sourceTree = ""; }; 78 | 3609175D279F62550031FF14 /* BaseImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseImageView.swift; sourceTree = ""; }; 79 | 362435AB27CE832A00DBDA35 /* UserEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEndpoint.swift; sourceTree = ""; }; 80 | 363F40BA27AEE9C5006C79AB /* EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateView.swift; sourceTree = ""; }; 81 | 363F40BD27AF174A006C79AB /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; 82 | 363F40BF27AF1C2F006C79AB /* UserService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = ""; }; 83 | 363F40C127AF1C80006C79AB /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; 84 | 363F40C327AF1CA3006C79AB /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 85 | 363F40C627AF28F9006C79AB /* ProfileHeaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderViewController.swift; sourceTree = ""; }; 86 | 363F40CB27AFDF1D006C79AB /* GithubItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubItemInfoView.swift; sourceTree = ""; }; 87 | 363F40CE27AFE450006C79AB /* GithubInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubInfoViewController.swift; sourceTree = ""; }; 88 | 364D4AAF27B48F0600E773B6 /* RepoInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepoInfoViewController.swift; sourceTree = ""; }; 89 | 364D4AB127B4906D00E773B6 /* FollowerInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerInfoViewController.swift; sourceTree = ""; }; 90 | 364D4AB327B496F900E773B6 /* String+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; 91 | 364D4AB527B497C000E773B6 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 92 | 364D4ABA27B5A9F900E773B6 /* UserDefaultsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsManager.swift; sourceTree = ""; }; 93 | 364D4ABC27B5B73400E773B6 /* UserDefaults+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Helper.swift"; sourceTree = ""; }; 94 | 364D4ABE27B5B85100E773B6 /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; }; 95 | 364D4AC027B5C7C900E773B6 /* FavoriteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteCell.swift; sourceTree = ""; }; 96 | 364E076F279522530085E799 /* GithubProfileWiki.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubProfileWiki.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97 | 364E0772279522530085E799 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 98 | 364E0774279522530085E799 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 99 | 364E077B279522540085E799 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 100 | 364E077E279522540085E799 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 101 | 364E0780279522540085E799 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 102 | 364E0786279527600085E799 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; 103 | 364E0788279527950085E799 /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = ""; }; 104 | 364E078A27989BB60085E799 /* BaseUITextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseUITextField.swift; sourceTree = ""; }; 105 | 364E078D279927BB0085E799 /* UIImage+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extension.swift"; sourceTree = ""; }; 106 | 3651CAED27CEF51F006D208A /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = ""; }; 107 | 365802DF27CAE7DF00925158 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; }; 108 | 365802E927CB098100925158 /* FavoritesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewModel.swift; sourceTree = ""; }; 109 | 365802ED27CBC8A700925158 /* FavoritesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesView.swift; sourceTree = ""; }; 110 | 365931BD279C409F00834D25 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 111 | 365931C8279C442700834D25 /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; 112 | 368CD921279D8CA7003FABA6 /* NetworkConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConstants.swift; sourceTree = ""; }; 113 | 368CD92D279D9347003FABA6 /* FollowersService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersService.swift; sourceTree = ""; }; 114 | 368CD930279D95F5003FABA6 /* Followers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Followers.swift; sourceTree = ""; }; 115 | 368CD932279D9937003FABA6 /* FollowersListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersListViewModel.swift; sourceTree = ""; }; 116 | 36AA948227BEC24400254E77 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 117 | 36AD8876279C16D000B0B5D3 /* BaseTitleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTitleLabel.swift; sourceTree = ""; }; 118 | 36AD8878279C183200B0B5D3 /* BaseBodyLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBodyLabel.swift; sourceTree = ""; }; 119 | 36AD887A279C198D00B0B5D3 /* AlertPopupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPopupViewController.swift; sourceTree = ""; }; 120 | 36AD887C279C25E900B0B5D3 /* UIViewController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extension.swift"; sourceTree = ""; }; 121 | 36B88A1627CEF89A0024659B /* followers_response.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = followers_response.json; sourceTree = ""; }; 122 | 36B88A1827CEFA410024659B /* user_response.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = user_response.json; sourceTree = ""; }; 123 | 36E0302C27CD919F0002DBF0 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; 124 | 36E0302E27CD92270002DBF0 /* HTTPMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPMethod.swift; sourceTree = ""; }; 125 | 36E0303027CD92750002DBF0 /* RequestError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestError.swift; sourceTree = ""; }; 126 | 36E0303227CD931F0002DBF0 /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; 127 | 36E0303927CD94880002DBF0 /* FollowersEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersEndpoint.swift; sourceTree = ""; }; 128 | 36F286BA2799302A006F389D /* FollowersListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersListViewController.swift; sourceTree = ""; }; 129 | 36F5975627CD605C006B048A /* GithubProfileWikiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GithubProfileWikiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 130 | 36F5975827CD605C006B048A /* GithubProfileWikiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubProfileWikiTests.swift; sourceTree = ""; }; 131 | 36F5975F27CD6594006B048A /* DateConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateConverterTests.swift; sourceTree = ""; }; 132 | 5FA7C310A09EA8FB121B2813 /* BaseUIButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseUIButton.swift; sourceTree = ""; }; 133 | /* End PBXFileReference section */ 134 | 135 | /* Begin PBXFrameworksBuildPhase section */ 136 | 364E076C279522530085E799 /* Frameworks */ = { 137 | isa = PBXFrameworksBuildPhase; 138 | buildActionMask = 2147483647; 139 | files = ( 140 | 363BF5A327ADD9F3005028C1 /* Kingfisher in Frameworks */, 141 | ); 142 | runOnlyForDeploymentPostprocessing = 0; 143 | }; 144 | 36F5975327CD605C006B048A /* Frameworks */ = { 145 | isa = PBXFrameworksBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | ); 149 | runOnlyForDeploymentPostprocessing = 0; 150 | }; 151 | /* End PBXFrameworksBuildPhase section */ 152 | 153 | /* Begin PBXGroup section */ 154 | 3609175C279F623E0031FF14 /* BaseImageView */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | 3609175D279F62550031FF14 /* BaseImageView.swift */, 158 | ); 159 | path = BaseImageView; 160 | sourceTree = ""; 161 | }; 162 | 363F40B927AEE9B0006C79AB /* EmptyStateView */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 363F40BA27AEE9C5006C79AB /* EmptyStateView.swift */, 166 | ); 167 | path = EmptyStateView; 168 | sourceTree = ""; 169 | }; 170 | 363F40BC27AF173A006C79AB /* Profile */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 363F40BD27AF174A006C79AB /* ProfileViewController.swift */, 174 | 363F40C127AF1C80006C79AB /* ProfileViewModel.swift */, 175 | 36AA948227BEC24400254E77 /* ProfileView.swift */, 176 | ); 177 | path = Profile; 178 | sourceTree = ""; 179 | }; 180 | 363F40C527AF28E7006C79AB /* ProfileHeaderView */ = { 181 | isa = PBXGroup; 182 | children = ( 183 | 363F40C627AF28F9006C79AB /* ProfileHeaderViewController.swift */, 184 | ); 185 | path = ProfileHeaderView; 186 | sourceTree = ""; 187 | }; 188 | 363F40CD27AFE430006C79AB /* GitHubInfoView */ = { 189 | isa = PBXGroup; 190 | children = ( 191 | 364D4AAE27B48EB300E773B6 /* SubViews */, 192 | 363F40CE27AFE450006C79AB /* GithubInfoViewController.swift */, 193 | ); 194 | path = GitHubInfoView; 195 | sourceTree = ""; 196 | }; 197 | 364D4AAE27B48EB300E773B6 /* SubViews */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | 363F40CB27AFDF1D006C79AB /* GithubItemInfoView.swift */, 201 | 364D4AAF27B48F0600E773B6 /* RepoInfoViewController.swift */, 202 | 364D4AB127B4906D00E773B6 /* FollowerInfoViewController.swift */, 203 | ); 204 | path = SubViews; 205 | sourceTree = ""; 206 | }; 207 | 364D4AB827B5A9DF00E773B6 /* Utilities */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | 364D4AB927B5A9E600E773B6 /* UserDefaults */, 211 | ); 212 | path = Utilities; 213 | sourceTree = ""; 214 | }; 215 | 364D4AB927B5A9E600E773B6 /* UserDefaults */ = { 216 | isa = PBXGroup; 217 | children = ( 218 | 364D4ABA27B5A9F900E773B6 /* UserDefaultsManager.swift */, 219 | 364D4ABC27B5B73400E773B6 /* UserDefaults+Helper.swift */, 220 | ); 221 | path = UserDefaults; 222 | sourceTree = ""; 223 | }; 224 | 364E0766279522530085E799 = { 225 | isa = PBXGroup; 226 | children = ( 227 | 364E0771279522530085E799 /* GithubProfileWiki */, 228 | 36F5975727CD605C006B048A /* GithubProfileWikiTests */, 229 | 364E0770279522530085E799 /* Products */, 230 | ); 231 | sourceTree = ""; 232 | }; 233 | 364E0770279522530085E799 /* Products */ = { 234 | isa = PBXGroup; 235 | children = ( 236 | 364E076F279522530085E799 /* GithubProfileWiki.app */, 237 | 36F5975627CD605C006B048A /* GithubProfileWikiTests.xctest */, 238 | ); 239 | name = Products; 240 | sourceTree = ""; 241 | }; 242 | 364E0771279522530085E799 /* GithubProfileWiki */ = { 243 | isa = PBXGroup; 244 | children = ( 245 | 368CD92F279D95EB003FABA6 /* Model */, 246 | 368CD91C279D8A3A003FABA6 /* Networking */, 247 | 365931BB279C3F2000834D25 /* ProjectFiles */, 248 | 36AD8875279C16BA00B0B5D3 /* Reusables */, 249 | 365931B9279C3EE900834D25 /* Screens */, 250 | 365931BA279C3F0F00834D25 /* StylingResources */, 251 | 364D4AB827B5A9DF00E773B6 /* Utilities */, 252 | ); 253 | path = GithubProfileWiki; 254 | sourceTree = ""; 255 | }; 256 | 364E078C279927A50085E799 /* Extensions */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | 364E078D279927BB0085E799 /* UIImage+Extension.swift */, 260 | 36AD887C279C25E900B0B5D3 /* UIViewController+Extension.swift */, 261 | 365931C8279C442700834D25 /* UIView+Constraints.swift */, 262 | 364D4AB327B496F900E773B6 /* String+Extension.swift */, 263 | 364D4AB527B497C000E773B6 /* Date+Extension.swift */, 264 | 364D4ABE27B5B85100E773B6 /* Optional+Extension.swift */, 265 | ); 266 | path = Extensions; 267 | sourceTree = ""; 268 | }; 269 | 3651CAEF27CEF5FC006D208A /* JSONResponses */ = { 270 | isa = PBXGroup; 271 | children = ( 272 | 36B88A1827CEFA410024659B /* user_response.json */, 273 | 36B88A1627CEF89A0024659B /* followers_response.json */, 274 | ); 275 | path = JSONResponses; 276 | sourceTree = ""; 277 | }; 278 | 365931B9279C3EE900834D25 /* Screens */ = { 279 | isa = PBXGroup; 280 | children = ( 281 | 363F40BC27AF173A006C79AB /* Profile */, 282 | 365931C7279C412600834D25 /* FollowersList */, 283 | 365931C6279C411D00834D25 /* Favorites */, 284 | 365931C5279C411600834D25 /* Search */, 285 | ); 286 | path = Screens; 287 | sourceTree = ""; 288 | }; 289 | 365931BA279C3F0F00834D25 /* StylingResources */ = { 290 | isa = PBXGroup; 291 | children = ( 292 | 364E077B279522540085E799 /* Assets.xcassets */, 293 | 364E077D279522540085E799 /* LaunchScreen.storyboard */, 294 | ); 295 | path = StylingResources; 296 | sourceTree = ""; 297 | }; 298 | 365931BB279C3F2000834D25 /* ProjectFiles */ = { 299 | isa = PBXGroup; 300 | children = ( 301 | 364E0774279522530085E799 /* SceneDelegate.swift */, 302 | 364E0780279522540085E799 /* Info.plist */, 303 | 364E0772279522530085E799 /* AppDelegate.swift */, 304 | ); 305 | path = ProjectFiles; 306 | sourceTree = ""; 307 | }; 308 | 365931BC279C409500834D25 /* Constants */ = { 309 | isa = PBXGroup; 310 | children = ( 311 | 365931BD279C409F00834D25 /* Constants.swift */, 312 | ); 313 | path = Constants; 314 | sourceTree = ""; 315 | }; 316 | 365931BF279C40BC00834D25 /* BaseButton */ = { 317 | isa = PBXGroup; 318 | children = ( 319 | 5FA7C310A09EA8FB121B2813 /* BaseUIButton.swift */, 320 | ); 321 | path = BaseButton; 322 | sourceTree = ""; 323 | }; 324 | 365931C0279C40CB00834D25 /* Views */ = { 325 | isa = PBXGroup; 326 | children = ( 327 | 363F40CD27AFE430006C79AB /* GitHubInfoView */, 328 | 363F40C527AF28E7006C79AB /* ProfileHeaderView */, 329 | 363F40B927AEE9B0006C79AB /* EmptyStateView */, 330 | 3609175C279F623E0031FF14 /* BaseImageView */, 331 | 365931C4279C40F500834D25 /* AlertPopupView */, 332 | 365931C3279C40E900834D25 /* BaseBodyLabel */, 333 | 365931C2279C40DE00834D25 /* BaseTitleLabel */, 334 | 365931C1279C40D300834D25 /* BaseTextField */, 335 | 365931BF279C40BC00834D25 /* BaseButton */, 336 | ); 337 | path = Views; 338 | sourceTree = ""; 339 | }; 340 | 365931C1279C40D300834D25 /* BaseTextField */ = { 341 | isa = PBXGroup; 342 | children = ( 343 | 364E078A27989BB60085E799 /* BaseUITextField.swift */, 344 | ); 345 | path = BaseTextField; 346 | sourceTree = ""; 347 | }; 348 | 365931C2279C40DE00834D25 /* BaseTitleLabel */ = { 349 | isa = PBXGroup; 350 | children = ( 351 | 36AD8876279C16D000B0B5D3 /* BaseTitleLabel.swift */, 352 | ); 353 | path = BaseTitleLabel; 354 | sourceTree = ""; 355 | }; 356 | 365931C3279C40E900834D25 /* BaseBodyLabel */ = { 357 | isa = PBXGroup; 358 | children = ( 359 | 36AD8878279C183200B0B5D3 /* BaseBodyLabel.swift */, 360 | ); 361 | path = BaseBodyLabel; 362 | sourceTree = ""; 363 | }; 364 | 365931C4279C40F500834D25 /* AlertPopupView */ = { 365 | isa = PBXGroup; 366 | children = ( 367 | 36AD887A279C198D00B0B5D3 /* AlertPopupViewController.swift */, 368 | ); 369 | path = AlertPopupView; 370 | sourceTree = ""; 371 | }; 372 | 365931C5279C411600834D25 /* Search */ = { 373 | isa = PBXGroup; 374 | children = ( 375 | 364E0786279527600085E799 /* SearchViewController.swift */, 376 | 365802DF27CAE7DF00925158 /* SearchView.swift */, 377 | ); 378 | path = Search; 379 | sourceTree = ""; 380 | }; 381 | 365931C6279C411D00834D25 /* Favorites */ = { 382 | isa = PBXGroup; 383 | children = ( 384 | 364E0788279527950085E799 /* FavoritesViewController.swift */, 385 | 364D4AC027B5C7C900E773B6 /* FavoriteCell.swift */, 386 | 365802E927CB098100925158 /* FavoritesViewModel.swift */, 387 | 365802ED27CBC8A700925158 /* FavoritesView.swift */, 388 | ); 389 | path = Favorites; 390 | sourceTree = ""; 391 | }; 392 | 365931C7279C412600834D25 /* FollowersList */ = { 393 | isa = PBXGroup; 394 | children = ( 395 | 36F286BA2799302A006F389D /* FollowersListViewController.swift */, 396 | 368CD932279D9937003FABA6 /* FollowersListViewModel.swift */, 397 | 3609175A279F609A0031FF14 /* FollowerCell.swift */, 398 | ); 399 | path = FollowersList; 400 | sourceTree = ""; 401 | }; 402 | 368CD91C279D8A3A003FABA6 /* Networking */ = { 403 | isa = PBXGroup; 404 | children = ( 405 | 36E0303627CD94380002DBF0 /* Services */, 406 | 36E0303427CD94230002DBF0 /* Endpoints */, 407 | 36E0302B27CD91920002DBF0 /* Base */, 408 | ); 409 | path = Networking; 410 | sourceTree = ""; 411 | }; 412 | 368CD92F279D95EB003FABA6 /* Model */ = { 413 | isa = PBXGroup; 414 | children = ( 415 | 368CD930279D95F5003FABA6 /* Followers.swift */, 416 | 363F40C327AF1CA3006C79AB /* User.swift */, 417 | ); 418 | path = Model; 419 | sourceTree = ""; 420 | }; 421 | 36AD8875279C16BA00B0B5D3 /* Reusables */ = { 422 | isa = PBXGroup; 423 | children = ( 424 | 365931C0279C40CB00834D25 /* Views */, 425 | 365931BC279C409500834D25 /* Constants */, 426 | 364E078C279927A50085E799 /* Extensions */, 427 | ); 428 | path = Reusables; 429 | sourceTree = ""; 430 | }; 431 | 36E0302B27CD91920002DBF0 /* Base */ = { 432 | isa = PBXGroup; 433 | children = ( 434 | 368CD921279D8CA7003FABA6 /* NetworkConstants.swift */, 435 | 36E0302C27CD919F0002DBF0 /* Endpoint.swift */, 436 | 36E0302E27CD92270002DBF0 /* HTTPMethod.swift */, 437 | 36E0303027CD92750002DBF0 /* RequestError.swift */, 438 | 36E0303227CD931F0002DBF0 /* NetworkManager.swift */, 439 | ); 440 | path = Base; 441 | sourceTree = ""; 442 | }; 443 | 36E0303427CD94230002DBF0 /* Endpoints */ = { 444 | isa = PBXGroup; 445 | children = ( 446 | 36E0303927CD94880002DBF0 /* FollowersEndpoint.swift */, 447 | 362435AB27CE832A00DBDA35 /* UserEndpoint.swift */, 448 | ); 449 | path = Endpoints; 450 | sourceTree = ""; 451 | }; 452 | 36E0303627CD94380002DBF0 /* Services */ = { 453 | isa = PBXGroup; 454 | children = ( 455 | 363F40BF27AF1C2F006C79AB /* UserService.swift */, 456 | 368CD92D279D9347003FABA6 /* FollowersService.swift */, 457 | ); 458 | path = Services; 459 | sourceTree = ""; 460 | }; 461 | 36F5975727CD605C006B048A /* GithubProfileWikiTests */ = { 462 | isa = PBXGroup; 463 | children = ( 464 | 3651CAEF27CEF5FC006D208A /* JSONResponses */, 465 | 36F5975827CD605C006B048A /* GithubProfileWikiTests.swift */, 466 | 36F5975F27CD6594006B048A /* DateConverterTests.swift */, 467 | 3651CAED27CEF51F006D208A /* Mockable.swift */, 468 | ); 469 | path = GithubProfileWikiTests; 470 | sourceTree = ""; 471 | }; 472 | /* End PBXGroup section */ 473 | 474 | /* Begin PBXNativeTarget section */ 475 | 364E076E279522530085E799 /* GithubProfileWiki */ = { 476 | isa = PBXNativeTarget; 477 | buildConfigurationList = 364E0783279522540085E799 /* Build configuration list for PBXNativeTarget "GithubProfileWiki" */; 478 | buildPhases = ( 479 | 364E076B279522530085E799 /* Sources */, 480 | 364E076C279522530085E799 /* Frameworks */, 481 | 364E076D279522530085E799 /* Resources */, 482 | 365802EF27CBDA6300925158 /* ShellScript */, 483 | ); 484 | buildRules = ( 485 | ); 486 | dependencies = ( 487 | ); 488 | name = GithubProfileWiki; 489 | packageProductDependencies = ( 490 | 363BF5A227ADD9F3005028C1 /* Kingfisher */, 491 | ); 492 | productName = GithubProfileWiki; 493 | productReference = 364E076F279522530085E799 /* GithubProfileWiki.app */; 494 | productType = "com.apple.product-type.application"; 495 | }; 496 | 36F5975527CD605C006B048A /* GithubProfileWikiTests */ = { 497 | isa = PBXNativeTarget; 498 | buildConfigurationList = 36F5975E27CD605C006B048A /* Build configuration list for PBXNativeTarget "GithubProfileWikiTests" */; 499 | buildPhases = ( 500 | 36F5975227CD605C006B048A /* Sources */, 501 | 36F5975327CD605C006B048A /* Frameworks */, 502 | 36F5975427CD605C006B048A /* Resources */, 503 | ); 504 | buildRules = ( 505 | ); 506 | dependencies = ( 507 | 36F5975B27CD605C006B048A /* PBXTargetDependency */, 508 | ); 509 | name = GithubProfileWikiTests; 510 | productName = GithubProfileWikiTests; 511 | productReference = 36F5975627CD605C006B048A /* GithubProfileWikiTests.xctest */; 512 | productType = "com.apple.product-type.bundle.unit-test"; 513 | }; 514 | /* End PBXNativeTarget section */ 515 | 516 | /* Begin PBXProject section */ 517 | 364E0767279522530085E799 /* Project object */ = { 518 | isa = PBXProject; 519 | attributes = { 520 | LastSwiftUpdateCheck = 1320; 521 | LastUpgradeCheck = 1250; 522 | TargetAttributes = { 523 | 364E076E279522530085E799 = { 524 | CreatedOnToolsVersion = 12.5.1; 525 | }; 526 | 36F5975527CD605C006B048A = { 527 | CreatedOnToolsVersion = 13.2.1; 528 | TestTargetID = 364E076E279522530085E799; 529 | }; 530 | }; 531 | }; 532 | buildConfigurationList = 364E076A279522530085E799 /* Build configuration list for PBXProject "GithubProfileWiki" */; 533 | compatibilityVersion = "Xcode 9.3"; 534 | developmentRegion = en; 535 | hasScannedForEncodings = 0; 536 | knownRegions = ( 537 | en, 538 | Base, 539 | ); 540 | mainGroup = 364E0766279522530085E799; 541 | packageReferences = ( 542 | 363BF5A127ADD9F3005028C1 /* XCRemoteSwiftPackageReference "Kingfisher" */, 543 | ); 544 | productRefGroup = 364E0770279522530085E799 /* Products */; 545 | projectDirPath = ""; 546 | projectRoot = ""; 547 | targets = ( 548 | 364E076E279522530085E799 /* GithubProfileWiki */, 549 | 36F5975527CD605C006B048A /* GithubProfileWikiTests */, 550 | ); 551 | }; 552 | /* End PBXProject section */ 553 | 554 | /* Begin PBXResourcesBuildPhase section */ 555 | 364E076D279522530085E799 /* Resources */ = { 556 | isa = PBXResourcesBuildPhase; 557 | buildActionMask = 2147483647; 558 | files = ( 559 | 364E077F279522540085E799 /* LaunchScreen.storyboard in Resources */, 560 | 364E077C279522540085E799 /* Assets.xcassets in Resources */, 561 | ); 562 | runOnlyForDeploymentPostprocessing = 0; 563 | }; 564 | 36F5975427CD605C006B048A /* Resources */ = { 565 | isa = PBXResourcesBuildPhase; 566 | buildActionMask = 2147483647; 567 | files = ( 568 | 36B88A1727CEF89A0024659B /* followers_response.json in Resources */, 569 | 36B88A1927CEFA410024659B /* user_response.json in Resources */, 570 | ); 571 | runOnlyForDeploymentPostprocessing = 0; 572 | }; 573 | /* End PBXResourcesBuildPhase section */ 574 | 575 | /* Begin PBXShellScriptBuildPhase section */ 576 | 365802EF27CBDA6300925158 /* ShellScript */ = { 577 | isa = PBXShellScriptBuildPhase; 578 | buildActionMask = 2147483647; 579 | files = ( 580 | ); 581 | inputFileListPaths = ( 582 | ); 583 | inputPaths = ( 584 | ); 585 | outputFileListPaths = ( 586 | ); 587 | outputPaths = ( 588 | ); 589 | runOnlyForDeploymentPostprocessing = 0; 590 | shellPath = /bin/sh; 591 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 592 | }; 593 | /* End PBXShellScriptBuildPhase section */ 594 | 595 | /* Begin PBXSourcesBuildPhase section */ 596 | 364E076B279522530085E799 /* Sources */ = { 597 | isa = PBXSourcesBuildPhase; 598 | buildActionMask = 2147483647; 599 | files = ( 600 | 364D4AB227B4906D00E773B6 /* FollowerInfoViewController.swift in Sources */, 601 | 36AD887D279C25E900B0B5D3 /* UIViewController+Extension.swift in Sources */, 602 | 364E078B27989BB60085E799 /* BaseUITextField.swift in Sources */, 603 | 364E078E279927BB0085E799 /* UIImage+Extension.swift in Sources */, 604 | 363F40C027AF1C2F006C79AB /* UserService.swift in Sources */, 605 | 362435AC27CE832A00DBDA35 /* UserEndpoint.swift in Sources */, 606 | 363F40C427AF1CA3006C79AB /* User.swift in Sources */, 607 | 364D4ABB27B5A9F900E773B6 /* UserDefaultsManager.swift in Sources */, 608 | 365931C9279C442700834D25 /* UIView+Constraints.swift in Sources */, 609 | 368CD933279D9937003FABA6 /* FollowersListViewModel.swift in Sources */, 610 | 364D4AC127B5C7C900E773B6 /* FavoriteCell.swift in Sources */, 611 | 368CD92E279D9347003FABA6 /* FollowersService.swift in Sources */, 612 | 36AD8877279C16D000B0B5D3 /* BaseTitleLabel.swift in Sources */, 613 | 36AA948327BEC24400254E77 /* ProfileView.swift in Sources */, 614 | 364E0773279522530085E799 /* AppDelegate.swift in Sources */, 615 | 3609175E279F62550031FF14 /* BaseImageView.swift in Sources */, 616 | 364E0787279527600085E799 /* SearchViewController.swift in Sources */, 617 | 36F286BB2799302A006F389D /* FollowersListViewController.swift in Sources */, 618 | 363F40CC27AFDF1D006C79AB /* GithubItemInfoView.swift in Sources */, 619 | 363F40BE27AF174A006C79AB /* ProfileViewController.swift in Sources */, 620 | 364D4ABF27B5B85100E773B6 /* Optional+Extension.swift in Sources */, 621 | 364D4AB627B497C000E773B6 /* Date+Extension.swift in Sources */, 622 | 36E0303327CD931F0002DBF0 /* NetworkManager.swift in Sources */, 623 | 36E0303A27CD94880002DBF0 /* FollowersEndpoint.swift in Sources */, 624 | 363F40C727AF28F9006C79AB /* ProfileHeaderViewController.swift in Sources */, 625 | 365931BE279C409F00834D25 /* Constants.swift in Sources */, 626 | 363F40C227AF1C80006C79AB /* ProfileViewModel.swift in Sources */, 627 | 364E0789279527950085E799 /* FavoritesViewController.swift in Sources */, 628 | 36AD8879279C183200B0B5D3 /* BaseBodyLabel.swift in Sources */, 629 | 36AD887B279C198D00B0B5D3 /* AlertPopupViewController.swift in Sources */, 630 | 365802EA27CB098100925158 /* FavoritesViewModel.swift in Sources */, 631 | 363F40BB27AEE9C5006C79AB /* EmptyStateView.swift in Sources */, 632 | 3609175B279F609A0031FF14 /* FollowerCell.swift in Sources */, 633 | 365802E027CAE7DF00925158 /* SearchView.swift in Sources */, 634 | 365802EE27CBC8A700925158 /* FavoritesView.swift in Sources */, 635 | 36E0303127CD92750002DBF0 /* RequestError.swift in Sources */, 636 | 364D4ABD27B5B73400E773B6 /* UserDefaults+Helper.swift in Sources */, 637 | 364E0775279522530085E799 /* SceneDelegate.swift in Sources */, 638 | 36E0302D27CD919F0002DBF0 /* Endpoint.swift in Sources */, 639 | 363F40CF27AFE451006C79AB /* GithubInfoViewController.swift in Sources */, 640 | 368CD922279D8CA7003FABA6 /* NetworkConstants.swift in Sources */, 641 | 5FA7CF5E94C4D36BF5C3DBFD /* BaseUIButton.swift in Sources */, 642 | 36E0302F27CD92270002DBF0 /* HTTPMethod.swift in Sources */, 643 | 364D4AB427B496F900E773B6 /* String+Extension.swift in Sources */, 644 | 368CD931279D95F5003FABA6 /* Followers.swift in Sources */, 645 | 364D4AB027B48F0600E773B6 /* RepoInfoViewController.swift in Sources */, 646 | ); 647 | runOnlyForDeploymentPostprocessing = 0; 648 | }; 649 | 36F5975227CD605C006B048A /* Sources */ = { 650 | isa = PBXSourcesBuildPhase; 651 | buildActionMask = 2147483647; 652 | files = ( 653 | 36F5976027CD6594006B048A /* DateConverterTests.swift in Sources */, 654 | 3651CAEE27CEF51F006D208A /* Mockable.swift in Sources */, 655 | 36F5975927CD605C006B048A /* GithubProfileWikiTests.swift in Sources */, 656 | ); 657 | runOnlyForDeploymentPostprocessing = 0; 658 | }; 659 | /* End PBXSourcesBuildPhase section */ 660 | 661 | /* Begin PBXTargetDependency section */ 662 | 36F5975B27CD605C006B048A /* PBXTargetDependency */ = { 663 | isa = PBXTargetDependency; 664 | target = 364E076E279522530085E799 /* GithubProfileWiki */; 665 | targetProxy = 36F5975A27CD605C006B048A /* PBXContainerItemProxy */; 666 | }; 667 | /* End PBXTargetDependency section */ 668 | 669 | /* Begin PBXVariantGroup section */ 670 | 364E077D279522540085E799 /* LaunchScreen.storyboard */ = { 671 | isa = PBXVariantGroup; 672 | children = ( 673 | 364E077E279522540085E799 /* Base */, 674 | ); 675 | name = LaunchScreen.storyboard; 676 | sourceTree = ""; 677 | }; 678 | /* End PBXVariantGroup section */ 679 | 680 | /* Begin XCBuildConfiguration section */ 681 | 364E0781279522540085E799 /* Debug */ = { 682 | isa = XCBuildConfiguration; 683 | buildSettings = { 684 | ALWAYS_SEARCH_USER_PATHS = NO; 685 | CLANG_ANALYZER_NONNULL = YES; 686 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 687 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 688 | CLANG_CXX_LIBRARY = "libc++"; 689 | CLANG_ENABLE_MODULES = YES; 690 | CLANG_ENABLE_OBJC_ARC = YES; 691 | CLANG_ENABLE_OBJC_WEAK = YES; 692 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 693 | CLANG_WARN_BOOL_CONVERSION = YES; 694 | CLANG_WARN_COMMA = YES; 695 | CLANG_WARN_CONSTANT_CONVERSION = YES; 696 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 697 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 698 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 699 | CLANG_WARN_EMPTY_BODY = YES; 700 | CLANG_WARN_ENUM_CONVERSION = YES; 701 | CLANG_WARN_INFINITE_RECURSION = YES; 702 | CLANG_WARN_INT_CONVERSION = YES; 703 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 704 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 705 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 706 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 707 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 708 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 709 | CLANG_WARN_STRICT_PROTOTYPES = YES; 710 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 711 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 712 | CLANG_WARN_UNREACHABLE_CODE = YES; 713 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 714 | COPY_PHASE_STRIP = NO; 715 | DEBUG_INFORMATION_FORMAT = dwarf; 716 | ENABLE_STRICT_OBJC_MSGSEND = YES; 717 | ENABLE_TESTABILITY = YES; 718 | GCC_C_LANGUAGE_STANDARD = gnu11; 719 | GCC_DYNAMIC_NO_PIC = NO; 720 | GCC_NO_COMMON_BLOCKS = YES; 721 | GCC_OPTIMIZATION_LEVEL = 0; 722 | GCC_PREPROCESSOR_DEFINITIONS = ( 723 | "DEBUG=1", 724 | "$(inherited)", 725 | ); 726 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 727 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 728 | GCC_WARN_UNDECLARED_SELECTOR = YES; 729 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 730 | GCC_WARN_UNUSED_FUNCTION = YES; 731 | GCC_WARN_UNUSED_VARIABLE = YES; 732 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 733 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 734 | MTL_FAST_MATH = YES; 735 | ONLY_ACTIVE_ARCH = YES; 736 | SDKROOT = iphoneos; 737 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 738 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 739 | }; 740 | name = Debug; 741 | }; 742 | 364E0782279522540085E799 /* Release */ = { 743 | isa = XCBuildConfiguration; 744 | buildSettings = { 745 | ALWAYS_SEARCH_USER_PATHS = NO; 746 | CLANG_ANALYZER_NONNULL = YES; 747 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 748 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 749 | CLANG_CXX_LIBRARY = "libc++"; 750 | CLANG_ENABLE_MODULES = YES; 751 | CLANG_ENABLE_OBJC_ARC = YES; 752 | CLANG_ENABLE_OBJC_WEAK = YES; 753 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 754 | CLANG_WARN_BOOL_CONVERSION = YES; 755 | CLANG_WARN_COMMA = YES; 756 | CLANG_WARN_CONSTANT_CONVERSION = YES; 757 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 758 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 759 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 760 | CLANG_WARN_EMPTY_BODY = YES; 761 | CLANG_WARN_ENUM_CONVERSION = YES; 762 | CLANG_WARN_INFINITE_RECURSION = YES; 763 | CLANG_WARN_INT_CONVERSION = YES; 764 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 765 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 766 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 767 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 768 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 769 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 770 | CLANG_WARN_STRICT_PROTOTYPES = YES; 771 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 772 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 773 | CLANG_WARN_UNREACHABLE_CODE = YES; 774 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 775 | COPY_PHASE_STRIP = NO; 776 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 777 | ENABLE_NS_ASSERTIONS = NO; 778 | ENABLE_STRICT_OBJC_MSGSEND = YES; 779 | GCC_C_LANGUAGE_STANDARD = gnu11; 780 | GCC_NO_COMMON_BLOCKS = YES; 781 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 782 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 783 | GCC_WARN_UNDECLARED_SELECTOR = YES; 784 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 785 | GCC_WARN_UNUSED_FUNCTION = YES; 786 | GCC_WARN_UNUSED_VARIABLE = YES; 787 | IPHONEOS_DEPLOYMENT_TARGET = 14.5; 788 | MTL_ENABLE_DEBUG_INFO = NO; 789 | MTL_FAST_MATH = YES; 790 | SDKROOT = iphoneos; 791 | SWIFT_COMPILATION_MODE = wholemodule; 792 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 793 | VALIDATE_PRODUCT = YES; 794 | }; 795 | name = Release; 796 | }; 797 | 364E0784279522540085E799 /* Debug */ = { 798 | isa = XCBuildConfiguration; 799 | buildSettings = { 800 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 801 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 802 | CODE_SIGN_STYLE = Automatic; 803 | DEVELOPMENT_TEAM = ""; 804 | INFOPLIST_FILE = GithubProfileWiki/ProjectFiles/Info.plist; 805 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 806 | LD_RUNPATH_SEARCH_PATHS = ( 807 | "$(inherited)", 808 | "@executable_path/Frameworks", 809 | ); 810 | MARKETING_VERSION = 0.0.1; 811 | PRODUCT_BUNDLE_IDENTIFIER = ilter.personal.GithubProfileWiki; 812 | PRODUCT_NAME = "$(TARGET_NAME)"; 813 | SWIFT_VERSION = 5.0; 814 | TARGETED_DEVICE_FAMILY = 1; 815 | }; 816 | name = Debug; 817 | }; 818 | 364E0785279522540085E799 /* Release */ = { 819 | isa = XCBuildConfiguration; 820 | buildSettings = { 821 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 822 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 823 | CODE_SIGN_STYLE = Automatic; 824 | DEVELOPMENT_TEAM = ""; 825 | INFOPLIST_FILE = GithubProfileWiki/ProjectFiles/Info.plist; 826 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 827 | LD_RUNPATH_SEARCH_PATHS = ( 828 | "$(inherited)", 829 | "@executable_path/Frameworks", 830 | ); 831 | MARKETING_VERSION = 0.0.1; 832 | PRODUCT_BUNDLE_IDENTIFIER = ilter.personal.GithubProfileWiki; 833 | PRODUCT_NAME = "$(TARGET_NAME)"; 834 | SWIFT_VERSION = 5.0; 835 | TARGETED_DEVICE_FAMILY = 1; 836 | }; 837 | name = Release; 838 | }; 839 | 36F5975C27CD605C006B048A /* Debug */ = { 840 | isa = XCBuildConfiguration; 841 | buildSettings = { 842 | BUNDLE_LOADER = "$(TEST_HOST)"; 843 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 844 | CODE_SIGN_STYLE = Automatic; 845 | CURRENT_PROJECT_VERSION = 1; 846 | GENERATE_INFOPLIST_FILE = YES; 847 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 848 | MARKETING_VERSION = 1.0; 849 | PRODUCT_BUNDLE_IDENTIFIER = ilter.personal.GithubProfileWikiTests; 850 | PRODUCT_NAME = "$(TARGET_NAME)"; 851 | SWIFT_EMIT_LOC_STRINGS = NO; 852 | SWIFT_VERSION = 5.0; 853 | TARGETED_DEVICE_FAMILY = "1,2"; 854 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GithubProfileWiki.app/GithubProfileWiki"; 855 | }; 856 | name = Debug; 857 | }; 858 | 36F5975D27CD605C006B048A /* Release */ = { 859 | isa = XCBuildConfiguration; 860 | buildSettings = { 861 | BUNDLE_LOADER = "$(TEST_HOST)"; 862 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 863 | CODE_SIGN_STYLE = Automatic; 864 | CURRENT_PROJECT_VERSION = 1; 865 | GENERATE_INFOPLIST_FILE = YES; 866 | IPHONEOS_DEPLOYMENT_TARGET = 15.2; 867 | MARKETING_VERSION = 1.0; 868 | PRODUCT_BUNDLE_IDENTIFIER = ilter.personal.GithubProfileWikiTests; 869 | PRODUCT_NAME = "$(TARGET_NAME)"; 870 | SWIFT_EMIT_LOC_STRINGS = NO; 871 | SWIFT_VERSION = 5.0; 872 | TARGETED_DEVICE_FAMILY = "1,2"; 873 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/GithubProfileWiki.app/GithubProfileWiki"; 874 | }; 875 | name = Release; 876 | }; 877 | /* End XCBuildConfiguration section */ 878 | 879 | /* Begin XCConfigurationList section */ 880 | 364E076A279522530085E799 /* Build configuration list for PBXProject "GithubProfileWiki" */ = { 881 | isa = XCConfigurationList; 882 | buildConfigurations = ( 883 | 364E0781279522540085E799 /* Debug */, 884 | 364E0782279522540085E799 /* Release */, 885 | ); 886 | defaultConfigurationIsVisible = 0; 887 | defaultConfigurationName = Release; 888 | }; 889 | 364E0783279522540085E799 /* Build configuration list for PBXNativeTarget "GithubProfileWiki" */ = { 890 | isa = XCConfigurationList; 891 | buildConfigurations = ( 892 | 364E0784279522540085E799 /* Debug */, 893 | 364E0785279522540085E799 /* Release */, 894 | ); 895 | defaultConfigurationIsVisible = 0; 896 | defaultConfigurationName = Release; 897 | }; 898 | 36F5975E27CD605C006B048A /* Build configuration list for PBXNativeTarget "GithubProfileWikiTests" */ = { 899 | isa = XCConfigurationList; 900 | buildConfigurations = ( 901 | 36F5975C27CD605C006B048A /* Debug */, 902 | 36F5975D27CD605C006B048A /* Release */, 903 | ); 904 | defaultConfigurationIsVisible = 0; 905 | defaultConfigurationName = Release; 906 | }; 907 | /* End XCConfigurationList section */ 908 | 909 | /* Begin XCRemoteSwiftPackageReference section */ 910 | 363BF5A127ADD9F3005028C1 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { 911 | isa = XCRemoteSwiftPackageReference; 912 | repositoryURL = "https://github.com/onevcat/Kingfisher.git"; 913 | requirement = { 914 | kind = upToNextMajorVersion; 915 | minimumVersion = 7.1.2; 916 | }; 917 | }; 918 | /* End XCRemoteSwiftPackageReference section */ 919 | 920 | /* Begin XCSwiftPackageProductDependency section */ 921 | 363BF5A227ADD9F3005028C1 /* Kingfisher */ = { 922 | isa = XCSwiftPackageProductDependency; 923 | package = 363BF5A127ADD9F3005028C1 /* XCRemoteSwiftPackageReference "Kingfisher" */; 924 | productName = Kingfisher; 925 | }; 926 | /* End XCSwiftPackageProductDependency section */ 927 | }; 928 | rootObject = 364E0767279522530085E799 /* Project object */; 929 | } 930 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Kingfisher", 6 | "repositoryURL": "https://github.com/onevcat/Kingfisher.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "0c02c46cfdc0656ce74fd0963a75e5000a0b7f23", 10 | "version": "7.1.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki.xcodeproj/xcshareddata/xcschemes/GithubProfileWiki.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki.xcodeproj/xcshareddata/xcschemes/GithubProfileWikiTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Model/Followers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowerModel.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 23.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Follower: Codable, Hashable { 11 | var login: String 12 | var avatarUrl: String 13 | } 14 | 15 | typealias Followers = [Follower] 16 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Model/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 5.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User: Codable, Hashable { 11 | var login: String 12 | var avatarUrl: String 13 | var name: String? 14 | var location: String? 15 | var bio: String? 16 | var publicRepos: Int 17 | var publicGists: Int 18 | var htmlUrl: String 19 | let following: Int 20 | let followers: Int 21 | let createdAt: String 22 | } 23 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Base/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 1.03.2022. 6 | // 7 | 8 | protocol Endpoint { 9 | var baseURL: String { get } 10 | var path: String { get } 11 | var method: HTTPMethod { get } 12 | var header: [String: String]? { get } 13 | var body: [String: String]? { get } 14 | } 15 | 16 | extension Endpoint { 17 | var baseURL: String { 18 | return "https://api.github.com/" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Base/HTTPMethod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPMethod.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 1.03.2022. 6 | // 7 | 8 | enum HTTPMethod: String { 9 | case delete = "DELETE" 10 | case get = "GET" 11 | case patch = "PATCH" 12 | case post = "POST" 13 | case put = "PUT" 14 | } 15 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Base/NetworkConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkConstants.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 23.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NetworkConstants { 11 | enum APIHeaders { 12 | static let defaultContentType = "Content-Type" 13 | static let defaultContentTypeValue = "application/json" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Base/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 1.03.2022. 6 | // 7 | import Foundation 8 | 9 | protocol NetworkManager { 10 | func sendRequest(endpoint: Endpoint, responseModel: T.Type) async throws -> Result 11 | } 12 | 13 | extension NetworkManager { 14 | func sendRequest(endpoint: Endpoint, responseModel: T.Type) async throws -> Result { 15 | guard let url = URL(string: endpoint.baseURL + endpoint.path) else { 16 | return .failure(.invalidURL) 17 | } 18 | 19 | var request = URLRequest(url: url) 20 | request.httpMethod = endpoint.method.rawValue 21 | request.allHTTPHeaderFields = endpoint.header 22 | 23 | if let body = endpoint.body { 24 | request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: []) 25 | } 26 | 27 | do { 28 | let (data, response) = try await URLSession.shared.data(for: request, delegate: nil) 29 | guard let response = response as? HTTPURLResponse else { 30 | return .failure(.noResponse) 31 | } 32 | switch response.statusCode { 33 | case 200...299: 34 | let jsonDecoder = JSONDecoder() 35 | jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase 36 | guard let decodedResponse = try? jsonDecoder.decode(responseModel, from: data) else { 37 | return .failure(.decode) 38 | } 39 | return .success(decodedResponse) 40 | case 401: 41 | return .failure(.unauthorized) 42 | default: 43 | return .failure(.unexpectedStatusCode) 44 | } 45 | } catch { 46 | return .failure(.unknown) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Base/RequestError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestError.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 1.03.2022. 6 | // 7 | 8 | enum RequestError: Error { 9 | case decode 10 | case invalidURL 11 | case noResponse 12 | case unauthorized 13 | case unexpectedStatusCode 14 | case unknown 15 | 16 | var customMessage: String { 17 | switch self { 18 | case .decode: 19 | return "[NetworkError] Decode error" 20 | case .unauthorized: 21 | return "[NetworkError] Session expired" 22 | case .invalidURL: 23 | return "[NetworkError] Invalid URL" 24 | case .noResponse: 25 | return "[NetworkError] No Response" 26 | case .unexpectedStatusCode: 27 | return "[NetworkError] Unexpected Status Code" 28 | default: 29 | return "Unknown error" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Endpoints/FollowersEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersEndpoint.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 1.03.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FollowersEndpoint { 11 | case followers(userName: String, pageNumber: Int = 1) 12 | } 13 | 14 | extension FollowersEndpoint: Endpoint { 15 | private enum RequestConstants: String { 16 | case page 17 | } 18 | 19 | var path: String { 20 | switch self { 21 | case .followers(let userName, let pagenumber): 22 | return "users/\(userName)/followers?\(RequestConstants.page.rawValue)=\(pagenumber)" 23 | } 24 | } 25 | 26 | var method: HTTPMethod { 27 | switch self { 28 | case .followers: 29 | return .get 30 | } 31 | } 32 | 33 | var header: [String: String]? { 34 | // Access Token to use in Bearer header 35 | // let accessToken = "insert your access token here" 36 | switch self { 37 | case .followers: 38 | return [ 39 | // "Authorization": "Bearer \(accessToken)", 40 | NetworkConstants.APIHeaders.defaultContentType: NetworkConstants.APIHeaders.defaultContentTypeValue 41 | ] 42 | } 43 | } 44 | var body: [String: String]? { 45 | switch self { 46 | case .followers: 47 | return nil 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Endpoints/UserEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserEndpoint.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 1.03.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserEndpoint { 11 | case user(userName: String) 12 | } 13 | 14 | extension UserEndpoint: Endpoint { 15 | var path: String { 16 | switch self { 17 | case .user(let userName): 18 | return "users/\(userName)" 19 | } 20 | } 21 | 22 | var method: HTTPMethod { 23 | switch self { 24 | case .user: 25 | return .get 26 | } 27 | } 28 | 29 | var header: [String: String]? { 30 | switch self { 31 | case .user: 32 | return [ 33 | NetworkConstants.APIHeaders.defaultContentType: NetworkConstants.APIHeaders.defaultContentTypeValue 34 | ] 35 | } 36 | } 37 | 38 | var body: [String: String]? { 39 | switch self { 40 | case .user: 41 | return nil 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Services/FollowersService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersService.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 23.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FollowersServiceable { 11 | func getFollowers(username: String, pageNumber: Int) async throws -> Result 12 | } 13 | 14 | struct FollowersService: NetworkManager, FollowersServiceable { 15 | func getFollowers(username: String, pageNumber: Int) async throws -> Result { 16 | return try await sendRequest(endpoint: FollowersEndpoint.followers(userName: username, pageNumber: pageNumber), responseModel: Followers.self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Networking/Services/UserService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserService.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 5.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol UserServiceable { 11 | func getUser(userName: String) async throws -> Result 12 | } 13 | 14 | struct UserService: NetworkManager, UserServiceable { 15 | func getUser(userName: String) async throws -> Result { 16 | return try await sendRequest(endpoint: UserEndpoint.user(userName: userName), responseModel: User.self) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/ProjectFiles/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 17.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | func application(_ application: UIApplication, 13 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | return true 15 | } 16 | 17 | // MARK: UISceneSession Lifecycle 18 | 19 | func application(_ application: UIApplication, 20 | configurationForConnecting connectingSceneSession: UISceneSession, 21 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 23 | } 24 | 25 | func application(_ application: UIApplication, 26 | didDiscardSceneSessions sceneSessions: Set) { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/ProjectFiles/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 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSMainNibFile 24 | I 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | UISceneConfigurations 30 | 31 | UIWindowSceneSessionRoleApplication 32 | 33 | 34 | UISceneConfigurationName 35 | Default Configuration 36 | UISceneDelegateClassName 37 | $(PRODUCT_MODULE_NAME).SceneDelegate 38 | 39 | 40 | 41 | 42 | UIApplicationSupportsIndirectInputEvents 43 | 44 | UILaunchStoryboardName 45 | LaunchScreen 46 | UIRequiredDeviceCapabilities 47 | 48 | armv7 49 | 50 | UISupportedInterfaceOrientations 51 | 52 | UIInterfaceOrientationPortrait 53 | UIInterfaceOrientationLandscapeLeft 54 | UIInterfaceOrientationLandscapeRight 55 | 56 | UISupportedInterfaceOrientations~ipad 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationPortraitUpsideDown 60 | UIInterfaceOrientationLandscapeLeft 61 | UIInterfaceOrientationLandscapeRight 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/ProjectFiles/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 17.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let windowScene = (scene as? UIWindowScene) else { return } 16 | 17 | window = UIWindow(frame: windowScene.coordinateSpace.bounds) 18 | window?.windowScene = windowScene 19 | window?.rootViewController = createTabbar() 20 | window?.makeKeyAndVisible() 21 | 22 | configureNavigationBar() 23 | } 24 | 25 | private func createSearchNavigationController() -> UINavigationController { 26 | let searchVC = SearchViewController() 27 | searchVC.title = "Search" 28 | searchVC.tabBarItem = UITabBarItem(tabBarSystemItem: .search, tag: .zero) 29 | 30 | return UINavigationController(rootViewController: searchVC) 31 | } 32 | 33 | private func createFavoritesNavigationController() -> UINavigationController { 34 | let favoritesVC = FavoritesViewController() 35 | favoritesVC.title = "Favorites" 36 | favoritesVC.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 1) 37 | 38 | return UINavigationController(rootViewController: favoritesVC) 39 | } 40 | 41 | private func createTabbar() -> UITabBarController { 42 | let tabbar = UITabBarController() 43 | UITabBar.appearance().tintColor = .systemGreen 44 | tabbar.viewControllers = [createSearchNavigationController(), createFavoritesNavigationController()] 45 | 46 | return tabbar 47 | } 48 | 49 | private func configureNavigationBar() { 50 | UINavigationBar.appearance().tintColor = .systemGreen 51 | } 52 | 53 | func sceneDidDisconnect(_ scene: UIScene) { 54 | 55 | } 56 | 57 | func sceneDidBecomeActive(_ scene: UIScene) { 58 | 59 | } 60 | 61 | func sceneWillResignActive(_ scene: UIScene) { 62 | 63 | } 64 | 65 | func sceneWillEnterForeground(_ scene: UIScene) { 66 | 67 | } 68 | 69 | func sceneDidEnterBackground(_ scene: UIScene) { 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Constants/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 22.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | struct Constants { 11 | struct Styling { 12 | static let defaultCornerRadius: CGFloat = 10.0 13 | static let maxCornerRadius: CGFloat = 16.0 14 | static let minimumBorderWidth: CGFloat = 2.0 15 | static let defaultScaleFactor: CGFloat = 0.9 16 | static let minScaleFactor: CGFloat = 0.75 17 | 18 | static let maxSpacing: CGFloat = 20.0 19 | static let minimumSpacing: CGFloat = 8.0 20 | static let defaultSpacing: CGFloat = 12.0 21 | static let searchPageDefaultSpacing: CGFloat = 50.0 22 | static let anormousSpacing: CGFloat = 80.0 23 | 24 | static let profilePhotoWidthHeight: CGFloat = 100.0 25 | static let gitHubInfoViewHeight: CGFloat = 120.0 26 | static let profileHeaderContainerHeight: CGFloat = 200.0 27 | static let mainLogoHeight: CGFloat = 200.0 28 | 29 | static let followerCellImg: CGFloat = 100.0 30 | } 31 | 32 | struct Font { 33 | static let headlineFont: UIFont = UIFont.preferredFont(forTextStyle: .headline) 34 | static let bodyFont: UIFont = UIFont.preferredFont(forTextStyle: .body) 35 | 36 | static let minimumFontSize: CGFloat = 12.0 37 | static let mediumFontSize: CGFloat = 20.0 38 | } 39 | 40 | struct InfoTexts { 41 | static let textFieldPlaceholder: String = "Enter username." 42 | static let closeButtonText: String = "Close" 43 | static let success: String = "Success!" 44 | 45 | static let createdAt = "Created at" 46 | static let followerButtonTitle: String = "Get Followers" 47 | static let githubProfileText: String = "GitHub Profile" 48 | 49 | static let repos: String = "Public Repos" 50 | static let gists: String = "Public Gists" 51 | static let followers: String = "Followers" 52 | static let following: String = "Following" 53 | 54 | static let favorited: String = "You have successfully favorited this user." 55 | } 56 | 57 | struct WarningTexts { 58 | static let errorTitle = "Error" 59 | static let errorMessage = "An Error occured." 60 | static let searchPopUpMessage: String = "Please enter a username. We need to know who you are looking for." 61 | } 62 | 63 | enum SFSymbols { 64 | static let location = "mappin.and.ellipse" 65 | static let repos = "folder" 66 | static let gists = "text.alignleft" 67 | static let followers = "heart" 68 | static let following = "person.2" 69 | } 70 | 71 | enum PageTitles: String { 72 | case favorites = "Favorites" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Extensions/Date+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Extension.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 10.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | func convertToMonthYearFormat() -> String { 12 | let dateFormatter = DateFormatter() 13 | dateFormatter.dateFormat = "MMM d, yyyy" 14 | 15 | return dateFormatter.string(from: self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Extensions/Optional+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+Extension.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 11.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Swift.Optional where Wrapped == String { 11 | 12 | } 13 | 14 | protocol AnyOptional { 15 | var isNil: Bool { get } 16 | } 17 | 18 | extension Optional: AnyOptional { 19 | var isNil: Bool { self == nil} 20 | } 21 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 10.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | 12 | func convertToDate() -> Date? { 13 | let dateFormatter = DateFormatter() 14 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 15 | dateFormatter.locale = Locale(identifier: "en_GB") 16 | dateFormatter.timeZone = .current 17 | 18 | return dateFormatter.date(from: self) 19 | } 20 | 21 | func convertDateToDisplayFormat() -> String { 22 | guard let date = self.convertToDate() else { return "N/A" } 23 | return date.convertToMonthYearFormat() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Extensions/UIImage+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extension.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 20.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | 12 | class var githubLogo: UIImage { 13 | return UIImage(named: "github-search")! 14 | } 15 | 16 | class var emptyStateLogo: UIImage { 17 | return UIImage(named: "empty-state-logo")! 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Extensions/UIView+Constraints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+Constraints.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 22.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | 12 | func configureConstraint(top: (NSLayoutYAxisAnchor, CGFloat)? = nil, 13 | bottom: (NSLayoutYAxisAnchor, CGFloat)? = nil, 14 | leading: (NSLayoutXAxisAnchor, CGFloat)? = nil, 15 | trailing: (NSLayoutXAxisAnchor, CGFloat)? = nil, 16 | centerX: (NSLayoutXAxisAnchor, CGFloat)? = nil, 17 | centerY: (NSLayoutYAxisAnchor, CGFloat)? = nil) { 18 | 19 | translatesAutoresizingMaskIntoConstraints = false 20 | 21 | if let top = top { 22 | topAnchor.constraint(equalTo: top.0, constant: top.1).isActive = true 23 | } 24 | 25 | if let bottom = bottom { 26 | bottomAnchor.constraint(equalTo: bottom.0, constant: bottom.1).isActive = true 27 | } 28 | 29 | if let leading = leading { 30 | leadingAnchor.constraint(equalTo: leading.0, constant: leading.1).isActive = true 31 | } 32 | 33 | if let trailing = trailing { 34 | trailingAnchor.constraint(equalTo: trailing.0, constant: trailing.1).isActive = true 35 | } 36 | 37 | if let centerX = centerX { 38 | centerXAnchor.constraint(equalTo: centerX.0, constant: centerX.1).isActive = true 39 | } 40 | 41 | if let centerY = centerY { 42 | centerYAnchor.constraint(equalTo: centerY.0, constant: centerY.1).isActive = true 43 | 44 | } 45 | } 46 | 47 | func configureWidth(width: CGFloat) { 48 | widthAnchor.constraint(equalToConstant: width).isActive = true 49 | } 50 | 51 | func configureHeight(height: CGFloat) { 52 | heightAnchor.constraint(equalToConstant: height).isActive = true 53 | } 54 | 55 | func alignFitEdges(insetedBy: CGFloat = .zero) { 56 | translatesAutoresizingMaskIntoConstraints = false 57 | leadingAnchor.constraint(equalTo: superview!.leadingAnchor, constant: insetedBy).isActive = true 58 | trailingAnchor.constraint(equalTo: superview!.trailingAnchor, constant: -insetedBy).isActive = true 59 | topAnchor.constraint(equalTo: superview!.topAnchor, constant: insetedBy).isActive = true 60 | bottomAnchor.constraint(equalTo: superview!.bottomAnchor, constant: -insetedBy).isActive = true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Extensions/UIViewController+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewController+Extension.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 22.01.2022. 6 | // 7 | 8 | import UIKit 9 | import SafariServices 10 | 11 | private var containerView: UIView! 12 | 13 | extension UIViewController { 14 | func presentAlertPopupOnMainThread(title: String, message: String, buttonTitle: String) { 15 | DispatchQueue.main.async { 16 | let alertPopUp = AlertPopupViewController(title: title, message: message, buttonTitle: buttonTitle) 17 | 18 | alertPopUp.modalPresentationStyle = .overFullScreen 19 | alertPopUp.modalTransitionStyle = .crossDissolve 20 | self.present(alertPopUp, animated: true) 21 | } 22 | } 23 | 24 | func showLoadingViewWithActivityIndicator() { 25 | containerView = UIView(frame: view.bounds) 26 | view.addSubview(containerView) 27 | 28 | containerView.backgroundColor = .systemBackground 29 | containerView.alpha = .zero 30 | 31 | UIView.animate(withDuration: 0.25) { containerView.alpha = 0.8} 32 | 33 | let activityIndicator: UIActivityIndicatorView = { 34 | let view = UIActivityIndicatorView(style: .large) 35 | view.translatesAutoresizingMaskIntoConstraints = false 36 | return view 37 | }() 38 | 39 | containerView.addSubview(activityIndicator) 40 | 41 | activityIndicator.configureConstraint(centerX: (view.centerXAnchor, .zero), 42 | centerY: (view.centerYAnchor, .zero)) 43 | 44 | activityIndicator.startAnimating() 45 | } 46 | 47 | func dismissLoadingView() { 48 | // Put it to the main thread because of the closure call on getFollower 49 | containerView.isHidden = true 50 | } 51 | 52 | func showEmptyStateView(with message: String, in view: UIView) { 53 | let emptyStateView: EmptyStateView = EmptyStateView(message: message) 54 | emptyStateView.frame = view.bounds 55 | view.addSubview(emptyStateView) 56 | } 57 | 58 | func presentSafariVC(with url: URL) { 59 | let safariVC = SFSafariViewController(url: url) 60 | safariVC.preferredControlTintColor = .systemGreen 61 | present(safariVC, animated: true) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/AlertPopupView/AlertPopupViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertPopupViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 22.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class AlertPopupViewController: UIViewController { 11 | 12 | private enum PopUpConstants { 13 | static let popUpWidth: CGFloat = 280 14 | static let popUpHeight: CGFloat = 220 15 | } 16 | 17 | private lazy var containerView: UIView = { 18 | let containerView = UIView() 19 | containerView.translatesAutoresizingMaskIntoConstraints = false 20 | return containerView 21 | }() 22 | 23 | let titleLabel = BaseTitleLabel(textAlignment: .center, fontSize: Constants.Font.mediumFontSize, fontWeight: .bold) 24 | let messageLabel = BaseBodyLabel(textAlignment: .center) 25 | let button = BaseUIButton(backgroundColor: .systemPink, title: "OK") 26 | 27 | private var alertTitle: String? 28 | private var message: String? 29 | private var buttonTitle: String? 30 | 31 | init(title: String, message: String, buttonTitle: String) { 32 | super.init(nibName: nil, bundle: nil) 33 | self.alertTitle = title 34 | self.message = message 35 | self.buttonTitle = buttonTitle 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.70) 45 | setupUI() 46 | } 47 | 48 | private func setupUI() { 49 | view.addSubview(containerView) 50 | [titleLabel, messageLabel, button].forEach { containerView.addSubview($0) } 51 | 52 | configureContainerView() 53 | configureTitleView() 54 | configureMessageLabel() 55 | configureButton() 56 | } 57 | } 58 | 59 | extension AlertPopupViewController { 60 | 61 | private func configureContainerView() { 62 | containerView.backgroundColor = .systemBackground 63 | containerView.layer.cornerRadius = Constants.Styling.maxCornerRadius 64 | containerView.layer.borderWidth = Constants.Styling.minimumBorderWidth 65 | containerView.layer.borderColor = UIColor.white.cgColor 66 | 67 | containerView.configureConstraint(centerX: (view.centerXAnchor, .zero), centerY: (view.centerYAnchor, .zero)) 68 | containerView.configureWidth(width: PopUpConstants.popUpWidth) 69 | containerView.configureHeight(height: PopUpConstants.popUpHeight) 70 | } 71 | 72 | private func configureTitleView() { 73 | titleLabel.text = alertTitle 74 | 75 | titleLabel.configureConstraint(top: (containerView.topAnchor, Constants.Styling.maxSpacing), 76 | bottom: (messageLabel.topAnchor, -Constants.Styling.defaultSpacing), 77 | leading: (containerView.leadingAnchor, Constants.Styling.maxSpacing), 78 | trailing: (containerView.trailingAnchor, -Constants.Styling.maxSpacing)) 79 | } 80 | 81 | private func configureMessageLabel() { 82 | messageLabel.text = message 83 | messageLabel.numberOfLines = 4 84 | 85 | messageLabel.configureConstraint(top: (titleLabel.bottomAnchor, Constants.Styling.minimumSpacing), 86 | bottom: (button.topAnchor, -Constants.Styling.maxSpacing), 87 | leading: (containerView.leadingAnchor, Constants.Styling.maxSpacing), 88 | trailing: (containerView.trailingAnchor, -Constants.Styling.maxSpacing)) 89 | } 90 | 91 | private func configureButton() { 92 | button.setTitle(buttonTitle, for: .normal) 93 | button.addTarget(self, action: #selector(dismissAlertPopup), for: .touchUpInside) 94 | 95 | button.configureConstraint(top: (messageLabel.bottomAnchor, Constants.Styling.defaultSpacing), 96 | bottom: (containerView.bottomAnchor, -Constants.Styling.maxSpacing), 97 | leading: (containerView.leadingAnchor, Constants.Styling.maxSpacing), 98 | trailing: (containerView.trailingAnchor, -Constants.Styling.maxSpacing)) 99 | 100 | button.configureHeight(height: 42) 101 | } 102 | 103 | @objc private func dismissAlertPopup() { 104 | dismiss(animated: true, completion: nil) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/BaseBodyLabel/BaseBodyLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseBodyLabel.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 22.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class BaseBodyLabel: UILabel { 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | configureLabel() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | return nil 19 | } 20 | 21 | init(textAlignment: NSTextAlignment) { 22 | super.init(frame: .zero) 23 | self.textAlignment = textAlignment 24 | configureLabel() 25 | } 26 | 27 | private func configureLabel() { 28 | textColor = .secondaryLabel 29 | font = Constants.Font.bodyFont 30 | adjustsFontSizeToFitWidth = true 31 | minimumScaleFactor = Constants.Styling.minScaleFactor 32 | lineBreakMode = .byWordWrapping 33 | translatesAutoresizingMaskIntoConstraints = false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/BaseButton/BaseUIButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by ilter on 19.01.2022. 3 | // 4 | 5 | import Foundation 6 | import UIKit 7 | 8 | class BaseUIButton: UIButton { 9 | override init(frame: CGRect) { 10 | super.init(frame: frame) 11 | } 12 | 13 | required init?(coder: NSCoder) { 14 | fatalError("init(coder:) has not been implemented") 15 | } 16 | 17 | init(backgroundColor: UIColor, title: String) { 18 | super.init(frame: .zero) 19 | self.backgroundColor = backgroundColor 20 | self.setTitle(title, for: .normal) 21 | configure() 22 | } 23 | 24 | private func configure() { 25 | layer.cornerRadius = Constants.Styling.defaultCornerRadius 26 | titleLabel?.textColor = .white 27 | titleLabel?.font = Constants.Font.headlineFont 28 | translatesAutoresizingMaskIntoConstraints = false 29 | } 30 | 31 | func configureButton(backgroundColor: UIColor, title: String) { 32 | self.backgroundColor = backgroundColor 33 | self.setTitle(title, for: .normal) 34 | configure() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/BaseImageView/BaseImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseImageView.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 25.01.2022. 6 | // 7 | 8 | import UIKit 9 | import Kingfisher 10 | 11 | class BaseImageView: UIImageView { 12 | 13 | let placeholderImage: UIImage = .githubLogo 14 | 15 | override init(frame: CGRect) { 16 | super.init(frame: frame) 17 | configureUI() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | private func configureUI() { 25 | layer.cornerRadius = Constants.Styling.defaultCornerRadius 26 | clipsToBounds = true 27 | image = placeholderImage 28 | translatesAutoresizingMaskIntoConstraints = false 29 | } 30 | 31 | func setImage(from urlString: String) { 32 | kf.setImage(with: URL(string: urlString)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/BaseTextField/BaseUITextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseUITextField.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 19.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class BaseUITextField: UITextField { 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | configure() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | fatalError("error") 19 | } 20 | 21 | private func configure() { 22 | translatesAutoresizingMaskIntoConstraints = false 23 | layer.cornerRadius = Constants.Styling.defaultCornerRadius 24 | layer.borderWidth = Constants.Styling.minimumBorderWidth 25 | layer.borderColor = UIColor.systemGray4.cgColor 26 | 27 | textColor = .label 28 | tintColor = .label 29 | textAlignment = .center 30 | font = UIFont.preferredFont(forTextStyle: .title2) 31 | adjustsFontSizeToFitWidth = true 32 | minimumFontSize = Constants.Font.minimumFontSize 33 | 34 | autocorrectionType = .no 35 | 36 | placeholder = Constants.InfoTexts.textFieldPlaceholder 37 | 38 | returnKeyType = .done 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/BaseTitleLabel/BaseTitleLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseUILabel.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 22.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class BaseTitleLabel: UILabel { 11 | 12 | override init(frame: CGRect) { 13 | super.init(frame: frame) 14 | configureLabel() 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | return nil 19 | } 20 | 21 | init(textAlignment: NSTextAlignment, fontSize: CGFloat, fontWeight: UIFont.Weight) { 22 | super.init(frame: .zero) 23 | self.textAlignment = textAlignment 24 | self.font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight) 25 | configureLabel() 26 | } 27 | 28 | private func configureLabel() { 29 | textColor = .label 30 | adjustsFontSizeToFitWidth = true 31 | minimumScaleFactor = Constants.Styling.defaultScaleFactor 32 | lineBreakMode = .byTruncatingTail 33 | translatesAutoresizingMaskIntoConstraints = false 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/EmptyStateView/EmptyStateView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmptyStateView.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 5.02.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | final class EmptyStateView: UIView { 11 | 12 | private enum StylingConstants { 13 | static let imageBottom: CGFloat = 40 14 | static let imageTrailing: CGFloat = 170 15 | static let awayFromCenter: CGFloat = -150 16 | static let horizontalPadding: CGFloat = 40 17 | 18 | static let messageLabelHeight: CGFloat = 200 19 | } 20 | 21 | private let messageLabel = BaseTitleLabel(textAlignment: .center, fontSize: 32, fontWeight: .bold) 22 | private let logoImageView: UIImageView = { 23 | let view = UIImageView() 24 | view.translatesAutoresizingMaskIntoConstraints = false 25 | view.image = .emptyStateLogo 26 | return view 27 | }() 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | return nil 35 | } 36 | 37 | init(message: String) { 38 | super.init(frame: .zero) 39 | messageLabel.text = message 40 | configureUI() 41 | } 42 | 43 | private func configureUI() { 44 | [messageLabel, logoImageView].forEach { 45 | addSubview($0) 46 | } 47 | 48 | messageLabel.numberOfLines = 3 49 | messageLabel.textColor = .secondaryLabel 50 | 51 | configureConstraints() 52 | } 53 | 54 | private func configureConstraints() { 55 | messageLabel.configureConstraint(leading: (leadingAnchor, StylingConstants.horizontalPadding), 56 | trailing: (trailingAnchor, -StylingConstants.horizontalPadding), 57 | centerY: (centerYAnchor, StylingConstants.awayFromCenter)) 58 | 59 | messageLabel.configureHeight(height: StylingConstants.messageLabelHeight) 60 | 61 | logoImageView.configureConstraint(bottom: (bottomAnchor, StylingConstants.horizontalPadding), 62 | trailing: (trailingAnchor, StylingConstants.imageTrailing)) 63 | logoImageView.configureHeight(height: 500) 64 | logoImageView.configureWidth(width: 500) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/GitHubInfoView/GithubInfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubInfoViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 6.02.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class GithubInfoViewController: UIViewController { 11 | private let containerStackView: UIStackView = { 12 | let view = UIStackView() 13 | view.translatesAutoresizingMaskIntoConstraints = false 14 | view.axis = .horizontal 15 | view.distribution = .equalSpacing 16 | return view 17 | }() 18 | 19 | let firstItemInfoView = GithubItemInfoView() 20 | let secondItemInfoView = GithubItemInfoView() 21 | let actionButton = BaseUIButton() 22 | 23 | var user: User? 24 | weak var delegate: ProfileViewControllerDelegate? 25 | 26 | init(user: User) { 27 | super.init(nibName: nil, bundle: nil) 28 | self.user = user 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | configureBackgroundView() 38 | setupUI() 39 | configureConstraints() 40 | configureActionButton() 41 | } 42 | 43 | } 44 | 45 | // MARK: - Configure View 46 | 47 | extension GithubInfoViewController { 48 | 49 | private func configureBackgroundView() { 50 | view.layer.cornerRadius = Constants.Styling.defaultCornerRadius 51 | view.backgroundColor = .secondarySystemBackground 52 | } 53 | 54 | private func setupUI() { 55 | [containerStackView, actionButton].forEach { view.addSubview($0) } 56 | [firstItemInfoView, secondItemInfoView].forEach { containerStackView.addArrangedSubview($0) } 57 | } 58 | 59 | private func configureConstraints() { 60 | containerStackView.configureConstraint(top: (view.topAnchor, Constants.Styling.defaultSpacing), 61 | leading: (view.leadingAnchor, Constants.Styling.defaultSpacing), 62 | trailing: (view.trailingAnchor, -Constants.Styling.defaultSpacing)) 63 | 64 | actionButton.configureConstraint(top: (containerStackView.bottomAnchor, Constants.Styling.defaultSpacing), 65 | bottom: (view.bottomAnchor, -Constants.Styling.defaultSpacing), 66 | leading: (view.leadingAnchor, Constants.Styling.defaultSpacing), 67 | trailing: (view.trailingAnchor, -Constants.Styling.defaultSpacing)) 68 | } 69 | 70 | private func configureActionButton() { 71 | actionButton.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside) 72 | } 73 | 74 | @objc func actionButtonTapped() { 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/GitHubInfoView/SubViews/FollowerInfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowerInfoViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 10.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | final class FollowerInfoViewController: GithubInfoViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | setupUI() 15 | } 16 | 17 | private func setupUI() { 18 | firstItemInfoView.set(itemInfoType: .followers, withCount: user?.followers ?? .zero) 19 | secondItemInfoView.set(itemInfoType: .following, withCount: user?.following ?? .zero) 20 | actionButton.configureButton(backgroundColor: .systemGreen, title: Constants.InfoTexts.followerButtonTitle) 21 | } 22 | 23 | override func actionButtonTapped() { 24 | guard let user = user else { return } 25 | delegate?.didTappedGetFollowers(for: user) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/GitHubInfoView/SubViews/GithubItemInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubItemInfoView.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 6.02.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class GithubItemInfoView: UIView { 11 | 12 | enum ItemInfoType { 13 | case repos, gists, followers, following 14 | } 15 | 16 | private let symbolImageView: UIImageView = { 17 | let imageView = UIImageView() 18 | imageView.translatesAutoresizingMaskIntoConstraints = false 19 | imageView.contentMode = .scaleAspectFill 20 | imageView.tintColor = .label 21 | return imageView 22 | }() 23 | 24 | private let titleLabel: BaseTitleLabel = BaseTitleLabel(textAlignment: .left, fontSize: 14, fontWeight: .semibold) 25 | private let countLabel: BaseTitleLabel = BaseTitleLabel(textAlignment: .center, fontSize: 14, fontWeight: .semibold) 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: .zero) 29 | setupUI() 30 | } 31 | 32 | required init?(coder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | private func setupUI() { 37 | [symbolImageView, titleLabel, countLabel].forEach { addSubview($0) } 38 | 39 | symbolImageView.configureConstraint(top: (topAnchor, .zero), 40 | leading: (leadingAnchor, .zero)) 41 | 42 | titleLabel.configureConstraint(leading: (symbolImageView.trailingAnchor, Constants.Styling.defaultSpacing), 43 | trailing: (trailingAnchor, -Constants.Styling.defaultSpacing), 44 | centerY: (symbolImageView.centerYAnchor, .zero)) 45 | 46 | countLabel.configureConstraint(top: (symbolImageView.bottomAnchor, Constants.Styling.minimumSpacing), 47 | bottom: (bottomAnchor, .zero), 48 | centerX: (centerXAnchor, .zero)) 49 | } 50 | 51 | func set(itemInfoType: ItemInfoType, withCount count: Int) { 52 | switch itemInfoType { 53 | case .repos: 54 | symbolImageView.image = UIImage(systemName: Constants.SFSymbols.repos) 55 | titleLabel.text = Constants.InfoTexts.repos 56 | case .gists: 57 | symbolImageView.image = UIImage(systemName: Constants.SFSymbols.gists) 58 | titleLabel.text = Constants.InfoTexts.gists 59 | case .followers: 60 | symbolImageView.image = UIImage(systemName: Constants.SFSymbols.followers) 61 | titleLabel.text = Constants.InfoTexts.followers 62 | case .following: 63 | symbolImageView.image = UIImage(systemName: Constants.SFSymbols.following) 64 | titleLabel.text = Constants.InfoTexts.following 65 | } 66 | 67 | countLabel.text = String(count) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/GitHubInfoView/SubViews/RepoInfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepoInfoViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 10.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | final class RepoInfoViewController: GithubInfoViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | setupUI() 15 | } 16 | 17 | private func setupUI() { 18 | firstItemInfoView.set(itemInfoType: .repos, withCount: user?.publicRepos ?? .zero) 19 | secondItemInfoView.set(itemInfoType: .gists, withCount: user?.publicGists ?? .zero) 20 | actionButton.configureButton(backgroundColor: .systemPurple, title: Constants.InfoTexts.githubProfileText) 21 | } 22 | 23 | override func actionButtonTapped() { 24 | guard let user = user else { return } 25 | delegate?.didTappedGitHubProfile(for: user) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Reusables/Views/ProfileHeaderView/ProfileHeaderViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileHeaderViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 6.02.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class ProfileHeaderViewController: UIViewController { 11 | 12 | private lazy var containerStackView: UIStackView = { 13 | let stackView = UIStackView() 14 | stackView.alignment = .fill 15 | stackView.distribution = .fill 16 | stackView.spacing = Constants.Styling.defaultSpacing 17 | return stackView 18 | }() 19 | 20 | private lazy var userIdentityStackView: UIStackView = { 21 | let stackView = UIStackView() 22 | stackView.axis = .vertical 23 | stackView.alignment = .fill 24 | stackView.distribution = .fill 25 | stackView.spacing = Constants.Styling.defaultSpacing 26 | return stackView 27 | }() 28 | 29 | private let avatarImageView: BaseImageView = BaseImageView(frame: .zero) 30 | 31 | private let userNameLabel: BaseTitleLabel = BaseTitleLabel(textAlignment: .left, 32 | fontSize: 24, 33 | fontWeight: .bold) 34 | 35 | private let nameLabel: BaseTitleLabel = BaseTitleLabel(textAlignment: .left, 36 | fontSize: 18, 37 | fontWeight: .regular) 38 | 39 | private let bioLabel: BaseBodyLabel = BaseBodyLabel(textAlignment: .left) 40 | 41 | private lazy var locationContainer: UIView = { 42 | let view = UIView() 43 | view.translatesAutoresizingMaskIntoConstraints = false 44 | return view 45 | }() 46 | 47 | private let locationImageView: BaseImageView = BaseImageView(frame: .zero) 48 | private let locationLabel: BaseTitleLabel = BaseTitleLabel(textAlignment: .left, 49 | fontSize: 18, 50 | fontWeight: .regular) 51 | 52 | private var user: User? 53 | 54 | init(user: User) { 55 | super.init(nibName: nil, bundle: nil) 56 | self.user = user 57 | } 58 | 59 | required init?(coder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | setupUI() 66 | configureConstraints() 67 | } 68 | 69 | private func setupUI() { 70 | 71 | view.addSubview(containerStackView) 72 | view.addSubview(bioLabel) 73 | 74 | [userNameLabel, nameLabel, locationContainer].forEach { 75 | userIdentityStackView.addArrangedSubview($0) 76 | } 77 | [avatarImageView, userIdentityStackView].forEach { containerStackView.addArrangedSubview($0)} 78 | locationContainer.addSubview(locationImageView) 79 | locationContainer.addSubview(locationLabel) 80 | 81 | if let user = user { 82 | userNameLabel.text = user.login 83 | nameLabel.text = user.name 84 | bioLabel.text = user.bio 85 | locationLabel.text = user.location 86 | avatarImageView.setImage(from: user.avatarUrl) 87 | locationImageView.image = UIImage(systemName: Constants.SFSymbols.location) 88 | } 89 | bioLabel.numberOfLines = 2 90 | } 91 | 92 | private func configureConstraints() { 93 | containerStackView.configureConstraint(top: (view.topAnchor, Constants.Styling.defaultSpacing), 94 | leading: (view.leadingAnchor, .zero), 95 | trailing: (view.trailingAnchor, .zero)) 96 | 97 | bioLabel.configureConstraint(top: (containerStackView.bottomAnchor, .zero), 98 | bottom: (view.bottomAnchor, .zero), 99 | leading: (view.leadingAnchor, .zero), 100 | trailing: (view.trailingAnchor, .zero)) 101 | 102 | avatarImageView.configureWidth(width: Constants.Styling.profilePhotoWidthHeight) 103 | avatarImageView.configureHeight(height: Constants.Styling.profilePhotoWidthHeight) 104 | 105 | locationLabel.configureConstraint(leading: (locationImageView.trailingAnchor, Constants.Styling.minimumSpacing)) 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Favorites/FavoriteCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoriteCell.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 11.02.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class FavoriteCell: UITableViewCell { 11 | static let reuseIdentifier = "FavoriteCell" 12 | private lazy var avatarImageView: BaseImageView = { 13 | let imageView = BaseImageView(frame: .zero) 14 | imageView.translatesAutoresizingMaskIntoConstraints = false 15 | imageView.configureHeight(height: 60) 16 | imageView.configureWidth(width: 60) 17 | return imageView 18 | }() 19 | 20 | private lazy var userNameLabel: BaseTitleLabel = { 21 | let label = BaseTitleLabel(textAlignment: .left, fontSize: Constants.Styling.maxSpacing, fontWeight: .bold) 22 | label.translatesAutoresizingMaskIntoConstraints = false 23 | return label 24 | }() 25 | 26 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 27 | super.init(style: style, reuseIdentifier: reuseIdentifier) 28 | setupUI() 29 | } 30 | 31 | func setFavoriteCell(favorite: Follower) { 32 | userNameLabel.text = favorite.login 33 | avatarImageView.setImage(from: favorite.avatarUrl) 34 | } 35 | 36 | required init?(coder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | private func setupUI() { 41 | [avatarImageView, userNameLabel].forEach { addSubview($0)} 42 | accessoryType = .disclosureIndicator 43 | 44 | avatarImageView.configureConstraint(leading: (leadingAnchor, Constants.Styling.defaultSpacing), 45 | centerY: (centerYAnchor, .zero)) 46 | 47 | userNameLabel.configureConstraint(leading: (avatarImageView.trailingAnchor, Constants.Styling.maxSpacing), 48 | trailing: (trailingAnchor, -Constants.Styling.defaultSpacing), 49 | centerY: (centerYAnchor, .zero)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Favorites/FavoritesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesView.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 27.02.2022. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class FavoritesView: UIView { 12 | lazy var tableView: UITableView = { 13 | let tableView = UITableView() 14 | tableView.translatesAutoresizingMaskIntoConstraints = false 15 | return tableView 16 | }() 17 | 18 | init() { 19 | super.init(frame: .zero) 20 | setupUI() 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | fatalError("init(coder:) has not been implemented") 25 | } 26 | 27 | private func setupUI() { 28 | addSubview(tableView) 29 | tableView.alignFitEdges() 30 | tableView.rowHeight = Constants.Styling.maxSpacing * 4 31 | 32 | tableView.register(FavoriteCell.self, forCellReuseIdentifier: FavoriteCell.reuseIdentifier) 33 | } 34 | 35 | func showEmptyStateView(with message: String) { 36 | let emptyStateView: EmptyStateView = EmptyStateView(message: message) 37 | emptyStateView.frame = bounds 38 | addSubview(emptyStateView) 39 | } 40 | 41 | func reloadTableViewData() { 42 | tableView.reloadData() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Favorites/FavoritesViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 17.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FavoritesViewController: UIViewController { 11 | 12 | private lazy var viewModel = FavoritesViewModel() 13 | private lazy var viewSource = FavoritesView() 14 | 15 | override func loadView() { 16 | view = viewSource 17 | } 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | setupUI() 22 | } 23 | 24 | override func viewWillAppear(_ animated: Bool) { 25 | viewModel.loadFavorites() 26 | viewSource.reloadTableViewData() 27 | } 28 | 29 | private func setupUI() { 30 | view.backgroundColor = .systemBackground 31 | title = Constants.PageTitles.favorites.rawValue 32 | navigationController?.navigationBar.prefersLargeTitles = true 33 | viewSource.tableView.delegate = self 34 | viewSource.tableView.dataSource = self 35 | } 36 | } 37 | 38 | // MARK: - UITableView Delegates 39 | extension FavoritesViewController: UITableViewDataSource, UITableViewDelegate { 40 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 41 | return viewModel.favoriteUsers.count 42 | } 43 | 44 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 45 | guard let cell = tableView.dequeueReusableCell(withIdentifier: FavoriteCell.reuseIdentifier) as? FavoriteCell else { 46 | return UITableViewCell() 47 | } 48 | let favorite = viewModel.favoriteUsers[indexPath.row] 49 | cell.setFavoriteCell(favorite: favorite) 50 | return cell 51 | } 52 | 53 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 54 | let favorite = viewModel.favoriteUsers[indexPath.row] 55 | let destVC = FollowersListViewController(username: favorite.login) 56 | navigationController?.pushViewController(destVC, animated: true) 57 | } 58 | 59 | func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { 60 | guard editingStyle == .delete else { return } 61 | viewModel.favoriteUsers.remove(at: indexPath.row) 62 | tableView.deleteRows(at: [indexPath], with: .left) 63 | UserDefaultsManager().setArrayToLocal(key: .favorites, array: viewModel.favoriteUsers) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Favorites/FavoritesViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FavoritesViewModel.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 27.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FavoritesViewModelInput: AnyObject { 11 | func loadFavorites() 12 | } 13 | 14 | protocol FavoritesViewModelOutput: AnyObject { 15 | func getFavorites() -> [Follower] 16 | } 17 | 18 | final class FavoritesViewModel { 19 | var favoriteUsers: [Follower] = [] 20 | } 21 | 22 | extension FavoritesViewModel: FavoritesViewModelInput { 23 | func loadFavorites() { 24 | let favorites: [Follower] = UserDefaultsManager().getArrayFromLocal(key: .favorites) 25 | favoriteUsers = favorites 26 | } 27 | } 28 | 29 | extension FavoritesViewModel: FavoritesViewModelOutput { 30 | func getFavorites() -> [Follower] { 31 | return favoriteUsers 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/FollowersList/FollowerCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowerCell.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 25.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | class FollowerCell: UICollectionViewCell { 11 | static let reuseIdentifier = "FollowerCell" 12 | 13 | let avatarImgView = BaseImageView(frame: .zero) 14 | let userNameLabel = BaseTitleLabel(textAlignment: .center, fontSize: 12, fontWeight: .medium) 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | configureUI() 19 | } 20 | 21 | required init?(coder: NSCoder) { 22 | return nil 23 | } 24 | 25 | func set(follower: Follower) { 26 | userNameLabel.text = follower.login 27 | avatarImgView.setImage(from: follower.avatarUrl) 28 | } 29 | 30 | private func configureUI() { 31 | [avatarImgView, userNameLabel].forEach { addSubview($0) } 32 | 33 | avatarImgView.configureConstraint(top: (contentView.topAnchor, Constants.Styling.minimumSpacing), 34 | bottom: (userNameLabel.topAnchor, -Constants.Styling.minimumSpacing), 35 | leading: (contentView.leadingAnchor, Constants.Styling.minimumSpacing), 36 | trailing: (contentView.trailingAnchor, -Constants.Styling.minimumSpacing)) 37 | 38 | avatarImgView.configureHeight(height: Constants.Styling.followerCellImg) 39 | avatarImgView.configureWidth(width: Constants.Styling.followerCellImg) 40 | 41 | userNameLabel.configureConstraint(top: (avatarImgView.bottomAnchor, Constants.Styling.defaultSpacing), 42 | bottom: (contentView.bottomAnchor, -Constants.Styling.defaultSpacing), 43 | leading: (contentView.leadingAnchor, Constants.Styling.minimumSpacing), 44 | trailing: (contentView.trailingAnchor, -Constants.Styling.minimumSpacing)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/FollowersList/FollowersListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersListViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 20.01.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol FollowersListVCDelegate: AnyObject { 11 | func didRequestFollowers(for username: String) 12 | } 13 | 14 | final class FollowersListViewController: UIViewController { 15 | public enum Section { 16 | case main 17 | } 18 | 19 | var collectionView: UICollectionView! 20 | var dataSource: UICollectionViewDiffableDataSource! 21 | 22 | var username: String! 23 | private lazy var viewModel = FollowersListViewModel() 24 | private var isSearching: Bool = false 25 | 26 | init(username: String) { 27 | self.username = username 28 | super.init(nibName: nil, bundle: nil) 29 | 30 | } 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | configureViewController() 38 | configureSearchController() 39 | configureCollectionView() 40 | configureDataSource() 41 | 42 | let addButton = UIBarButtonItem(barButtonSystemItem: .add, 43 | target: self, 44 | action: #selector(addButtonTapped)) 45 | 46 | navigationItem.rightBarButtonItem = addButton 47 | 48 | viewModel.output = self 49 | viewModel.loadFollowers(userName: username, page: viewModel.getPageNumber()) 50 | viewModel.resetPageNumber() 51 | } 52 | 53 | @objc func addButtonTapped() { 54 | viewModel.addCurrentUserToFavorites(userName: username) 55 | } 56 | 57 | override func viewWillAppear(_ animated: Bool) { 58 | super.viewWillAppear(animated) 59 | viewModel.resetPageNumber() 60 | } 61 | 62 | } 63 | 64 | // MARK: - Configure CollectionView 65 | 66 | extension FollowersListViewController { 67 | private func configureCollectionView() { 68 | collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createThreeColumnFlowLayout()) 69 | view.addSubview(collectionView) 70 | collectionView.delegate = self 71 | collectionView.backgroundColor = .systemBackground 72 | collectionView.register(FollowerCell.self, 73 | forCellWithReuseIdentifier: FollowerCell.reuseIdentifier) 74 | 75 | collectionView.configureConstraint(top: (view.topAnchor, .zero), 76 | bottom: (view.bottomAnchor, .zero), 77 | leading: (view.safeAreaLayoutGuide.leadingAnchor, .zero), 78 | trailing: (view.safeAreaLayoutGuide.trailingAnchor, .zero)) 79 | } 80 | 81 | private func configureViewController() { 82 | view.backgroundColor = .systemBackground 83 | navigationController?.navigationBar.prefersLargeTitles = true 84 | navigationController?.setNavigationBarHidden(false, animated: true) 85 | title = username 86 | } 87 | 88 | private func configureSearchController() { 89 | let searchController = UISearchController() 90 | searchController.searchResultsUpdater = self 91 | searchController.searchBar.delegate = self 92 | searchController.searchBar.placeholder = "Search for a username" 93 | navigationItem.searchController = searchController 94 | } 95 | 96 | private func createThreeColumnFlowLayout() -> UICollectionViewFlowLayout { 97 | let width = view.bounds.width 98 | let minimumItemSpacesing: CGFloat = 10 99 | let availableWidth = width - (Constants.Styling.defaultSpacing * 2) - (minimumItemSpacesing * 2) 100 | let itemWidth = availableWidth / 3 101 | let flowLayout = UICollectionViewFlowLayout() 102 | flowLayout.sectionInset = UIEdgeInsets(top: Constants.Styling.defaultSpacing, 103 | left: Constants.Styling.defaultSpacing, 104 | bottom: Constants.Styling.defaultSpacing, 105 | right: Constants.Styling.defaultSpacing) 106 | flowLayout.itemSize = CGSize(width: itemWidth, height: itemWidth + 48) 107 | 108 | return flowLayout 109 | } 110 | 111 | private func configureDataSource() { 112 | dataSource = UICollectionViewDiffableDataSource( 113 | collectionView: collectionView, 114 | cellProvider: { (collectionView, indexPath, follower ) -> UICollectionViewCell? in 115 | let cell = collectionView.dequeueReusableCell( 116 | withReuseIdentifier: FollowerCell.reuseIdentifier, 117 | for: indexPath 118 | ) as? FollowerCell 119 | cell?.set(follower: follower) 120 | return cell 121 | } 122 | ) 123 | } 124 | } 125 | 126 | // MARK: - CollectionView Delegate 127 | 128 | extension FollowersListViewController: UICollectionViewDelegate { 129 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { 130 | let offsetY = scrollView.contentOffset.y 131 | let contentHeight = scrollView.contentSize.height 132 | let height = scrollView.frame.size.height 133 | 134 | if offsetY > contentHeight - height { 135 | guard viewModel.userHasMoreFollower() else { return } 136 | viewModel.increasePageNumber() 137 | let pageNumber = viewModel.getPageNumber() 138 | viewModel.loadFollowers(userName: username, page: pageNumber) 139 | } 140 | } 141 | 142 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 143 | let activeArray: [Follower] = isSearching ? viewModel.filteredFollowers : viewModel.followers 144 | let follower: Follower = activeArray[indexPath.row] 145 | let destVC = ProfileViewController(user: follower) 146 | destVC.delegate = self 147 | let navController = UINavigationController(rootViewController: destVC) 148 | present(navController, animated: true) 149 | } 150 | } 151 | 152 | // MARK: - Search Controller Delegate 153 | 154 | extension FollowersListViewController: UISearchResultsUpdating, UISearchBarDelegate { 155 | func updateSearchResults(for searchController: UISearchController) { 156 | guard let keyword = searchController.searchBar.text, 157 | !keyword.isEmpty else { 158 | return 159 | } 160 | isSearching = true 161 | viewModel.filteredFollowers = viewModel.followers.filter {$0.login.lowercased().contains(keyword.lowercased())} 162 | viewModel.updateData(on: viewModel.filteredFollowers) 163 | } 164 | 165 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 166 | isSearching = false 167 | viewModel.updateData(on: viewModel.followers) 168 | } 169 | } 170 | 171 | // MARK: - FollowersListVCDelegate 172 | 173 | extension FollowersListViewController: FollowersListVCDelegate { 174 | func didRequestFollowers(for username: String) { 175 | self.username = username 176 | title = username 177 | viewModel.resetPageNumber() 178 | viewModel.followers.removeAll() 179 | viewModel.filteredFollowers.removeAll() 180 | collectionView.setContentOffset(.zero, animated: true) 181 | let pageNumber = viewModel.getPageNumber() 182 | viewModel.loadFollowers(userName: username, page: pageNumber) 183 | } 184 | } 185 | 186 | extension FollowersListViewController: FollowersListViewModelOutput { 187 | func displayAlertPopup(title: String, message: String, buttonTitle: String) { 188 | presentAlertPopupOnMainThread(title: title, message: message, buttonTitle: buttonTitle) 189 | } 190 | 191 | func displayLoading() { 192 | showLoadingViewWithActivityIndicator() 193 | } 194 | 195 | func dismissLoading() { 196 | dismissLoadingView() 197 | } 198 | 199 | func updateData(on followers: [Follower]?) { 200 | guard let followers = followers else { 201 | return 202 | } 203 | var snapshot = NSDiffableDataSourceSnapshot() 204 | snapshot.appendSections([.main]) 205 | snapshot.appendItems(followers) 206 | DispatchQueue.main.async { 207 | self.dataSource.apply(snapshot, animatingDifferences: true) 208 | } 209 | } 210 | 211 | func showFollowersEmpty() { 212 | DispatchQueue.main.async { 213 | self.showEmptyStateView(with: "This User does not have any followers.😞", in: self.view) 214 | } 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/FollowersList/FollowersListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersListViewModel.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 23.01.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FollowersListViewModelInput: AnyObject { 11 | func loadFollowers(userName: String, page: Int) 12 | func addCurrentUserToFavorites(userName: String) 13 | func userHasMoreFollower() -> Bool 14 | func resetPageNumber() 15 | func increasePageNumber() 16 | func getPageNumber() -> Int 17 | func updateData(on followers: [Follower]?) 18 | } 19 | 20 | protocol FollowersListViewModelOutput: AnyObject { 21 | func displayAlertPopup(title: String, message: String, buttonTitle: String) 22 | func displayLoading() 23 | func dismissLoading() 24 | func updateData(on followers: [Follower]?) 25 | func showFollowersEmpty() 26 | 27 | } 28 | 29 | private enum FollowersListConstants { 30 | static var hasMoreFollower: Bool = true 31 | static var pageNumber: Int = 1 32 | } 33 | 34 | final class FollowersListViewModel { 35 | var followers: [Follower] = [] 36 | weak var output: FollowersListViewModelOutput? 37 | var filteredFollowers: [Follower] = [] 38 | let followersService: FollowersServiceable 39 | let userService: UserServiceable 40 | 41 | init(service: FollowersServiceable = FollowersService(), 42 | userService: UserServiceable = UserService()) { 43 | self.followersService = service 44 | self.userService = userService 45 | } 46 | 47 | private func addFavoriteUserToUserDefaults(with user: Follower) { 48 | let userDefaultsManager = UserDefaultsManager() 49 | var favorites: [Follower] = userDefaultsManager.getArrayFromLocal(key: .favorites) 50 | favorites.append(user) 51 | userDefaultsManager.setArrayToLocal(key: .favorites, array: favorites) 52 | } 53 | } 54 | 55 | extension FollowersListViewModel: FollowersListViewModelInput { 56 | 57 | func updateData(on followers: [Follower]?) { 58 | output?.updateData(on: followers) 59 | } 60 | 61 | func loadFollowers(userName: String, page: Int) { 62 | self.output?.displayLoading() 63 | Task(priority: .background) { 64 | self.output?.dismissLoading() 65 | let result = try await followersService.getFollowers(username: userName, pageNumber: page) 66 | switch result { 67 | case .success(let followersResponse): 68 | if followersResponse.isEmpty && self.followers.isEmpty { 69 | self.output?.showFollowersEmpty() 70 | } 71 | self.followers.append(contentsOf: followersResponse) 72 | self.output?.updateData(on: self.followers) 73 | case .failure(let error): 74 | self.output?.displayAlertPopup(title: "Error", message: error.customMessage, buttonTitle: "Tamam") 75 | } 76 | } 77 | } 78 | 79 | func addCurrentUserToFavorites(userName: String) { 80 | Task(priority: .background) { 81 | let result = try await userService.getUser(userName: userName) 82 | switch result { 83 | case .success(let response): 84 | let favoriteUser: Follower = Follower(login: response.login, avatarUrl: response.avatarUrl) 85 | addFavoriteUserToUserDefaults(with: favoriteUser) 86 | self.output?.displayAlertPopup(title: Constants.InfoTexts.success, 87 | message: Constants.InfoTexts.favorited, 88 | buttonTitle: Constants.InfoTexts.closeButtonText) 89 | case .failure(let error): 90 | self.output?.displayAlertPopup(title: Constants.WarningTexts.errorTitle, 91 | message: error.customMessage, 92 | buttonTitle: Constants.InfoTexts.closeButtonText) 93 | } 94 | } 95 | } 96 | 97 | func userHasMoreFollower() -> Bool { 98 | return FollowersListConstants.hasMoreFollower 99 | } 100 | 101 | func resetPageNumber() { 102 | FollowersListConstants.pageNumber = 1 103 | } 104 | 105 | func increasePageNumber() { 106 | FollowersListConstants.pageNumber += 1 107 | } 108 | 109 | func getPageNumber() -> Int { 110 | return FollowersListConstants.pageNumber 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Profile/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 17.02.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ProfileView: UIView { 11 | 12 | let headerView: UIView = { 13 | let view = UIView() 14 | view.translatesAutoresizingMaskIntoConstraints = false 15 | view.configureHeight(height: Constants.Styling.profileHeaderContainerHeight) 16 | return view 17 | }() 18 | 19 | let followersView: UIView = { 20 | let view = UIView() 21 | view.translatesAutoresizingMaskIntoConstraints = false 22 | view.configureHeight(height: Constants.Styling.gitHubInfoViewHeight) 23 | return view 24 | }() 25 | 26 | let reposView: UIView = { 27 | let view = UIView() 28 | view.translatesAutoresizingMaskIntoConstraints = false 29 | view.configureHeight(height: Constants.Styling.gitHubInfoViewHeight) 30 | return view 31 | }() 32 | 33 | let howOldLabel: BaseBodyLabel = BaseBodyLabel(textAlignment: .center) 34 | 35 | init() { 36 | super.init(frame: .zero) 37 | setupUI() 38 | } 39 | 40 | required init?(coder: NSCoder) { 41 | fatalError("init(coder:) has not been implemented") 42 | } 43 | 44 | private func setupUI() { 45 | [headerView, followersView, reposView, howOldLabel].forEach { addSubview($0)} 46 | 47 | headerView.configureConstraint(top: (safeAreaLayoutGuide.topAnchor, .zero), 48 | leading: (leadingAnchor, Constants.Styling.defaultSpacing), 49 | trailing: (trailingAnchor, -Constants.Styling.defaultSpacing)) 50 | 51 | followersView.configureConstraint(top: (headerView.bottomAnchor, Constants.Styling.maxSpacing), 52 | leading: (leadingAnchor, Constants.Styling.defaultSpacing), 53 | trailing: (trailingAnchor, -Constants.Styling.defaultSpacing)) 54 | 55 | reposView.configureConstraint(top: (followersView.bottomAnchor, Constants.Styling.maxSpacing), 56 | leading: (leadingAnchor, Constants.Styling.defaultSpacing), 57 | trailing: (trailingAnchor, -Constants.Styling.defaultSpacing)) 58 | 59 | howOldLabel.configureConstraint(top: (reposView.bottomAnchor, Constants.Styling.defaultSpacing), 60 | bottom: (safeAreaLayoutGuide.bottomAnchor, -Constants.Styling.maxSpacing), 61 | leading: (leadingAnchor, Constants.Styling.defaultSpacing), 62 | trailing: (trailingAnchor, -Constants.Styling.defaultSpacing)) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Profile/ProfileViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 5.02.2022. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ProfileViewControllerDelegate: AnyObject { 11 | func didTappedGitHubProfile(for user: User) 12 | func didTappedGetFollowers(for user: User) 13 | } 14 | 15 | class ProfileViewController: UIViewController { 16 | private let viewSource = ProfileView() 17 | private var userName: String 18 | private var viewModel = ProfileViewModel() 19 | 20 | weak var delegate: FollowersListVCDelegate? 21 | 22 | init(user: Follower) { 23 | self.userName = user.login 24 | super.init(nibName: nil, bundle: nil) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | view.backgroundColor = .systemBackground 34 | 35 | let doneButton = UIBarButtonItem(barButtonSystemItem: .done, 36 | target: self, 37 | action: #selector(dismissViewController)) 38 | 39 | navigationItem.rightBarButtonItem = doneButton 40 | viewModel.output = self 41 | viewModel.loadUserInfo(userName: userName) 42 | } 43 | 44 | override func loadView() { 45 | view = viewSource 46 | } 47 | 48 | @objc private func dismissViewController() { 49 | dismiss(animated: true) 50 | } 51 | 52 | func setHowOldText(createdAt: String) { 53 | viewSource.howOldLabel.text = "\(Constants.InfoTexts.createdAt) \(createdAt)" 54 | } 55 | 56 | private func addChildViewController(child: UIViewController, to containerView: UIView) { 57 | addChild(child) 58 | containerView.addSubview(child.view) 59 | child.view.frame = containerView.bounds 60 | child.didMove(toParent: self) 61 | } 62 | } 63 | 64 | extension ProfileViewController: ProfileViewModelOutput { 65 | func displayError(title: String, message: String, buttonTitle: String) { 66 | presentAlertPopupOnMainThread(title: title, message: message, buttonTitle: buttonTitle) 67 | } 68 | 69 | func configureUIElements(with user: User) { 70 | let repoViewController = RepoInfoViewController(user: user) 71 | repoViewController.delegate = self 72 | let followerInfoViewController = FollowerInfoViewController(user: user) 73 | followerInfoViewController.delegate = self 74 | 75 | addChildViewController(child: ProfileHeaderViewController(user: user), to: viewSource.headerView) 76 | addChildViewController(child: repoViewController, to: viewSource.reposView) 77 | addChildViewController(child: followerInfoViewController, to: viewSource.followersView) 78 | setHowOldText(createdAt: user.createdAt.convertDateToDisplayFormat()) 79 | } 80 | 81 | func showGitHubProfile(for user: User) { 82 | guard let url = URL(string: user.htmlUrl) else { return } 83 | presentSafariVC(with: url) 84 | } 85 | 86 | func showUserFollowers(for user: User) { 87 | guard user.followers != .zero else { 88 | presentAlertPopupOnMainThread(title: Constants.WarningTexts.errorTitle, 89 | message: Constants.WarningTexts.errorMessage, 90 | buttonTitle: Constants.InfoTexts.closeButtonText) 91 | return 92 | } 93 | delegate?.didRequestFollowers(for: user.login) 94 | dismissViewController() 95 | } 96 | } 97 | 98 | // MARK: - Delegate 99 | 100 | extension ProfileViewController: ProfileViewControllerDelegate { 101 | func didTappedGitHubProfile(for user: User) { 102 | viewModel.showGitHubProfile(for: user) 103 | } 104 | 105 | func didTappedGetFollowers(for user: User) { 106 | viewModel.showUserFollowers(for: user) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Profile/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 5.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ProfileViewModelInput: AnyObject { 11 | func loadUserInfo(userName: String) 12 | func showGitHubProfile(for user: User) 13 | func showUserFollowers(for user: User) 14 | } 15 | 16 | protocol ProfileViewModelOutput: AnyObject { 17 | func displayError(title: String, message: String, buttonTitle: String) 18 | func configureUIElements(with user: User) 19 | func showUserFollowers(for user: User) 20 | func showGitHubProfile(for user: User) 21 | } 22 | 23 | class ProfileViewModel { 24 | weak var output: ProfileViewModelOutput? 25 | let userService: UserServiceable 26 | 27 | init(userService: UserServiceable = UserService()) { 28 | self.userService = userService 29 | } 30 | } 31 | 32 | extension ProfileViewModel: ProfileViewModelInput { 33 | func showUserFollowers(for user: User) { 34 | output?.showUserFollowers(for: user) 35 | } 36 | 37 | func showGitHubProfile(for user: User) { 38 | output?.showGitHubProfile(for: user) 39 | } 40 | 41 | func loadUserInfo(userName: String) { 42 | Task { 43 | let result = try await userService.getUser(userName: userName) 44 | switch result { 45 | case .success(let response): 46 | DispatchQueue.main.async { 47 | self.output?.configureUIElements(with: response) 48 | } 49 | case .failure(let error): 50 | self.output?.displayError(title: Constants.WarningTexts.errorTitle, 51 | message: error.customMessage, 52 | buttonTitle: Constants.InfoTexts.closeButtonText) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Search/SearchView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchView.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 27.02.2022. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class SearchView: UIView { 12 | 13 | private lazy var logoImageView: UIImageView = { 14 | let logoImageView = UIImageView() 15 | logoImageView.translatesAutoresizingMaskIntoConstraints = false 16 | return logoImageView 17 | }() 18 | 19 | let userNameTextField = BaseUITextField() 20 | let searchButton = BaseUIButton(backgroundColor: .systemGreen, title: Constants.InfoTexts.followerButtonTitle) 21 | 22 | init() { 23 | super.init(frame: .zero) 24 | setupUI() 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | private func setupUI() { 32 | configureLogoImageView() 33 | configureTextField() 34 | configureSearchButton() 35 | createDismissKeyboardTapGesture() 36 | } 37 | 38 | private func configureLogoImageView() { 39 | addSubview(logoImageView) 40 | logoImageView.image = .githubLogo 41 | 42 | logoImageView.configureConstraint(top: (safeAreaLayoutGuide.topAnchor, Constants.Styling.anormousSpacing), 43 | centerX: (centerXAnchor, .zero)) 44 | logoImageView.configureHeight(height: Constants.Styling.mainLogoHeight) 45 | logoImageView.configureWidth(width: Constants.Styling.mainLogoHeight) 46 | } 47 | 48 | private func configureTextField() { 49 | addSubview(userNameTextField) 50 | 51 | userNameTextField.configureConstraint(top: (logoImageView.bottomAnchor, Constants.Styling.searchPageDefaultSpacing), 52 | leading: (leadingAnchor, Constants.Styling.searchPageDefaultSpacing), 53 | trailing: (trailingAnchor, -Constants.Styling.searchPageDefaultSpacing)) 54 | userNameTextField.configureHeight(height: Constants.Styling.searchPageDefaultSpacing) 55 | } 56 | 57 | private func configureSearchButton() { 58 | addSubview(searchButton) 59 | 60 | searchButton.configureConstraint(bottom: (safeAreaLayoutGuide.bottomAnchor, -Constants.Styling.searchPageDefaultSpacing), 61 | leading: (leadingAnchor, Constants.Styling.searchPageDefaultSpacing), 62 | trailing: (trailingAnchor, -Constants.Styling.searchPageDefaultSpacing)) 63 | searchButton.configureHeight(height: Constants.Styling.searchPageDefaultSpacing) 64 | } 65 | 66 | private func createDismissKeyboardTapGesture() { 67 | let tap = UITapGestureRecognizer(target: self, action: #selector(UIView.endEditing)) 68 | self.addGestureRecognizer(tap) 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Screens/Search/SearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchViewController.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 17.01.2022. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class SearchViewController: UIViewController { 12 | 13 | private let viewSource = SearchView() 14 | 15 | override func loadView() { 16 | view = viewSource 17 | viewSource.searchButton.addTarget(self, action: #selector(pushFollowersListVC), for: .touchUpInside) 18 | viewSource.userNameTextField.delegate = self 19 | } 20 | 21 | private var isUserNameEntered: Bool { 22 | guard let userName = viewSource.userNameTextField.text else { return false } 23 | return !userName.isEmpty 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | view.backgroundColor = .systemBackground 30 | } 31 | 32 | override func viewWillAppear(_ animated: Bool) { 33 | super.viewWillAppear(animated) 34 | navigationController?.setNavigationBarHidden(true, animated: true) 35 | } 36 | } 37 | 38 | extension SearchViewController { 39 | @objc private func pushFollowersListVC() { 40 | guard isUserNameEntered, 41 | let username = viewSource.userNameTextField.text else { 42 | presentAlertPopupOnMainThread(title: Constants.InfoTexts.textFieldPlaceholder, 43 | message: Constants.WarningTexts.searchPopUpMessage, 44 | buttonTitle: Constants.InfoTexts.closeButtonText) 45 | return 46 | } 47 | let followersListViewController = FollowersListViewController(username: username) 48 | navigationController?.pushViewController(followersListViewController, animated: true) 49 | } 50 | } 51 | 52 | extension SearchViewController: UITextFieldDelegate { 53 | func textFieldShouldReturn(_ textField: UITextField) -> Bool { 54 | pushFollowersListVC() 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/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 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_20pt@2x-1.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "icon_20pt@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "icon_29pt.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "icon_29pt@2x-1.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "icon_29pt@3x.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "icon_40pt@2x-1.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "icon_40pt@3x.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "icon_60pt@2x.png", 47 | "idiom" : "iphone", 48 | "scale" : "2x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "icon_60pt@3x.png", 53 | "idiom" : "iphone", 54 | "scale" : "3x", 55 | "size" : "60x60" 56 | }, 57 | { 58 | "idiom" : "ipad", 59 | "scale" : "1x", 60 | "size" : "20x20" 61 | }, 62 | { 63 | "idiom" : "ipad", 64 | "scale" : "2x", 65 | "size" : "20x20" 66 | }, 67 | { 68 | "idiom" : "ipad", 69 | "scale" : "1x", 70 | "size" : "29x29" 71 | }, 72 | { 73 | "idiom" : "ipad", 74 | "scale" : "2x", 75 | "size" : "29x29" 76 | }, 77 | { 78 | "idiom" : "ipad", 79 | "scale" : "1x", 80 | "size" : "40x40" 81 | }, 82 | { 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "icon_76pt.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "icon_76pt@2x.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "icon_83.5@2x.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "Icon.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | } 111 | ], 112 | "info" : { 113 | "author" : "xcode", 114 | "version" : 1 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/Icon.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_29pt.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_76pt.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/Icons/github-search.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "github-search.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/Icons/github-search.imageset/github-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/Icons/github-search.imageset/github-search.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo-dark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "empty-state-logo-dark@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "empty-state-logo-dark@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo-dark.imageset/empty-state-logo-dark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo-dark.imageset/empty-state-logo-dark@2x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo-dark.imageset/empty-state-logo-dark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo-dark.imageset/empty-state-logo-dark@3x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "empty-state-logo@2x.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "filename" : "empty-state-logo@3x.png", 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@2x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ilter/GithubProfileWiki/24b288fa12350b16b79467d1a8ffd355a1d4ad86/GithubProfileWiki/GithubProfileWiki/StylingResources/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@3x.png -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/StylingResources/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 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Utilities/UserDefaults/UserDefaults+Helper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Helper.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 11.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper struct UserDefaultsHelper { 11 | private let key: String 12 | private let initialValue: Value 13 | private var defaultsStorage: UserDefaults { 14 | UserDefaults.standard 15 | } 16 | 17 | var wrappedValue: Value { 18 | get { 19 | let value = defaultsStorage.value(forKey: key) as? Value 20 | return value ?? initialValue 21 | } 22 | set { 23 | if let newValue = newValue as? AnyOptional, newValue.isNil { 24 | defaultsStorage.removeObject(forKey: key) 25 | } else { 26 | defaultsStorage.set(newValue, forKey: key) 27 | } 28 | } 29 | } 30 | 31 | init(wrappedValue initialValue: Value, key: String) { 32 | self.initialValue = initialValue 33 | self.key = key 34 | } 35 | } 36 | 37 | extension UserDefaultsHelper where Value: ExpressibleByNilLiteral { 38 | init(key: String) { 39 | self.init(wrappedValue: nil, key: key) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWiki/Utilities/UserDefaults/UserDefaultsManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsManager.swift 3 | // GithubProfileWiki 4 | // 5 | // Created by ilter on 10.02.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserDefaultsKeys: String { 11 | case favorites 12 | } 13 | 14 | final class UserDefaultsManager { 15 | @UserDefaultsHelper(key: UserDefaultsKeys.favorites.rawValue) 16 | var favorites: [Follower]? 17 | 18 | func setArrayToLocal(key: UserDefaultsKeys, array: [T]) { 19 | guard let encode = try? JSONEncoder().encode(array) else { return } 20 | let userDefaults = UserDefaults.standard 21 | userDefaults.set(encode, forKey: key.rawValue) 22 | } 23 | 24 | func getArrayFromLocal(key: UserDefaultsKeys) -> [T] { 25 | let userDefaults = UserDefaults.standard 26 | guard let encode = userDefaults.data(forKey: key.rawValue), 27 | let decode = try? JSONDecoder().decode([T].self, from: encode) else { return []} 28 | 29 | return decode 30 | } 31 | 32 | func removeFavorites(key: UserDefaultsKeys) { 33 | UserDefaults.standard.removeObject(forKey: key.rawValue) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWikiTests/DateConverterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateConverterTests.swift 3 | // GithubProfileWikiTests 4 | // 5 | // Created by ilter on 28.02.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import GithubProfileWiki 10 | 11 | class DateConverterTests: XCTestCase { 12 | func test__itConvertsDateToDisplayCorrectly() { 13 | let mockDate = "2016-10-22T21:17:16Z" 14 | XCTAssertEqual("Oct 22, 2016", mockDate.convertDateToDisplayFormat()) 15 | } 16 | } 17 | 18 | extension String { 19 | func convertToDate() -> Date? { 20 | let dateFormatter = DateFormatter() 21 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" 22 | dateFormatter.locale = Locale(identifier: "en_US") 23 | dateFormatter.timeZone = .current 24 | 25 | return dateFormatter.date(from: self) 26 | } 27 | 28 | func convertDateToDisplayFormat() -> String { 29 | guard let date = self.convertToDate() else { return "N/A" } 30 | return date.convertToMonthYearFormat() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWikiTests/GithubProfileWikiTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubProfileWikiTests.swift 3 | // GithubProfileWikiTests 4 | // 5 | // Created by ilter on 28.02.2022. 6 | // 7 | 8 | import XCTest 9 | @testable import GithubProfileWiki 10 | 11 | class GithubProfileWikiTests: XCTestCase { 12 | var user: User! 13 | var title: String! 14 | var message: String! 15 | var buttonTitle: String! 16 | 17 | override func setUp() { 18 | user = User(login: "", 19 | avatarUrl: "", 20 | name: "", 21 | location: "", 22 | bio: "", 23 | publicRepos: .zero, 24 | publicGists: .zero, 25 | htmlUrl: "", 26 | following: .zero, 27 | followers: .zero, 28 | createdAt: "") 29 | title = "" 30 | message = "" 31 | buttonTitle = "" 32 | } 33 | 34 | override func tearDown() { 35 | user = nil 36 | title = nil 37 | message = nil 38 | buttonTitle = nil 39 | } 40 | 41 | func test__itShowsUserFollowersWhenTapped() { 42 | let viewModel = ProfileViewModel() 43 | viewModel.output = self 44 | let mockUser = User(login: "alperenduran", 45 | avatarUrl: "https://avatars.githubusercontent.com/u/23004475?v=4", 46 | name: "Alperen Duran", 47 | location: "Berlin, Germany", 48 | bio: nil, 49 | publicRepos: 4, publicGists: 0, htmlUrl: "https://github.com/alperenduran", 50 | following: 19, 51 | followers: 14, 52 | createdAt: "2016-10-22T21:17:16Z") 53 | viewModel.showUserFollowers(for: mockUser) 54 | 55 | XCTAssertEqual(mockUser, self.user) 56 | } 57 | 58 | func test__FollowersServiceMock() async { 59 | let serviceMock = FollowersServiceMock() 60 | do { 61 | let failingResult = try await serviceMock.getFollowers(username: "ilter", pageNumber: 1) 62 | 63 | switch failingResult { 64 | case .success(let followers): 65 | XCTAssertEqual(followers.first?.login, "keremkusmezer") 66 | case .failure: 67 | XCTFail("The Followers Service request with ilter param should not fail") 68 | } 69 | } catch { 70 | XCTFail("The Followers Service request should not fail") 71 | } 72 | } 73 | 74 | func test__UserServiceMock() async { 75 | let userServiceMock = UserServiceMock() 76 | 77 | do { 78 | let failingResult = try await userServiceMock.getUser(userName: "ilter") 79 | 80 | switch failingResult { 81 | case .success(let user): 82 | XCTAssertEqual(user.login, "ilter") 83 | XCTAssertEqual(user.publicRepos, 10) 84 | case .failure: 85 | XCTFail("The User Service request with ilter param should not fail") 86 | } 87 | } catch { 88 | XCTFail("The User Service request should not fail") 89 | } 90 | } 91 | 92 | func test__FollowersServiceFailure() async { 93 | let serviceMock = FollowersServiceMock() 94 | do { 95 | let failingResult = try await serviceMock.getFollowersError(userName: "ilter", pageNumber: 1) 96 | 97 | switch failingResult { 98 | case .success(_): 99 | XCTFail("This case should be fail") 100 | case .failure(let fail): 101 | XCTAssertEqual(fail, RequestError.decode) 102 | } 103 | } catch { 104 | XCTFail("The Followers Service request should not fail") 105 | } 106 | } 107 | 108 | func test__UserServiceFailure() async { 109 | let serviceMock = UserServiceMock() 110 | do { 111 | let failingResult = try await serviceMock.getUserFailure(userName: "ilter") 112 | 113 | switch failingResult { 114 | case .success(_): 115 | XCTFail("This case should be fail") 116 | case .failure(let fail): 117 | XCTAssertEqual(fail, RequestError.noResponse) 118 | } 119 | } catch { 120 | XCTFail("The User Service request should not fail") 121 | } 122 | } 123 | 124 | func testPerformanceExample() throws { 125 | // This is an example of a performance test case. 126 | measure { 127 | // Put the code you want to measure the time of here. 128 | } 129 | } 130 | 131 | } 132 | 133 | extension GithubProfileWikiTests: ProfileViewModelOutput { 134 | func displayError(title: String, message: String, buttonTitle: String) { 135 | self.title = title 136 | self.message = message 137 | self.buttonTitle = buttonTitle 138 | } 139 | 140 | func configureUIElements(with user: User) { 141 | self.user = user 142 | } 143 | 144 | func showUserFollowers(for user: User) { 145 | self.user = user 146 | } 147 | func showGitHubProfile(for user: User) { 148 | self.user = user 149 | } 150 | } 151 | 152 | final class FollowersServiceMock: Mockable, FollowersServiceable { 153 | func getFollowers(username: String, pageNumber: Int) async throws -> Result { 154 | return .success(loadJSON(filename: "followers_response", type: Followers.self)) 155 | } 156 | 157 | func getFollowersError(userName: String, pageNumber: Int) async throws -> Result { 158 | return .failure(.decode) 159 | } 160 | } 161 | 162 | final class UserServiceMock: Mockable, UserServiceable { 163 | func getUser(userName: String) async throws -> Result { 164 | return .success(loadJSON(filename: "user_response", type: User.self)) 165 | } 166 | 167 | func getUserFailure(userName: String) async throws -> Result { 168 | return .failure(.noResponse) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWikiTests/JSONResponses/followers_response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "login": "keremkusmezer", 4 | "id": 370568, 5 | "node_id": "MDQ6VXNlcjM3MDU2OA==", 6 | "avatar_url": "https://avatars.githubusercontent.com/u/370568?v=4", 7 | "gravatar_id": "", 8 | "url": "https://api.github.com/users/keremkusmezer", 9 | "html_url": "https://github.com/keremkusmezer", 10 | "followers_url": "https://api.github.com/users/keremkusmezer/followers", 11 | "following_url": "https://api.github.com/users/keremkusmezer/following{/other_user}", 12 | "gists_url": "https://api.github.com/users/keremkusmezer/gists{/gist_id}", 13 | "starred_url": "https://api.github.com/users/keremkusmezer/starred{/owner}{/repo}", 14 | "subscriptions_url": "https://api.github.com/users/keremkusmezer/subscriptions", 15 | "organizations_url": "https://api.github.com/users/keremkusmezer/orgs", 16 | "repos_url": "https://api.github.com/users/keremkusmezer/repos", 17 | "events_url": "https://api.github.com/users/keremkusmezer/events{/privacy}", 18 | "received_events_url": "https://api.github.com/users/keremkusmezer/received_events", 19 | "type": "User", 20 | "site_admin": false 21 | }, 22 | { 23 | "login": "Ge0", 24 | "id": 955950, 25 | "node_id": "MDQ6VXNlcjk1NTk1MA==", 26 | "avatar_url": "https://avatars.githubusercontent.com/u/955950?v=4", 27 | "gravatar_id": "", 28 | "url": "https://api.github.com/users/Ge0", 29 | "html_url": "https://github.com/Ge0", 30 | "followers_url": "https://api.github.com/users/Ge0/followers", 31 | "following_url": "https://api.github.com/users/Ge0/following{/other_user}", 32 | "gists_url": "https://api.github.com/users/Ge0/gists{/gist_id}", 33 | "starred_url": "https://api.github.com/users/Ge0/starred{/owner}{/repo}", 34 | "subscriptions_url": "https://api.github.com/users/Ge0/subscriptions", 35 | "organizations_url": "https://api.github.com/users/Ge0/orgs", 36 | "repos_url": "https://api.github.com/users/Ge0/repos", 37 | "events_url": "https://api.github.com/users/Ge0/events{/privacy}", 38 | "received_events_url": "https://api.github.com/users/Ge0/received_events", 39 | "type": "User", 40 | "site_admin": false 41 | }, 42 | { 43 | "login": "MetinSeylan", 44 | "id": 1490081, 45 | "node_id": "MDQ6VXNlcjE0OTAwODE=", 46 | "avatar_url": "https://avatars.githubusercontent.com/u/1490081?v=4", 47 | "gravatar_id": "", 48 | "url": "https://api.github.com/users/MetinSeylan", 49 | "html_url": "https://github.com/MetinSeylan", 50 | "followers_url": "https://api.github.com/users/MetinSeylan/followers", 51 | "following_url": "https://api.github.com/users/MetinSeylan/following{/other_user}", 52 | "gists_url": "https://api.github.com/users/MetinSeylan/gists{/gist_id}", 53 | "starred_url": "https://api.github.com/users/MetinSeylan/starred{/owner}{/repo}", 54 | "subscriptions_url": "https://api.github.com/users/MetinSeylan/subscriptions", 55 | "organizations_url": "https://api.github.com/users/MetinSeylan/orgs", 56 | "repos_url": "https://api.github.com/users/MetinSeylan/repos", 57 | "events_url": "https://api.github.com/users/MetinSeylan/events{/privacy}", 58 | "received_events_url": "https://api.github.com/users/MetinSeylan/received_events", 59 | "type": "User", 60 | "site_admin": false 61 | }, 62 | { 63 | "login": "eric-otto", 64 | "id": 2243814, 65 | "node_id": "MDQ6VXNlcjIyNDM4MTQ=", 66 | "avatar_url": "https://avatars.githubusercontent.com/u/2243814?v=4", 67 | "gravatar_id": "", 68 | "url": "https://api.github.com/users/eric-otto", 69 | "html_url": "https://github.com/eric-otto", 70 | "followers_url": "https://api.github.com/users/eric-otto/followers", 71 | "following_url": "https://api.github.com/users/eric-otto/following{/other_user}", 72 | "gists_url": "https://api.github.com/users/eric-otto/gists{/gist_id}", 73 | "starred_url": "https://api.github.com/users/eric-otto/starred{/owner}{/repo}", 74 | "subscriptions_url": "https://api.github.com/users/eric-otto/subscriptions", 75 | "organizations_url": "https://api.github.com/users/eric-otto/orgs", 76 | "repos_url": "https://api.github.com/users/eric-otto/repos", 77 | "events_url": "https://api.github.com/users/eric-otto/events{/privacy}", 78 | "received_events_url": "https://api.github.com/users/eric-otto/received_events", 79 | "type": "User", 80 | "site_admin": false 81 | }, 82 | { 83 | "login": "akinayturan", 84 | "id": 3206344, 85 | "node_id": "MDQ6VXNlcjMyMDYzNDQ=", 86 | "avatar_url": "https://avatars.githubusercontent.com/u/3206344?v=4", 87 | "gravatar_id": "", 88 | "url": "https://api.github.com/users/akinayturan", 89 | "html_url": "https://github.com/akinayturan", 90 | "followers_url": "https://api.github.com/users/akinayturan/followers", 91 | "following_url": "https://api.github.com/users/akinayturan/following{/other_user}", 92 | "gists_url": "https://api.github.com/users/akinayturan/gists{/gist_id}", 93 | "starred_url": "https://api.github.com/users/akinayturan/starred{/owner}{/repo}", 94 | "subscriptions_url": "https://api.github.com/users/akinayturan/subscriptions", 95 | "organizations_url": "https://api.github.com/users/akinayturan/orgs", 96 | "repos_url": "https://api.github.com/users/akinayturan/repos", 97 | "events_url": "https://api.github.com/users/akinayturan/events{/privacy}", 98 | "received_events_url": "https://api.github.com/users/akinayturan/received_events", 99 | "type": "User", 100 | "site_admin": false 101 | }, 102 | { 103 | "login": "kiliczsh", 104 | "id": 3473393, 105 | "node_id": "MDQ6VXNlcjM0NzMzOTM=", 106 | "avatar_url": "https://avatars.githubusercontent.com/u/3473393?v=4", 107 | "gravatar_id": "", 108 | "url": "https://api.github.com/users/kiliczsh", 109 | "html_url": "https://github.com/kiliczsh", 110 | "followers_url": "https://api.github.com/users/kiliczsh/followers", 111 | "following_url": "https://api.github.com/users/kiliczsh/following{/other_user}", 112 | "gists_url": "https://api.github.com/users/kiliczsh/gists{/gist_id}", 113 | "starred_url": "https://api.github.com/users/kiliczsh/starred{/owner}{/repo}", 114 | "subscriptions_url": "https://api.github.com/users/kiliczsh/subscriptions", 115 | "organizations_url": "https://api.github.com/users/kiliczsh/orgs", 116 | "repos_url": "https://api.github.com/users/kiliczsh/repos", 117 | "events_url": "https://api.github.com/users/kiliczsh/events{/privacy}", 118 | "received_events_url": "https://api.github.com/users/kiliczsh/received_events", 119 | "type": "User", 120 | "site_admin": false 121 | }, 122 | { 123 | "login": "hamzaerbay", 124 | "id": 5136093, 125 | "node_id": "MDQ6VXNlcjUxMzYwOTM=", 126 | "avatar_url": "https://avatars.githubusercontent.com/u/5136093?v=4", 127 | "gravatar_id": "", 128 | "url": "https://api.github.com/users/hamzaerbay", 129 | "html_url": "https://github.com/hamzaerbay", 130 | "followers_url": "https://api.github.com/users/hamzaerbay/followers", 131 | "following_url": "https://api.github.com/users/hamzaerbay/following{/other_user}", 132 | "gists_url": "https://api.github.com/users/hamzaerbay/gists{/gist_id}", 133 | "starred_url": "https://api.github.com/users/hamzaerbay/starred{/owner}{/repo}", 134 | "subscriptions_url": "https://api.github.com/users/hamzaerbay/subscriptions", 135 | "organizations_url": "https://api.github.com/users/hamzaerbay/orgs", 136 | "repos_url": "https://api.github.com/users/hamzaerbay/repos", 137 | "events_url": "https://api.github.com/users/hamzaerbay/events{/privacy}", 138 | "received_events_url": "https://api.github.com/users/hamzaerbay/received_events", 139 | "type": "User", 140 | "site_admin": false 141 | }, 142 | { 143 | "login": "berkbuzcu", 144 | "id": 6241929, 145 | "node_id": "MDQ6VXNlcjYyNDE5Mjk=", 146 | "avatar_url": "https://avatars.githubusercontent.com/u/6241929?v=4", 147 | "gravatar_id": "", 148 | "url": "https://api.github.com/users/berkbuzcu", 149 | "html_url": "https://github.com/berkbuzcu", 150 | "followers_url": "https://api.github.com/users/berkbuzcu/followers", 151 | "following_url": "https://api.github.com/users/berkbuzcu/following{/other_user}", 152 | "gists_url": "https://api.github.com/users/berkbuzcu/gists{/gist_id}", 153 | "starred_url": "https://api.github.com/users/berkbuzcu/starred{/owner}{/repo}", 154 | "subscriptions_url": "https://api.github.com/users/berkbuzcu/subscriptions", 155 | "organizations_url": "https://api.github.com/users/berkbuzcu/orgs", 156 | "repos_url": "https://api.github.com/users/berkbuzcu/repos", 157 | "events_url": "https://api.github.com/users/berkbuzcu/events{/privacy}", 158 | "received_events_url": "https://api.github.com/users/berkbuzcu/received_events", 159 | "type": "User", 160 | "site_admin": false 161 | }, 162 | { 163 | "login": "onurkara", 164 | "id": 7956511, 165 | "node_id": "MDQ6VXNlcjc5NTY1MTE=", 166 | "avatar_url": "https://avatars.githubusercontent.com/u/7956511?v=4", 167 | "gravatar_id": "", 168 | "url": "https://api.github.com/users/onurkara", 169 | "html_url": "https://github.com/onurkara", 170 | "followers_url": "https://api.github.com/users/onurkara/followers", 171 | "following_url": "https://api.github.com/users/onurkara/following{/other_user}", 172 | "gists_url": "https://api.github.com/users/onurkara/gists{/gist_id}", 173 | "starred_url": "https://api.github.com/users/onurkara/starred{/owner}{/repo}", 174 | "subscriptions_url": "https://api.github.com/users/onurkara/subscriptions", 175 | "organizations_url": "https://api.github.com/users/onurkara/orgs", 176 | "repos_url": "https://api.github.com/users/onurkara/repos", 177 | "events_url": "https://api.github.com/users/onurkara/events{/privacy}", 178 | "received_events_url": "https://api.github.com/users/onurkara/received_events", 179 | "type": "User", 180 | "site_admin": false 181 | }, 182 | { 183 | "login": "taylanguneyaktas", 184 | "id": 8115828, 185 | "node_id": "MDQ6VXNlcjgxMTU4Mjg=", 186 | "avatar_url": "https://avatars.githubusercontent.com/u/8115828?v=4", 187 | "gravatar_id": "", 188 | "url": "https://api.github.com/users/taylanguneyaktas", 189 | "html_url": "https://github.com/taylanguneyaktas", 190 | "followers_url": "https://api.github.com/users/taylanguneyaktas/followers", 191 | "following_url": "https://api.github.com/users/taylanguneyaktas/following{/other_user}", 192 | "gists_url": "https://api.github.com/users/taylanguneyaktas/gists{/gist_id}", 193 | "starred_url": "https://api.github.com/users/taylanguneyaktas/starred{/owner}{/repo}", 194 | "subscriptions_url": "https://api.github.com/users/taylanguneyaktas/subscriptions", 195 | "organizations_url": "https://api.github.com/users/taylanguneyaktas/orgs", 196 | "repos_url": "https://api.github.com/users/taylanguneyaktas/repos", 197 | "events_url": "https://api.github.com/users/taylanguneyaktas/events{/privacy}", 198 | "received_events_url": "https://api.github.com/users/taylanguneyaktas/received_events", 199 | "type": "User", 200 | "site_admin": false 201 | }, 202 | { 203 | "login": "knissophiliac", 204 | "id": 13528014, 205 | "node_id": "MDQ6VXNlcjEzNTI4MDE0", 206 | "avatar_url": "https://avatars.githubusercontent.com/u/13528014?v=4", 207 | "gravatar_id": "", 208 | "url": "https://api.github.com/users/knissophiliac", 209 | "html_url": "https://github.com/knissophiliac", 210 | "followers_url": "https://api.github.com/users/knissophiliac/followers", 211 | "following_url": "https://api.github.com/users/knissophiliac/following{/other_user}", 212 | "gists_url": "https://api.github.com/users/knissophiliac/gists{/gist_id}", 213 | "starred_url": "https://api.github.com/users/knissophiliac/starred{/owner}{/repo}", 214 | "subscriptions_url": "https://api.github.com/users/knissophiliac/subscriptions", 215 | "organizations_url": "https://api.github.com/users/knissophiliac/orgs", 216 | "repos_url": "https://api.github.com/users/knissophiliac/repos", 217 | "events_url": "https://api.github.com/users/knissophiliac/events{/privacy}", 218 | "received_events_url": "https://api.github.com/users/knissophiliac/received_events", 219 | "type": "User", 220 | "site_admin": false 221 | }, 222 | { 223 | "login": "demirciy", 224 | "id": 15861353, 225 | "node_id": "MDQ6VXNlcjE1ODYxMzUz", 226 | "avatar_url": "https://avatars.githubusercontent.com/u/15861353?v=4", 227 | "gravatar_id": "", 228 | "url": "https://api.github.com/users/demirciy", 229 | "html_url": "https://github.com/demirciy", 230 | "followers_url": "https://api.github.com/users/demirciy/followers", 231 | "following_url": "https://api.github.com/users/demirciy/following{/other_user}", 232 | "gists_url": "https://api.github.com/users/demirciy/gists{/gist_id}", 233 | "starred_url": "https://api.github.com/users/demirciy/starred{/owner}{/repo}", 234 | "subscriptions_url": "https://api.github.com/users/demirciy/subscriptions", 235 | "organizations_url": "https://api.github.com/users/demirciy/orgs", 236 | "repos_url": "https://api.github.com/users/demirciy/repos", 237 | "events_url": "https://api.github.com/users/demirciy/events{/privacy}", 238 | "received_events_url": "https://api.github.com/users/demirciy/received_events", 239 | "type": "User", 240 | "site_admin": false 241 | }, 242 | { 243 | "login": "alpererdogan8", 244 | "id": 19363785, 245 | "node_id": "MDQ6VXNlcjE5MzYzNzg1", 246 | "avatar_url": "https://avatars.githubusercontent.com/u/19363785?v=4", 247 | "gravatar_id": "", 248 | "url": "https://api.github.com/users/alpererdogan8", 249 | "html_url": "https://github.com/alpererdogan8", 250 | "followers_url": "https://api.github.com/users/alpererdogan8/followers", 251 | "following_url": "https://api.github.com/users/alpererdogan8/following{/other_user}", 252 | "gists_url": "https://api.github.com/users/alpererdogan8/gists{/gist_id}", 253 | "starred_url": "https://api.github.com/users/alpererdogan8/starred{/owner}{/repo}", 254 | "subscriptions_url": "https://api.github.com/users/alpererdogan8/subscriptions", 255 | "organizations_url": "https://api.github.com/users/alpererdogan8/orgs", 256 | "repos_url": "https://api.github.com/users/alpererdogan8/repos", 257 | "events_url": "https://api.github.com/users/alpererdogan8/events{/privacy}", 258 | "received_events_url": "https://api.github.com/users/alpererdogan8/received_events", 259 | "type": "User", 260 | "site_admin": false 261 | }, 262 | { 263 | "login": "ilteriskeskin", 264 | "id": 20879375, 265 | "node_id": "MDQ6VXNlcjIwODc5Mzc1", 266 | "avatar_url": "https://avatars.githubusercontent.com/u/20879375?v=4", 267 | "gravatar_id": "", 268 | "url": "https://api.github.com/users/ilteriskeskin", 269 | "html_url": "https://github.com/ilteriskeskin", 270 | "followers_url": "https://api.github.com/users/ilteriskeskin/followers", 271 | "following_url": "https://api.github.com/users/ilteriskeskin/following{/other_user}", 272 | "gists_url": "https://api.github.com/users/ilteriskeskin/gists{/gist_id}", 273 | "starred_url": "https://api.github.com/users/ilteriskeskin/starred{/owner}{/repo}", 274 | "subscriptions_url": "https://api.github.com/users/ilteriskeskin/subscriptions", 275 | "organizations_url": "https://api.github.com/users/ilteriskeskin/orgs", 276 | "repos_url": "https://api.github.com/users/ilteriskeskin/repos", 277 | "events_url": "https://api.github.com/users/ilteriskeskin/events{/privacy}", 278 | "received_events_url": "https://api.github.com/users/ilteriskeskin/received_events", 279 | "type": "User", 280 | "site_admin": false 281 | }, 282 | { 283 | "login": "Adem68", 284 | "id": 21019611, 285 | "node_id": "MDQ6VXNlcjIxMDE5NjEx", 286 | "avatar_url": "https://avatars.githubusercontent.com/u/21019611?v=4", 287 | "gravatar_id": "", 288 | "url": "https://api.github.com/users/Adem68", 289 | "html_url": "https://github.com/Adem68", 290 | "followers_url": "https://api.github.com/users/Adem68/followers", 291 | "following_url": "https://api.github.com/users/Adem68/following{/other_user}", 292 | "gists_url": "https://api.github.com/users/Adem68/gists{/gist_id}", 293 | "starred_url": "https://api.github.com/users/Adem68/starred{/owner}{/repo}", 294 | "subscriptions_url": "https://api.github.com/users/Adem68/subscriptions", 295 | "organizations_url": "https://api.github.com/users/Adem68/orgs", 296 | "repos_url": "https://api.github.com/users/Adem68/repos", 297 | "events_url": "https://api.github.com/users/Adem68/events{/privacy}", 298 | "received_events_url": "https://api.github.com/users/Adem68/received_events", 299 | "type": "User", 300 | "site_admin": false 301 | }, 302 | { 303 | "login": "bufgix", 304 | "id": 22038798, 305 | "node_id": "MDQ6VXNlcjIyMDM4Nzk4", 306 | "avatar_url": "https://avatars.githubusercontent.com/u/22038798?v=4", 307 | "gravatar_id": "", 308 | "url": "https://api.github.com/users/bufgix", 309 | "html_url": "https://github.com/bufgix", 310 | "followers_url": "https://api.github.com/users/bufgix/followers", 311 | "following_url": "https://api.github.com/users/bufgix/following{/other_user}", 312 | "gists_url": "https://api.github.com/users/bufgix/gists{/gist_id}", 313 | "starred_url": "https://api.github.com/users/bufgix/starred{/owner}{/repo}", 314 | "subscriptions_url": "https://api.github.com/users/bufgix/subscriptions", 315 | "organizations_url": "https://api.github.com/users/bufgix/orgs", 316 | "repos_url": "https://api.github.com/users/bufgix/repos", 317 | "events_url": "https://api.github.com/users/bufgix/events{/privacy}", 318 | "received_events_url": "https://api.github.com/users/bufgix/received_events", 319 | "type": "User", 320 | "site_admin": false 321 | }, 322 | { 323 | "login": "YasinAkcokrak", 324 | "id": 22328903, 325 | "node_id": "MDQ6VXNlcjIyMzI4OTAz", 326 | "avatar_url": "https://avatars.githubusercontent.com/u/22328903?v=4", 327 | "gravatar_id": "", 328 | "url": "https://api.github.com/users/YasinAkcokrak", 329 | "html_url": "https://github.com/YasinAkcokrak", 330 | "followers_url": "https://api.github.com/users/YasinAkcokrak/followers", 331 | "following_url": "https://api.github.com/users/YasinAkcokrak/following{/other_user}", 332 | "gists_url": "https://api.github.com/users/YasinAkcokrak/gists{/gist_id}", 333 | "starred_url": "https://api.github.com/users/YasinAkcokrak/starred{/owner}{/repo}", 334 | "subscriptions_url": "https://api.github.com/users/YasinAkcokrak/subscriptions", 335 | "organizations_url": "https://api.github.com/users/YasinAkcokrak/orgs", 336 | "repos_url": "https://api.github.com/users/YasinAkcokrak/repos", 337 | "events_url": "https://api.github.com/users/YasinAkcokrak/events{/privacy}", 338 | "received_events_url": "https://api.github.com/users/YasinAkcokrak/received_events", 339 | "type": "User", 340 | "site_admin": false 341 | }, 342 | { 343 | "login": "alperenduran", 344 | "id": 23004475, 345 | "node_id": "MDQ6VXNlcjIzMDA0NDc1", 346 | "avatar_url": "https://avatars.githubusercontent.com/u/23004475?v=4", 347 | "gravatar_id": "", 348 | "url": "https://api.github.com/users/alperenduran", 349 | "html_url": "https://github.com/alperenduran", 350 | "followers_url": "https://api.github.com/users/alperenduran/followers", 351 | "following_url": "https://api.github.com/users/alperenduran/following{/other_user}", 352 | "gists_url": "https://api.github.com/users/alperenduran/gists{/gist_id}", 353 | "starred_url": "https://api.github.com/users/alperenduran/starred{/owner}{/repo}", 354 | "subscriptions_url": "https://api.github.com/users/alperenduran/subscriptions", 355 | "organizations_url": "https://api.github.com/users/alperenduran/orgs", 356 | "repos_url": "https://api.github.com/users/alperenduran/repos", 357 | "events_url": "https://api.github.com/users/alperenduran/events{/privacy}", 358 | "received_events_url": "https://api.github.com/users/alperenduran/received_events", 359 | "type": "User", 360 | "site_admin": false 361 | }, 362 | { 363 | "login": "barbarosaffan", 364 | "id": 23074716, 365 | "node_id": "MDQ6VXNlcjIzMDc0NzE2", 366 | "avatar_url": "https://avatars.githubusercontent.com/u/23074716?v=4", 367 | "gravatar_id": "", 368 | "url": "https://api.github.com/users/barbarosaffan", 369 | "html_url": "https://github.com/barbarosaffan", 370 | "followers_url": "https://api.github.com/users/barbarosaffan/followers", 371 | "following_url": "https://api.github.com/users/barbarosaffan/following{/other_user}", 372 | "gists_url": "https://api.github.com/users/barbarosaffan/gists{/gist_id}", 373 | "starred_url": "https://api.github.com/users/barbarosaffan/starred{/owner}{/repo}", 374 | "subscriptions_url": "https://api.github.com/users/barbarosaffan/subscriptions", 375 | "organizations_url": "https://api.github.com/users/barbarosaffan/orgs", 376 | "repos_url": "https://api.github.com/users/barbarosaffan/repos", 377 | "events_url": "https://api.github.com/users/barbarosaffan/events{/privacy}", 378 | "received_events_url": "https://api.github.com/users/barbarosaffan/received_events", 379 | "type": "User", 380 | "site_admin": false 381 | }, 382 | { 383 | "login": "elisguler", 384 | "id": 23150185, 385 | "node_id": "MDQ6VXNlcjIzMTUwMTg1", 386 | "avatar_url": "https://avatars.githubusercontent.com/u/23150185?v=4", 387 | "gravatar_id": "", 388 | "url": "https://api.github.com/users/elisguler", 389 | "html_url": "https://github.com/elisguler", 390 | "followers_url": "https://api.github.com/users/elisguler/followers", 391 | "following_url": "https://api.github.com/users/elisguler/following{/other_user}", 392 | "gists_url": "https://api.github.com/users/elisguler/gists{/gist_id}", 393 | "starred_url": "https://api.github.com/users/elisguler/starred{/owner}{/repo}", 394 | "subscriptions_url": "https://api.github.com/users/elisguler/subscriptions", 395 | "organizations_url": "https://api.github.com/users/elisguler/orgs", 396 | "repos_url": "https://api.github.com/users/elisguler/repos", 397 | "events_url": "https://api.github.com/users/elisguler/events{/privacy}", 398 | "received_events_url": "https://api.github.com/users/elisguler/received_events", 399 | "type": "User", 400 | "site_admin": false 401 | }, 402 | { 403 | "login": "AndiChiou", 404 | "id": 23460812, 405 | "node_id": "MDQ6VXNlcjIzNDYwODEy", 406 | "avatar_url": "https://avatars.githubusercontent.com/u/23460812?v=4", 407 | "gravatar_id": "", 408 | "url": "https://api.github.com/users/AndiChiou", 409 | "html_url": "https://github.com/AndiChiou", 410 | "followers_url": "https://api.github.com/users/AndiChiou/followers", 411 | "following_url": "https://api.github.com/users/AndiChiou/following{/other_user}", 412 | "gists_url": "https://api.github.com/users/AndiChiou/gists{/gist_id}", 413 | "starred_url": "https://api.github.com/users/AndiChiou/starred{/owner}{/repo}", 414 | "subscriptions_url": "https://api.github.com/users/AndiChiou/subscriptions", 415 | "organizations_url": "https://api.github.com/users/AndiChiou/orgs", 416 | "repos_url": "https://api.github.com/users/AndiChiou/repos", 417 | "events_url": "https://api.github.com/users/AndiChiou/events{/privacy}", 418 | "received_events_url": "https://api.github.com/users/AndiChiou/received_events", 419 | "type": "User", 420 | "site_admin": false 421 | }, 422 | { 423 | "login": "AyhanALTINOK", 424 | "id": 23553009, 425 | "node_id": "MDQ6VXNlcjIzNTUzMDA5", 426 | "avatar_url": "https://avatars.githubusercontent.com/u/23553009?v=4", 427 | "gravatar_id": "", 428 | "url": "https://api.github.com/users/AyhanALTINOK", 429 | "html_url": "https://github.com/AyhanALTINOK", 430 | "followers_url": "https://api.github.com/users/AyhanALTINOK/followers", 431 | "following_url": "https://api.github.com/users/AyhanALTINOK/following{/other_user}", 432 | "gists_url": "https://api.github.com/users/AyhanALTINOK/gists{/gist_id}", 433 | "starred_url": "https://api.github.com/users/AyhanALTINOK/starred{/owner}{/repo}", 434 | "subscriptions_url": "https://api.github.com/users/AyhanALTINOK/subscriptions", 435 | "organizations_url": "https://api.github.com/users/AyhanALTINOK/orgs", 436 | "repos_url": "https://api.github.com/users/AyhanALTINOK/repos", 437 | "events_url": "https://api.github.com/users/AyhanALTINOK/events{/privacy}", 438 | "received_events_url": "https://api.github.com/users/AyhanALTINOK/received_events", 439 | "type": "User", 440 | "site_admin": false 441 | }, 442 | { 443 | "login": "damla", 444 | "id": 24878421, 445 | "node_id": "MDQ6VXNlcjI0ODc4NDIx", 446 | "avatar_url": "https://avatars.githubusercontent.com/u/24878421?v=4", 447 | "gravatar_id": "", 448 | "url": "https://api.github.com/users/damla", 449 | "html_url": "https://github.com/damla", 450 | "followers_url": "https://api.github.com/users/damla/followers", 451 | "following_url": "https://api.github.com/users/damla/following{/other_user}", 452 | "gists_url": "https://api.github.com/users/damla/gists{/gist_id}", 453 | "starred_url": "https://api.github.com/users/damla/starred{/owner}{/repo}", 454 | "subscriptions_url": "https://api.github.com/users/damla/subscriptions", 455 | "organizations_url": "https://api.github.com/users/damla/orgs", 456 | "repos_url": "https://api.github.com/users/damla/repos", 457 | "events_url": "https://api.github.com/users/damla/events{/privacy}", 458 | "received_events_url": "https://api.github.com/users/damla/received_events", 459 | "type": "User", 460 | "site_admin": false 461 | }, 462 | { 463 | "login": "bilgin", 464 | "id": 27520396, 465 | "node_id": "MDQ6VXNlcjI3NTIwMzk2", 466 | "avatar_url": "https://avatars.githubusercontent.com/u/27520396?v=4", 467 | "gravatar_id": "", 468 | "url": "https://api.github.com/users/bilgin", 469 | "html_url": "https://github.com/bilgin", 470 | "followers_url": "https://api.github.com/users/bilgin/followers", 471 | "following_url": "https://api.github.com/users/bilgin/following{/other_user}", 472 | "gists_url": "https://api.github.com/users/bilgin/gists{/gist_id}", 473 | "starred_url": "https://api.github.com/users/bilgin/starred{/owner}{/repo}", 474 | "subscriptions_url": "https://api.github.com/users/bilgin/subscriptions", 475 | "organizations_url": "https://api.github.com/users/bilgin/orgs", 476 | "repos_url": "https://api.github.com/users/bilgin/repos", 477 | "events_url": "https://api.github.com/users/bilgin/events{/privacy}", 478 | "received_events_url": "https://api.github.com/users/bilgin/received_events", 479 | "type": "User", 480 | "site_admin": false 481 | }, 482 | { 483 | "login": "atakancengizkurt", 484 | "id": 28401804, 485 | "node_id": "MDQ6VXNlcjI4NDAxODA0", 486 | "avatar_url": "https://avatars.githubusercontent.com/u/28401804?v=4", 487 | "gravatar_id": "", 488 | "url": "https://api.github.com/users/atakancengizkurt", 489 | "html_url": "https://github.com/atakancengizkurt", 490 | "followers_url": "https://api.github.com/users/atakancengizkurt/followers", 491 | "following_url": "https://api.github.com/users/atakancengizkurt/following{/other_user}", 492 | "gists_url": "https://api.github.com/users/atakancengizkurt/gists{/gist_id}", 493 | "starred_url": "https://api.github.com/users/atakancengizkurt/starred{/owner}{/repo}", 494 | "subscriptions_url": "https://api.github.com/users/atakancengizkurt/subscriptions", 495 | "organizations_url": "https://api.github.com/users/atakancengizkurt/orgs", 496 | "repos_url": "https://api.github.com/users/atakancengizkurt/repos", 497 | "events_url": "https://api.github.com/users/atakancengizkurt/events{/privacy}", 498 | "received_events_url": "https://api.github.com/users/atakancengizkurt/received_events", 499 | "type": "User", 500 | "site_admin": false 501 | }, 502 | { 503 | "login": "ahmetkorkmaz3", 504 | "id": 29120746, 505 | "node_id": "MDQ6VXNlcjI5MTIwNzQ2", 506 | "avatar_url": "https://avatars.githubusercontent.com/u/29120746?v=4", 507 | "gravatar_id": "", 508 | "url": "https://api.github.com/users/ahmetkorkmaz3", 509 | "html_url": "https://github.com/ahmetkorkmaz3", 510 | "followers_url": "https://api.github.com/users/ahmetkorkmaz3/followers", 511 | "following_url": "https://api.github.com/users/ahmetkorkmaz3/following{/other_user}", 512 | "gists_url": "https://api.github.com/users/ahmetkorkmaz3/gists{/gist_id}", 513 | "starred_url": "https://api.github.com/users/ahmetkorkmaz3/starred{/owner}{/repo}", 514 | "subscriptions_url": "https://api.github.com/users/ahmetkorkmaz3/subscriptions", 515 | "organizations_url": "https://api.github.com/users/ahmetkorkmaz3/orgs", 516 | "repos_url": "https://api.github.com/users/ahmetkorkmaz3/repos", 517 | "events_url": "https://api.github.com/users/ahmetkorkmaz3/events{/privacy}", 518 | "received_events_url": "https://api.github.com/users/ahmetkorkmaz3/received_events", 519 | "type": "User", 520 | "site_admin": false 521 | }, 522 | { 523 | "login": "preethamb97", 524 | "id": 31663960, 525 | "node_id": "MDQ6VXNlcjMxNjYzOTYw", 526 | "avatar_url": "https://avatars.githubusercontent.com/u/31663960?v=4", 527 | "gravatar_id": "", 528 | "url": "https://api.github.com/users/preethamb97", 529 | "html_url": "https://github.com/preethamb97", 530 | "followers_url": "https://api.github.com/users/preethamb97/followers", 531 | "following_url": "https://api.github.com/users/preethamb97/following{/other_user}", 532 | "gists_url": "https://api.github.com/users/preethamb97/gists{/gist_id}", 533 | "starred_url": "https://api.github.com/users/preethamb97/starred{/owner}{/repo}", 534 | "subscriptions_url": "https://api.github.com/users/preethamb97/subscriptions", 535 | "organizations_url": "https://api.github.com/users/preethamb97/orgs", 536 | "repos_url": "https://api.github.com/users/preethamb97/repos", 537 | "events_url": "https://api.github.com/users/preethamb97/events{/privacy}", 538 | "received_events_url": "https://api.github.com/users/preethamb97/received_events", 539 | "type": "User", 540 | "site_admin": false 541 | }, 542 | { 543 | "login": "hayriyigit", 544 | "id": 31963436, 545 | "node_id": "MDQ6VXNlcjMxOTYzNDM2", 546 | "avatar_url": "https://avatars.githubusercontent.com/u/31963436?v=4", 547 | "gravatar_id": "", 548 | "url": "https://api.github.com/users/hayriyigit", 549 | "html_url": "https://github.com/hayriyigit", 550 | "followers_url": "https://api.github.com/users/hayriyigit/followers", 551 | "following_url": "https://api.github.com/users/hayriyigit/following{/other_user}", 552 | "gists_url": "https://api.github.com/users/hayriyigit/gists{/gist_id}", 553 | "starred_url": "https://api.github.com/users/hayriyigit/starred{/owner}{/repo}", 554 | "subscriptions_url": "https://api.github.com/users/hayriyigit/subscriptions", 555 | "organizations_url": "https://api.github.com/users/hayriyigit/orgs", 556 | "repos_url": "https://api.github.com/users/hayriyigit/repos", 557 | "events_url": "https://api.github.com/users/hayriyigit/events{/privacy}", 558 | "received_events_url": "https://api.github.com/users/hayriyigit/received_events", 559 | "type": "User", 560 | "site_admin": false 561 | }, 562 | { 563 | "login": "AbdullahAki", 564 | "id": 33224738, 565 | "node_id": "MDQ6VXNlcjMzMjI0NzM4", 566 | "avatar_url": "https://avatars.githubusercontent.com/u/33224738?v=4", 567 | "gravatar_id": "", 568 | "url": "https://api.github.com/users/AbdullahAki", 569 | "html_url": "https://github.com/AbdullahAki", 570 | "followers_url": "https://api.github.com/users/AbdullahAki/followers", 571 | "following_url": "https://api.github.com/users/AbdullahAki/following{/other_user}", 572 | "gists_url": "https://api.github.com/users/AbdullahAki/gists{/gist_id}", 573 | "starred_url": "https://api.github.com/users/AbdullahAki/starred{/owner}{/repo}", 574 | "subscriptions_url": "https://api.github.com/users/AbdullahAki/subscriptions", 575 | "organizations_url": "https://api.github.com/users/AbdullahAki/orgs", 576 | "repos_url": "https://api.github.com/users/AbdullahAki/repos", 577 | "events_url": "https://api.github.com/users/AbdullahAki/events{/privacy}", 578 | "received_events_url": "https://api.github.com/users/AbdullahAki/received_events", 579 | "type": "User", 580 | "site_admin": false 581 | }, 582 | { 583 | "login": "ecoderat", 584 | "id": 33634115, 585 | "node_id": "MDQ6VXNlcjMzNjM0MTE1", 586 | "avatar_url": "https://avatars.githubusercontent.com/u/33634115?v=4", 587 | "gravatar_id": "", 588 | "url": "https://api.github.com/users/ecoderat", 589 | "html_url": "https://github.com/ecoderat", 590 | "followers_url": "https://api.github.com/users/ecoderat/followers", 591 | "following_url": "https://api.github.com/users/ecoderat/following{/other_user}", 592 | "gists_url": "https://api.github.com/users/ecoderat/gists{/gist_id}", 593 | "starred_url": "https://api.github.com/users/ecoderat/starred{/owner}{/repo}", 594 | "subscriptions_url": "https://api.github.com/users/ecoderat/subscriptions", 595 | "organizations_url": "https://api.github.com/users/ecoderat/orgs", 596 | "repos_url": "https://api.github.com/users/ecoderat/repos", 597 | "events_url": "https://api.github.com/users/ecoderat/events{/privacy}", 598 | "received_events_url": "https://api.github.com/users/ecoderat/received_events", 599 | "type": "User", 600 | "site_admin": false 601 | } 602 | ] 603 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWikiTests/JSONResponses/user_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "ilter", 3 | "id": 37029827, 4 | "node_id": "MDQ6VXNlcjM3MDI5ODI3", 5 | "avatar_url": "https://avatars.githubusercontent.com/u/37029827?v=4", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/ilter", 8 | "html_url": "https://github.com/ilter", 9 | "followers_url": "https://api.github.com/users/ilter/followers", 10 | "following_url": "https://api.github.com/users/ilter/following{/other_user}", 11 | "gists_url": "https://api.github.com/users/ilter/gists{/gist_id}", 12 | "starred_url": "https://api.github.com/users/ilter/starred{/owner}{/repo}", 13 | "subscriptions_url": "https://api.github.com/users/ilter/subscriptions", 14 | "organizations_url": "https://api.github.com/users/ilter/orgs", 15 | "repos_url": "https://api.github.com/users/ilter/repos", 16 | "events_url": "https://api.github.com/users/ilter/events{/privacy}", 17 | "received_events_url": "https://api.github.com/users/ilter/received_events", 18 | "type": "User", 19 | "site_admin": false, 20 | "name": "ilter", 21 | "company": "@getir", 22 | "blog": "", 23 | "location": "Istanbul, Turkey", 24 | "email": null, 25 | "hireable": null, 26 | "bio": "iOS dev @getir", 27 | "twitter_username": null, 28 | "public_repos": 10, 29 | "public_gists": 0, 30 | "followers": 65, 31 | "following": 47, 32 | "created_at": "2018-03-03T23:42:07Z", 33 | "updated_at": "2022-02-27T23:58:14Z" 34 | } 35 | -------------------------------------------------------------------------------- /GithubProfileWiki/GithubProfileWikiTests/Mockable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mockable.swift 3 | // GithubProfileWikiTests 4 | // 5 | // Created by ilter on 2.03.2022. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Mockable: AnyObject { 11 | var bundle: Bundle { get } 12 | func loadJSON(filename: String, type: T.Type) -> T 13 | } 14 | 15 | extension Mockable { 16 | var bundle: Bundle { 17 | return Bundle(for: type(of: self)) 18 | } 19 | 20 | func loadJSON(filename: String, type: T.Type) -> T { 21 | guard let path = bundle.url(forResource: filename, withExtension: "json") else { 22 | fatalError("Failed to load JSON") 23 | } 24 | 25 | do { 26 | let data = try Data(contentsOf: path) 27 | let jsonDecoder = JSONDecoder() 28 | jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase 29 | let decodedObject = try jsonDecoder.decode(type, from: data) 30 | 31 | return decodedObject 32 | } catch { 33 | fatalError("Failed to decode loaded JSON") 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /GithubProfileWiki/fastlane/Appfile: -------------------------------------------------------------------------------- 1 | app_identifier("ilter.personal.GithubProfileWiki") # The bundle identifier of your app 2 | # apple_id("[[APPLE_ID]]") # Your Apple email address 3 | 4 | 5 | # For more information about the Appfile, see: 6 | # https://docs.fastlane.tools/advanced/#appfile 7 | -------------------------------------------------------------------------------- /GithubProfileWiki/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | # This file contains the fastlane.tools configuration 2 | # You can find the documentation at https://docs.fastlane.tools 3 | # 4 | # For a list of all available actions, check out 5 | # 6 | # https://docs.fastlane.tools/actions 7 | # 8 | # For a list of all available plugins, check out 9 | # 10 | # https://docs.fastlane.tools/plugins/available-plugins 11 | # 12 | 13 | # Uncomment the line if you want fastlane to automatically update itself 14 | # update_fastlane 15 | 16 | default_platform(:ios) 17 | 18 | before_all do 19 | xcversion(version: "~> 13.0") 20 | clear_derived_data 21 | end 22 | 23 | platform :ios do 24 | desc "Runs unit tests" 25 | lane :unittest do 26 | scan( 27 | scheme: "GithubProfileWikiTests", 28 | clean: true, 29 | reset_simulator: true, 30 | device: "iPhone 12" 31 | ) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /GithubProfileWiki/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ## iOS 17 | 18 | ### ios unittest 19 | 20 | ```sh 21 | [bundle exec] fastlane ios unittest 22 | ``` 23 | 24 | Runs unit tests 25 | 26 | ---- 27 | 28 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 29 | 30 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 31 | 32 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GithubProfileWiki iOS Application 2 | 3 | This project stands for an app which you can see the list followers of a GitHub user via searching their username on Github. 4 | - You can see open the profile detail of your follower not only to learn about their repo-gist count, following-follower count and when they joined the GitHub. But also, you can open their GitHub Page via one click on Safari. 5 | - You can add your favorite followers to your favorites list to reach their profile easily. When you decide someone is not your favorite you can remove them too. 6 | - You can search someone from your followers to find the one you are looking for. 7 | 8 |

9 | animated 10 |

11 | 12 | 13 | 14 | ## Table of contents 15 | 16 | * [Tech Stack](#techstack) 17 | * [Getting Started](#gettingstarted) 18 | * [Screen Shots](#screenshots) 19 | * [Roadmap](#roadmap) 20 | * [Contributing](#contributing) 21 | * [Acknowledgments](#acknowledgments) 22 | 23 | ## Tech Stack 24 | 25 | - Swift 5 26 | - MVVM Architectural Pattern 27 | - SPM 28 | - Programmatic UI without Storyboards 29 | - Fully Generic Network Layer with Protocol extensions 30 | - Continuous Integration flows for iOS Builds, SwiftLint with GitHub Actions 31 | - Generic User Defaults Management 32 | 33 | 34 | ## Getting Started 35 | 36 | **You should have XCode on your mac to run this project.** 37 | ``` 38 | It can be build with both XCode 12.5 and XCode 13 39 | ``` 40 | **1 - Clone the project** 41 | 42 | **2 - Open GithubProfileWiki.xcodeproj with XCode then wait for fetching SPM packages.** 43 | 44 | **3 - Build and run** 45 | 46 | ## Screenshots 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ## Roadmap 55 | 56 | - [X] _Improve coverage of Unit Tests._ 57 | - [X] _Improve Network Layer with Async-Await_ 58 | - [X] _Integrate Fastlane for CI with GitHub Actions_ 59 | - [ ] Create components library with SPM. 60 | 61 | See the [open issues](https://github.com/ilter/GithubProfileWiki/issues) for a full list of proposed features (and known issues). 62 | 63 |

(back to top)

64 | 65 | 66 | ## Contributing 67 | 68 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 69 | 70 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 71 | Don't forget to give the project a star! Thanks again! 72 | 73 | 1. Fork the Project 74 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 75 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 76 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 77 | 5. Open a Pull Request 78 | 79 |

(back to top)

80 | 81 | 82 | 83 | 84 | ## Acknowledgments 85 | 86 | This project is inspired by Sean Allen's course, used its icons and pics on UI. However, codebase and architecture is totally different and improved so if you are following it you should continue to follow the cource until you have finished. 87 | * [Sean Allen](https://seanallen.teachable.com/p/take-home) 88 | 89 |

(back to top)

90 | --------------------------------------------------------------------------------