diff --git a/GHFollowers.xcodeproj/project.pbxproj b/GHFollowers.xcodeproj/project.pbxproj index b6e5947..16a2c81 100644 --- a/GHFollowers.xcodeproj/project.pbxproj +++ b/GHFollowers.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 7DA45F6F242BC47D00AB426F /* FollowerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA45F6E242BC47D00AB426F /* FollowerCell.swift */; }; 7DA45F71242BC5C100AB426F /* GFAvatarImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA45F70242BC5C100AB426F /* GFAvatarImageView.swift */; }; 7DA45F73242E332000AB426F /* UIHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA45F72242E332000AB426F /* UIHelper.swift */; }; + 7DA45F7A242FB95700AB426F /* GFEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA45F79242FB95700AB426F /* GFEmptyStateView.swift */; }; 7DFAC88A242A86D200F5780C /* GFAlertContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DFAC889242A86D200F5780C /* GFAlertContainerView.swift */; }; /* End PBXBuildFile section */ @@ -53,6 +54,7 @@ 7DA45F6E242BC47D00AB426F /* FollowerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowerCell.swift; sourceTree = ""; }; 7DA45F70242BC5C100AB426F /* GFAvatarImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFAvatarImageView.swift; sourceTree = ""; }; 7DA45F72242E332000AB426F /* UIHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIHelper.swift; sourceTree = ""; }; + 7DA45F79242FB95700AB426F /* GFEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFEmptyStateView.swift; sourceTree = ""; }; 7DFAC889242A86D200F5780C /* GFAlertContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GFAlertContainerView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -112,13 +114,12 @@ 7D98BB4E242A7BD5006C53E9 /* Components */ = { isa = PBXGroup; children = ( + 7DA45F78242FB8DB00AB426F /* Labels */, + 7DA45F77242FB8C300AB426F /* ImageViews */, + 7DA45F76242FB8A000AB426F /* TextFields */, + 7DA45F75242FB89900AB426F /* Buttons */, + 7DA45F74242FB89100AB426F /* Views */, 7DA45F6D242BC44F00AB426F /* Cells */, - 7D98BB3F242917E4006C53E9 /* GFButton.swift */, - 7D98BB4124291DF3006C53E9 /* GFTextField.swift */, - 7D98BB45242A52B7006C53E9 /* GFTitleLabel.swift */, - 7D98BB47242A5507006C53E9 /* GFBodyLabel.swift */, - 7DFAC889242A86D200F5780C /* GFAlertContainerView.swift */, - 7DA45F70242BC5C100AB426F /* GFAvatarImageView.swift */, ); path = Components; sourceTree = ""; @@ -176,6 +177,48 @@ path = Cells; sourceTree = ""; }; + 7DA45F74242FB89100AB426F /* Views */ = { + isa = PBXGroup; + children = ( + 7DFAC889242A86D200F5780C /* GFAlertContainerView.swift */, + 7DA45F79242FB95700AB426F /* GFEmptyStateView.swift */, + ); + path = Views; + sourceTree = ""; + }; + 7DA45F75242FB89900AB426F /* Buttons */ = { + isa = PBXGroup; + children = ( + 7D98BB3F242917E4006C53E9 /* GFButton.swift */, + ); + path = Buttons; + sourceTree = ""; + }; + 7DA45F76242FB8A000AB426F /* TextFields */ = { + isa = PBXGroup; + children = ( + 7D98BB4124291DF3006C53E9 /* GFTextField.swift */, + ); + path = TextFields; + sourceTree = ""; + }; + 7DA45F77242FB8C300AB426F /* ImageViews */ = { + isa = PBXGroup; + children = ( + 7DA45F70242BC5C100AB426F /* GFAvatarImageView.swift */, + ); + path = ImageViews; + sourceTree = ""; + }; + 7DA45F78242FB8DB00AB426F /* Labels */ = { + isa = PBXGroup; + children = ( + 7D98BB45242A52B7006C53E9 /* GFTitleLabel.swift */, + 7D98BB47242A5507006C53E9 /* GFBodyLabel.swift */, + ); + path = Labels; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -251,6 +294,7 @@ 7D98BB3E242907BB006C53E9 /* FavoriteListVC.swift in Sources */, 7D98BB40242917E4006C53E9 /* GFButton.swift in Sources */, 7D98BB3C24290771006C53E9 /* SearchVC.swift in Sources */, + 7DA45F7A242FB95700AB426F /* GFEmptyStateView.swift in Sources */, 7D98BB4A242A55ED006C53E9 /* GFAlertVC.swift in Sources */, 7DA45F66242B760500AB426F /* User.swift in Sources */, 7DA45F6F242BC47D00AB426F /* FollowerCell.swift in Sources */, diff --git a/GHFollowers.xcodeproj/project.xcworkspace/xcuserdata/vasilis.xcuserdatad/UserInterfaceState.xcuserstate b/GHFollowers.xcodeproj/project.xcworkspace/xcuserdata/vasilis.xcuserdatad/UserInterfaceState.xcuserstate index b762ac7..18bca15 100644 Binary files a/GHFollowers.xcodeproj/project.xcworkspace/xcuserdata/vasilis.xcuserdatad/UserInterfaceState.xcuserstate and b/GHFollowers.xcodeproj/project.xcworkspace/xcuserdata/vasilis.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/GHFollowers/Components/GFButton.swift b/GHFollowers/Components/Buttons/GFButton.swift similarity index 100% rename from GHFollowers/Components/GFButton.swift rename to GHFollowers/Components/Buttons/GFButton.swift diff --git a/GHFollowers/Components/GFAvatarImageView.swift b/GHFollowers/Components/ImageViews/GFAvatarImageView.swift similarity index 100% rename from GHFollowers/Components/GFAvatarImageView.swift rename to GHFollowers/Components/ImageViews/GFAvatarImageView.swift diff --git a/GHFollowers/Components/GFBodyLabel.swift b/GHFollowers/Components/Labels/GFBodyLabel.swift similarity index 100% rename from GHFollowers/Components/GFBodyLabel.swift rename to GHFollowers/Components/Labels/GFBodyLabel.swift diff --git a/GHFollowers/Components/GFTitleLabel.swift b/GHFollowers/Components/Labels/GFTitleLabel.swift similarity index 100% rename from GHFollowers/Components/GFTitleLabel.swift rename to GHFollowers/Components/Labels/GFTitleLabel.swift diff --git a/GHFollowers/Components/GFTextField.swift b/GHFollowers/Components/TextFields/GFTextField.swift similarity index 100% rename from GHFollowers/Components/GFTextField.swift rename to GHFollowers/Components/TextFields/GFTextField.swift diff --git a/GHFollowers/Components/GFAlertContainerView.swift b/GHFollowers/Components/Views/GFAlertContainerView.swift similarity index 100% rename from GHFollowers/Components/GFAlertContainerView.swift rename to GHFollowers/Components/Views/GFAlertContainerView.swift diff --git a/GHFollowers/Components/Views/GFEmptyStateView.swift b/GHFollowers/Components/Views/GFEmptyStateView.swift new file mode 100644 index 0000000..d2c2daf --- /dev/null +++ b/GHFollowers/Components/Views/GFEmptyStateView.swift @@ -0,0 +1,64 @@ +// +// GFEmptyStateView.swift +// GHFollowers +// +// Created by Vasileios Gkreen on 28/03/2020. +// Copyright © 2020 Vasileios Gkreen. All rights reserved. +// + +import UIKit + +class GFEmptyStateView: UIView { + + let messageLabel = GFTitleLabel(textAlignment: .center, fontSize: 28) + let logoImageView = UIImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + configure() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // with the customs inits we can pass params when we call this view and initialize them + init(message: String) { + super.init(frame: .zero) + messageLabel.text = message + configure() + } + + + private func configure() { + + addSubview(messageLabel) + addSubview(logoImageView) + + messageLabel.numberOfLines = 3 + messageLabel.textColor = .secondaryLabel + + logoImageView.image = UIImage(named: "empty-state-logo") + logoImageView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + //center item on screen verrtically and then push up a litle bit + messageLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor, constant: -150), + messageLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 40), + messageLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -40), + // hard code height since we know the text message already + messageLabel.heightAnchor.constraint(equalToConstant: 200), + + // take the image and make it 1.3 times larger then it curretn width (scale: 1.3) + logoImageView.widthAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1.3), + // make height same as width (height will be 1.3 times larger) + logoImageView.heightAnchor.constraint(equalTo: self.widthAnchor, multiplier: 1.3), + // we use 200 to push the element TOWARDS the edge and not pull it like we did any other time + logoImageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 170), + // same as above (push insted of pull) so not using a negative number in constant + logoImageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 40) + ]) + } + + +} diff --git a/GHFollowers/Extensions/UIViewController+EXT.swift b/GHFollowers/Extensions/UIViewController+EXT.swift index 39820de..ed4d272 100644 --- a/GHFollowers/Extensions/UIViewController+EXT.swift +++ b/GHFollowers/Extensions/UIViewController+EXT.swift @@ -8,10 +8,15 @@ import UIKit + +// We cant save variables inside an extension so we decalre a 'global' variable inside this file +// This global var with the keyword 'fileprivate' is available only in this file and not the entire programm +fileprivate var containerView: UIView! + + // This extension is to be applied to all UIViewControllers used in this app extension UIViewController { - /* we need to set this alert to be presented in the main thread because sometimes we will need to call it from a background thread and it's ilegal to mount UI elements called from the background thread @@ -29,4 +34,56 @@ extension UIViewController { } + + func showLoadingView() { + // init container view and set it to fill the entire screen + containerView = UIView(frame: view.bounds) + // add the container view into the VC view (which ever is going to call this func) + view.addSubview(containerView) + + containerView.backgroundColor = .systemBackground + containerView.alpha = 0 + + // animate alpha + UIView.animate(withDuration: 0.25) { + containerView.alpha = 0.8 + } + + + // add activity indicator + let activityIndicator = UIActivityIndicatorView(style: .large) + containerView.addSubview(activityIndicator) + + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + + // center indicator on containerView + NSLayoutConstraint.activate([ + activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor), + activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor) + ]) + + activityIndicator.startAnimating() + + } + + func dismissLoadingView() { + // We will always going to dismiss Loading View from a background thread + // because we call this function from the Network manager closure (promise) + // to avoid that we need to throw it in the main thread + DispatchQueue.main.async { + containerView.removeFromSuperview() + containerView = nil + } + } + + + // calling this function from a VC we wiil pass the message and a the view so we know where to constrain it + func showEmptyStateView(with message: String, in view: UIView) { + let emptyStateView = GFEmptyStateView(message: message) + // fill up the entire screen + emptyStateView.frame = view.bounds + // add it to the VC subView + view.addSubview(emptyStateView) + } + } diff --git a/GHFollowers/Screens/FollowerListVC.swift b/GHFollowers/Screens/FollowerListVC.swift index 1e839a9..11c751b 100644 --- a/GHFollowers/Screens/FollowerListVC.swift +++ b/GHFollowers/Screens/FollowerListVC.swift @@ -102,10 +102,17 @@ class FollowerListVC: UIViewController { // CHECK PLAYGROUND FOR DETAILS // [week self] --> capture list // when a self becomes weak the value of that becomes optional + + // show activity indicator before the network call --- EXTENSIONS FILE --- + showLoadingView() + NetworkManager.shared.getFollowers(for: username, page: page) { [weak self] result in // instead of adding self?. to all values below use guard for the self guard let self = self else { return } + // call to dismiss the loading indicator --- EXTENSIONS FILE --- + self.dismissLoadingView() + switch result { case .success(let followers): // if no more follower turn switch off @@ -114,6 +121,17 @@ class FollowerListVC: UIViewController { // save the response in the followers array we declared and use append to save the next 100 results (concat, push) self.followers.append(contentsOf: followers) // call update data to keep the list updated with latest response + + // control if array is returned empty and show empty state view + if self.followers.isEmpty { + let message = "This user doesn't have any followers. Go follow them! 😜" + DispatchQueue.main.async { + self.showEmptyStateView(with: message, in: self.view) + } + return + } + + // call to update snapshot self.updateData() case .failure(let error): @@ -138,7 +156,6 @@ extension FollowerListVC: UICollectionViewDelegate { // get height of scrollView on screen let scrollViewHeight = scrollView.frame.size.height - // if our offset is more than the entire scroll distance plus the screenheight // it means we reached the end so we need to call the next page if offsetY > contentHeight - scrollViewHeight {