├── README.md
├── GitHubFollowers
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Icon.png
│ │ ├── icon_20pt.png
│ │ ├── icon_29pt.png
│ │ ├── icon_40pt.png
│ │ ├── icon_76pt.png
│ │ ├── icon_20pt@2x.png
│ │ ├── icon_20pt@3x.png
│ │ ├── icon_29pt-1.png
│ │ ├── icon_29pt@2x.png
│ │ ├── icon_29pt@3x.png
│ │ ├── icon_40pt@2x.png
│ │ ├── icon_40pt@3x.png
│ │ ├── icon_60pt@2x.png
│ │ ├── icon_60pt@3x.png
│ │ ├── icon_76pt@2x.png
│ │ ├── icon_83.5@2x.png
│ │ ├── icon_20pt@2x-1.png
│ │ ├── icon_29pt@2x-1.png
│ │ ├── icon_40pt@2x-1.png
│ │ └── Contents.json
│ ├── gh-logo.imageset
│ │ ├── gh-logo@2x.png
│ │ ├── gh-logo@3x.png
│ │ ├── gh-logo-dark@2x.png
│ │ ├── gh-logo-dark@3x.png
│ │ └── Contents.json
│ ├── empty-state-logo.imageset
│ │ ├── empty-state-logo@2x.png
│ │ ├── empty-state-logo@3x.png
│ │ ├── empty-state-logo-dark@2x.png
│ │ ├── empty-state-logo-dark@3x.png
│ │ └── Contents.json
│ └── avatar-placeholder.imageset
│ │ ├── avatar-placeholder@2x.png
│ │ ├── avatar-placeholder@3x.png
│ │ ├── avatar-placeholder-dark@2x.png
│ │ ├── avatar-placeholder-dark@3x.png
│ │ └── Contents.json
├── Model
│ ├── Follower.swift
│ └── User.swift
├── Controller
│ ├── Search
│ │ ├── SearchVC +TextFieldDelegate.swift
│ │ └── SearchVC.swift
│ ├── FavoritesVC.swift
│ ├── UIViewController +Ext.swift
│ ├── UserInfoVC.swift
│ ├── GFAlertVC.swift
│ ├── GFUserInfoHeaderVC.swift
│ └── FollowersVC.swift
├── Utilities
│ ├── Constants.swift
│ ├── GFError.swift
│ └── UIHelper.swift
├── View
│ ├── GFSecondaryTitleLabel.swift
│ ├── GFBodyLabel.swift
│ ├── GFTitleLabel.swift
│ ├── GFTextField.swift
│ ├── GFButton.swift
│ ├── GFAvatarImageView.swift
│ ├── GFEmptyStateView.swift
│ ├── Cells
│ │ └── FollowerCell.swift
│ └── GFItemInfoView.swift
├── AppDelegate.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── SceneDelegate.swift
├── Info.plist
└── Managers
│ └── NetworkManager.swift
├── .swiftlint.yml
├── GitHubFollowers.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── max.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
└── .gitignore
/README.md:
--------------------------------------------------------------------------------
1 | # GitHub Followers
2 |
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | disabled_rules:
2 | - identifier_name
3 | - trailing_whitespace
4 | - vertical_whitespace
5 |
6 | line_length: 200
7 | function_body_length: 50
8 |
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/Icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/Icon.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_76pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_76pt.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt-1.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_60pt@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_60pt@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_76pt@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_83.5@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo-dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo-dark@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo-dark@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/gh-logo.imageset/gh-logo-dark@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_20pt@2x-1.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_29pt@2x-1.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/icon_40pt@2x-1.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo-dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo-dark@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo-dark@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/empty-state-logo-dark@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder-dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder-dark@2x.png
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder-dark@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maxnabokow/GitHubFollowers/HEAD/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/avatar-placeholder-dark@3x.png
--------------------------------------------------------------------------------
/GitHubFollowers.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/GitHubFollowers.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/GitHubFollowers/Model/Follower.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Follower.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Follower: Codable, Hashable {
12 |
13 | var login: String
14 | var avatarUrl: String
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/GitHubFollowers.xcodeproj/xcuserdata/max.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | GitHubFollowers.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/Search/SearchVC +TextFieldDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchVC +TextFieldDelegate.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | extension SearchVC: UITextFieldDelegate {
12 |
13 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
14 | pushFollowersVC()
15 | return true
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/GitHubFollowers/Utilities/Constants.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Constants.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 2/8/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum SFSymbols {
12 | static let location = "mappin.and.ellipse"
13 | static let repos = "folder"
14 | static let gists = "text.alignLeft"
15 | static let followers = "heart"
16 | static let following = "person.2"
17 | }
18 |
--------------------------------------------------------------------------------
/GitHubFollowers/Model/User.swift:
--------------------------------------------------------------------------------
1 | //
2 | // User.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct User: Codable {
12 |
13 | let login: String
14 | let avatarUrl: String
15 |
16 | var name: String?
17 | var location: String?
18 | var bio: String?
19 |
20 | let publicRepos: Int
21 | let publicGists: Int
22 |
23 | let htmlUrl: String
24 |
25 | let following: Int
26 | let followers: Int
27 | let createdAt: String
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/GitHubFollowers/Utilities/GFError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFError.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/4/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | enum GFError: String, Error {
12 |
13 | case invalidUsername = "This username created an invalid request. Please try again."
14 | case unableToComplete = "Unable to complete your request. Please check your internet connection"
15 | case invalidResponse = "Invalid response from the server. Please try again."
16 | case invalidData = "The data received from the server was invalid. Please try again."
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 |
3 | ## User settings
4 | xcuserdata/
5 | *.xcuserstate
6 |
7 | .DS_Store
8 |
9 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
10 | *.xcscmblueprint
11 | *.xccheckout
12 |
13 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
14 | build/
15 | DerivedData/
16 | *.moved-aside
17 | *.pbxuser
18 | !default.pbxuser
19 | *.mode1v3
20 | !default.mode1v3
21 | *.mode2v3
22 | !default.mode2v3
23 | *.perspectivev3
24 | !default.perspectivev3
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 |
29 | ## App packaging
30 | *.ipa
31 | *.dSYM.zip
32 | *.dSYM
33 |
34 | ## Playgrounds
35 | timeline.xctimeline
36 | playground.xcworkspace
37 |
38 | .build/
39 |
40 | iOSInjectionProject/
41 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/FavoritesVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoritesListVC.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 12/28/19.
6 | // Copyright © 2019 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class FavoritesVC: UIViewController {
12 |
13 | // MARK: - UI Elements
14 |
15 | // MARK: - Properties
16 |
17 | // MARK: - Lifecycle
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 |
22 | setupUI()
23 | setupLayout()
24 | }
25 |
26 | // MARK: - UI Setup
27 |
28 | fileprivate func setupUI() {
29 | view.backgroundColor = .systemBlue
30 | }
31 |
32 | fileprivate func setupLayout() {
33 |
34 | }
35 |
36 | // MARK: - Selectors
37 |
38 | // MARK: - Helper Functions
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/GitHubFollowers/Utilities/UIHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIHelper.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/15/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | struct UIHelper {
12 |
13 | static func createThreeColumnFlowLayout(in view: UIView) -> UICollectionViewFlowLayout {
14 | let layout = UICollectionViewFlowLayout()
15 |
16 | let width = view.bounds.width
17 | let padding: CGFloat = 12
18 | let minimumItemSpacing: CGFloat = 10
19 | let availableWidth = width - (padding * 2) - (minimumItemSpacing * 2)
20 | let itemWidth = availableWidth / 3
21 |
22 | layout.sectionInset = .init(top: padding, left: padding, bottom: padding, right: padding)
23 | layout.itemSize = .init(width: itemWidth, height: itemWidth + 40)
24 |
25 | return layout
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFSecondaryTitleLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFSecondaryTitleLabel.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 2/8/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFSecondaryTitleLabel: UILabel {
12 |
13 | // MARK: - Init
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 |
18 | setupUI()
19 | }
20 |
21 | init(fontSize: CGFloat) {
22 | super.init(frame: .zero)
23 | font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
24 |
25 | setupUI()
26 | }
27 |
28 | // MARK: - UI Setup
29 |
30 | fileprivate func setupUI() {
31 | textColor = .secondaryLabel
32 | adjustsFontSizeToFitWidth = true
33 | minimumScaleFactor = 0.9
34 | lineBreakMode = .byTruncatingTail
35 | }
36 |
37 | required init?(coder: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFBodyLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFBodyLabel.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFBodyLabel: UILabel {
12 |
13 | // MARK: - Init
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 |
18 | setupUI()
19 | }
20 |
21 | init(textAlignment: NSTextAlignment) {
22 | super.init(frame: .zero)
23 | self.textAlignment = textAlignment
24 |
25 | setupUI()
26 | }
27 |
28 | // MARK: - UI Setup
29 |
30 | fileprivate func setupUI() {
31 | textColor = .secondaryLabel
32 | font = .preferredFont(forTextStyle: .body)
33 | adjustsFontSizeToFitWidth = true
34 | minimumScaleFactor = 0.75
35 | lineBreakMode = .byWordWrapping
36 | }
37 |
38 | required init?(coder: NSCoder) {
39 | fatalError("init(coder:) has not been implemented")
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFTitleLabel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFTitleLabel.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFTitleLabel: UILabel {
12 |
13 | // MARK: - Init
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 |
18 | setupUI()
19 | }
20 |
21 | init(textAlignment: NSTextAlignment, fontSize: CGFloat) {
22 | super.init(frame: .zero)
23 | self.textAlignment = textAlignment
24 | self.font = .systemFont(ofSize: fontSize, weight: .bold)
25 |
26 | setupUI()
27 | }
28 |
29 | // MARK: - UI Setup
30 |
31 | fileprivate func setupUI() {
32 | textColor = .label
33 | adjustsFontSizeToFitWidth = true
34 | minimumScaleFactor = 0.9
35 | lineBreakMode = .byTruncatingTail
36 | }
37 |
38 | required init?(coder: NSCoder) {
39 | fatalError("init(coder:) has not been implemented")
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFTextField.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFTextField.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 12/28/19.
6 | // Copyright © 2019 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFTextField: UITextField {
12 |
13 | // MARK: - Init
14 | override init(frame: CGRect) {
15 | super.init(frame: frame)
16 |
17 | setupUI()
18 | }
19 |
20 | // MARK: - UI Setup
21 | fileprivate func setupUI() {
22 | backgroundColor = .tertiarySystemBackground
23 | placeholder = "Enter a username"
24 | autocorrectionType = .no
25 | autocapitalizationType = .none
26 | returnKeyType = .go
27 |
28 | layer.cornerRadius = 12
29 | layer.borderWidth = 1
30 | layer.borderColor = UIColor.systemGray3.cgColor
31 |
32 | textColor = .label
33 | tintColor = .systemGreen
34 | textAlignment = .center
35 | font = .preferredFont(forTextStyle: .title2)
36 | adjustsFontSizeToFitWidth = true
37 | minimumFontSize = 12
38 | }
39 |
40 | required init?(coder: NSCoder) {
41 | fatalError("init(coder:) has not been implemented")
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/gh-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "1x",
10 | "appearances" : [
11 | {
12 | "appearance" : "luminosity",
13 | "value" : "dark"
14 | }
15 | ]
16 | },
17 | {
18 | "idiom" : "universal",
19 | "filename" : "gh-logo@2x.png",
20 | "scale" : "2x"
21 | },
22 | {
23 | "idiom" : "universal",
24 | "filename" : "gh-logo-dark@2x.png",
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "universal",
35 | "filename" : "gh-logo@3x.png",
36 | "scale" : "3x"
37 | },
38 | {
39 | "idiom" : "universal",
40 | "filename" : "gh-logo-dark@3x.png",
41 | "appearances" : [
42 | {
43 | "appearance" : "luminosity",
44 | "value" : "dark"
45 | }
46 | ],
47 | "scale" : "3x"
48 | }
49 | ],
50 | "info" : {
51 | "version" : 1,
52 | "author" : "xcode"
53 | }
54 | }
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/empty-state-logo.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "1x",
10 | "appearances" : [
11 | {
12 | "appearance" : "luminosity",
13 | "value" : "dark"
14 | }
15 | ]
16 | },
17 | {
18 | "idiom" : "universal",
19 | "filename" : "empty-state-logo@2x.png",
20 | "scale" : "2x"
21 | },
22 | {
23 | "idiom" : "universal",
24 | "filename" : "empty-state-logo-dark@2x.png",
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "universal",
35 | "filename" : "empty-state-logo@3x.png",
36 | "scale" : "3x"
37 | },
38 | {
39 | "idiom" : "universal",
40 | "filename" : "empty-state-logo-dark@3x.png",
41 | "appearances" : [
42 | {
43 | "appearance" : "luminosity",
44 | "value" : "dark"
45 | }
46 | ],
47 | "scale" : "3x"
48 | }
49 | ],
50 | "info" : {
51 | "version" : 1,
52 | "author" : "xcode"
53 | }
54 | }
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/avatar-placeholder.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "scale" : "1x",
10 | "appearances" : [
11 | {
12 | "appearance" : "luminosity",
13 | "value" : "dark"
14 | }
15 | ]
16 | },
17 | {
18 | "idiom" : "universal",
19 | "filename" : "avatar-placeholder@2x.png",
20 | "scale" : "2x"
21 | },
22 | {
23 | "idiom" : "universal",
24 | "filename" : "avatar-placeholder-dark@2x.png",
25 | "appearances" : [
26 | {
27 | "appearance" : "luminosity",
28 | "value" : "dark"
29 | }
30 | ],
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "universal",
35 | "filename" : "avatar-placeholder@3x.png",
36 | "scale" : "3x"
37 | },
38 | {
39 | "idiom" : "universal",
40 | "filename" : "avatar-placeholder-dark@3x.png",
41 | "appearances" : [
42 | {
43 | "appearance" : "luminosity",
44 | "value" : "dark"
45 | }
46 | ],
47 | "scale" : "3x"
48 | }
49 | ],
50 | "info" : {
51 | "version" : 1,
52 | "author" : "xcode"
53 | }
54 | }
--------------------------------------------------------------------------------
/GitHubFollowers/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 12/28/19.
6 | // Copyright © 2019 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
15 | // Override point for customization after application launch.
16 | return true
17 | }
18 |
19 | // MARK: UISceneSession Lifecycle
20 |
21 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
22 | // Called when a new scene session is being created.
23 | // Use this method to select a configuration to create the new scene with.
24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
25 | }
26 |
27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
28 | // Called when the user discards a scene session.
29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/GitHubFollowers/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 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFButton.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 12/28/19.
6 | // Copyright © 2019 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFButton: UIButton {
12 |
13 | // MARK: - Init
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 |
18 | setupUI()
19 | }
20 |
21 | init(backgroundColor: UIColor, title: String) {
22 | super.init(frame: .zero)
23 | self.backgroundColor = backgroundColor
24 | self.setTitle(title, for: .normal)
25 |
26 | setupUI()
27 |
28 | addTarget(self, action: #selector(animatePressDown), for: .touchDown)
29 | addTarget(self, action: #selector(animatePressUp), for: .touchDragExit)
30 | addTarget(self, action: #selector(animatePressDown), for: .touchDragEnter)
31 | addTarget(self, action: #selector(animatePressUp), for: .touchUpInside)
32 | }
33 |
34 | // MARK: - Helper Functions
35 |
36 | @objc fileprivate func animatePressDown() {
37 | UIButton.animate(withDuration: 0.1, animations: {
38 | self.transform = CGAffineTransform(scaleX: 0.975, y: 0.96)
39 | })
40 | }
41 |
42 | @objc fileprivate func animatePressUp() {
43 | UIButton.animate(withDuration: 0.1, animations: {
44 | self.transform = .identity
45 | })
46 | }
47 |
48 | // MARK: - UI Setup
49 |
50 | fileprivate func setupUI() {
51 | layer.cornerRadius = 12
52 | setTitleColor(.white, for: .normal)
53 | titleLabel?.font = .preferredFont(forTextStyle: .headline)
54 | }
55 |
56 | required init?(coder: NSCoder) {
57 | fatalError("init(coder:) has not been implemented")
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/GitHubFollowers/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 12/28/19.
6 | // Copyright © 2019 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 |
17 | guard let windowScene = scene as? UIWindowScene else { return }
18 |
19 | window = UIWindow(frame: windowScene.coordinateSpace.bounds)
20 | window?.windowScene = windowScene
21 | window?.rootViewController = createTabBarC()
22 | window?.makeKeyAndVisible()
23 |
24 | configureNavigationBar()
25 | }
26 |
27 | func createTabBarC() -> UITabBarController {
28 | let tabBarC = UITabBarController()
29 | UITabBar.appearance().tintColor = .systemGreen
30 | tabBarC.viewControllers = [createSearchNC(), createFavoritesNC()]
31 |
32 | return tabBarC
33 | }
34 |
35 | func configureNavigationBar() {
36 | UINavigationBar.appearance().tintColor = .systemGreen
37 | }
38 |
39 | func createSearchNC() -> UINavigationController {
40 | let searchVC = SearchVC()
41 | searchVC.title = "Search"
42 | searchVC.tabBarItem = UITabBarItem(tabBarSystemItem: .search, tag: 0)
43 |
44 | return UINavigationController(rootViewController: searchVC)
45 | }
46 |
47 | func createFavoritesNC() -> UINavigationController {
48 | let favoritesVC = FavoritesVC()
49 | favoritesVC.title = "Favorites"
50 | favoritesVC.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 1)
51 |
52 | return UINavigationController(rootViewController: favoritesVC)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFAvatarImageView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFAvatarImageView.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/10/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFAvatarImageView: UIImageView {
12 |
13 | fileprivate let placeholderImage = #imageLiteral(resourceName: "avatar-placeholder")
14 | let cache = NetworkManager.shared.cache
15 |
16 | // MARK: - Init
17 |
18 | override init(frame: CGRect) {
19 | super.init(frame: frame)
20 |
21 | setupUI()
22 | }
23 |
24 | // MARK: - UI Setup
25 |
26 | fileprivate func setupUI() {
27 | layer.cornerRadius = 10
28 | clipsToBounds = true
29 | image = placeholderImage
30 | }
31 |
32 | // MARK: - Helper Functions
33 |
34 | func downloadImage(from urlString: String) {
35 |
36 | let cacheKey = NSString(string: urlString)
37 |
38 | if let image = cache.object(forKey: cacheKey) {
39 | self.image = image
40 | return
41 | }
42 |
43 | guard let url = URL(string: urlString) else { return }
44 |
45 | URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
46 | guard let self = self else { return }
47 |
48 | if error != nil { return }
49 | guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { return }
50 | guard let data = data else { return }
51 |
52 | guard let image = UIImage(data: data) else { return }
53 |
54 | self.cache.setObject(image, forKey: cacheKey)
55 |
56 | DispatchQueue.main.async {
57 | self.image = image
58 | }
59 |
60 | }.resume()
61 | }
62 |
63 | required init?(coder: NSCoder) {
64 | fatalError("init(coder:) has not been implemented")
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/GitHubFollowers/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 |
37 |
38 |
39 |
40 | UILaunchStoryboardName
41 | LaunchScreen
42 | UIRequiredDeviceCapabilities
43 |
44 | armv7
45 |
46 | UISupportedInterfaceOrientations
47 |
48 | UIInterfaceOrientationPortrait
49 |
50 | UISupportedInterfaceOrientations~ipad
51 |
52 | UIInterfaceOrientationPortrait
53 | UIInterfaceOrientationPortraitUpsideDown
54 | UIInterfaceOrientationLandscapeLeft
55 | UIInterfaceOrientationLandscapeRight
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/UIViewController +Ext.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController +Ext.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | // swiftlint:disable:next private_over_fileprivate
12 | fileprivate var containerView: UIView!
13 |
14 | extension UIViewController {
15 |
16 | func presentGFAlertOnMainThread(title: String, message: String, buttonTitle: String) {
17 | DispatchQueue.main.async {
18 | UIImpactFeedbackGenerator(style: .medium).impactOccurred()
19 | let alertVC = GFAlertVC(title: title, message: message, buttonTitle: buttonTitle)
20 | alertVC.modalPresentationStyle = .overFullScreen
21 | alertVC.modalTransitionStyle = .crossDissolve
22 |
23 | self.present(alertVC, animated: true)
24 | }
25 | }
26 |
27 | func showLoadingView() {
28 | containerView = UIView(frame: view.bounds)
29 | containerView.backgroundColor = .systemBackground
30 | containerView.alpha = 0
31 |
32 | view.addSubview(containerView)
33 | UIView.animate(withDuration: 0.25) {
34 | containerView.alpha = 0.8
35 | }
36 |
37 | let activityIndicator = UIActivityIndicatorView(style: .large)
38 |
39 | containerView.addSubview(activityIndicator)
40 | activityIndicator.translatesAutoresizingMaskIntoConstraints = false
41 |
42 | NSLayoutConstraint.activate([
43 | activityIndicator.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
44 | activityIndicator.centerXAnchor.constraint(equalTo: containerView.centerXAnchor)
45 | ])
46 |
47 | activityIndicator.startAnimating()
48 | }
49 |
50 | func dismissLoadingView() {
51 |
52 | DispatchQueue.main.async {
53 | containerView.removeFromSuperview()
54 | containerView = nil
55 | }
56 | }
57 |
58 | func showEmptyStateView(in view: UIView, with message: String) {
59 | let emptyStateView = GFEmptyStateView(message: message)
60 | emptyStateView.frame = view.bounds
61 | view.addSubview(emptyStateView)
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFEmptyStateView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFEmptyStateView.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/16/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFEmptyStateView: UIView {
12 |
13 | // MARK: - UI Elements
14 |
15 | fileprivate let messageLabel: UILabel = {
16 | let l = GFTitleLabel(textAlignment: .center, fontSize: 24)
17 | l.numberOfLines = 3
18 | l.textColor = .secondaryLabel
19 | return l
20 | }()
21 |
22 | fileprivate let logoImageView: UIImageView = {
23 | let iv = UIImageView(image: #imageLiteral(resourceName: "empty-state-logo"))
24 | return iv
25 | }()
26 |
27 | // MARK: - Init
28 |
29 | override init(frame: CGRect) {
30 | super.init(frame: frame)
31 |
32 | setupLayout()
33 | }
34 |
35 | init(message: String) {
36 | super.init(frame: .zero)
37 | messageLabel.text = message
38 | setupLayout()
39 | }
40 |
41 | // MARK: - UI Setup
42 |
43 | fileprivate func setupLayout() {
44 | addSubview(messageLabel)
45 | messageLabel.translatesAutoresizingMaskIntoConstraints = false
46 | NSLayoutConstraint.activate([
47 | messageLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -150),
48 | messageLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 40),
49 | messageLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -40),
50 | messageLabel.heightAnchor.constraint(equalToConstant: 200)
51 | ])
52 |
53 | addSubview(logoImageView)
54 | logoImageView.translatesAutoresizingMaskIntoConstraints = false
55 | NSLayoutConstraint.activate([
56 | logoImageView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1.3),
57 | logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor),
58 | logoImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 170),
59 | logoImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 40)
60 | ])
61 | }
62 |
63 | required init?(coder: NSCoder) {
64 | fatalError("init(coder:) has not been implemented")
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/GitHubFollowers/View/Cells/FollowerCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FollowerCell.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/10/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class FollowerCell: UICollectionViewCell {
12 |
13 | // MARK: - UI Elements
14 |
15 | fileprivate let avatarImageView = GFAvatarImageView(frame: .zero)
16 |
17 | fileprivate let usernameLabel = GFTitleLabel(textAlignment: .center, fontSize: 14)
18 |
19 | // MARK: - Properties
20 |
21 | static let reuseId = "FollowerCell"
22 |
23 | // MARK: - Init
24 |
25 | override init(frame: CGRect) {
26 | super.init(frame: frame)
27 |
28 | setupUI()
29 | setupLayout()
30 | }
31 |
32 | func configure(follower: Follower) {
33 | usernameLabel.text = follower.login
34 | avatarImageView.downloadImage(from: follower.avatarUrl)
35 | }
36 |
37 | // MARK: - UI Setup
38 |
39 | fileprivate func setupUI() {
40 |
41 | }
42 |
43 | fileprivate func setupLayout() {
44 |
45 | let padding: CGFloat = 8
46 |
47 | addSubview(avatarImageView)
48 | avatarImageView.translatesAutoresizingMaskIntoConstraints = false
49 | NSLayoutConstraint.activate([
50 | avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
51 | avatarImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
52 | avatarImageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
53 | avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor)
54 | ])
55 |
56 | addSubview(usernameLabel)
57 | usernameLabel.translatesAutoresizingMaskIntoConstraints = false
58 | NSLayoutConstraint.activate([
59 | usernameLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 12),
60 | usernameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding),
61 | usernameLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -padding),
62 | usernameLabel.heightAnchor.constraint(equalToConstant: 20)
63 | ])
64 |
65 | }
66 |
67 | required init?(coder: NSCoder) {
68 | fatalError("init(coder:) has not been implemented")
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/GitHubFollowers/Managers/NetworkManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NetworkManager.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/4/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class NetworkManager {
12 | static let shared = NetworkManager()
13 | private let baseUrl = "https://api.github.com/users/"
14 | let cache = NSCache()
15 |
16 | private init() {}
17 |
18 | func getFollowers(for username: String, pageNr: Int, completion: @escaping (Result<[Follower], GFError>) -> Void) {
19 | let endpoint = baseUrl + "\(username)/followers?per_page=100&page=\(pageNr)"
20 |
21 | guard let url = URL(string: endpoint) else {
22 | completion(.failure(.invalidUsername))
23 | return
24 | }
25 |
26 | URLSession.shared.dataTask(with: url) { (data, response, error) in
27 | if error != nil {
28 | completion(.failure(.unableToComplete))
29 | }
30 |
31 | guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
32 | completion(.failure(.invalidResponse))
33 | return
34 | }
35 |
36 | guard let data = data else {
37 | completion(.failure(.invalidData))
38 | return
39 | }
40 |
41 | do {
42 | let decoder = JSONDecoder()
43 | decoder.keyDecodingStrategy = .convertFromSnakeCase
44 | let followers = try decoder.decode([Follower].self, from: data)
45 | completion(.success(followers))
46 | } catch {
47 | completion(.failure(.invalidData))
48 | }
49 | }.resume()
50 | }
51 |
52 | func getUserInfo(for username: String, completion: @escaping (Result) -> Void) {
53 | let endpoint = baseUrl + "\(username)"
54 |
55 | guard let url = URL(string: endpoint) else {
56 | completion(.failure(.invalidUsername))
57 | return
58 | }
59 |
60 | URLSession.shared.dataTask(with: url) { (data, response, error) in
61 | if error != nil {
62 | completion(.failure(.unableToComplete))
63 | }
64 |
65 | guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
66 | completion(.failure(.invalidResponse))
67 | return
68 | }
69 |
70 | guard let data = data else {
71 | completion(.failure(.invalidData))
72 | return
73 | }
74 |
75 | do {
76 | let decoder = JSONDecoder()
77 | decoder.keyDecodingStrategy = .convertFromSnakeCase
78 | let user = try decoder.decode(User.self, from: data)
79 | completion(.success(user))
80 | } catch {
81 | completion(.failure(.invalidData))
82 | }
83 | }.resume()
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/GitHubFollowers/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "icon_20pt@2x-1.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "icon_20pt@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "icon_29pt.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "icon_29pt@2x-1.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "icon_29pt@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "icon_40pt@2x-1.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "icon_40pt@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "icon_60pt@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "icon_60pt@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "icon_20pt.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "icon_20pt@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "icon_29pt-1.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "icon_29pt@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "icon_40pt.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "icon_40pt@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "icon_76pt.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "icon_76pt@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "icon_83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
--------------------------------------------------------------------------------
/GitHubFollowers/View/GFItemInfoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFItemInfoView.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 2/8/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | enum ItemInfoType {
12 | case repos, gists, followers, following
13 | }
14 |
15 | class GFItemInfoView: UIView {
16 |
17 | fileprivate let symbolImageView = UIImageView()
18 | fileprivate let titleLabel = GFTitleLabel(textAlignment: .left, fontSize: 14)
19 | fileprivate let countLabel = GFTitleLabel(textAlignment: .center, fontSize: 14)
20 |
21 |
22 | // MARK: - Init
23 |
24 | override init(frame: CGRect) {
25 | super.init(frame: frame)
26 |
27 | setupUI()
28 | setupLayout()
29 | }
30 |
31 | // MARK: - UI Setup
32 |
33 | fileprivate func setupUI() {
34 | symbolImageView.contentMode = .scaleAspectFill
35 | symbolImageView.tintColor = .label
36 | }
37 |
38 | fileprivate func setupLayout() {
39 | addSubview(symbolImageView)
40 | symbolImageView.translatesAutoresizingMaskIntoConstraints = false
41 | NSLayoutConstraint.activate([
42 | symbolImageView.topAnchor.constraint(equalTo: topAnchor),
43 | symbolImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
44 | symbolImageView.widthAnchor.constraint(equalToConstant: 20),
45 | symbolImageView.heightAnchor.constraint(equalToConstant: 20)
46 | ])
47 |
48 | addSubview(titleLabel)
49 | titleLabel.translatesAutoresizingMaskIntoConstraints = false
50 | NSLayoutConstraint.activate([
51 | titleLabel.centerYAnchor.constraint(equalTo: symbolImageView.centerYAnchor),
52 | titleLabel.leadingAnchor.constraint(equalTo: symbolImageView.trailingAnchor, constant: 12),
53 | titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
54 | titleLabel.heightAnchor.constraint(equalToConstant: 18)
55 | ])
56 |
57 | addSubview(countLabel)
58 | countLabel.translatesAutoresizingMaskIntoConstraints = false
59 | NSLayoutConstraint.activate([
60 | countLabel.topAnchor.constraint(equalTo: symbolImageView.bottomAnchor, constant: 4),
61 | countLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
62 | countLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
63 | countLabel.heightAnchor.constraint(equalToConstant: 18)
64 | ])
65 | }
66 |
67 | func set(itemInfoType: ItemInfoType, withCount count: Int) {
68 | switch itemInfoType {
69 | case .repos:
70 | symbolImageView.image = UIImage(systemName: SFSymbols.repos)
71 | titleLabel.text = "Public Repos"
72 | case .gists:
73 | symbolImageView.image = UIImage(systemName: SFSymbols.gists)
74 | titleLabel.text = "Public Gists"
75 | case .followers:
76 | symbolImageView.image = UIImage(systemName: SFSymbols.followers)
77 | titleLabel.text = "Followers"
78 | case .following:
79 | symbolImageView.image = UIImage(systemName: SFSymbols.following)
80 | titleLabel.text = "Following"
81 | }
82 |
83 | countLabel.text = String(count)
84 | }
85 |
86 | required init?(coder: NSCoder) {
87 | fatalError("init(coder:) has not been implemented")
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/UserInfoVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserInfoVC.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/16/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class UserInfoVC: UIViewController {
12 |
13 | // MARK: - UI Elements
14 | fileprivate let headerView = UIView()
15 | fileprivate let itemView1 = UIView()
16 | fileprivate let itemView2 = UIView()
17 |
18 | fileprivate var itemViews: [UIView] = []
19 |
20 | // MARK: - Properties
21 |
22 | var username: String?
23 |
24 | // MARK: - Lifecycle
25 |
26 | override func viewDidLoad() {
27 | super.viewDidLoad()
28 |
29 | setupUI()
30 | setupLayout()
31 | setupDoneButton()
32 | getUserInfo()
33 | }
34 |
35 | // MARK: - UI Setup
36 |
37 | fileprivate func setupUI() {
38 | view.backgroundColor = .systemBackground
39 |
40 | itemView1.backgroundColor = .systemPink
41 | itemView2.backgroundColor = .systemBlue
42 | }
43 |
44 | fileprivate func setupLayout() {
45 |
46 | itemViews = [headerView, itemView1, itemView2]
47 |
48 | let padding: CGFloat = 20
49 | let itemHeight: CGFloat = 140
50 |
51 | for itemView in itemViews {
52 | view.addSubview(itemView)
53 | itemView.translatesAutoresizingMaskIntoConstraints = false
54 |
55 | NSLayoutConstraint.activate([
56 | itemView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding),
57 | itemView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -padding)
58 | ])
59 | }
60 |
61 | NSLayoutConstraint.activate([
62 | headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
63 | headerView.heightAnchor.constraint(equalToConstant: 180),
64 |
65 | itemView1.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: padding),
66 | itemView1.heightAnchor.constraint(equalToConstant: itemHeight),
67 |
68 | itemView2.topAnchor.constraint(equalTo: itemView1.bottomAnchor, constant: padding),
69 | itemView2.heightAnchor.constraint(equalToConstant: itemHeight)
70 | ])
71 | }
72 |
73 | fileprivate func setupDoneButton() {
74 | let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
75 | navigationItem.rightBarButtonItem = doneButton
76 | }
77 |
78 | fileprivate func add(childVC: UIViewController, to containerView: UIView) {
79 | addChild(childVC)
80 | containerView.addSubview(childVC.view)
81 | childVC.view.frame = containerView.bounds
82 | }
83 |
84 | // MARK: - Selectors
85 |
86 | @objc func doneButtonTapped() {
87 | dismiss(animated: true)
88 | }
89 |
90 | // MARK: - Helper Functions
91 | fileprivate func getUserInfo() {
92 | guard let username = username else { return }
93 |
94 | NetworkManager.shared.getUserInfo(for: username) { [weak self] (result) in
95 | guard let self = self else { return }
96 |
97 | switch result {
98 |
99 | case .success(let user):
100 | DispatchQueue.main.async {
101 | self.add(childVC: GFUserInfoHeaderVC(user: user), to: self.headerView)
102 | }
103 | case .failure(let error):
104 | self.presentGFAlertOnMainThread(title: "Something went wrong", message: error.rawValue, buttonTitle: "OK")
105 |
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/Search/SearchVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchVC.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 12/28/19.
6 | // Copyright © 2019 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SearchVC: UIViewController {
12 |
13 | // MARK: - UI Elements
14 |
15 | fileprivate let logoImageView: UIImageView = {
16 | let iv = UIImageView(image: #imageLiteral(resourceName: "gh-logo"))
17 | return iv
18 | }()
19 |
20 | fileprivate let usernameTextField = GFTextField()
21 |
22 | fileprivate let getFollowersButton: UIButton = {
23 | let b = GFButton(backgroundColor: .systemGreen, title: "Get Followers")
24 | b.addTarget(self, action: #selector(pushFollowersVC), for: .touchUpInside)
25 | return b
26 | }()
27 |
28 | // MARK: - Properties
29 |
30 | var isUsernameEntered: Bool { return !usernameTextField.text!.isEmpty }
31 |
32 | // MARK: - Lifecycle
33 |
34 | override func viewDidLoad() {
35 | super.viewDidLoad()
36 |
37 | usernameTextField.delegate = self
38 |
39 | setupUI()
40 | setupLayout()
41 | }
42 |
43 | override func viewWillAppear(_ animated: Bool) {
44 | super.viewWillAppear(animated)
45 |
46 | navigationController?.setNavigationBarHidden(true, animated: true)
47 | }
48 |
49 | // MARK: - Selectors
50 |
51 | @objc func pushFollowersVC() {
52 | guard isUsernameEntered else {
53 | presentGFAlertOnMainThread(title: "Empty Username", message: "Please enter a username. We need to know who to look for.", buttonTitle: "OK")
54 | return
55 | }
56 |
57 | view.endEditing(true)
58 | let followersVC = FollowersVC()
59 | followersVC.username = usernameTextField.text
60 | followersVC.title = usernameTextField.text
61 | navigationController?.pushViewController(followersVC, animated: true)
62 | }
63 |
64 | // MARK: - Helper Functions
65 |
66 | override func touchesBegan(_ touches: Set, with event: UIEvent?) {
67 | view.endEditing(true)
68 | }
69 |
70 | // MARK: - UI Setup
71 |
72 | fileprivate func setupUI() {
73 | view.backgroundColor = .systemBackground
74 | }
75 |
76 | fileprivate func setupLayout() {
77 | view.addSubview(logoImageView)
78 | logoImageView.translatesAutoresizingMaskIntoConstraints = false
79 | NSLayoutConstraint.activate([
80 | logoImageView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
81 | logoImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
82 | logoImageView.widthAnchor.constraint(equalToConstant: 200),
83 | logoImageView.heightAnchor.constraint(equalToConstant: 200)
84 | ])
85 |
86 | view.addSubview(usernameTextField)
87 | usernameTextField.translatesAutoresizingMaskIntoConstraints = false
88 | NSLayoutConstraint.activate([
89 | usernameTextField.topAnchor.constraint(equalTo: logoImageView.bottomAnchor, constant: 48),
90 | usernameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
91 | usernameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
92 | usernameTextField.heightAnchor.constraint(equalToConstant: 50)
93 | ])
94 |
95 | view.addSubview(getFollowersButton)
96 | getFollowersButton.translatesAutoresizingMaskIntoConstraints = false
97 | NSLayoutConstraint.activate([
98 | getFollowersButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 50),
99 | getFollowersButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50),
100 | getFollowersButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -50),
101 | getFollowersButton.heightAnchor.constraint(equalToConstant: 50)
102 | ])
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/GFAlertVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFAlertVC.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFAlertVC: UIViewController {
12 |
13 | // MARK: - UI Elements
14 |
15 | fileprivate let containerView: UIView = {
16 | let v = UIView()
17 | v.backgroundColor = .systemBackground
18 | v.layer.cornerRadius = 16
19 | v.layer.borderWidth = 2
20 | v.layer.borderColor = UIColor.white.cgColor
21 | return v
22 | }()
23 |
24 | fileprivate lazy var titleLabel: UILabel = {
25 | let l = GFTitleLabel(textAlignment: .center, fontSize: 28)
26 | l.text = alertTitle ?? "Something went wrong."
27 | return l
28 | }()
29 |
30 | fileprivate lazy var messageLabel: UILabel = {
31 | let l = GFBodyLabel(textAlignment: .center)
32 | l.text = message ?? "Unable to complete request"
33 | l.numberOfLines = 4
34 | return l
35 | }()
36 |
37 | fileprivate lazy var actionButton: UIButton = {
38 | let b = GFButton(backgroundColor: .systemPink, title: "OK")
39 | b.setTitle(buttonTitle ?? "OK", for: .normal)
40 | b.addTarget(self, action: #selector(dismissVC), for: .touchUpInside)
41 | return b
42 | }()
43 |
44 | // MARK: - Properties
45 |
46 | var alertTitle: String?
47 | var message: String?
48 | var buttonTitle: String?
49 |
50 | let padding: CGFloat = 20
51 |
52 | // MARK: - Init
53 |
54 | init(title: String, message: String, buttonTitle: String) {
55 | super.init(nibName: nil, bundle: nil)
56 |
57 | self.alertTitle = title
58 | self.message = message
59 | self.buttonTitle = buttonTitle
60 | }
61 |
62 | // MARK: - Lifecycle
63 |
64 | override func viewDidLoad() {
65 | super.viewDidLoad()
66 |
67 | setupUI()
68 | setupLayout()
69 | }
70 |
71 | // MARK: - Selectors
72 |
73 | @objc fileprivate func dismissVC() {
74 | dismiss(animated: true)
75 | }
76 |
77 | // MARK: - Helper Functions
78 |
79 | // MARK: - UI Setup
80 |
81 | fileprivate func setupUI() {
82 | view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
83 | }
84 |
85 | fileprivate func setupLayout() {
86 | view.addSubview(containerView)
87 | containerView.translatesAutoresizingMaskIntoConstraints = false
88 | NSLayoutConstraint.activate([
89 | containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
90 | containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
91 | containerView.widthAnchor.constraint(equalToConstant: 280),
92 | containerView.heightAnchor.constraint(equalToConstant: 220)
93 | ])
94 |
95 | containerView.addSubview(titleLabel)
96 | titleLabel.translatesAutoresizingMaskIntoConstraints = false
97 | NSLayoutConstraint.activate([
98 | titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: padding),
99 | titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: padding),
100 | titleLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -padding),
101 | titleLabel.heightAnchor.constraint(equalToConstant: 28)
102 | ])
103 |
104 | containerView.addSubview(actionButton)
105 | actionButton.translatesAutoresizingMaskIntoConstraints = false
106 | NSLayoutConstraint.activate([
107 | actionButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -padding),
108 | actionButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: padding),
109 | actionButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -padding),
110 | actionButton.heightAnchor.constraint(equalToConstant: 44)
111 | ])
112 |
113 | containerView.addSubview(messageLabel)
114 | messageLabel.translatesAutoresizingMaskIntoConstraints = false
115 | NSLayoutConstraint.activate([
116 | messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
117 | messageLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: padding),
118 | messageLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -padding),
119 | messageLabel.bottomAnchor.constraint(equalTo: actionButton.topAnchor, constant: -12)
120 | ])
121 | }
122 |
123 | required init?(coder: NSCoder) {
124 | fatalError("init(coder:) has not been implemented")
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/GFUserInfoHeaderVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GFUserInfoHeaderVC.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 2/8/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class GFUserInfoHeaderVC: UIViewController {
12 |
13 | // MARK: - UI Elements
14 |
15 | fileprivate let avatarImageView = GFAvatarImageView(frame: .zero)
16 | fileprivate let usernameLabel = GFTitleLabel(textAlignment: .left, fontSize: 34)
17 | fileprivate let nameLabel = GFSecondaryTitleLabel(fontSize: 18)
18 |
19 | fileprivate let locationImageView = UIImageView()
20 |
21 | fileprivate let locationLabel = GFSecondaryTitleLabel(fontSize: 18)
22 | fileprivate let bioLabel = GFBodyLabel(textAlignment: .left)
23 |
24 | // MARK: - Properties
25 |
26 | var user: User!
27 |
28 | // MARK: Init
29 |
30 | init(user: User) {
31 | super.init(nibName: nil, bundle: nil)
32 | self.user = user
33 | }
34 |
35 | // MARK: - Lifecycle
36 |
37 | override func viewDidLoad() {
38 | super.viewDidLoad()
39 |
40 | setupUI()
41 | setupLayout()
42 | }
43 |
44 | // MARK: - UI Setup
45 |
46 | fileprivate func setupUI() {
47 | avatarImageView.downloadImage(from: user.avatarUrl)
48 | usernameLabel.text = user.login
49 | nameLabel.text = user.name ?? ""
50 | locationImageView.image = UIImage(systemName: SFSymbols.location)
51 | locationImageView.tintColor = .secondaryLabel
52 | locationLabel.text = user.location ?? "No Location available"
53 | bioLabel.text = user.bio ?? "No Bio available"
54 | bioLabel.numberOfLines = 3
55 | }
56 |
57 | fileprivate func setupLayout() {
58 | let padding: CGFloat = 20
59 | let textImagePadding: CGFloat = 20
60 |
61 | view.addSubview(avatarImageView)
62 | avatarImageView.translatesAutoresizingMaskIntoConstraints = false
63 | NSLayoutConstraint.activate([
64 | avatarImageView.topAnchor.constraint(equalTo: view.topAnchor, constant: padding),
65 | avatarImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
66 | avatarImageView.widthAnchor.constraint(equalToConstant: 90),
67 | avatarImageView.heightAnchor.constraint(equalToConstant: 90)
68 | ])
69 |
70 | view.addSubview(usernameLabel)
71 | usernameLabel.translatesAutoresizingMaskIntoConstraints = false
72 | NSLayoutConstraint.activate([
73 | usernameLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor),
74 | usernameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: textImagePadding),
75 | usernameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
76 | usernameLabel.heightAnchor.constraint(equalToConstant: 38)
77 | ])
78 |
79 | view.addSubview(nameLabel)
80 | nameLabel.translatesAutoresizingMaskIntoConstraints = false
81 | NSLayoutConstraint.activate([
82 | nameLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor, constant: 8),
83 | nameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: textImagePadding),
84 | nameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
85 | nameLabel.heightAnchor.constraint(equalToConstant: 20)
86 | ])
87 |
88 | view.addSubview(locationImageView)
89 | locationImageView.translatesAutoresizingMaskIntoConstraints = false
90 | NSLayoutConstraint.activate([
91 | locationImageView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor),
92 | locationImageView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: textImagePadding),
93 | locationImageView.widthAnchor.constraint(equalToConstant: 20),
94 | locationImageView.heightAnchor.constraint(equalToConstant: 20)
95 | ])
96 |
97 | view.addSubview(locationLabel)
98 | locationLabel.translatesAutoresizingMaskIntoConstraints = false
99 | NSLayoutConstraint.activate([
100 | locationLabel.centerYAnchor.constraint(equalTo: locationImageView.centerYAnchor),
101 | locationLabel.leadingAnchor.constraint(equalTo: locationImageView.trailingAnchor, constant: 5),
102 | locationLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
103 | locationLabel.heightAnchor.constraint(equalToConstant: 20)
104 | ])
105 |
106 | view.addSubview(bioLabel)
107 | bioLabel.translatesAutoresizingMaskIntoConstraints = false
108 | NSLayoutConstraint.activate([
109 | bioLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: textImagePadding),
110 | bioLabel.leadingAnchor.constraint(equalTo: avatarImageView.leadingAnchor),
111 | bioLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor),
112 | bioLabel.heightAnchor.constraint(equalToConstant: 60)
113 | ])
114 | }
115 |
116 | // MARK: - Selectors
117 |
118 |
119 |
120 | // MARK: - Helper Functions
121 |
122 |
123 | required init?(coder: NSCoder) {
124 | fatalError("init(coder:) has not been implemented")
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/GitHubFollowers/Controller/FollowersVC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FollowersVC.swift
3 | // GitHubFollowers
4 | //
5 | // Created by Max Nabokow on 1/1/20.
6 | // Copyright © 2020 Maximilian Nabokow. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class FollowersVC: UIViewController {
12 |
13 | // MARK: - UI Elements
14 |
15 | fileprivate lazy var collectionView: UICollectionView = {
16 | let cv = UICollectionView(frame: .zero, collectionViewLayout: UIHelper.createThreeColumnFlowLayout(in: view))
17 | cv.backgroundColor = .systemBackground
18 | cv.register(FollowerCell.self, forCellWithReuseIdentifier: FollowerCell.reuseId)
19 | cv.delegate = self
20 | return cv
21 | }()
22 |
23 | // MARK: - Properties
24 |
25 | var username: String?
26 |
27 | enum Section {
28 | case main
29 | }
30 |
31 | var dataSource: UICollectionViewDiffableDataSource!
32 | var followers = [Follower]()
33 | var originalFollowers = [Follower]()
34 | var page = 1
35 | var hasMoreFollowers = true
36 |
37 | // MARK: - Lifecycle
38 |
39 | override func viewDidLoad() {
40 | super.viewDidLoad()
41 |
42 | setupUI()
43 | setupLayout()
44 | fetchFollowers(username: username, page: page)
45 | configureDataSource()
46 | configureSearchController()
47 | }
48 |
49 | override func viewWillAppear(_ animated: Bool) {
50 | navigationController?.setNavigationBarHidden(false, animated: true)
51 | }
52 |
53 | // MARK: - UI Setup
54 |
55 | fileprivate func setupUI() {
56 | view.backgroundColor = .systemBackground
57 | navigationController?.navigationBar.prefersLargeTitles = true
58 | }
59 |
60 | fileprivate func setupLayout() {
61 | view.addSubview(collectionView)
62 | collectionView.frame = view.bounds
63 | }
64 |
65 | // MARK: - Selectors
66 |
67 | // MARK: - Helper Functions
68 |
69 | fileprivate func fetchFollowers(username: String?, page: Int) {
70 | showLoadingView()
71 |
72 | guard let username = username else { return }
73 |
74 | NetworkManager.shared.getFollowers(for: username, pageNr: page) { [weak self] (result) in
75 | guard let self = self else { return }
76 | self.dismissLoadingView()
77 |
78 | switch result {
79 |
80 | case .success(let followers):
81 | if followers.count < 100 { self.hasMoreFollowers = false }
82 | self.followers.append(contentsOf: followers)
83 | self.originalFollowers.append(contentsOf: followers)
84 |
85 | if self.followers.isEmpty {
86 | let message = "This user doesn't have any followers. Go follow them!"
87 | DispatchQueue.main.async {
88 | self.showEmptyStateView(in: self.view, with: message)
89 | return
90 | }
91 | }
92 |
93 | self.updateData(with: self.followers)
94 |
95 | case.failure(let error):
96 | self.presentGFAlertOnMainThread(title: "Bad Stuff Happened", message: error.rawValue, buttonTitle: "OK")
97 | }
98 | }
99 | }
100 |
101 | fileprivate func configureDataSource() {
102 | dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { (collectionView, indexPath, follower) -> UICollectionViewCell? in
103 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: FollowerCell.reuseId, for: indexPath) as? FollowerCell else { return nil}
104 | cell.configure(follower: follower)
105 | return cell
106 | })
107 | }
108 |
109 | fileprivate func updateData(with followers: [Follower]) {
110 | var snapshot = NSDiffableDataSourceSnapshot()
111 | snapshot.appendSections([.main])
112 | snapshot.appendItems(followers)
113 |
114 | DispatchQueue.main.async {
115 | self.dataSource.apply(snapshot, animatingDifferences: true)
116 | }
117 | }
118 |
119 | fileprivate func configureSearchController() {
120 | let searchController = UISearchController()
121 | searchController.searchResultsUpdater = self
122 | searchController.searchBar.delegate = self
123 | searchController.searchBar.placeholder = "Search for a username"
124 | searchController.obscuresBackgroundDuringPresentation = false
125 | navigationItem.searchController = searchController
126 | }
127 | }
128 |
129 | extension FollowersVC: UICollectionViewDelegate {
130 |
131 | func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
132 | let offsetY = scrollView.contentOffset.y
133 | let contentHeight = scrollView.contentSize.height
134 | let height = scrollView.frame.size.height
135 |
136 | if offsetY > contentHeight - height {
137 | guard hasMoreFollowers else { return }
138 | page += 1
139 | fetchFollowers(username: username, page: page)
140 | }
141 | }
142 |
143 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
144 | let follower = followers[indexPath.item]
145 |
146 | let userInfoVC = UserInfoVC()
147 | userInfoVC.username = follower.login
148 |
149 | let navC = UINavigationController(rootViewController: userInfoVC)
150 |
151 | present(navC, animated: true)
152 | }
153 | }
154 |
155 | extension FollowersVC: UISearchResultsUpdating, UISearchBarDelegate {
156 |
157 | func updateSearchResults(for searchController: UISearchController) {
158 | guard let filter = searchController.searchBar.text, !filter.isEmpty else { return }
159 |
160 | followers = originalFollowers.filter { $0.login.lowercased().contains(filter.lowercased()) }
161 | updateData(with: followers)
162 | }
163 |
164 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
165 | followers = originalFollowers
166 | updateData(with: followers)
167 | }
168 |
169 | }
170 |
--------------------------------------------------------------------------------
/GitHubFollowers.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 500D8E4323EF4A6100779E4D /* GFUserInfoHeaderVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500D8E4223EF4A6100779E4D /* GFUserInfoHeaderVC.swift */; };
11 | 500D8E4523EF4B0400779E4D /* GFSecondaryTitleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500D8E4423EF4B0400779E4D /* GFSecondaryTitleLabel.swift */; };
12 | 500D8E4723EFA71100779E4D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500D8E4623EFA71100779E4D /* Constants.swift */; };
13 | 500D8E4923EFB16E00779E4D /* GFItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500D8E4823EFB16E00779E4D /* GFItemInfoView.swift */; };
14 | 501CB27423BCC9E600FA6407 /* Follower.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501CB27323BCC9E600FA6407 /* Follower.swift */; };
15 | 501CB27623BCCAAC00FA6407 /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501CB27523BCCAAC00FA6407 /* User.swift */; };
16 | 506B0EFA23B803A000FE5982 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506B0EF923B803A000FE5982 /* AppDelegate.swift */; };
17 | 506B0EFC23B803A000FE5982 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506B0EFB23B803A000FE5982 /* SceneDelegate.swift */; };
18 | 506B0F0323B803AA00FE5982 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 506B0F0223B803AA00FE5982 /* Assets.xcassets */; };
19 | 506B0F0623B803AA00FE5982 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 506B0F0423B803AA00FE5982 /* LaunchScreen.storyboard */; };
20 | 506B0F1923B806DC00FE5982 /* SearchVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506B0F1823B806DC00FE5982 /* SearchVC.swift */; };
21 | 506B0F1D23B809AC00FE5982 /* FavoritesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506B0F1C23B809AC00FE5982 /* FavoritesVC.swift */; };
22 | 506B0F2323B80D8400FE5982 /* GFButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506B0F2223B80D8400FE5982 /* GFButton.swift */; };
23 | 506B0F2623B8134800FE5982 /* GFTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506B0F2523B8134800FE5982 /* GFTextField.swift */; };
24 | 506CAA3723C9746C00A00E66 /* FollowerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506CAA3623C9746C00A00E66 /* FollowerCell.swift */; };
25 | 506CAA3923C9750F00A00E66 /* GFAvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506CAA3823C9750F00A00E66 /* GFAvatarImageView.swift */; };
26 | 506E96F223D0198E00249802 /* UIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506E96F123D0198E00249802 /* UIHelper.swift */; };
27 | 506E96F423D02B6900249802 /* GFEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506E96F323D02B6900249802 /* GFEmptyStateView.swift */; };
28 | 506E96F623D0354400249802 /* UserInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506E96F523D0354400249802 /* UserInfoVC.swift */; };
29 | 506E96F823D03D5600249802 /* GFError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 506E96F723D03D5600249802 /* GFError.swift */; };
30 | 50A86C9923C0C025005D014A /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A86C9823C0C025005D014A /* NetworkManager.swift */; };
31 | 50B9B0E423BC353500819BEB /* SearchVC +TextFieldDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9B0E323BC353500819BEB /* SearchVC +TextFieldDelegate.swift */; };
32 | 50B9B0E723BC360600819BEB /* FollowersVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9B0E623BC360600819BEB /* FollowersVC.swift */; };
33 | 50B9B0EB23BC3FCC00819BEB /* GFTitleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9B0EA23BC3FCC00819BEB /* GFTitleLabel.swift */; };
34 | 50B9B0ED23BC410300819BEB /* GFBodyLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9B0EC23BC410300819BEB /* GFBodyLabel.swift */; };
35 | 50B9B0EF23BC41D000819BEB /* GFAlertVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9B0EE23BC41D000819BEB /* GFAlertVC.swift */; };
36 | 50B9B0F123BC48D400819BEB /* UIViewController +Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B9B0F023BC48D400819BEB /* UIViewController +Ext.swift */; };
37 | /* End PBXBuildFile section */
38 |
39 | /* Begin PBXFileReference section */
40 | 500D8E4223EF4A6100779E4D /* GFUserInfoHeaderVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFUserInfoHeaderVC.swift; sourceTree = ""; };
41 | 500D8E4423EF4B0400779E4D /* GFSecondaryTitleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFSecondaryTitleLabel.swift; sourceTree = ""; };
42 | 500D8E4623EFA71100779E4D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
43 | 500D8E4823EFB16E00779E4D /* GFItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFItemInfoView.swift; sourceTree = ""; };
44 | 501CB27323BCC9E600FA6407 /* Follower.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Follower.swift; sourceTree = ""; };
45 | 501CB27523BCCAAC00FA6407 /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; };
46 | 506B0EF623B803A000FE5982 /* GitHubFollowers.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GitHubFollowers.app; sourceTree = BUILT_PRODUCTS_DIR; };
47 | 506B0EF923B803A000FE5982 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
48 | 506B0EFB23B803A000FE5982 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
49 | 506B0F0223B803AA00FE5982 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
50 | 506B0F0523B803AA00FE5982 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
51 | 506B0F0723B803AA00FE5982 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
52 | 506B0F1823B806DC00FE5982 /* SearchVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchVC.swift; sourceTree = ""; };
53 | 506B0F1C23B809AC00FE5982 /* FavoritesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesVC.swift; sourceTree = ""; };
54 | 506B0F2223B80D8400FE5982 /* GFButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFButton.swift; sourceTree = ""; };
55 | 506B0F2523B8134800FE5982 /* GFTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFTextField.swift; sourceTree = ""; };
56 | 506CAA3623C9746C00A00E66 /* FollowerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerCell.swift; sourceTree = ""; };
57 | 506CAA3823C9750F00A00E66 /* GFAvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFAvatarImageView.swift; sourceTree = ""; };
58 | 506E96F123D0198E00249802 /* UIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHelper.swift; sourceTree = ""; };
59 | 506E96F323D02B6900249802 /* GFEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFEmptyStateView.swift; sourceTree = ""; };
60 | 506E96F523D0354400249802 /* UserInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoVC.swift; sourceTree = ""; };
61 | 506E96F723D03D5600249802 /* GFError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFError.swift; sourceTree = ""; };
62 | 50A86C9823C0C025005D014A /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; };
63 | 50B9B0E323BC353500819BEB /* SearchVC +TextFieldDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchVC +TextFieldDelegate.swift"; sourceTree = ""; };
64 | 50B9B0E623BC360600819BEB /* FollowersVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersVC.swift; sourceTree = ""; };
65 | 50B9B0EA23BC3FCC00819BEB /* GFTitleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFTitleLabel.swift; sourceTree = ""; };
66 | 50B9B0EC23BC410300819BEB /* GFBodyLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFBodyLabel.swift; sourceTree = ""; };
67 | 50B9B0EE23BC41D000819BEB /* GFAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFAlertVC.swift; sourceTree = ""; };
68 | 50B9B0F023BC48D400819BEB /* UIViewController +Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController +Ext.swift"; sourceTree = ""; };
69 | /* End PBXFileReference section */
70 |
71 | /* Begin PBXFrameworksBuildPhase section */
72 | 506B0EF323B803A000FE5982 /* Frameworks */ = {
73 | isa = PBXFrameworksBuildPhase;
74 | buildActionMask = 2147483647;
75 | files = (
76 | );
77 | runOnlyForDeploymentPostprocessing = 0;
78 | };
79 | /* End PBXFrameworksBuildPhase section */
80 |
81 | /* Begin PBXGroup section */
82 | 029B8B1736BD1A02C170117B /* Frameworks */ = {
83 | isa = PBXGroup;
84 | children = (
85 | );
86 | name = Frameworks;
87 | sourceTree = "";
88 | };
89 | 501CB27223BCC8C000FA6407 /* Model */ = {
90 | isa = PBXGroup;
91 | children = (
92 | 501CB27323BCC9E600FA6407 /* Follower.swift */,
93 | 501CB27523BCCAAC00FA6407 /* User.swift */,
94 | );
95 | path = Model;
96 | sourceTree = "";
97 | };
98 | 506B0EED23B803A000FE5982 = {
99 | isa = PBXGroup;
100 | children = (
101 | 506B0EF823B803A000FE5982 /* GitHubFollowers */,
102 | 506B0EF723B803A000FE5982 /* Products */,
103 | 029B8B1736BD1A02C170117B /* Frameworks */,
104 | );
105 | sourceTree = "";
106 | };
107 | 506B0EF723B803A000FE5982 /* Products */ = {
108 | isa = PBXGroup;
109 | children = (
110 | 506B0EF623B803A000FE5982 /* GitHubFollowers.app */,
111 | );
112 | name = Products;
113 | sourceTree = "";
114 | };
115 | 506B0EF823B803A000FE5982 /* GitHubFollowers */ = {
116 | isa = PBXGroup;
117 | children = (
118 | 506B0EF923B803A000FE5982 /* AppDelegate.swift */,
119 | 506B0EFB23B803A000FE5982 /* SceneDelegate.swift */,
120 | 501CB27223BCC8C000FA6407 /* Model */,
121 | 506B0F2423B80D8800FE5982 /* View */,
122 | 506B0F2123B80D4C00FE5982 /* Controller */,
123 | 50F4771423C02C0D00B68B7F /* Managers */,
124 | 50A86C9523C0BDC1005D014A /* Utilities */,
125 | 506B0F0223B803AA00FE5982 /* Assets.xcassets */,
126 | 506B0F0423B803AA00FE5982 /* LaunchScreen.storyboard */,
127 | 506B0F0723B803AA00FE5982 /* Info.plist */,
128 | );
129 | path = GitHubFollowers;
130 | sourceTree = "";
131 | };
132 | 506B0F2123B80D4C00FE5982 /* Controller */ = {
133 | isa = PBXGroup;
134 | children = (
135 | 50B9B0E523BC356100819BEB /* Search */,
136 | 506B0F1C23B809AC00FE5982 /* FavoritesVC.swift */,
137 | 50B9B0E623BC360600819BEB /* FollowersVC.swift */,
138 | 50B9B0EE23BC41D000819BEB /* GFAlertVC.swift */,
139 | 50B9B0F023BC48D400819BEB /* UIViewController +Ext.swift */,
140 | 506E96F523D0354400249802 /* UserInfoVC.swift */,
141 | 500D8E4223EF4A6100779E4D /* GFUserInfoHeaderVC.swift */,
142 | );
143 | path = Controller;
144 | sourceTree = "";
145 | };
146 | 506B0F2423B80D8800FE5982 /* View */ = {
147 | isa = PBXGroup;
148 | children = (
149 | 506CAA3523C9744600A00E66 /* Cells */,
150 | 506B0F2523B8134800FE5982 /* GFTextField.swift */,
151 | 506B0F2223B80D8400FE5982 /* GFButton.swift */,
152 | 50B9B0EA23BC3FCC00819BEB /* GFTitleLabel.swift */,
153 | 50B9B0EC23BC410300819BEB /* GFBodyLabel.swift */,
154 | 506CAA3823C9750F00A00E66 /* GFAvatarImageView.swift */,
155 | 506E96F323D02B6900249802 /* GFEmptyStateView.swift */,
156 | 500D8E4423EF4B0400779E4D /* GFSecondaryTitleLabel.swift */,
157 | 500D8E4823EFB16E00779E4D /* GFItemInfoView.swift */,
158 | );
159 | path = View;
160 | sourceTree = "";
161 | };
162 | 506CAA3523C9744600A00E66 /* Cells */ = {
163 | isa = PBXGroup;
164 | children = (
165 | 506CAA3623C9746C00A00E66 /* FollowerCell.swift */,
166 | );
167 | path = Cells;
168 | sourceTree = "";
169 | };
170 | 50A86C9523C0BDC1005D014A /* Utilities */ = {
171 | isa = PBXGroup;
172 | children = (
173 | 506E96F123D0198E00249802 /* UIHelper.swift */,
174 | 506E96F723D03D5600249802 /* GFError.swift */,
175 | 500D8E4623EFA71100779E4D /* Constants.swift */,
176 | );
177 | path = Utilities;
178 | sourceTree = "";
179 | };
180 | 50B9B0E523BC356100819BEB /* Search */ = {
181 | isa = PBXGroup;
182 | children = (
183 | 506B0F1823B806DC00FE5982 /* SearchVC.swift */,
184 | 50B9B0E323BC353500819BEB /* SearchVC +TextFieldDelegate.swift */,
185 | );
186 | path = Search;
187 | sourceTree = "";
188 | };
189 | 50F4771423C02C0D00B68B7F /* Managers */ = {
190 | isa = PBXGroup;
191 | children = (
192 | 50A86C9823C0C025005D014A /* NetworkManager.swift */,
193 | );
194 | path = Managers;
195 | sourceTree = "";
196 | };
197 | /* End PBXGroup section */
198 |
199 | /* Begin PBXNativeTarget section */
200 | 506B0EF523B803A000FE5982 /* GitHubFollowers */ = {
201 | isa = PBXNativeTarget;
202 | buildConfigurationList = 506B0F0A23B803AA00FE5982 /* Build configuration list for PBXNativeTarget "GitHubFollowers" */;
203 | buildPhases = (
204 | 506B0EF223B803A000FE5982 /* Sources */,
205 | 506B0EF323B803A000FE5982 /* Frameworks */,
206 | 506B0EF423B803A000FE5982 /* Resources */,
207 | 50C6327C23D7307D00B9F4B7 /* Run Script: SwiftLint */,
208 | );
209 | buildRules = (
210 | );
211 | dependencies = (
212 | );
213 | name = GitHubFollowers;
214 | productName = GitHubFollowers;
215 | productReference = 506B0EF623B803A000FE5982 /* GitHubFollowers.app */;
216 | productType = "com.apple.product-type.application";
217 | };
218 | /* End PBXNativeTarget section */
219 |
220 | /* Begin PBXProject section */
221 | 506B0EEE23B803A000FE5982 /* Project object */ = {
222 | isa = PBXProject;
223 | attributes = {
224 | LastSwiftUpdateCheck = 1130;
225 | LastUpgradeCheck = 1130;
226 | ORGANIZATIONNAME = "Maximilian Nabokow";
227 | TargetAttributes = {
228 | 506B0EF523B803A000FE5982 = {
229 | CreatedOnToolsVersion = 11.3;
230 | };
231 | };
232 | };
233 | buildConfigurationList = 506B0EF123B803A000FE5982 /* Build configuration list for PBXProject "GitHubFollowers" */;
234 | compatibilityVersion = "Xcode 9.3";
235 | developmentRegion = en;
236 | hasScannedForEncodings = 0;
237 | knownRegions = (
238 | en,
239 | Base,
240 | );
241 | mainGroup = 506B0EED23B803A000FE5982;
242 | productRefGroup = 506B0EF723B803A000FE5982 /* Products */;
243 | projectDirPath = "";
244 | projectRoot = "";
245 | targets = (
246 | 506B0EF523B803A000FE5982 /* GitHubFollowers */,
247 | );
248 | };
249 | /* End PBXProject section */
250 |
251 | /* Begin PBXResourcesBuildPhase section */
252 | 506B0EF423B803A000FE5982 /* Resources */ = {
253 | isa = PBXResourcesBuildPhase;
254 | buildActionMask = 2147483647;
255 | files = (
256 | 506B0F0623B803AA00FE5982 /* LaunchScreen.storyboard in Resources */,
257 | 506B0F0323B803AA00FE5982 /* Assets.xcassets in Resources */,
258 | );
259 | runOnlyForDeploymentPostprocessing = 0;
260 | };
261 | /* End PBXResourcesBuildPhase section */
262 |
263 | /* Begin PBXShellScriptBuildPhase section */
264 | 50C6327C23D7307D00B9F4B7 /* Run Script: SwiftLint */ = {
265 | isa = PBXShellScriptBuildPhase;
266 | buildActionMask = 2147483647;
267 | files = (
268 | );
269 | inputFileListPaths = (
270 | );
271 | inputPaths = (
272 | );
273 | name = "Run Script: SwiftLint";
274 | outputFileListPaths = (
275 | );
276 | outputPaths = (
277 | );
278 | runOnlyForDeploymentPostprocessing = 0;
279 | shellPath = /bin/sh;
280 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint autocorrect\n swiftlint\nelse\n echo \"warning: Swiftlint is not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
281 | };
282 | /* End PBXShellScriptBuildPhase section */
283 |
284 | /* Begin PBXSourcesBuildPhase section */
285 | 506B0EF223B803A000FE5982 /* Sources */ = {
286 | isa = PBXSourcesBuildPhase;
287 | buildActionMask = 2147483647;
288 | files = (
289 | 506B0F2623B8134800FE5982 /* GFTextField.swift in Sources */,
290 | 506B0F1D23B809AC00FE5982 /* FavoritesVC.swift in Sources */,
291 | 50B9B0E723BC360600819BEB /* FollowersVC.swift in Sources */,
292 | 500D8E4323EF4A6100779E4D /* GFUserInfoHeaderVC.swift in Sources */,
293 | 50B9B0F123BC48D400819BEB /* UIViewController +Ext.swift in Sources */,
294 | 50A86C9923C0C025005D014A /* NetworkManager.swift in Sources */,
295 | 506E96F423D02B6900249802 /* GFEmptyStateView.swift in Sources */,
296 | 500D8E4523EF4B0400779E4D /* GFSecondaryTitleLabel.swift in Sources */,
297 | 506B0F2323B80D8400FE5982 /* GFButton.swift in Sources */,
298 | 50B9B0E423BC353500819BEB /* SearchVC +TextFieldDelegate.swift in Sources */,
299 | 506E96F823D03D5600249802 /* GFError.swift in Sources */,
300 | 506CAA3723C9746C00A00E66 /* FollowerCell.swift in Sources */,
301 | 506CAA3923C9750F00A00E66 /* GFAvatarImageView.swift in Sources */,
302 | 501CB27623BCCAAC00FA6407 /* User.swift in Sources */,
303 | 506E96F623D0354400249802 /* UserInfoVC.swift in Sources */,
304 | 50B9B0EF23BC41D000819BEB /* GFAlertVC.swift in Sources */,
305 | 501CB27423BCC9E600FA6407 /* Follower.swift in Sources */,
306 | 506B0F1923B806DC00FE5982 /* SearchVC.swift in Sources */,
307 | 50B9B0EB23BC3FCC00819BEB /* GFTitleLabel.swift in Sources */,
308 | 506B0EFA23B803A000FE5982 /* AppDelegate.swift in Sources */,
309 | 506E96F223D0198E00249802 /* UIHelper.swift in Sources */,
310 | 506B0EFC23B803A000FE5982 /* SceneDelegate.swift in Sources */,
311 | 500D8E4923EFB16E00779E4D /* GFItemInfoView.swift in Sources */,
312 | 50B9B0ED23BC410300819BEB /* GFBodyLabel.swift in Sources */,
313 | 500D8E4723EFA71100779E4D /* Constants.swift in Sources */,
314 | );
315 | runOnlyForDeploymentPostprocessing = 0;
316 | };
317 | /* End PBXSourcesBuildPhase section */
318 |
319 | /* Begin PBXVariantGroup section */
320 | 506B0F0423B803AA00FE5982 /* LaunchScreen.storyboard */ = {
321 | isa = PBXVariantGroup;
322 | children = (
323 | 506B0F0523B803AA00FE5982 /* Base */,
324 | );
325 | name = LaunchScreen.storyboard;
326 | sourceTree = "";
327 | };
328 | /* End PBXVariantGroup section */
329 |
330 | /* Begin XCBuildConfiguration section */
331 | 506B0F0823B803AA00FE5982 /* Debug */ = {
332 | isa = XCBuildConfiguration;
333 | buildSettings = {
334 | ALWAYS_SEARCH_USER_PATHS = NO;
335 | CLANG_ANALYZER_NONNULL = YES;
336 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
338 | CLANG_CXX_LIBRARY = "libc++";
339 | CLANG_ENABLE_MODULES = YES;
340 | CLANG_ENABLE_OBJC_ARC = YES;
341 | CLANG_ENABLE_OBJC_WEAK = YES;
342 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
343 | CLANG_WARN_BOOL_CONVERSION = YES;
344 | CLANG_WARN_COMMA = YES;
345 | CLANG_WARN_CONSTANT_CONVERSION = YES;
346 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
347 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
348 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
349 | CLANG_WARN_EMPTY_BODY = YES;
350 | CLANG_WARN_ENUM_CONVERSION = YES;
351 | CLANG_WARN_INFINITE_RECURSION = YES;
352 | CLANG_WARN_INT_CONVERSION = YES;
353 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
354 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
355 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
356 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
357 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
358 | CLANG_WARN_STRICT_PROTOTYPES = YES;
359 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
360 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
361 | CLANG_WARN_UNREACHABLE_CODE = YES;
362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
363 | COPY_PHASE_STRIP = NO;
364 | DEBUG_INFORMATION_FORMAT = dwarf;
365 | ENABLE_STRICT_OBJC_MSGSEND = YES;
366 | ENABLE_TESTABILITY = YES;
367 | GCC_C_LANGUAGE_STANDARD = gnu11;
368 | GCC_DYNAMIC_NO_PIC = NO;
369 | GCC_NO_COMMON_BLOCKS = YES;
370 | GCC_OPTIMIZATION_LEVEL = 0;
371 | GCC_PREPROCESSOR_DEFINITIONS = (
372 | "DEBUG=1",
373 | "$(inherited)",
374 | );
375 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
376 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
377 | GCC_WARN_UNDECLARED_SELECTOR = YES;
378 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
379 | GCC_WARN_UNUSED_FUNCTION = YES;
380 | GCC_WARN_UNUSED_VARIABLE = YES;
381 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
382 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
383 | MTL_FAST_MATH = YES;
384 | ONLY_ACTIVE_ARCH = YES;
385 | SDKROOT = iphoneos;
386 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
387 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
388 | };
389 | name = Debug;
390 | };
391 | 506B0F0923B803AA00FE5982 /* Release */ = {
392 | isa = XCBuildConfiguration;
393 | buildSettings = {
394 | ALWAYS_SEARCH_USER_PATHS = NO;
395 | CLANG_ANALYZER_NONNULL = YES;
396 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
397 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
398 | CLANG_CXX_LIBRARY = "libc++";
399 | CLANG_ENABLE_MODULES = YES;
400 | CLANG_ENABLE_OBJC_ARC = YES;
401 | CLANG_ENABLE_OBJC_WEAK = YES;
402 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
403 | CLANG_WARN_BOOL_CONVERSION = YES;
404 | CLANG_WARN_COMMA = YES;
405 | CLANG_WARN_CONSTANT_CONVERSION = YES;
406 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
407 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
408 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
409 | CLANG_WARN_EMPTY_BODY = YES;
410 | CLANG_WARN_ENUM_CONVERSION = YES;
411 | CLANG_WARN_INFINITE_RECURSION = YES;
412 | CLANG_WARN_INT_CONVERSION = YES;
413 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
414 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
415 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
417 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
418 | CLANG_WARN_STRICT_PROTOTYPES = YES;
419 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
420 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
421 | CLANG_WARN_UNREACHABLE_CODE = YES;
422 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
423 | COPY_PHASE_STRIP = NO;
424 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
425 | ENABLE_NS_ASSERTIONS = NO;
426 | ENABLE_STRICT_OBJC_MSGSEND = YES;
427 | GCC_C_LANGUAGE_STANDARD = gnu11;
428 | GCC_NO_COMMON_BLOCKS = YES;
429 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
430 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
431 | GCC_WARN_UNDECLARED_SELECTOR = YES;
432 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
433 | GCC_WARN_UNUSED_FUNCTION = YES;
434 | GCC_WARN_UNUSED_VARIABLE = YES;
435 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
436 | MTL_ENABLE_DEBUG_INFO = NO;
437 | MTL_FAST_MATH = YES;
438 | SDKROOT = iphoneos;
439 | SWIFT_COMPILATION_MODE = wholemodule;
440 | SWIFT_OPTIMIZATION_LEVEL = "-O";
441 | VALIDATE_PRODUCT = YES;
442 | };
443 | name = Release;
444 | };
445 | 506B0F0B23B803AA00FE5982 /* Debug */ = {
446 | isa = XCBuildConfiguration;
447 | buildSettings = {
448 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
449 | CODE_SIGN_STYLE = Automatic;
450 | DEVELOPMENT_TEAM = U5KYHQSZAZ;
451 | INFOPLIST_FILE = GitHubFollowers/Info.plist;
452 | LD_RUNPATH_SEARCH_PATHS = (
453 | "$(inherited)",
454 | "@executable_path/Frameworks",
455 | );
456 | PRODUCT_BUNDLE_IDENTIFIER = com.mnabokow.GitHubFollowers;
457 | PRODUCT_NAME = "$(TARGET_NAME)";
458 | SWIFT_VERSION = 5.0;
459 | TARGETED_DEVICE_FAMILY = "1,2";
460 | };
461 | name = Debug;
462 | };
463 | 506B0F0C23B803AA00FE5982 /* Release */ = {
464 | isa = XCBuildConfiguration;
465 | buildSettings = {
466 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
467 | CODE_SIGN_STYLE = Automatic;
468 | DEVELOPMENT_TEAM = U5KYHQSZAZ;
469 | INFOPLIST_FILE = GitHubFollowers/Info.plist;
470 | LD_RUNPATH_SEARCH_PATHS = (
471 | "$(inherited)",
472 | "@executable_path/Frameworks",
473 | );
474 | PRODUCT_BUNDLE_IDENTIFIER = com.mnabokow.GitHubFollowers;
475 | PRODUCT_NAME = "$(TARGET_NAME)";
476 | SWIFT_VERSION = 5.0;
477 | TARGETED_DEVICE_FAMILY = "1,2";
478 | };
479 | name = Release;
480 | };
481 | /* End XCBuildConfiguration section */
482 |
483 | /* Begin XCConfigurationList section */
484 | 506B0EF123B803A000FE5982 /* Build configuration list for PBXProject "GitHubFollowers" */ = {
485 | isa = XCConfigurationList;
486 | buildConfigurations = (
487 | 506B0F0823B803AA00FE5982 /* Debug */,
488 | 506B0F0923B803AA00FE5982 /* Release */,
489 | );
490 | defaultConfigurationIsVisible = 0;
491 | defaultConfigurationName = Release;
492 | };
493 | 506B0F0A23B803AA00FE5982 /* Build configuration list for PBXNativeTarget "GitHubFollowers" */ = {
494 | isa = XCConfigurationList;
495 | buildConfigurations = (
496 | 506B0F0B23B803AA00FE5982 /* Debug */,
497 | 506B0F0C23B803AA00FE5982 /* Release */,
498 | );
499 | defaultConfigurationIsVisible = 0;
500 | defaultConfigurationName = Release;
501 | };
502 | /* End XCConfigurationList section */
503 | };
504 | rootObject = 506B0EEE23B803A000FE5982 /* Project object */;
505 | }
506 |
--------------------------------------------------------------------------------