├── GithubUsers.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── ahmedmenaim.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── GithubUsers ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── github.imageset │ │ ├── Contents.json │ │ └── github.png │ ├── github2.imageset │ │ ├── Contents.json │ │ └── github2.png │ └── user1.imageset │ │ ├── Contents.json │ │ └── thumb-minions-the-rise-of-gru.jpeg ├── Extensions │ └── String+Extensions.swift ├── GithubUsersApp.swift ├── Network │ ├── Followers │ │ ├── FollowersAPIClient.swift │ │ └── Model │ │ │ └── FollowersNetworkResponse.swift │ ├── NetworkService.swift │ ├── Repositories │ │ ├── Model │ │ │ └── RepositoriesNetworkResponse.swift │ │ └── RepositoriesAPIClient.swift │ ├── UserDetailsNetworkResponse.swift │ ├── UserNetworkResponse.swift │ └── UserRepositories │ │ ├── Model │ │ └── UserRepositoriesNetworkResponse.swift │ │ └── UserRepositoriesAPIClient.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── Views │ ├── CommonViews │ ├── CustomizedBackButton.swift │ ├── ImagePlaceholderView.swift │ ├── OptionView.swift │ └── SearchNoResultsView.swift │ ├── Followers │ ├── FollowerCard.swift │ └── FollowersView.swift │ ├── HomeView.swift │ ├── Repositories │ └── RepositoriesView.swift │ ├── UserDetails │ └── UserDetails.swift │ ├── UserRepositories │ ├── UserRepositoriesView.swift │ └── UserRepositoryCard.swift │ └── Users │ ├── UserCard.swift │ └── UsersView.swift └── README.md /GithubUsers.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A2920DA22A5049F0004FBC34 /* UserDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2920DA12A5049F0004FBC34 /* UserDetails.swift */; }; 11 | A2A41CE02A51B840005DB375 /* FollowersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CDF2A51B840005DB375 /* FollowersView.swift */; }; 12 | A2A41CE22A51BC82005DB375 /* UserDetailsNetworkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CE12A51BC82005DB375 /* UserDetailsNetworkResponse.swift */; }; 13 | A2A41CE52A52B083005DB375 /* ImagePlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CE42A52B083005DB375 /* ImagePlaceholderView.swift */; }; 14 | A2A41CE72A52B15B005DB375 /* OptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CE62A52B15B005DB375 /* OptionView.swift */; }; 15 | A2A41CE92A53150E005DB375 /* UsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CE82A53150E005DB375 /* UsersView.swift */; }; 16 | A2A41CEB2A533034005DB375 /* CustomizedBackButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CEA2A533034005DB375 /* CustomizedBackButton.swift */; }; 17 | A2A41CEE2A533680005DB375 /* FollowersAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CED2A533680005DB375 /* FollowersAPIClient.swift */; }; 18 | A2A41CF12A5336CD005DB375 /* FollowersNetworkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CF02A5336CD005DB375 /* FollowersNetworkResponse.swift */; }; 19 | A2A41CF32A533BCE005DB375 /* FollowerCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CF22A533BCE005DB375 /* FollowerCard.swift */; }; 20 | A2A41CF62A53F0C3005DB375 /* UserRepositoriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CF52A53F0C3005DB375 /* UserRepositoriesView.swift */; }; 21 | A2A41CF82A53F0D2005DB375 /* UserRepositoryCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CF72A53F0D1005DB375 /* UserRepositoryCard.swift */; }; 22 | A2A41CFB2A53F10D005DB375 /* UserRepositoriesAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CFA2A53F10D005DB375 /* UserRepositoriesAPIClient.swift */; }; 23 | A2A41CFE2A53F128005DB375 /* UserRepositoriesNetworkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41CFD2A53F128005DB375 /* UserRepositoriesNetworkResponse.swift */; }; 24 | A2A41D012A541B03005DB375 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41D002A541B03005DB375 /* String+Extensions.swift */; }; 25 | A2A41D042A543BAD005DB375 /* RepositoriesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41D032A543BAD005DB375 /* RepositoriesView.swift */; }; 26 | A2A41D062A543E46005DB375 /* SearchNoResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41D052A543E46005DB375 /* SearchNoResultsView.swift */; }; 27 | A2A41D082A547253005DB375 /* RepositoriesAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41D072A547253005DB375 /* RepositoriesAPIClient.swift */; }; 28 | A2A41D0A2A5472BE005DB375 /* RepositoriesNetworkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A41D092A5472BE005DB375 /* RepositoriesNetworkResponse.swift */; }; 29 | A2A54E232A48D8D500D1DD7C /* GithubUsersApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A54E222A48D8D500D1DD7C /* GithubUsersApp.swift */; }; 30 | A2A54E252A48D8D500D1DD7C /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A54E242A48D8D500D1DD7C /* HomeView.swift */; }; 31 | A2A54E272A48D8D600D1DD7C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A2A54E262A48D8D600D1DD7C /* Assets.xcassets */; }; 32 | A2A54E2A2A48D8D600D1DD7C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A2A54E292A48D8D600D1DD7C /* Preview Assets.xcassets */; }; 33 | A2A54E312A48D9A200D1DD7C /* UserCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A54E302A48D9A200D1DD7C /* UserCard.swift */; }; 34 | A2A54E362A48DBBB00D1DD7C /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A54E352A48DBBB00D1DD7C /* NetworkService.swift */; }; 35 | A2A54E382A48DC0700D1DD7C /* UserNetworkResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A54E372A48DC0700D1DD7C /* UserNetworkResponse.swift */; }; 36 | /* End PBXBuildFile section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | A2920DA12A5049F0004FBC34 /* UserDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetails.swift; sourceTree = ""; }; 40 | A2A41CDF2A51B840005DB375 /* FollowersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersView.swift; sourceTree = ""; }; 41 | A2A41CE12A51BC82005DB375 /* UserDetailsNetworkResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailsNetworkResponse.swift; sourceTree = ""; }; 42 | A2A41CE42A52B083005DB375 /* ImagePlaceholderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlaceholderView.swift; sourceTree = ""; }; 43 | A2A41CE62A52B15B005DB375 /* OptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionView.swift; sourceTree = ""; }; 44 | A2A41CE82A53150E005DB375 /* UsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersView.swift; sourceTree = ""; }; 45 | A2A41CEA2A533034005DB375 /* CustomizedBackButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizedBackButton.swift; sourceTree = ""; }; 46 | A2A41CED2A533680005DB375 /* FollowersAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersAPIClient.swift; sourceTree = ""; }; 47 | A2A41CF02A5336CD005DB375 /* FollowersNetworkResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersNetworkResponse.swift; sourceTree = ""; }; 48 | A2A41CF22A533BCE005DB375 /* FollowerCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerCard.swift; sourceTree = ""; }; 49 | A2A41CF52A53F0C3005DB375 /* UserRepositoriesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoriesView.swift; sourceTree = ""; }; 50 | A2A41CF72A53F0D1005DB375 /* UserRepositoryCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoryCard.swift; sourceTree = ""; }; 51 | A2A41CFA2A53F10D005DB375 /* UserRepositoriesAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoriesAPIClient.swift; sourceTree = ""; }; 52 | A2A41CFD2A53F128005DB375 /* UserRepositoriesNetworkResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRepositoriesNetworkResponse.swift; sourceTree = ""; }; 53 | A2A41D002A541B03005DB375 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 54 | A2A41D032A543BAD005DB375 /* RepositoriesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesView.swift; sourceTree = ""; }; 55 | A2A41D052A543E46005DB375 /* SearchNoResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchNoResultsView.swift; sourceTree = ""; }; 56 | A2A41D072A547253005DB375 /* RepositoriesAPIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesAPIClient.swift; sourceTree = ""; }; 57 | A2A41D092A5472BE005DB375 /* RepositoriesNetworkResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepositoriesNetworkResponse.swift; sourceTree = ""; }; 58 | A2A54E1F2A48D8D500D1DD7C /* GithubUsers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GithubUsers.app; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | A2A54E222A48D8D500D1DD7C /* GithubUsersApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubUsersApp.swift; sourceTree = ""; }; 60 | A2A54E242A48D8D500D1DD7C /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HomeView.swift; path = GithubUsers/Views/HomeView.swift; sourceTree = SOURCE_ROOT; }; 61 | A2A54E262A48D8D600D1DD7C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 62 | A2A54E292A48D8D600D1DD7C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 63 | A2A54E302A48D9A200D1DD7C /* UserCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCard.swift; sourceTree = ""; }; 64 | A2A54E352A48DBBB00D1DD7C /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = ""; }; 65 | A2A54E372A48DC0700D1DD7C /* UserNetworkResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNetworkResponse.swift; sourceTree = ""; }; 66 | /* End PBXFileReference section */ 67 | 68 | /* Begin PBXFrameworksBuildPhase section */ 69 | A2A54E1C2A48D8D500D1DD7C /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | A2920D9F2A5049AC004FBC34 /* Users */ = { 80 | isa = PBXGroup; 81 | children = ( 82 | A2A54E302A48D9A200D1DD7C /* UserCard.swift */, 83 | A2A54E242A48D8D500D1DD7C /* HomeView.swift */, 84 | A2A41CE82A53150E005DB375 /* UsersView.swift */, 85 | ); 86 | path = Users; 87 | sourceTree = ""; 88 | }; 89 | A2920DA02A5049C4004FBC34 /* UserDetails */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | A2920DA12A5049F0004FBC34 /* UserDetails.swift */, 93 | ); 94 | path = UserDetails; 95 | sourceTree = ""; 96 | }; 97 | A2A41CDE2A51B831005DB375 /* Followers */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | A2A41CDF2A51B840005DB375 /* FollowersView.swift */, 101 | A2A41CF22A533BCE005DB375 /* FollowerCard.swift */, 102 | ); 103 | path = Followers; 104 | sourceTree = ""; 105 | }; 106 | A2A41CE32A52B06F005DB375 /* CommonViews */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | A2A41CE42A52B083005DB375 /* ImagePlaceholderView.swift */, 110 | A2A41CE62A52B15B005DB375 /* OptionView.swift */, 111 | A2A41CEA2A533034005DB375 /* CustomizedBackButton.swift */, 112 | A2A41D052A543E46005DB375 /* SearchNoResultsView.swift */, 113 | ); 114 | path = CommonViews; 115 | sourceTree = ""; 116 | }; 117 | A2A41CEC2A533663005DB375 /* Followers */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | A2A41CED2A533680005DB375 /* FollowersAPIClient.swift */, 121 | A2A41CEF2A5336B4005DB375 /* Model */, 122 | ); 123 | path = Followers; 124 | sourceTree = ""; 125 | }; 126 | A2A41CEF2A5336B4005DB375 /* Model */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | A2A41CF02A5336CD005DB375 /* FollowersNetworkResponse.swift */, 130 | ); 131 | path = Model; 132 | sourceTree = ""; 133 | }; 134 | A2A41CF42A53F0AD005DB375 /* UserRepositories */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | A2A41CF52A53F0C3005DB375 /* UserRepositoriesView.swift */, 138 | A2A41CF72A53F0D1005DB375 /* UserRepositoryCard.swift */, 139 | ); 140 | path = UserRepositories; 141 | sourceTree = ""; 142 | }; 143 | A2A41CF92A53F100005DB375 /* Repositories */ = { 144 | isa = PBXGroup; 145 | children = ( 146 | A2A41D072A547253005DB375 /* RepositoriesAPIClient.swift */, 147 | A2A41D0C2A54827A005DB375 /* Model */, 148 | ); 149 | path = Repositories; 150 | sourceTree = ""; 151 | }; 152 | A2A41CFC2A53F111005DB375 /* Model */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | A2A41CFD2A53F128005DB375 /* UserRepositoriesNetworkResponse.swift */, 156 | ); 157 | path = Model; 158 | sourceTree = ""; 159 | }; 160 | A2A41CFF2A541AEE005DB375 /* Extensions */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | A2A41D002A541B03005DB375 /* String+Extensions.swift */, 164 | ); 165 | path = Extensions; 166 | sourceTree = ""; 167 | }; 168 | A2A41D022A543B97005DB375 /* Repositories */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | A2A41D032A543BAD005DB375 /* RepositoriesView.swift */, 172 | ); 173 | path = Repositories; 174 | sourceTree = ""; 175 | }; 176 | A2A41D0B2A548260005DB375 /* UserRepositories */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | A2A41CFA2A53F10D005DB375 /* UserRepositoriesAPIClient.swift */, 180 | A2A41CFC2A53F111005DB375 /* Model */, 181 | ); 182 | path = UserRepositories; 183 | sourceTree = ""; 184 | }; 185 | A2A41D0C2A54827A005DB375 /* Model */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | A2A41D092A5472BE005DB375 /* RepositoriesNetworkResponse.swift */, 189 | ); 190 | path = Model; 191 | sourceTree = ""; 192 | }; 193 | A2A54E162A48D8D400D1DD7C = { 194 | isa = PBXGroup; 195 | children = ( 196 | A2A54E212A48D8D500D1DD7C /* GithubUsers */, 197 | A2A54E202A48D8D500D1DD7C /* Products */, 198 | ); 199 | sourceTree = ""; 200 | }; 201 | A2A54E202A48D8D500D1DD7C /* Products */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | A2A54E1F2A48D8D500D1DD7C /* GithubUsers.app */, 205 | ); 206 | name = Products; 207 | sourceTree = ""; 208 | }; 209 | A2A54E212A48D8D500D1DD7C /* GithubUsers */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | A2A41CFF2A541AEE005DB375 /* Extensions */, 213 | A2A54E332A48DB9E00D1DD7C /* Network */, 214 | A2A54E322A48DB9000D1DD7C /* Views */, 215 | A2A54E222A48D8D500D1DD7C /* GithubUsersApp.swift */, 216 | A2A54E262A48D8D600D1DD7C /* Assets.xcassets */, 217 | A2A54E282A48D8D600D1DD7C /* Preview Content */, 218 | ); 219 | path = GithubUsers; 220 | sourceTree = ""; 221 | }; 222 | A2A54E282A48D8D600D1DD7C /* Preview Content */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | A2A54E292A48D8D600D1DD7C /* Preview Assets.xcassets */, 226 | ); 227 | path = "Preview Content"; 228 | sourceTree = ""; 229 | }; 230 | A2A54E322A48DB9000D1DD7C /* Views */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | A2A41D022A543B97005DB375 /* Repositories */, 234 | A2A41CF42A53F0AD005DB375 /* UserRepositories */, 235 | A2A41CE32A52B06F005DB375 /* CommonViews */, 236 | A2A41CDE2A51B831005DB375 /* Followers */, 237 | A2920D9F2A5049AC004FBC34 /* Users */, 238 | A2920DA02A5049C4004FBC34 /* UserDetails */, 239 | ); 240 | path = Views; 241 | sourceTree = ""; 242 | }; 243 | A2A54E332A48DB9E00D1DD7C /* Network */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | A2A41D0B2A548260005DB375 /* UserRepositories */, 247 | A2A41CF92A53F100005DB375 /* Repositories */, 248 | A2A41CEC2A533663005DB375 /* Followers */, 249 | A2A54E352A48DBBB00D1DD7C /* NetworkService.swift */, 250 | A2A54E372A48DC0700D1DD7C /* UserNetworkResponse.swift */, 251 | A2A41CE12A51BC82005DB375 /* UserDetailsNetworkResponse.swift */, 252 | ); 253 | path = Network; 254 | sourceTree = ""; 255 | }; 256 | /* End PBXGroup section */ 257 | 258 | /* Begin PBXNativeTarget section */ 259 | A2A54E1E2A48D8D500D1DD7C /* GithubUsers */ = { 260 | isa = PBXNativeTarget; 261 | buildConfigurationList = A2A54E2D2A48D8D600D1DD7C /* Build configuration list for PBXNativeTarget "GithubUsers" */; 262 | buildPhases = ( 263 | A2A54E1B2A48D8D500D1DD7C /* Sources */, 264 | A2A54E1C2A48D8D500D1DD7C /* Frameworks */, 265 | A2A54E1D2A48D8D500D1DD7C /* Resources */, 266 | ); 267 | buildRules = ( 268 | ); 269 | dependencies = ( 270 | ); 271 | name = GithubUsers; 272 | productName = GithubUsers; 273 | productReference = A2A54E1F2A48D8D500D1DD7C /* GithubUsers.app */; 274 | productType = "com.apple.product-type.application"; 275 | }; 276 | /* End PBXNativeTarget section */ 277 | 278 | /* Begin PBXProject section */ 279 | A2A54E172A48D8D400D1DD7C /* Project object */ = { 280 | isa = PBXProject; 281 | attributes = { 282 | BuildIndependentTargetsInParallel = 1; 283 | LastSwiftUpdateCheck = 1420; 284 | LastUpgradeCheck = 1420; 285 | TargetAttributes = { 286 | A2A54E1E2A48D8D500D1DD7C = { 287 | CreatedOnToolsVersion = 14.2; 288 | }; 289 | }; 290 | }; 291 | buildConfigurationList = A2A54E1A2A48D8D400D1DD7C /* Build configuration list for PBXProject "GithubUsers" */; 292 | compatibilityVersion = "Xcode 14.0"; 293 | developmentRegion = en; 294 | hasScannedForEncodings = 0; 295 | knownRegions = ( 296 | en, 297 | Base, 298 | ); 299 | mainGroup = A2A54E162A48D8D400D1DD7C; 300 | productRefGroup = A2A54E202A48D8D500D1DD7C /* Products */; 301 | projectDirPath = ""; 302 | projectRoot = ""; 303 | targets = ( 304 | A2A54E1E2A48D8D500D1DD7C /* GithubUsers */, 305 | ); 306 | }; 307 | /* End PBXProject section */ 308 | 309 | /* Begin PBXResourcesBuildPhase section */ 310 | A2A54E1D2A48D8D500D1DD7C /* Resources */ = { 311 | isa = PBXResourcesBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | A2A54E2A2A48D8D600D1DD7C /* Preview Assets.xcassets in Resources */, 315 | A2A54E272A48D8D600D1DD7C /* Assets.xcassets in Resources */, 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | /* End PBXResourcesBuildPhase section */ 320 | 321 | /* Begin PBXSourcesBuildPhase section */ 322 | A2A54E1B2A48D8D500D1DD7C /* Sources */ = { 323 | isa = PBXSourcesBuildPhase; 324 | buildActionMask = 2147483647; 325 | files = ( 326 | A2A41D042A543BAD005DB375 /* RepositoriesView.swift in Sources */, 327 | A2A41D082A547253005DB375 /* RepositoriesAPIClient.swift in Sources */, 328 | A2A41D012A541B03005DB375 /* String+Extensions.swift in Sources */, 329 | A2A54E252A48D8D500D1DD7C /* HomeView.swift in Sources */, 330 | A2A41CE02A51B840005DB375 /* FollowersView.swift in Sources */, 331 | A2A41CE22A51BC82005DB375 /* UserDetailsNetworkResponse.swift in Sources */, 332 | A2A41CE72A52B15B005DB375 /* OptionView.swift in Sources */, 333 | A2A41CF62A53F0C3005DB375 /* UserRepositoriesView.swift in Sources */, 334 | A2A54E312A48D9A200D1DD7C /* UserCard.swift in Sources */, 335 | A2A41CE52A52B083005DB375 /* ImagePlaceholderView.swift in Sources */, 336 | A2A54E362A48DBBB00D1DD7C /* NetworkService.swift in Sources */, 337 | A2A41CF12A5336CD005DB375 /* FollowersNetworkResponse.swift in Sources */, 338 | A2A54E232A48D8D500D1DD7C /* GithubUsersApp.swift in Sources */, 339 | A2A41CF32A533BCE005DB375 /* FollowerCard.swift in Sources */, 340 | A2A41CFB2A53F10D005DB375 /* UserRepositoriesAPIClient.swift in Sources */, 341 | A2A41CEE2A533680005DB375 /* FollowersAPIClient.swift in Sources */, 342 | A2A41CFE2A53F128005DB375 /* UserRepositoriesNetworkResponse.swift in Sources */, 343 | A2A41D062A543E46005DB375 /* SearchNoResultsView.swift in Sources */, 344 | A2A54E382A48DC0700D1DD7C /* UserNetworkResponse.swift in Sources */, 345 | A2A41CEB2A533034005DB375 /* CustomizedBackButton.swift in Sources */, 346 | A2A41CE92A53150E005DB375 /* UsersView.swift in Sources */, 347 | A2A41CF82A53F0D2005DB375 /* UserRepositoryCard.swift in Sources */, 348 | A2920DA22A5049F0004FBC34 /* UserDetails.swift in Sources */, 349 | A2A41D0A2A5472BE005DB375 /* RepositoriesNetworkResponse.swift in Sources */, 350 | ); 351 | runOnlyForDeploymentPostprocessing = 0; 352 | }; 353 | /* End PBXSourcesBuildPhase section */ 354 | 355 | /* Begin XCBuildConfiguration section */ 356 | A2A54E2B2A48D8D600D1DD7C /* Debug */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | ALWAYS_SEARCH_USER_PATHS = NO; 360 | CLANG_ANALYZER_NONNULL = YES; 361 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 362 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 363 | CLANG_ENABLE_MODULES = YES; 364 | CLANG_ENABLE_OBJC_ARC = YES; 365 | CLANG_ENABLE_OBJC_WEAK = YES; 366 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 367 | CLANG_WARN_BOOL_CONVERSION = YES; 368 | CLANG_WARN_COMMA = YES; 369 | CLANG_WARN_CONSTANT_CONVERSION = YES; 370 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 371 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 372 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 373 | CLANG_WARN_EMPTY_BODY = YES; 374 | CLANG_WARN_ENUM_CONVERSION = YES; 375 | CLANG_WARN_INFINITE_RECURSION = YES; 376 | CLANG_WARN_INT_CONVERSION = YES; 377 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 378 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 379 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 380 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 381 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 382 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 383 | CLANG_WARN_STRICT_PROTOTYPES = YES; 384 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 385 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 386 | CLANG_WARN_UNREACHABLE_CODE = YES; 387 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 388 | COPY_PHASE_STRIP = NO; 389 | DEBUG_INFORMATION_FORMAT = dwarf; 390 | ENABLE_STRICT_OBJC_MSGSEND = YES; 391 | ENABLE_TESTABILITY = YES; 392 | GCC_C_LANGUAGE_STANDARD = gnu11; 393 | GCC_DYNAMIC_NO_PIC = NO; 394 | GCC_NO_COMMON_BLOCKS = YES; 395 | GCC_OPTIMIZATION_LEVEL = 0; 396 | GCC_PREPROCESSOR_DEFINITIONS = ( 397 | "DEBUG=1", 398 | "$(inherited)", 399 | ); 400 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 401 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 402 | GCC_WARN_UNDECLARED_SELECTOR = YES; 403 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 404 | GCC_WARN_UNUSED_FUNCTION = YES; 405 | GCC_WARN_UNUSED_VARIABLE = YES; 406 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 407 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 408 | MTL_FAST_MATH = YES; 409 | ONLY_ACTIVE_ARCH = YES; 410 | SDKROOT = iphoneos; 411 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 412 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 413 | }; 414 | name = Debug; 415 | }; 416 | A2A54E2C2A48D8D600D1DD7C /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | ALWAYS_SEARCH_USER_PATHS = NO; 420 | CLANG_ANALYZER_NONNULL = YES; 421 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 422 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 423 | CLANG_ENABLE_MODULES = YES; 424 | CLANG_ENABLE_OBJC_ARC = YES; 425 | CLANG_ENABLE_OBJC_WEAK = YES; 426 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 427 | CLANG_WARN_BOOL_CONVERSION = YES; 428 | CLANG_WARN_COMMA = YES; 429 | CLANG_WARN_CONSTANT_CONVERSION = YES; 430 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 431 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 432 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 433 | CLANG_WARN_EMPTY_BODY = YES; 434 | CLANG_WARN_ENUM_CONVERSION = YES; 435 | CLANG_WARN_INFINITE_RECURSION = YES; 436 | CLANG_WARN_INT_CONVERSION = YES; 437 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 438 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 439 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 440 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 441 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 442 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 443 | CLANG_WARN_STRICT_PROTOTYPES = YES; 444 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 445 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 446 | CLANG_WARN_UNREACHABLE_CODE = YES; 447 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 448 | COPY_PHASE_STRIP = NO; 449 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 450 | ENABLE_NS_ASSERTIONS = NO; 451 | ENABLE_STRICT_OBJC_MSGSEND = YES; 452 | GCC_C_LANGUAGE_STANDARD = gnu11; 453 | GCC_NO_COMMON_BLOCKS = YES; 454 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 455 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 456 | GCC_WARN_UNDECLARED_SELECTOR = YES; 457 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 458 | GCC_WARN_UNUSED_FUNCTION = YES; 459 | GCC_WARN_UNUSED_VARIABLE = YES; 460 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 461 | MTL_ENABLE_DEBUG_INFO = NO; 462 | MTL_FAST_MATH = YES; 463 | SDKROOT = iphoneos; 464 | SWIFT_COMPILATION_MODE = wholemodule; 465 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 466 | VALIDATE_PRODUCT = YES; 467 | }; 468 | name = Release; 469 | }; 470 | A2A54E2E2A48D8D600D1DD7C /* Debug */ = { 471 | isa = XCBuildConfiguration; 472 | buildSettings = { 473 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 474 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 475 | CODE_SIGN_STYLE = Automatic; 476 | CURRENT_PROJECT_VERSION = 1; 477 | DEVELOPMENT_ASSET_PATHS = "\"GithubUsers/Preview Content\""; 478 | DEVELOPMENT_TEAM = 6GY6SSRWWX; 479 | ENABLE_PREVIEWS = YES; 480 | GENERATE_INFOPLIST_FILE = YES; 481 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 482 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 483 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 484 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 485 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 486 | INFOPLIST_KEY_UIUserInterfaceStyle = Dark; 487 | LD_RUNPATH_SEARCH_PATHS = ( 488 | "$(inherited)", 489 | "@executable_path/Frameworks", 490 | ); 491 | MARKETING_VERSION = 1.0; 492 | PRODUCT_BUNDLE_IDENTIFIER = com.Menaim.GithubUsers; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | SWIFT_EMIT_LOC_STRINGS = YES; 495 | SWIFT_VERSION = 5.0; 496 | TARGETED_DEVICE_FAMILY = "1,2"; 497 | }; 498 | name = Debug; 499 | }; 500 | A2A54E2F2A48D8D600D1DD7C /* Release */ = { 501 | isa = XCBuildConfiguration; 502 | buildSettings = { 503 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 504 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 505 | CODE_SIGN_STYLE = Automatic; 506 | CURRENT_PROJECT_VERSION = 1; 507 | DEVELOPMENT_ASSET_PATHS = "\"GithubUsers/Preview Content\""; 508 | DEVELOPMENT_TEAM = 6GY6SSRWWX; 509 | ENABLE_PREVIEWS = YES; 510 | GENERATE_INFOPLIST_FILE = YES; 511 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 512 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 513 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 514 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 515 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 516 | INFOPLIST_KEY_UIUserInterfaceStyle = Dark; 517 | LD_RUNPATH_SEARCH_PATHS = ( 518 | "$(inherited)", 519 | "@executable_path/Frameworks", 520 | ); 521 | MARKETING_VERSION = 1.0; 522 | PRODUCT_BUNDLE_IDENTIFIER = com.Menaim.GithubUsers; 523 | PRODUCT_NAME = "$(TARGET_NAME)"; 524 | SWIFT_EMIT_LOC_STRINGS = YES; 525 | SWIFT_VERSION = 5.0; 526 | TARGETED_DEVICE_FAMILY = "1,2"; 527 | }; 528 | name = Release; 529 | }; 530 | /* End XCBuildConfiguration section */ 531 | 532 | /* Begin XCConfigurationList section */ 533 | A2A54E1A2A48D8D400D1DD7C /* Build configuration list for PBXProject "GithubUsers" */ = { 534 | isa = XCConfigurationList; 535 | buildConfigurations = ( 536 | A2A54E2B2A48D8D600D1DD7C /* Debug */, 537 | A2A54E2C2A48D8D600D1DD7C /* Release */, 538 | ); 539 | defaultConfigurationIsVisible = 0; 540 | defaultConfigurationName = Release; 541 | }; 542 | A2A54E2D2A48D8D600D1DD7C /* Build configuration list for PBXNativeTarget "GithubUsers" */ = { 543 | isa = XCConfigurationList; 544 | buildConfigurations = ( 545 | A2A54E2E2A48D8D600D1DD7C /* Debug */, 546 | A2A54E2F2A48D8D600D1DD7C /* Release */, 547 | ); 548 | defaultConfigurationIsVisible = 0; 549 | defaultConfigurationName = Release; 550 | }; 551 | /* End XCConfigurationList section */ 552 | }; 553 | rootObject = A2A54E172A48D8D400D1DD7C /* Project object */; 554 | } 555 | -------------------------------------------------------------------------------- /GithubUsers.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /GithubUsers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /GithubUsers.xcodeproj/xcuserdata/ahmedmenaim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /GithubUsers.xcodeproj/xcuserdata/ahmedmenaim.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | GithubUsers.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "watchos", 6 | "reference" : "labelColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/github.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "github.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/github.imageset/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedMenaim/GitHubFullApp/db2f4690fc5dd18be815283dc8a2489679534ca1/GithubUsers/Assets.xcassets/github.imageset/github.png -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/github2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "github2.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/github2.imageset/github2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedMenaim/GitHubFullApp/db2f4690fc5dd18be815283dc8a2489679534ca1/GithubUsers/Assets.xcassets/github2.imageset/github2.png -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/user1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "thumb-minions-the-rise-of-gru.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /GithubUsers/Assets.xcassets/user1.imageset/thumb-minions-the-rise-of-gru.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedMenaim/GitHubFullApp/db2f4690fc5dd18be815283dc8a2489679534ca1/GithubUsers/Assets.xcassets/user1.imageset/thumb-minions-the-rise-of-gru.jpeg -------------------------------------------------------------------------------- /GithubUsers/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func formattedDateString() -> String { 12 | 13 | /// This doesn't work cause you need to convert to a readable date format first before formatting it to the needed format ❌ 14 | // let dateFormatter = DateFormatter() 15 | // dateFormatter.locale = Locale(identifier: "en_US_POSIX") 16 | // dateFormatter.dateFormat = "MMM d, yyyy" 17 | // guard let date = dateFormatter.date(from: self) else { return ""} 18 | // return dateFormatter.string(from: date) 19 | 20 | let dateFormatter = DateFormatter() 21 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z" 22 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 23 | 24 | guard let date = dateFormatter.date(from: self) else { return "" } 25 | let newDateFormatter = DateFormatter() 26 | newDateFormatter.dateFormat = "MMM d, yyyy" 27 | let newStr = newDateFormatter.string(from: date) 28 | return newStr 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /GithubUsers/GithubUsersApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GithubUsersApp.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 25/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct GithubUsersApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | HomeView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /GithubUsers/Network/Followers/FollowersAPIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersAPIClient.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 03/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FollowersAPIClientProtocol { 11 | func getUserFollowers( 12 | username: String, 13 | completion: @escaping (Result<[FollowersNetworkResponse], 14 | SessionDataTaskError>) -> Void 15 | ) 16 | } 17 | 18 | class FollowersAPIClient: FollowersAPIClientProtocol { 19 | static let shared = FollowersAPIClient() 20 | 21 | func getUserFollowers( 22 | username: String, 23 | completion: @escaping (Result<[FollowersNetworkResponse], SessionDataTaskError>) -> Void 24 | ) { 25 | guard let url = URL(string: "https://api.github.com/users/\(username)/followers") else { 26 | completion(.failure(.notFound)) 27 | return 28 | } 29 | let session = URLSession.shared 30 | let request = URLRequest(url: url) 31 | session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in 32 | 33 | if let error = error, 34 | let response = response as? HTTPURLResponse { 35 | let statusCode = response.statusCode 36 | switch statusCode { 37 | /// 1020 means dataNotAllowed -> Internet is closed 38 | /// 1009 Internet is opened but no connection happens 39 | case 1009, 1020: 40 | completion(.failure(.noInternetConnection)) 41 | return 42 | case 404: 43 | completion(.failure(.notFound)) 44 | return 45 | case 400: 46 | completion(.failure(.notAuthorized)) 47 | return 48 | case 500 ... 599: 49 | completion(.failure(.server)) 50 | return 51 | default: 52 | completion(.failure(SessionDataTaskError.failWithError(error))) 53 | return 54 | } 55 | } 56 | guard let data = data 57 | else { 58 | completion(.failure(SessionDataTaskError.noData)) 59 | return 60 | } 61 | do { 62 | let decoder = JSONDecoder() 63 | let response = try decoder.decode([FollowersNetworkResponse].self, from: data) 64 | debugPrint(response) 65 | completion(.success(response)) 66 | 67 | } catch { 68 | completion(.failure(SessionDataTaskError.failWithError(error))) 69 | } 70 | }).resume() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /GithubUsers/Network/Followers/Model/FollowersNetworkResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersNetworkResponse.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 03/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FollowersNetworkResponse: Codable { 11 | var userName: String? 12 | var avatarURL: String? 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case userName = "login" 16 | case avatarURL = "avatar_url" 17 | } 18 | } 19 | 20 | struct Follower: Identifiable { 21 | var id = UUID() 22 | var userName: String 23 | var avatarURL: String 24 | } 25 | -------------------------------------------------------------------------------- /GithubUsers/Network/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 25/06/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | class APIService { 11 | static let shared = APIService() 12 | 13 | public func getUsers( 14 | completion: @escaping (Result<[UserNetworkResponse], SessionDataTaskError>) -> Void 15 | ) { 16 | guard let url = URL(string: "https://api.github.com/users") else { 17 | completion(.failure(.notFound)) 18 | return 19 | } 20 | let session = URLSession.shared 21 | let request = URLRequest(url: url) 22 | session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in 23 | 24 | if let error = error, 25 | let response = response as? HTTPURLResponse { 26 | let statusCode = response.statusCode 27 | switch statusCode { 28 | /// 1020 means dataNotAllowed -> Internet is closed 29 | /// 1009 Internet is opened but no connection happens 30 | case 1009, 1020: 31 | completion(.failure(.noInternetConnection)) 32 | return 33 | case 404: 34 | completion(.failure(.notFound)) 35 | return 36 | case 400: 37 | completion(.failure(.notAuthorized)) 38 | return 39 | case 500 ... 599: 40 | completion(.failure(.server)) 41 | return 42 | default: 43 | completion(.failure(SessionDataTaskError.failWithError(error))) 44 | return 45 | } 46 | } 47 | guard let data = data 48 | else { 49 | completion(.failure(SessionDataTaskError.noData)) 50 | return 51 | } 52 | do { 53 | let decoder = JSONDecoder() 54 | let response = try decoder.decode([UserNetworkResponse].self, from: data) 55 | debugPrint(response) 56 | completion(.success(response)) 57 | 58 | } catch { 59 | completion(.failure(SessionDataTaskError.failWithError(error))) 60 | } 61 | }).resume() 62 | } 63 | 64 | func getUserDetails( 65 | username: String, 66 | completion: @escaping (Result) -> Void 67 | ) { 68 | guard let url = URL(string: "https://api.github.com/users/\(username)") else { 69 | completion(.failure(.notFound)) 70 | return 71 | } 72 | let session = URLSession.shared 73 | let request = URLRequest(url: url) 74 | session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in 75 | 76 | if let error = error, 77 | let response = response as? HTTPURLResponse { 78 | let statusCode = response.statusCode 79 | switch statusCode { 80 | /// 1020 means dataNotAllowed -> Internet is closed 81 | /// 1009 Internet is opened but no connection happens 82 | case 1009, 1020: 83 | completion(.failure(.noInternetConnection)) 84 | return 85 | case 404: 86 | completion(.failure(.notFound)) 87 | return 88 | case 400: 89 | completion(.failure(.notAuthorized)) 90 | return 91 | case 500 ... 599: 92 | completion(.failure(.server)) 93 | return 94 | default: 95 | completion(.failure(SessionDataTaskError.failWithError(error))) 96 | return 97 | } 98 | } 99 | guard let data = data 100 | else { 101 | completion(.failure(SessionDataTaskError.noData)) 102 | return 103 | } 104 | do { 105 | let decoder = JSONDecoder() 106 | let response = try decoder.decode(UserDetailsNetworkResponse.self, from: data) 107 | debugPrint(response) 108 | completion(.success(response)) 109 | 110 | } catch { 111 | completion(.failure(SessionDataTaskError.failWithError(error))) 112 | } 113 | }).resume() 114 | } 115 | } 116 | 117 | enum SessionDataTaskError: Error { 118 | case failWithError(Error) 119 | case noData 120 | case notFound 121 | case notAuthorized 122 | case server 123 | case noInternetConnection 124 | } 125 | -------------------------------------------------------------------------------- /GithubUsers/Network/Repositories/Model/RepositoriesNetworkResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoriesNetworkResponse.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RepositoriesNetworkResponse: Codable { 11 | var totalRepositoriesCount: Int? 12 | var repositories: [RepositoryNetworkResponse]? 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case totalRepositoriesCount = "total_count" 16 | case repositories = "items" 17 | } 18 | } 19 | 20 | struct RepositoryNetworkResponse: Codable { 21 | var repositoryName: String? 22 | var isPrivate: Bool? 23 | var repositoryURL: String? 24 | var description: String? 25 | var repositorySize: Double? 26 | var repositoryForksCount: Int? 27 | var repositoryStarsCount: Int? 28 | var repositoryOpenIssuesCount: Int? 29 | var repositoryWatchersCount: Int? 30 | var repositoryDefaultBranch: String? 31 | var cloneURL: String? 32 | var programmingLanguage: String? 33 | var updatedAt: String? 34 | var license: RepositoryLicense? 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case repositoryName = "name" 38 | case isPrivate = "private" 39 | case repositoryURL = "html_url" 40 | case description 41 | case repositorySize = "size" 42 | case repositoryForksCount = "forks" 43 | case repositoryStarsCount = "stargazers_count" 44 | case repositoryOpenIssuesCount = "open_issues" 45 | case repositoryWatchersCount = "watchers" 46 | case repositoryDefaultBranch = "default_branch" 47 | case cloneURL = "clone_url" 48 | case programmingLanguage = "language" 49 | case updatedAt = "updated_at" 50 | case license 51 | } 52 | } 53 | 54 | struct RepositoryLicense: Codable { 55 | var licenseName: String? 56 | 57 | enum CodingKeys: String, CodingKey { 58 | case licenseName = "name" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /GithubUsers/Network/Repositories/RepositoriesAPIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoriesAPIClient.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol RepositoriesAPIClientProtocol { 11 | func getRepositories( 12 | searchText: String, 13 | completion: @escaping (Result<[RepositoryNetworkResponse], 14 | SessionDataTaskError>) -> Void 15 | ) 16 | } 17 | 18 | class RepositoriesAPIClient: RepositoriesAPIClientProtocol { 19 | static let shared = RepositoriesAPIClient() 20 | 21 | func getRepositories( 22 | searchText: String, 23 | completion: @escaping (Result<[RepositoryNetworkResponse], SessionDataTaskError>) -> Void 24 | ) { 25 | guard let url = URL(string: "https://api.github.com/search/repositories?q=\(searchText)") else { 26 | completion(.failure(.notFound)) 27 | return 28 | } 29 | let session = URLSession.shared 30 | let request = URLRequest(url: url) 31 | session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in 32 | 33 | if let error = error, 34 | let response = response as? HTTPURLResponse { 35 | let statusCode = response.statusCode 36 | switch statusCode { 37 | /// 1020 means dataNotAllowed -> Internet is closed 38 | /// 1009 Internet is opened but no connection happens 39 | case 1009, 1020: 40 | completion(.failure(.noInternetConnection)) 41 | return 42 | case 404: 43 | completion(.failure(.notFound)) 44 | return 45 | case 400: 46 | completion(.failure(.notAuthorized)) 47 | return 48 | case 500 ... 599: 49 | completion(.failure(.server)) 50 | return 51 | default: 52 | completion(.failure(SessionDataTaskError.failWithError(error))) 53 | return 54 | } 55 | } 56 | guard let data = data 57 | else { 58 | completion(.failure(SessionDataTaskError.noData)) 59 | return 60 | } 61 | do { 62 | let decoder = JSONDecoder() 63 | let response = try decoder.decode(RepositoriesNetworkResponse.self, from: data) 64 | debugPrint(response) 65 | completion(.success(response.repositories ?? [])) 66 | 67 | } catch { 68 | completion(.failure(SessionDataTaskError.failWithError(error))) 69 | } 70 | }).resume() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /GithubUsers/Network/UserDetailsNetworkResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailsNetworkResponse.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 02/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserDetailsNetworkResponse: Codable { 11 | var userName: String? 12 | var fullName: String? 13 | var company: String? 14 | var avatarURL: String? 15 | var userProfileURL: String? 16 | var location: String? 17 | var bio: String? 18 | var twitterUsername: String? 19 | var numberOfPublicRepos: Int? 20 | var numberOfPublicGists: Int? 21 | var numberOfFollowers: Int? 22 | var numberOfFollowing: Int? 23 | 24 | enum CodingKeys: String, CodingKey { 25 | case userName = "login" 26 | case fullName = "name" 27 | case company 28 | case avatarURL = "avatar_url" 29 | case userProfileURL = "html_url" 30 | case location 31 | case bio 32 | case twitterUsername = "twitter_username" 33 | case numberOfPublicRepos = "public_repos" 34 | case numberOfPublicGists = "public_gists" 35 | case numberOfFollowers = "followers" 36 | case numberOfFollowing = "following" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /GithubUsers/Network/UserNetworkResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserNetworkResponse.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 25/06/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserNetworkResponse: Codable { 11 | var userName: String? 12 | var userID: Int? 13 | var avatarURL: String? 14 | var userProfileURL: String? 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case userName = "login" 18 | case userID = "id" 19 | case avatarURL = "avatar_url" 20 | case userProfileURL = "url" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /GithubUsers/Network/UserRepositories/Model/UserRepositoriesNetworkResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepositoriesNetworkResponse.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserRepositoriesNetworkResponse: Codable { 11 | var repositoryName: String? 12 | var isPrivate: Bool? 13 | var repositoryURL: String? 14 | var description: String? 15 | var repositorySize: Double? 16 | var repositoryForksCount: Int? 17 | var repositoryStarsCount: Int? 18 | var repositoryOpenIssuesCount: Int? 19 | var repositoryWatchersCount: Int? 20 | var repositoryDefaultBranch: String? 21 | var cloneURL: String? 22 | var programmingLanguage: String? 23 | var updatedAt: String? 24 | var license: License? 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case repositoryName = "name" 28 | case isPrivate = "private" 29 | case repositoryURL = "html_url" 30 | case description 31 | case repositorySize = "size" 32 | case repositoryForksCount = "forks" 33 | case repositoryStarsCount = "stargazers_count" 34 | case repositoryOpenIssuesCount = "open_issues" 35 | case repositoryWatchersCount = "watchers" 36 | case repositoryDefaultBranch = "default_branch" 37 | case cloneURL = "clone_url" 38 | case programmingLanguage = "language" 39 | case updatedAt = "updated_at" 40 | case license 41 | } 42 | } 43 | 44 | struct License: Codable { 45 | var licenseName: String? 46 | 47 | enum CodingKeys: String, CodingKey { 48 | case licenseName = "name" 49 | } 50 | } 51 | 52 | struct Repository: Identifiable { 53 | var id = UUID() 54 | var repositoryName: String 55 | var isPrivate: Bool 56 | var repositoryURL: String 57 | var description: String 58 | var repositorySize: Double 59 | var repositoryForksCount: Int 60 | var repositoryStarsCount: Int 61 | var repositoryOpenIssuesCount: Int 62 | var repositoryWatchersCount: Int 63 | var repositoryDefaultBranch: String 64 | var cloneURL: String 65 | var programmingLanguage: String 66 | var updatedAt: String 67 | var licenseName: String 68 | } 69 | -------------------------------------------------------------------------------- /GithubUsers/Network/UserRepositories/UserRepositoriesAPIClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoriesAPIClient.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol UserRepositoriesAPIClientProtocol { 11 | func getUserRepositories( 12 | username: String, 13 | completion: @escaping (Result<[UserRepositoriesNetworkResponse], 14 | SessionDataTaskError>) -> Void 15 | ) 16 | } 17 | 18 | class UserRepositoriesAPIClient: UserRepositoriesAPIClientProtocol { 19 | static let shared = UserRepositoriesAPIClient() 20 | 21 | func getUserRepositories( 22 | username: String, 23 | completion: @escaping (Result<[UserRepositoriesNetworkResponse], SessionDataTaskError>) -> Void 24 | ) { 25 | guard let url = URL(string: "https://api.github.com/users/\(username)/repos") else { 26 | completion(.failure(.notFound)) 27 | return 28 | } 29 | let session = URLSession.shared 30 | let request = URLRequest(url: url) 31 | session.dataTask(with: request as URLRequest, completionHandler: { data, response, error in 32 | 33 | if let error = error, 34 | let response = response as? HTTPURLResponse { 35 | let statusCode = response.statusCode 36 | switch statusCode { 37 | /// 1020 means dataNotAllowed -> Internet is closed 38 | /// 1009 Internet is opened but no connection happens 39 | case 1009, 1020: 40 | completion(.failure(.noInternetConnection)) 41 | return 42 | case 404: 43 | completion(.failure(.notFound)) 44 | return 45 | case 400: 46 | completion(.failure(.notAuthorized)) 47 | return 48 | case 500 ... 599: 49 | completion(.failure(.server)) 50 | return 51 | default: 52 | completion(.failure(SessionDataTaskError.failWithError(error))) 53 | return 54 | } 55 | } 56 | guard let data = data 57 | else { 58 | completion(.failure(SessionDataTaskError.noData)) 59 | return 60 | } 61 | do { 62 | let decoder = JSONDecoder() 63 | let response = try decoder.decode([UserRepositoriesNetworkResponse].self, from: data) 64 | debugPrint(response) 65 | completion(.success(response)) 66 | 67 | } catch { 68 | completion(.failure(SessionDataTaskError.failWithError(error))) 69 | } 70 | }).resume() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /GithubUsers/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /GithubUsers/Views/CommonViews/CustomizedBackButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomizedBackButton.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 03/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CustomizedBackButton: View { 11 | @Environment(\.presentationMode) var presentationMode: Binding 12 | var body : some View { 13 | Button(action: { 14 | presentationMode.wrappedValue.dismiss() 15 | }) { 16 | HStack { 17 | Image(systemName: "arrowshape.backward.fill") 18 | .aspectRatio(contentMode: .fit) 19 | .foregroundColor(.white) 20 | } 21 | } 22 | } 23 | } 24 | 25 | struct CustomizedBackButton_Previews: PreviewProvider { 26 | static var previews: some View { 27 | CustomizedBackButton() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /GithubUsers/Views/CommonViews/ImagePlaceholderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePlaceholderView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 03/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ImagePlaceholderView: View { 11 | var body: some View { 12 | Image(systemName: "photo.fill") 13 | .resizable() 14 | .scaledToFit() 15 | .frame( 16 | width: 100, 17 | height: 80, 18 | alignment: .leading 19 | ) 20 | .foregroundColor(.gray) 21 | } 22 | } 23 | 24 | struct ImagePlaceholderView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | ImagePlaceholderView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /GithubUsers/Views/CommonViews/OptionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 03/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OptionView: View { 11 | @Binding var title: String 12 | var body: some View { 13 | Text(title) 14 | .foregroundColor(.white) 15 | .fontWeight(.semibold) 16 | .frame( 17 | minWidth: 80, 18 | maxWidth: .infinity, 19 | minHeight: 40, 20 | alignment: .center) 21 | .background(.purple.gradient.opacity(0.7)) 22 | .cornerRadius(8) 23 | } 24 | } 25 | 26 | #if DEBUG 27 | struct OptionView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | let title = "Option Title" 30 | OptionView(title: .constant(title)) 31 | } 32 | } 33 | #endif 34 | -------------------------------------------------------------------------------- /GithubUsers/Views/CommonViews/SearchNoResultsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchNoResultsView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SearchNoResultsView: View { 11 | var body: some View { 12 | VStack { 13 | ZStack { 14 | Image(systemName: "magnifyingglass") 15 | .resizable() 16 | .frame(width: 200, height: 200, alignment: .center) 17 | .foregroundColor(.purple.opacity(0.5)) 18 | Image("github2") 19 | .resizable() 20 | .frame(width: 100, height: 100, alignment: .center) 21 | .padding(.trailing, 35) 22 | .padding(.bottom, 30) 23 | } 24 | 25 | Text("Try to search for an existed repository") 26 | .font(.title) 27 | .fontWeight(.bold) 28 | .multilineTextAlignment(.center) 29 | .lineLimit(3) 30 | .foregroundColor(.purple.opacity(0.9)) 31 | } 32 | .padding(24) 33 | .background(.black) 34 | } 35 | } 36 | 37 | struct SearchNoResultsView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | SearchNoResultsView() 40 | .previewDevice("iPhone 14 Pro Max") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /GithubUsers/Views/Followers/FollowerCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowerCard.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 03/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FollowerCard: View { 11 | @Binding var follower: Follower 12 | 13 | var body: some View { 14 | VStack(alignment: .center, spacing: 8.0) { 15 | AsyncImage( 16 | url: URL(string: follower.avatarURL)) { image in 17 | image 18 | .resizable() 19 | .scaledToFill() 20 | .frame( 21 | width: 100, 22 | height: 100, 23 | alignment: .center 24 | ) 25 | .clipShape(Circle()) 26 | .overlay( 27 | Circle() 28 | .stroke(.purple.gradient, lineWidth: 2) 29 | ) 30 | 31 | } placeholder: { 32 | ImagePlaceholderView() 33 | } 34 | 35 | Text("@\(follower.userName)") 36 | .font(.title3) 37 | .fontWeight(.bold) 38 | .foregroundColor(.purple.opacity(0.5)) 39 | } 40 | .frame(width: 160, height: 200) 41 | .background(.black.gradient.opacity(0.7)) 42 | .clipShape(RoundedRectangle(cornerRadius: 12)) 43 | .overlay( 44 | RoundedRectangle(cornerRadius: 12) 45 | .stroke(.purple.gradient.opacity(0.3), lineWidth: 2) 46 | ) 47 | } 48 | } 49 | 50 | struct FollowerCard_Previews: PreviewProvider { 51 | static var previews: some View { 52 | let follower = Follower( 53 | userName: "CryptoOo", 54 | avatarURL: "https://static.wikia.nocookie.net/naruto/images/d/d6/Naruto_Part_I.png/revision/latest/scale-to-width-down/1200?cb=20210223094656" 55 | ) 56 | FollowerCard(follower: .constant(follower)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /GithubUsers/Views/Followers/FollowersView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowersView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 02/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FollowersView: View { 11 | @Binding var username: String 12 | @State var followersArray: [Follower] = [] 13 | 14 | let columns = Array(repeating: GridItem(), count: 2) 15 | 16 | var body: some View { 17 | ScrollView { 18 | LazyVGrid(columns: columns, spacing: 10.0) { 19 | ForEach($followersArray) { follower in 20 | FollowerCard(follower: follower) 21 | } 22 | } 23 | } 24 | .onAppear { 25 | fetchUserFollowers() 26 | } 27 | .navigationBarBackButtonHidden(true) 28 | .navigationBarItems(leading: CustomizedBackButton()) 29 | .navigationTitle("Followers") 30 | .navigationBarTitleDisplayMode(.inline) 31 | } 32 | } 33 | 34 | struct Followers_Previews: PreviewProvider { 35 | static var previews: some View { 36 | FollowersView(username: .constant("CryptoOo")) 37 | .previewDevice("iPhone 14 Pro Max") 38 | } 39 | } 40 | 41 | extension FollowersView { 42 | func fetchUserFollowers() { 43 | FollowersAPIClient.shared.getUserFollowers(username: username) { result in 44 | switch result { 45 | case .success(let response): 46 | followersArray = response.map({ follower in 47 | Follower( 48 | userName: follower.userName ?? "", 49 | avatarURL: follower.avatarURL ?? "" 50 | ) 51 | }) 52 | case .failure(let failure): 53 | print(failure) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /GithubUsers/Views/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 25/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeView: View { 11 | @State var usersArray: [User] = [] 12 | // usersArray = [ 13 | // User( 14 | // name: "User1", 15 | // userName: "User1_1", 16 | // image: "user1" 17 | // ), 18 | // User( 19 | // name: "User2", 20 | // userName: "User2_2", 21 | // image: "user1" 22 | // ), 23 | // User( 24 | // name: "User3", 25 | // userName: "User3_3", 26 | // image: "user1" 27 | // ), 28 | // User( 29 | // name: "User4", 30 | // userName: "User4_4", 31 | // image: "user1" 32 | // ), 33 | // ] 34 | var body: some View { 35 | TabView { 36 | NavigationView { 37 | UsersView() 38 | .navigationTitle("Users") 39 | } 40 | .tabItem { 41 | VStack { 42 | Image(systemName: "person") 43 | Text("Users") 44 | } 45 | } 46 | 47 | NavigationView { 48 | RepositoriesView() 49 | .navigationTitle("Repositories") 50 | } 51 | .tabItem { 52 | VStack { 53 | Image(systemName: "command") 54 | Text("Repositories") 55 | } 56 | } 57 | 58 | NavigationView { 59 | Text("My great Profile") 60 | .navigationTitle("My profile") 61 | } 62 | .tabItem { 63 | VStack { 64 | Image(systemName: "star") 65 | Text("My profile") 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | struct ContentView_Previews: PreviewProvider { 73 | static var previews: some View { 74 | HomeView() 75 | /// Ctrl + Option on previewDevice to select the needed device 76 | .previewDevice("iPhone 14 Pro Max") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /GithubUsers/Views/Repositories/RepositoriesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoriesView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RepositoriesView: View { 11 | @State var repositoriesArray: [Repository] = [] 12 | @State var searchText = "" 13 | @State var isSearchWorking = false 14 | 15 | var body: some View { 16 | ZStack { 17 | VStack { 18 | List { 19 | ForEach(repositoriesArray) { repository in 20 | // NavigationLink( 21 | // destination: UserDetails(username: user.userName)) { 22 | // UserCard(user: user) 23 | // } 24 | UserRepositoryCard(repository: repository) 25 | } 26 | .listRowSeparator(.visible) 27 | } 28 | .listStyle(.plain) 29 | .opacity(isSearchWorking ? 1 : 0) 30 | } 31 | SearchNoResultsView() 32 | .opacity(isSearchWorking ? 0 : 1) 33 | } 34 | .searchable(text: $searchText) 35 | .onChange(of: searchText) { newValue in 36 | if newValue.count >= 3 { 37 | fetchUserRepositories(using: searchText) 38 | } 39 | isSrearchStarted() 40 | } 41 | } 42 | } 43 | 44 | struct RepositoriesView_Previews: PreviewProvider { 45 | static var previews: some View { 46 | RepositoriesView() 47 | } 48 | } 49 | 50 | extension RepositoriesView { 51 | func isSrearchStarted() { 52 | if searchText.count >= 3 { 53 | isSearchWorking = true 54 | } else { 55 | isSearchWorking = false 56 | } 57 | } 58 | 59 | func fetchUserRepositories(using searchText: String) { 60 | RepositoriesAPIClient.shared.getRepositories(searchText: searchText) { result in 61 | switch result { 62 | case .success(let response): 63 | repositoriesArray = response.map({ repository in 64 | Repository( 65 | repositoryName: repository.repositoryName ?? "", 66 | isPrivate: repository.isPrivate ?? false, 67 | repositoryURL: repository.repositoryURL ?? "", 68 | description: repository.description ?? "", 69 | repositorySize: repository.repositorySize ?? 0.0, 70 | repositoryForksCount: repository.repositoryForksCount ?? 0, 71 | repositoryStarsCount: repository.repositoryStarsCount ?? 0, 72 | repositoryOpenIssuesCount: repository.repositoryOpenIssuesCount ?? 0, 73 | repositoryWatchersCount: repository.repositoryWatchersCount ?? 0, 74 | repositoryDefaultBranch: repository.repositoryDefaultBranch ?? "", 75 | cloneURL: repository.cloneURL ?? "", 76 | programmingLanguage: repository.programmingLanguage ?? "", 77 | updatedAt: repository.updatedAt?.formattedDateString() ?? "", 78 | licenseName: repository.license?.licenseName ?? "") 79 | 80 | }) 81 | case .failure(let failure): 82 | print(failure) 83 | } 84 | } 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /GithubUsers/Views/UserDetails/UserDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetails.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 01/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UserDetails: View { 11 | // MARK: - Properties 12 | 13 | @State var userDetails: UserDetailsNetworkResponse? 14 | 15 | // var user = User( 16 | // name: "User1", 17 | // userName: "User1_1", 18 | // image: "https://static.wikia.nocookie.net/naruto/images/d/d6/Naruto_Part_I.png/revision/latest/scale-to-width-down/1200?cb=20210223094656" 19 | // ) 20 | 21 | @State var repositoriesTitle = "Repositories" 22 | @State var followersTitle = "Followers" 23 | @State var followingTitle = "Following" 24 | @State var gistsTitle = "Gists" 25 | @State var company = "" 26 | @State var location = "" 27 | @State var twitterUsername = "" 28 | @State var bio = "" 29 | @State var profileURL = "" 30 | @State var isBioExisted = false 31 | @State var isTwitterExisted = false 32 | @State var isLocationExisted = false 33 | @State var isWorkExisted = false 34 | @State var username: String = "" 35 | 36 | // MARK: - Views 37 | 38 | var body: some View { 39 | ScrollView { 40 | VStack(spacing: 8.0) { 41 | AsyncImage( 42 | url: URL(string: userDetails?.avatarURL ?? "") 43 | ) { image in 44 | image 45 | .resizable() 46 | .scaledToFill() 47 | .frame( 48 | width: 200, 49 | height: 200, 50 | alignment: .center 51 | ) 52 | .clipShape(Circle()) 53 | } placeholder: { 54 | ImagePlaceholderView() 55 | } 56 | VStack(spacing: 4.0) { 57 | Text(userDetails?.fullName ?? "") 58 | .font(.largeTitle) 59 | .fontWeight(.heavy) 60 | .multilineTextAlignment(.center) 61 | .lineLimit(2) 62 | Text("@\(userDetails?.userName ?? "")") 63 | .font(.footnote) 64 | .fontWeight(.bold) 65 | .foregroundColor(.gray) 66 | } 67 | Divider() 68 | VStack(spacing: 10) { 69 | HStack(spacing: 12.0) { 70 | NavigationLink(destination: { 71 | FollowersView(username: $username) 72 | }, label: { 73 | OptionView(title: $followersTitle) 74 | }) 75 | OptionView(title: $followingTitle) 76 | } 77 | HStack(alignment: .center, spacing: 12.0) { 78 | NavigationLink(destination: { 79 | UserRepositoriesView(username: $username) 80 | }, label: { 81 | OptionView(title: $repositoriesTitle) 82 | }) 83 | OptionView(title: $gistsTitle) 84 | } 85 | } 86 | .padding(.leading, 16) 87 | .padding(.trailing, 16) 88 | VStack( 89 | alignment: .leading, 90 | spacing: 8.0 91 | ) { 92 | if isWorkExisted { 93 | HStack { 94 | Text("Companies:") 95 | .fontWeight(.black) 96 | .font(.title3) 97 | Text(company) 98 | } 99 | .frame(minWidth: 200, maxWidth: .infinity, alignment: .leading) 100 | } 101 | 102 | if isLocationExisted { 103 | HStack { 104 | Text("Location:") 105 | .fontWeight(.black) 106 | .font(.title3) 107 | Text(location) 108 | } 109 | .frame(minWidth: 200, maxWidth: .infinity, alignment: .leading) 110 | } 111 | 112 | if isTwitterExisted { 113 | HStack { 114 | Text("Twiter: ") 115 | .fontWeight(.black) 116 | .font(.title3) 117 | Text("@\(twitterUsername)") 118 | 119 | } 120 | .frame(minWidth: 200, maxWidth: .infinity, alignment: .leading) 121 | } 122 | 123 | if isBioExisted { 124 | VStack(spacing: 8.0) { 125 | Text("Bio:") 126 | .underline() 127 | .fontWeight(.black) 128 | .font(.title3) 129 | .frame(minWidth: 50, maxWidth: .infinity, alignment: .leading) 130 | Text(bio) 131 | .frame(maxWidth: .infinity, alignment: .leading) 132 | } 133 | } 134 | } 135 | .frame( 136 | maxHeight: .infinity, 137 | alignment: .top 138 | ) 139 | .padding(.top, 36) 140 | .padding(.leading, 16) 141 | .padding(.trailing, 16) 142 | } 143 | 144 | if let profileLink = URL(string: profileURL) { 145 | Link(destination: profileLink) { 146 | HStack { 147 | Text("See my Profile!") 148 | .font(.title) 149 | .fontWeight(.bold) 150 | .foregroundColor(.white) 151 | } 152 | .frame( 153 | minWidth: 240, 154 | maxWidth: .infinity, 155 | minHeight: 48, 156 | maxHeight: 48, 157 | alignment: .center 158 | ) 159 | .background(.purple.gradient.opacity(0.3)) 160 | } 161 | .cornerRadius(8) 162 | .padding(24) 163 | } 164 | } 165 | .navigationBarBackButtonHidden(true) 166 | .navigationBarItems(leading: CustomizedBackButton()) 167 | .navigationTitle(username) 168 | .navigationBarTitleDisplayMode(.inline) 169 | .onAppear { 170 | fetchUserDetails() 171 | } 172 | } 173 | } 174 | 175 | // MARK: - Preview 176 | 177 | struct UserDetails_Previews: PreviewProvider { 178 | static var previews: some View { 179 | let userDetails = UserDetailsNetworkResponse( 180 | userName: "UserName", 181 | fullName: "Menaim", 182 | company: "YASSIR", 183 | avatarURL: "https://static.wikia.nocookie.net/naruto/images/d/d6/Naruto_Part_I.png/revision/latest/scale-to-width-down/1200?cb=20210223094656", 184 | userProfileURL: "", 185 | location: "", 186 | bio: "", 187 | twitterUsername: "", 188 | numberOfPublicRepos: 10, 189 | numberOfPublicGists: 15, 190 | numberOfFollowers: 14, 191 | numberOfFollowing: 12 192 | ) 193 | UserDetails(userDetails: userDetails) 194 | } 195 | } 196 | 197 | // MARK: - APIService Extension 198 | 199 | extension UserDetails { 200 | func fetchUserDetails() { 201 | APIService.shared.getUserDetails( 202 | username: username 203 | ) { result in 204 | switch result { 205 | case .success(let response): 206 | userDetails = response 207 | repositoriesTitle = "\(userDetails?.numberOfPublicRepos ?? 0) Repositories" 208 | gistsTitle = "\(userDetails?.numberOfPublicGists ?? 0) Gists" 209 | followersTitle = "\(userDetails?.numberOfFollowers ?? 0) Followers" 210 | followingTitle = "\(userDetails?.numberOfFollowing ?? 0) Following" 211 | company = "\(userDetails?.company ?? "")" 212 | location = "\(userDetails?.location ?? "")" 213 | twitterUsername = "\(userDetails?.twitterUsername ?? "")" 214 | bio = "\(userDetails?.twitterUsername ?? "")" 215 | if bio == "" { 216 | isBioExisted = false 217 | } else { 218 | isBioExisted = true 219 | } 220 | if twitterUsername == "" { 221 | isTwitterExisted = false 222 | } else { 223 | isTwitterExisted = true 224 | } 225 | if location == "" { 226 | isLocationExisted = false 227 | } else { 228 | isLocationExisted = true 229 | } 230 | if company == "" { 231 | isWorkExisted = false 232 | } else { 233 | isWorkExisted = true 234 | } 235 | 236 | profileURL = userDetails?.userProfileURL ?? "" 237 | 238 | case .failure(let failure): 239 | print(failure) 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /GithubUsers/Views/UserRepositories/UserRepositoriesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoriesView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UserRepositoriesView: View { 11 | @Binding var username: String 12 | @State var repositoriesArray: [Repository] = [] 13 | 14 | var body: some View { 15 | VStack { 16 | List { 17 | ForEach(repositoriesArray) { repository in 18 | // NavigationLink( 19 | // destination: UserDetails(username: user.userName)) { 20 | // UserCard(user: user) 21 | // } 22 | UserRepositoryCard(repository: repository) 23 | } 24 | .listRowSeparator(.visible) 25 | } 26 | .listStyle(.plain) 27 | .onAppear { 28 | fetchUserRepositories() 29 | } 30 | } 31 | } 32 | } 33 | 34 | struct UserRepositoriesView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | let username = "cryptoOo" 37 | UserRepositoriesView(username: .constant(username)) 38 | } 39 | } 40 | 41 | extension UserRepositoriesView { 42 | func fetchUserRepositories() { 43 | UserRepositoriesAPIClient.shared.getUserRepositories(username: username) { result in 44 | switch result { 45 | case .success(let response): 46 | repositoriesArray = response.map({ repository in 47 | Repository( 48 | repositoryName: repository.repositoryName ?? "", 49 | isPrivate: repository.isPrivate ?? false, 50 | repositoryURL: repository.repositoryURL ?? "", 51 | description: repository.description ?? "", 52 | repositorySize: repository.repositorySize ?? 0.0, 53 | repositoryForksCount: repository.repositoryForksCount ?? 0, 54 | repositoryStarsCount: repository.repositoryStarsCount ?? 0, 55 | repositoryOpenIssuesCount: repository.repositoryOpenIssuesCount ?? 0, 56 | repositoryWatchersCount: repository.repositoryWatchersCount ?? 0, 57 | repositoryDefaultBranch: repository.repositoryDefaultBranch ?? "", 58 | cloneURL: repository.cloneURL ?? "", 59 | programmingLanguage: repository.programmingLanguage ?? "", 60 | updatedAt: repository.updatedAt?.formattedDateString() ?? "", 61 | licenseName: repository.license?.licenseName ?? "") 62 | 63 | }) 64 | case .failure(let failure): 65 | print(failure) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /GithubUsers/Views/UserRepositories/UserRepositoryCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryCard.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 04/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UserRepositoryCard: View { 11 | @State var repository: Repository 12 | var body: some View { 13 | VStack(alignment: .leading, spacing: 8.0) { 14 | HStack(spacing: 8.0) { 15 | Text(repository.repositoryName) 16 | .foregroundColor(.purple.opacity(0.8)) 17 | .font(.title3) 18 | .fontWeight(.heavy) 19 | Text(repository.isPrivate ? "Private" : "Public") 20 | .fontWeight(.bold) 21 | .foregroundColor(.gray) 22 | .padding(.horizontal, 8) 23 | .padding(.vertical, 4) 24 | .clipShape(RoundedRectangle(cornerRadius: 12.0)) 25 | .overlay( 26 | RoundedRectangle(cornerRadius: 16.0) 27 | .stroke(.gray.gradient, lineWidth: 1) 28 | ) 29 | } 30 | .frame(maxWidth: .infinity, alignment: .leading) 31 | if !repository.description.isEmpty { 32 | Text(repository.description) 33 | .foregroundColor(.gray.opacity(0.7)) 34 | .fontWeight(.semibold) 35 | .lineLimit(2) 36 | .multilineTextAlignment(.leading) 37 | } 38 | HStack(spacing: 16.0) { 39 | if !repository.programmingLanguage.isEmpty { 40 | Text(repository.programmingLanguage) 41 | } 42 | HStack(spacing: 2) { 43 | Image(systemName: "star") 44 | Text("\(repository.repositoryStarsCount)") 45 | } 46 | HStack(spacing: 2) { 47 | Image(systemName: "tuningfork") 48 | Text("\(repository.repositoryForksCount)") 49 | } 50 | if !repository.licenseName.isEmpty { 51 | Text(repository.licenseName) 52 | } 53 | 54 | VStack(spacing: 2) { 55 | Text("Updated on") 56 | Text("\(repository.updatedAt)") 57 | } 58 | } 59 | .foregroundColor(.gray.opacity(0.9)) 60 | } 61 | // .frame(maxWidth: .infinity, minHeight: 300) 62 | } 63 | } 64 | 65 | struct UserRepositoryCard_Previews: PreviewProvider { 66 | static var previews: some View { 67 | let repository = Repository( 68 | repositoryName: "30daysoflaptops.github.io", 69 | isPrivate: false, 70 | repositoryURL: "https://github.com/mojombo/30daysoflaptops.github.io", 71 | description: "description", 72 | repositorySize: 1197, 73 | repositoryForksCount: 4, 74 | repositoryStarsCount: 8, 75 | repositoryOpenIssuesCount: 0, 76 | repositoryWatchersCount: 7, 77 | repositoryDefaultBranch: "gh-pages", 78 | cloneURL: "https://github.com/mojombo/30daysoflaptops.github.io.git", 79 | programmingLanguage: "CSS", 80 | updatedAt: "Apr 5, 2023", 81 | licenseName: "MIT License" 82 | ) 83 | UserRepositoryCard(repository: repository) 84 | .previewDevice("iPhone 14 Pro Max") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /GithubUsers/Views/Users/UserCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCard.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 25/06/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UserCard: View { 11 | var user: User 12 | var body: some View { 13 | HStack(alignment: .center, spacing: 16) { 14 | AsyncImage( 15 | url: URL(string: user.image)) { image in 16 | image 17 | .resizable() 18 | .scaledToFill() 19 | .frame( 20 | width: 100, 21 | height: 80, 22 | alignment: .leading 23 | ) 24 | .clipShape(RoundedRectangle(cornerRadius: 8)) 25 | } placeholder: { 26 | ImagePlaceholderView() 27 | } 28 | 29 | VStack(alignment: .leading, spacing: 8) { 30 | Text(user.name) 31 | .foregroundColor(.purple) 32 | .fontWeight(.heavy) 33 | .font(.title2) 34 | Text(user.userName) 35 | .font(.footnote) 36 | .fontWeight(.semibold) 37 | .multilineTextAlignment(.leading) 38 | .lineLimit(3) 39 | } 40 | } 41 | 42 | } 43 | } 44 | 45 | struct userCard_Previews: PreviewProvider { 46 | static var previews: some View { 47 | let user = User( 48 | name: "User1", 49 | userName: "User1_2", 50 | image: "https://static.wikia.nocookie.net/naruto/images/d/d6/Naruto_Part_I.png/revision/latest/scale-to-width-down/1200?cb=20210223094656" 51 | ) 52 | UserCard(user: user) 53 | } 54 | } 55 | 56 | struct User: Identifiable { 57 | var id = UUID() 58 | var name: String 59 | var userName: String 60 | var image: String 61 | } 62 | 63 | -------------------------------------------------------------------------------- /GithubUsers/Views/Users/UsersView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersView.swift 3 | // GithubUsers 4 | // 5 | // Created by Menaim on 03/07/2023. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UsersView: View { 11 | @State var usersArray: [User] = [] 12 | 13 | var body: some View { 14 | VStack { 15 | List { 16 | ForEach(usersArray) { user in 17 | NavigationLink( 18 | destination: UserDetails(username: user.userName)) { 19 | UserCard(user: user) 20 | } 21 | } 22 | .listRowSeparator(.hidden) 23 | } 24 | .listStyle(.plain) 25 | .onAppear { 26 | fetchUsers() 27 | } 28 | } 29 | } 30 | } 31 | 32 | struct UsersView_Previews: PreviewProvider { 33 | static var previews: some View { 34 | UsersView() 35 | } 36 | } 37 | 38 | extension UsersView { 39 | func fetchUsers() { 40 | APIService.shared.getUsers { result in 41 | switch result { 42 | case .success(let response): 43 | usersArray = response.map({ user in 44 | User( 45 | name: "\(user.userID ?? 0)", 46 | userName: user.userName ?? "", 47 | image: user.avatarURL ?? "" 48 | ) 49 | }) 50 | print(usersArray) 51 | case .failure(let failure): 52 | print(failure) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHubFullApp 2 | 3 | 👉🏻 This repo is created to introduce the implementation of the clean architecture concepts over MVVM. 4 | 5 | 👉🏻 In this repo I'm following the approach of using the different branches and each branch contains some specific updates. 6 | 7 | 👉🏻 More design patterns can be added but I thought it's better to be like this for now, as they are not being used in the project and it's not a good thing to add more design patterns even if they are not being used or not needed at this time (**You can do that only in practicing/educational purposes)**. 8 | 9 | ![Simulator Screen Recording - iPhone 15 Pro - 2023-12-03 at 21 32 40](https://github.com/AhmedMenaim/GitHubFullApp/assets/26345314/4bc2024a-8649-4934-9921-d0627637b603) 10 | 11 | 12 | # Approaches followed 13 | 14 | 1. Clean architecture with the below diagram. 15 | 16 | ![MVVM-Clean-Diagram](https://github.com/AhmedMenaim/GitHubFullApp/assets/26345314/7ad00c4a-2ca4-467e-9a26-a33ff8fefe3a) 17 | 18 | 2. Design patterns like a repository to manage the network responses and the cache (If exists) 19 | 3. Design patterns like dataSource to manage the variables that exist in business logic. 20 | 4. Using **SwiftUI** in the UI layer. 21 | 5. Network layer from scratch. 22 | 6. Depending on modern concurrency (Async, Await) 23 | 7. Unit tests for two modules. 24 | 8. Dependency injection using [Resolver](https://github.com/hmlongco/Resolver). 25 | 9. Dependency injection using [Factory](https://github.com/hmlongco/Factory). 26 | 27 | # Branches 28 | 29 | [Main Branch](https://github.com/AhmedMenaim/GitHubFullApp/tree/main) 30 | 31 | This is the branch that contains the basic implementation for a couple of modules, You can consider it as your starter project. 32 | 33 | [MVVM Branch](https://github.com/AhmedMenaim/GitHubFullApp/tree/MVVM) 34 | 35 | I have implemented the normal (The most commonly used MVVM) with some files/folders organization with the base layers needed, You can consider it as your **(MVVM)** starter project. 36 | 37 | [MVVM-Clean branch](https://github.com/AhmedMenaim/GitHubFullApp/tree/MVVM-Clean) 38 | 39 | This branch is to show how to achieve the diagram mentioned before but I have completed only two modules -> **Users & UserDetails** with most of the needed layers but other modules are the same in this branch. 40 | 41 | [DI-Resolver branch](https://github.com/AhmedMenaim/GitHubFullApp/tree/DI-Resolver) 42 | 43 | Instead of using the normal dependency injection, I have chosen to use one of the third-party libraries that can support and facilitate the process which is [Resolver](https://github.com/hmlongco/Resolver) & I completed implementing it for all the needed, and this branch now contains all the modules completed with all the needed layers. 44 | 45 | [DI-Factory branch](https://github.com/AhmedMenaim/GitHubFullApp/tree/DI-Factory) 46 | 47 | As Resolver may not be supported in the future and all the support will go to the factory which is created by the same author, I thought that this is gonna be a good opportunity to have a look at [Factory](https://github.com/hmlongco/Factory) and the differences between it and the resolver it an opportunity in this branch DI-Factory 48 | 49 | 50 | # What could be improved? 51 | 52 | **Will be implemented soon **OR** Feel free to implement any of them ✊🏻** 53 | 54 | 1. Coordinator pattern. 55 | 2. Add caching. 56 | 3. Logger in the Network layer. 57 | 4. Combine. 58 | 5. Implement template design for the profile page. (For now, it is left empty for you to attach the suitable design you need to talk about yourself, Maybe I can set a template for it as a portfolio to facilitate the process. 59 | 60 | # Communication 61 | 62 | 📳 You can find me on: 63 | 64 | [![Linkedin: Ahmed Menaim](https://img.shields.io/badge/-Menaim-blue?style=flat-square&logo=Linkedin&logoColor=white&link=https://www.linkedin.com/in/menaim/)](https://www.linkedin.com/in/menaim/) [![YT-Logo](https://user-images.githubusercontent.com/26345314/162580151-8af04674-1da2-4934-98e1-9067dd93ea84.png)](https://www.youtube.com/@MenaimAcademy) 65 | 66 | --------------------------------------------------------------------------------