Last active
September 10, 2020 07:05
-
-
Save NeilsUltimateLab/91531aa15d05971c4792386706383faa to your computer and use it in GitHub Desktop.
WebRequest Classes and Structures
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
public class AppHUD { | |
public init() {} | |
public enum `Type` { | |
case definite(String?, String?) | |
case indefinite | |
case message(UIImage?, String?, String?, TimeInterval) | |
} | |
public enum Style { | |
case light | |
case dark | |
var blurEffect: UIBlurEffect.Style { | |
switch self { | |
case .light: | |
return .extraLight | |
case .dark: | |
return .dark | |
} | |
} | |
var textColor: UIColor { | |
switch self { | |
case .light: | |
return .black | |
case .dark: | |
return .white | |
} | |
} | |
} | |
var isAlreayRenderedOnWindow: Bool = false | |
public var animationDuration: TimeInterval = 0.2 | |
public var style: Style = .light | |
var backgroundView: UIView = { | |
return ViewFactory.view(forBackgroundColor: UIColor.black.withAlphaComponent(0.3), clipsToBounds: true) | |
}() | |
var containerView: UIView = { | |
let view = ViewFactory.view(forBackgroundColor: .clear, clipsToBounds: true) | |
view.layer.cornerRadius = 9.0 | |
view.layer.masksToBounds = true | |
return view | |
}() | |
lazy var visualEffectView: UIVisualEffectView = { | |
return ViewFactory.visualEffectView(blurEffectStyle: self.style.blurEffect) | |
}() | |
public var activityIndicator: UIActivityIndicatorView = { | |
return ViewFactory.activityIndicatorView(style: .whiteLarge, | |
hidesWhenStopped: true, | |
color: UIColor.gray) | |
}() | |
public lazy var titleLabel: UILabel = { | |
let label = ViewFactory.label(title: "", | |
textAlignment: .center, | |
numberOfLines: 1, | |
lineBreakMode: .byTruncatingTail) | |
label.font = UIFont.systemFont(ofSize: 19, weight: UIFont.Weight.medium) | |
label.textColor = self.style.textColor | |
return label | |
}() | |
public lazy var messageLabel: UILabel = { | |
let label = ViewFactory.label(title: "", | |
textAlignment: .center, | |
numberOfLines: 0, | |
lineBreakMode: .byWordWrapping) | |
label.font = UIFont.systemFont(ofSize: 17, weight: UIFont.Weight.regular) | |
label.textColor = self.style.textColor | |
return label | |
}() | |
public lazy var imageView: UIImageView = { | |
let imageView = UIImageView(frame: .zero) | |
imageView.contentMode = .scaleAspectFit | |
imageView.translatesAutoresizingMaskIntoConstraints = false | |
return imageView | |
}() | |
public var progressView: UIProgressView = { | |
return ViewFactory.progressView() | |
}() | |
lazy var stackView: UIStackView = { | |
let stackView = ViewFactory.stackView(forAxis: .vertical, | |
alignment: .center, | |
distribution: .fill, | |
spacing: 5.0) | |
return stackView | |
}() | |
private func constrainAllEdges(of childView: UIView, | |
to parentView: UIView, | |
top: CGFloat = 0, | |
left: CGFloat = 0, | |
right: CGFloat = 0, | |
bottom: CGFloat = 0) { | |
parentView.addSubview(childView) | |
NSLayoutConstraint.activate([ | |
childView.leftAnchor.constraint(equalTo: parentView.leftAnchor, constant: left), | |
childView.topAnchor.constraint(equalTo: parentView.topAnchor, constant: top), | |
childView.rightAnchor.constraint(equalTo: parentView.rightAnchor, constant: -right), | |
childView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor, constant: -bottom) | |
]) | |
} | |
private func constrainCenter(of childView: UIView, to parentView: UIView) { | |
parentView.addSubview(childView) | |
NSLayoutConstraint.activate([ | |
childView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor), | |
childView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor) | |
]) | |
} | |
private func setupHUD(for type: Type, completion: (()->Void)? = nil) { | |
guard let window = UIApplication.shared.keyWindow else { return } | |
constrainAllEdges(of: backgroundView, to: window) | |
backgroundView.alpha = 0 | |
let view = UIView(frame: .zero) | |
view.backgroundColor = .white | |
view.translatesAutoresizingMaskIntoConstraints = false | |
self.containerView = view | |
constrainAllEdges(of: visualEffectView, to: containerView) | |
constrainCenter(of: containerView, to: window) | |
self.containerView.layer.cornerRadius = 9.0 | |
self.containerView.clipsToBounds = true | |
let heightAnchorConstraint = containerView.heightAnchor.constraint(equalToConstant: 100) | |
let widthAnchorConstraint = containerView.widthAnchor.constraint(equalToConstant: 100) | |
let leftAnchorConstraint = containerView.leftAnchor.constraint(equalTo: window.leftAnchor, constant: 50) | |
switch type { | |
case .indefinite: | |
widthAnchorConstraint.isActive = true | |
heightAnchorConstraint.isActive = true | |
self.setIndifiniteHUDMode() | |
case let .definite(title, message): | |
widthAnchorConstraint.isActive = false | |
leftAnchorConstraint.isActive = true | |
self.setDefiniteHUDMode(title: title, message: message) | |
case .message(let image, let title, let message, let delay): | |
heightAnchorConstraint.isActive = false | |
widthAnchorConstraint.isActive = false | |
leftAnchorConstraint.isActive = true | |
self.setMessageHUDMode(image: image, title: title, message: message, delay: delay, completion: completion) | |
} | |
containerView.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) | |
containerView.alpha = 0 | |
window.endEditing(true) | |
} | |
private func setIndifiniteHUDMode() { | |
self.activityIndicator.removeFromSuperview() | |
self.titleLabel.removeFromSuperview() | |
self.messageLabel.removeFromSuperview() | |
self.progressView.removeFromSuperview() | |
constrainCenter(of: activityIndicator, to: containerView) | |
activityIndicator.startAnimating() | |
} | |
private func setDefiniteHUDMode(title: String? = nil, message: String? = nil) { | |
self.activityIndicator.removeFromSuperview() | |
self.titleLabel.removeFromSuperview() | |
self.messageLabel.removeFromSuperview() | |
self.progressView.removeFromSuperview() | |
self.progressView.setProgress(0, animated: false) | |
constrainAllEdges(of: stackView, to: containerView, top: 16, left: 16, right: 16, bottom: 16) | |
if let titleText = title { | |
stackView.addArrangedSubview(titleLabel) | |
titleLabel.text = titleText | |
} | |
if let messageText = message { | |
stackView.addArrangedSubview(messageLabel) | |
messageLabel.text = messageText | |
} | |
stackView.addArrangedSubview(progressView) | |
} | |
private func setMessageHUDMode(image: UIImage?, title: String?, message: String?, delay: TimeInterval, completion: (()->Void)?) { | |
self.activityIndicator.removeFromSuperview() | |
self.progressView.removeFromSuperview() | |
self.titleLabel.removeFromSuperview() | |
self.imageView.removeFromSuperview() | |
constrainAllEdges(of: stackView, to: containerView, top: 16, left: 16, right: 16, bottom: 16) | |
if let image = image { | |
stackView.addArrangedSubview(imageView) | |
imageView.image = image | |
} | |
if let titleText = title { | |
stackView.addArrangedSubview(titleLabel) | |
titleLabel.text = titleText | |
} | |
if let titleText = message { | |
stackView.addArrangedSubview(messageLabel) | |
messageLabel.text = titleText | |
} | |
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { | |
self.hideHUD(completion: completion) | |
} | |
} | |
private func animateHUD(completion: ((Bool)->Void)? = nil) { | |
UIView.animate(withDuration: animationDuration, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { | |
self.backgroundView.alpha = 1 | |
self.containerView.transform = .identity | |
self.containerView.alpha = 1 | |
}, completion: {(success)in | |
if success { | |
self.isAlreayRenderedOnWindow = true | |
completion?(success) | |
} | |
}) | |
} | |
public func hideHUD(completion: (()->Void)? = nil) { | |
UIView.animate(withDuration: animationDuration, delay: 0, options: .curveEaseIn, animations: { | |
self.backgroundView.alpha = 0 | |
self.containerView.alpha = 0 | |
}) { (succeed) in | |
if succeed { | |
self.backgroundView.removeFromSuperview() | |
self.containerView.removeFromSuperview() | |
self.activityIndicator.stopAnimating() | |
self.progressView.removeFromSuperview() | |
self.titleLabel.removeFromSuperview() | |
self.messageLabel.removeFromSuperview() | |
self.isAlreayRenderedOnWindow = false | |
completion?() | |
} | |
} | |
} | |
public func updateProgress(percetage: Float, shouldHideAfterCompletion: Bool, completion: (()->Void)? = nil) { | |
self.progressView.setProgress(percetage, animated: true) | |
if shouldHideAfterCompletion { | |
if percetage >= 1.0 { | |
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: { [weak self] in | |
self?.hideHUD(completion: completion) | |
}) | |
} | |
}else { | |
completion?() | |
} | |
} | |
public func showHUD() { | |
guard !isAlreayRenderedOnWindow else { return } | |
self.setupHUD(for: .indefinite) | |
self.animateHUD() | |
} | |
public func showHud(image: UIImage?, title: String?, message: String?, delay: TimeInterval, completion: (()->Void)? = nil) { | |
guard !isAlreayRenderedOnWindow else { return } | |
self.setupHUD(for: .message(image, title, message, delay), completion: completion) | |
self.animateHUD() | |
} | |
public func showDefiniteHUD(title: String?, message: String?) { | |
guard !isAlreayRenderedOnWindow else { return } | |
self.setupHUD(for: .definite(title, message)) | |
self.animateHUD() | |
} | |
} | |
extension AppHUD { | |
static let shared = AppHUD() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
extension FetchingView { | |
enum State { | |
case fetching | |
case fetched | |
case error(AppError) | |
} | |
} | |
class FetchingView { | |
var listView: UIView | |
var parentView: UIView | |
var centerYOffset: CGFloat = 0 { | |
didSet { | |
centerYConstraint?.constant = centerYOffset | |
} | |
} | |
private var centerYConstraint: NSLayoutConstraint! | |
// MARK: - Initialiser - | |
/// FetchingView Mechanism | |
/// | |
/// - Parameters: | |
/// - listView: This view could be your `tableView`, `collectionView`, `scrollView` or some other `UIView`. `listView` will hide when fetching state views were rendered. | |
/// - parentView: ParentView may be the `superview` of `listView`. ParentView will be the `containerView` for the fetching state views. | |
public init(listView: UIView, parentView: UIView, centerYOffset: CGFloat = -20) { | |
self.listView = listView | |
self.parentView = parentView | |
self.centerYOffset = centerYOffset | |
prepareViews() | |
} | |
// MARK: - State Machine - | |
/// Tracking states of web-request | |
public var fetchingState: State = .fetching { | |
didSet { | |
validate(state: fetchingState) | |
} | |
} | |
/// When `fetchingState` changes this method will be called. | |
/// | |
/// - Parameter state: `fetchingState` | |
private func validate(state: State) { | |
self.listView.isHidden = true | |
self.containerView.isHidden = false | |
switch state { | |
case .fetching: | |
self.imageView.removeFromSuperview() | |
self.buttonStackView.removeFromSuperview() | |
self.labelStackView.removeFromSuperview() | |
parentStackView.addArrangedSubview(loadingStackView) | |
indicatorView.startAnimating() | |
case .error(let error): | |
loadingStackView.removeFromSuperview() | |
buttonStackView.removeFromSuperview() | |
imageView.removeFromSuperview() | |
if let image = error.image { | |
imageView.image = image | |
parentStackView.addArrangedSubview(imageView) | |
} | |
parentStackView.addArrangedSubview(labelStackView) | |
titleLabel.text = error.title | |
descriptionLabel.text = error.subtitle | |
case .fetched: | |
self.listView.isHidden = false | |
self.containerView.isHidden = true | |
} | |
} | |
// MARK: - UIElements - | |
/// Parent `containerView` for the all stackViews. | |
lazy var containerView: UIView = { | |
let view = ViewFactory.view(forBackgroundColor: .clear, clipsToBounds: true) | |
view.addSubview(parentStackView) | |
parentStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true | |
parentStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true | |
parentStackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true | |
parentStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true | |
return view | |
}() | |
// MARK: UIStackViews | |
/// Parent StackView that contains `imageStackView` ,`titleStackView`, `textStackView`, `buttonStackView`. | |
var parentStackView: UIStackView = { | |
return ViewFactory.stackView(forAxis: .vertical, | |
alignment: .fill, | |
distribution: .fill, | |
spacing: 20) | |
}() | |
/// Loading StackView contains `UIActivityIndicatorView`, `UILabel`. | |
lazy var loadingStackView: UIStackView = { | |
let stackView = ViewFactory.stackView(forAxis: .vertical, | |
alignment: .center, | |
distribution: .fill, | |
spacing: 16) | |
stackView.addArrangedSubview(self.indicatorView) | |
stackView.addArrangedSubview(self.loadingLabel) | |
return stackView | |
}() | |
/// Label StackView contains `UILabel`s for `title` and `description` | |
lazy var labelStackView: UIStackView = { | |
let stackView = ViewFactory.stackView(forAxis: .vertical, | |
alignment: .center, | |
distribution: .fill, | |
spacing: 4) | |
stackView.addArrangedSubview(self.titleLabel) | |
stackView.addArrangedSubview(self.descriptionLabel) | |
return stackView | |
}() | |
/// Button StackView *will contain the response buttons provided by the* **API client** . | |
lazy var buttonStackView: UIStackView = { | |
return ViewFactory.stackView(forAxis: .vertical, | |
alignment: .center, | |
distribution: .fill, | |
spacing: 4) | |
}() | |
/// UIActivityIndicatorView will be rendered when `fetchingState` is `.fetching` | |
public var indicatorView: UIActivityIndicatorView = { | |
return ViewFactory.activityIndicatorView(style: .gray, | |
hidesWhenStopped: true) | |
}() | |
public var loadMoreIndicatorView: UIActivityIndicatorView = { | |
return ViewFactory.activityIndicatorView(style: .gray, hidesWhenStopped: true) | |
}() | |
// MARK: UILabels | |
/// `loadingLabel` will be rendered below `indicatorView` when `fetchingState` is `fetching`. | |
/// | |
/// The default text is "`LOADING`". | |
/// | |
/// `loadingLabel`'s text can be changed by API User. | |
public var loadingLabel: UILabel = { | |
let label = ViewFactory.label(title: "Loading".uppercased(), | |
textAlignment: .center, | |
textColor: .gray, | |
numberOfLines: 1) | |
label.font = UIFont.systemFont(ofSize: 13) | |
return label | |
}() | |
/// `titleLabel` will be rendered when `fetchingState` is `fetchedError(AppErrorProvider)`. | |
/// | |
/// The default text is empty text. | |
/// | |
/// `titleLabel`'s text will be changed `AppErrorProvider`'s `title` property. | |
public var titleLabel: UILabel = { | |
let label = ViewFactory.label(title: "", | |
textAlignment: .center, | |
textColor: .gray, | |
numberOfLines: 0, | |
lineBreakMode: .byWordWrapping) | |
label.font = UIFont.systemFont(ofSize: 21, weight: UIFont.Weight.medium) | |
return label | |
}() | |
/// `descriptionLabel` will be rendered when `fetchingState` is `fetchedError(AppErrorProvider)`. | |
/// | |
/// The default text is empty text. | |
/// | |
/// `descriptionLabel`'s text will be changed `AppErrorProvider`'s `subtitle` property. | |
public var descriptionLabel: UILabel = { | |
let label = ViewFactory.label(title: "", | |
textAlignment: .center, | |
textColor: .gray, | |
numberOfLines: 0, | |
lineBreakMode: .byWordWrapping) | |
label.font = UIFont.systemFont(ofSize: 17, weight: UIFont.Weight.regular) | |
return label | |
}() | |
// MARK: UIImageView | |
/// `imageView` will be rendered when `fetchingState` is `fetchedError(AppErrorProvider)`. | |
/// | |
/// The default image is `nil`. | |
/// | |
/// `imageView`'s image will be changed `AppErrorProvider`'s `image` property. | |
public var imageView: UIImageView = { | |
let imageView = ViewFactory.imageView(image: nil, | |
contentMode: .scaleAspectFit) | |
imageView.tintColor = UIColor.gray | |
return imageView | |
}() | |
// MARK: - Utilities | |
func prepareViews() { | |
self.parentView.addSubview(containerView) | |
containerView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor, constant: 0).isActive = true | |
containerView.leftAnchor.constraint(equalTo: parentView.leftAnchor, constant: 32).isActive = true | |
centerYConstraint = containerView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor, constant: self.centerYOffset) | |
centerYConstraint?.isActive = true | |
} | |
/// To add `UIButton`s to `buttonStackView`. `FetchingView` will not handle any `UIButton` touch `events` | |
/// | |
/// - Parameter buttons: `UIButton`'s `targetAction` must be set by API User. | |
public func add(_ buttons: [UIButton]) { | |
buttonStackView.arrangedSubviews.forEach({$0.removeFromSuperview()}) | |
for button in buttons { | |
buttonStackView.addArrangedSubview(button) | |
} | |
buttonStackView.removeFromSuperview() | |
parentStackView.addArrangedSubview(buttonStackView) | |
} | |
func stopWithAlert(error: AppError) { | |
if let rootVC = UIApplication.shared.keyWindow?.rootViewController { | |
let alert = UIAlertController(title: error.title, message: error.subtitle, preferredStyle: UIAlertController.Style.alert) | |
switch error { | |
case .sessionExpired: | |
let loginAction = UIAlertAction(title: "Login again", style: UIAlertAction.Style.default, handler: { (_) in | |
print("Redirect to login screen") | |
}) | |
alert.addAction(loginAction) | |
default: | |
break | |
} | |
let okAction = UIAlertAction(title: "Ok", style: .cancel, handler: { (_) in | |
print("Nothing will happen here") | |
}) | |
alert.addAction(okAction) | |
rootVC.present(alert, animated: true, completion: nil) | |
} | |
} | |
func stopWithAlert(with title: String? = nil, message: String) { | |
if let rootVC = UIApplication.shared.keyWindow?.rootViewController { | |
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert) | |
let okAction = UIAlertAction(title: "Ok", style: .cancel, handler: nil) | |
alert.addAction(okAction) | |
rootVC.present(alert, animated: true, completion: nil) | |
} | |
} | |
// MARK: - LoadMoreView - | |
func loadMoreView() -> UIView { | |
let view = UIView(frame: CGRect(x: 0, y: 0, width: self.listView.frame.width, height: 56)) | |
view.addSubview(loadMoreIndicatorView) | |
loadMoreIndicatorView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true | |
loadMoreIndicatorView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true | |
loadMoreIndicatorView.startAnimating() | |
return view | |
} | |
func stopLoadMore() { | |
loadMoreIndicatorView.stopAnimating() | |
loadMoreIndicatorView.removeFromSuperview() | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ViewFactory.swift | |
// FetchingView | |
// | |
// Created by Ratnesh Jain on 10/12/17. | |
// | |
import UIKit | |
public class ViewFactory { | |
/// A simple autolayout ready UIView with some default properties. | |
/// | |
/// - Parameters: | |
/// - backgroundColor: *default* is `.white` | |
/// - clipsToBounds: *default* is `false` | |
/// - Returns: returns UIView with above properties. | |
public static func view(forBackgroundColor backgroundColor: UIColor = .white, | |
clipsToBounds: Bool = false) -> UIView { | |
let view = UIView(frame: .zero) | |
view.translatesAutoresizingMaskIntoConstraints = false | |
view.backgroundColor = backgroundColor | |
view.clipsToBounds = clipsToBounds | |
return view | |
} | |
/// A simple autolayout ready `UIStackView` with some default properties. | |
/// | |
/// - Parameters: | |
/// - axis: *default* is `.vertical` | |
/// - alignment: *default* is `.fill` | |
/// - distribution: *default* is `.fill` | |
/// - spacing: *default* is `0` | |
/// - Returns: returns `UIStackView` with above properties. | |
public static func stackView(forAxis axis: NSLayoutConstraint.Axis = .vertical, | |
alignment: UIStackView.Alignment = .fill, | |
distribution: UIStackView.Distribution = .fill, | |
spacing: CGFloat = 0) -> UIStackView { | |
let stackView = UIStackView(frame: .zero) | |
stackView.translatesAutoresizingMaskIntoConstraints = false | |
stackView.axis = axis | |
stackView.alignment = alignment | |
stackView.distribution = distribution | |
stackView.spacing = spacing | |
return stackView | |
} | |
/// A simple autolayout ready `UILabel` with some default properties. | |
/// | |
/// - Parameters: | |
/// - textAlignment: *default* is `.center` | |
/// - textColor: *default* is `.black` | |
/// - numberOfLines: *default* is `0` | |
/// - lineBreakMode: *default* is `.byTruncatingTail` | |
/// - Returns: returns `UILabel` with above properties. | |
public static func label(title: String, | |
textAlignment: NSTextAlignment = .center, | |
textColor: UIColor = .black, | |
numberOfLines: Int = 0, | |
lineBreakMode: NSLineBreakMode = .byTruncatingTail) -> UILabel { | |
let label = UILabel(frame: .zero) | |
label.translatesAutoresizingMaskIntoConstraints = false | |
label.text = title | |
label.textAlignment = textAlignment | |
label.textColor = textColor | |
label.numberOfLines = numberOfLines | |
label.lineBreakMode = lineBreakMode | |
return label | |
} | |
/// A simple autolayout ready `UIImageView` with some default properties. | |
/// | |
/// - Parameters: | |
/// - image: *default* is `nil` | |
/// - contentMode: *default* is `.center` | |
/// - Returns: returns `UILabel` with above properties. | |
public static func imageView(image: UIImage? = nil, | |
contentMode: UIView.ContentMode = .center) -> UIImageView { | |
let imageView = UIImageView(frame: .zero) | |
imageView.translatesAutoresizingMaskIntoConstraints = false | |
imageView.image = image | |
imageView.contentMode = contentMode | |
return imageView | |
} | |
/// A simple autolayout ready `UIActivityIndicatorView` with some default properties. | |
/// | |
/// - Parameters: | |
/// - style: *default* is `.gray` | |
/// - hidesWhenStopped: *default* is `true` | |
/// - color: *default* is `nil` | |
/// - Returns: returns `UIActivityIndicatorView` with above properties. | |
public static func activityIndicatorView(style: UIActivityIndicatorView.Style = .gray, | |
hidesWhenStopped: Bool = true, | |
color: UIColor? = nil) -> UIActivityIndicatorView { | |
let indicatorView = UIActivityIndicatorView(style: style) | |
indicatorView.translatesAutoresizingMaskIntoConstraints = false | |
indicatorView.hidesWhenStopped = hidesWhenStopped | |
indicatorView.color = color | |
return indicatorView | |
} | |
/// A simple autolayout ready `UIButton` with some default properties. | |
/// | |
/// - Parameters: | |
/// - type: *default* is `.system` | |
/// - title: *default* is `"Button"` | |
/// - image: *default* is `nil` | |
/// - tintColor: *default* is `.white` | |
/// - Returns: returns `UIButton` with above properties. | |
public static func button(type: UIButton.ButtonType = .system, | |
title: String? = "Button", | |
image: UIImage? = nil, | |
tintColor: UIColor = .white) -> UIButton { | |
let button = UIButton(type: type) | |
button.translatesAutoresizingMaskIntoConstraints = false | |
button.setTitle(title, for: .normal) | |
button.setImage(image, for: .normal) | |
button.tintColor = tintColor | |
return button | |
} | |
/// A simple autolayout ready `UISegmentedControl` with some default properties. | |
/// | |
/// - Parameter titles: is a `variadic` parameter. | |
/// - Returns: returns `UISegmentedControl` with above properties. | |
public static func segmentedControl(titles: String...) -> UISegmentedControl { | |
let segmentedControl = UISegmentedControl(frame: .zero) | |
segmentedControl.translatesAutoresizingMaskIntoConstraints = false | |
for (index, title) in titles.enumerated() { | |
segmentedControl.insertSegment(withTitle: title, at: index, animated: false) | |
} | |
segmentedControl.selectedSegmentIndex = 0 | |
return segmentedControl | |
} | |
public static func visualEffectView(blurEffectStyle style: UIBlurEffect.Style) -> UIVisualEffectView { | |
let blurEffect = UIBlurEffect(style: style) | |
let visualEffectView = UIVisualEffectView(effect: blurEffect) | |
visualEffectView.translatesAutoresizingMaskIntoConstraints = false | |
return visualEffectView | |
} | |
public static func progressView(progressViewStyle style: UIProgressView.Style = .default, | |
isUserInterationEnabled interactionEnabled: Bool = false, | |
trackImage: UIImage = UIImage(), | |
trackTintColor: UIColor = .lightGray, | |
progressTintColor: UIColor = UIColor.blue) -> UIProgressView { | |
let pv = UIProgressView(progressViewStyle: style) | |
pv.isUserInteractionEnabled = interactionEnabled | |
pv.trackImage = trackImage | |
pv.trackTintColor = trackTintColor | |
pv.progressTintColor = progressTintColor | |
return pv | |
} | |
public static func shadowButton() -> UIButton { | |
let button = UIButton(frame: .zero) | |
button.backgroundColor = #colorLiteral(red: 0.007843137255, green: 0.6235294118, blue: 0.8588235294, alpha: 1) | |
button.layer.cornerRadius = button.frame.height/2 | |
button.layer.shadowColor = #colorLiteral(red: 0.5529411765, green: 0.7725490196, blue: 0.8666666667, alpha: 1) | |
button.layer.shadowOpacity = 1.0 | |
button.layer.shadowRadius = 4 | |
button.layer.shadowOffset = CGSize(width: 0, height: 0) | |
return button | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum AppConfig { | |
static let scheme: URLScheme = .https | |
static let host: URLHost = .live | |
} | |
enum URLPath { | |
case login | |
} | |
extension URLPath { | |
var path: String { | |
switch self { | |
case .login: | |
return "/login" | |
} | |
} | |
var queryItems: [URLQueryItem]? { | |
return nil | |
} | |
var url: URL? { | |
var urlComponent = URLComponents() | |
urlComponent.scheme = AppConfig.scheme.rawValue | |
urlComponent.host = AppConfig.host.rawValue | |
urlComponent.path = AppConfig.host.fixedPath + self.path | |
urlComponent.queryItems = self.queryItems | |
return urlComponent.url | |
} | |
} | |
extension URLPath: CustomStringConvertible { | |
var description: String { | |
return "\(self.url?.absoluteString ?? "Can not get the url!")" | |
} | |
} | |
extension URLPath { | |
func url(for host: URLHost) -> URL? { | |
var urlComponent = URLComponents() | |
urlComponent.scheme = AppConfig.scheme.rawValue | |
urlComponent.host = host.rawValue | |
urlComponent.path = host.fixedPath + self.path | |
urlComponent.queryItems = self.queryItems | |
return urlComponent.url | |
} | |
} | |
enum URLScheme: String { | |
case https | |
case http | |
} | |
enum URLHost: String { | |
case live = "www.myshopdata.com" | |
var host: String { | |
return self.rawValue | |
} | |
var fixedPath: String { | |
switch self { | |
case .live: | |
return "/api/gpn" | |
} | |
} | |
} | |
typealias JSONType = [String: Any] | |
enum HTTPMethod { | |
case get | |
case post(parameter: JSONType) | |
} | |
import Alamofire | |
extension HTTPMethod { | |
var parameter: JSONType? { | |
switch self { | |
case .post(let parameter): | |
return parameter | |
default: | |
return nil | |
} | |
} | |
var alamofireMethod: Alamofire.HTTPMethod { | |
switch self { | |
case .get: | |
return Alamofire.HTTPMethod.get | |
case .post: | |
return Alamofire.HTTPMethod.post | |
} | |
} | |
} | |
class RequestToken { | |
weak var urlTask: URLSessionTask? | |
init(task: URLSessionTask?) { | |
self.urlTask = task | |
} | |
func cancel() { | |
self.urlTask?.cancel() | |
} | |
} | |
enum AppError: Error, Equatable { | |
case noRecords(image: UIImage?, title: String, message: String?) | |
case canNotParse | |
case notReachable | |
case requestTimedOut | |
case sessionExpired | |
case serverError(String) | |
} | |
extension AppError { | |
var title: String? { | |
switch self { | |
case .noRecords(_, let title, _): | |
return title | |
case .canNotParse: | |
return "Oops" | |
case .notReachable: | |
return "Oops" | |
case .requestTimedOut: | |
return "Request Timed Out" | |
case .sessionExpired: | |
return "Session Expired!" | |
case .serverError: | |
return "Error" | |
} | |
} | |
var subtitle: String? { | |
switch self { | |
case .noRecords(_, _, let message): | |
return message | |
case .canNotParse: | |
return "Something went wrong" | |
case .notReachable: | |
return "Internet connection not reachable." | |
case .requestTimedOut: | |
return nil | |
case .sessionExpired: | |
return "Please login again to continue." | |
case .serverError(let message): | |
return message | |
} | |
} | |
var image: UIImage? { | |
return nil | |
} | |
} | |
enum Result<A> { | |
case value(A) | |
case error(AppError) | |
} | |
extension Result { | |
var value: A? { | |
switch self { | |
case .value(let val): | |
return val | |
default: | |
return nil | |
} | |
} | |
var error: AppError? { | |
switch self { | |
case .error(let err): | |
return err | |
default: | |
return nil | |
} | |
} | |
} | |
extension Decodable { | |
static func decode<A>(from data: Data) -> Result<A> where A: Decodable { | |
do { | |
let jsonDecoded = try JSONDecoder().decode(A.self, from: data) | |
return .value(jsonDecoded) | |
} catch { | |
print("Can not parse: \(A.self), because of error: ", error) | |
return .error(AppError.canNotParse) | |
} | |
} | |
} | |
struct WebResource<A: Decodable> { | |
var path: URLPath | |
var header: [String: String]? | |
var method: HTTPMethod | |
func request(completion: @escaping (Result<A>)->Void) -> RequestToken? { | |
return WebResourceManager.shared.fetchResource(self, completion: completion) | |
} | |
} | |
extension WebResource: CustomStringConvertible { | |
var description: String { | |
let dic = NSMutableDictionary() | |
dic["ParseDataType"] = A.self | |
dic["URL"] = self.path | |
dic["HTTPMethod"] = self.method | |
dic["Header"] = self.header ?? [:] | |
dic["Parameters"] = self.method.parameter ?? [:] | |
return "\(String.consoleSeperator) WebResource = \(dic) \(String.consoleSeperator)" | |
} | |
} | |
import Alamofire | |
class WebResourceManager { | |
static let shared = WebResourceManager() | |
static let timeout: TimeInterval = 30.0 | |
static var sessionManager: SessionManager = { | |
let config = URLSessionConfiguration.default | |
config.timeoutIntervalForRequest = timeout | |
let manager = SessionManager(configuration: config) | |
return manager | |
}() | |
var isReachable: Bool { | |
return NetworkReachabilityManager()?.isReachable ?? false | |
} | |
func fetchResource<A>(_ resource: WebResource<A>, completion: @escaping (Result<A>)-> Void) -> RequestToken? where A: Decodable { | |
guard let url = resource.path.url else { | |
return nil | |
} | |
let header = resource.header | |
let paremeter = resource.method.parameter | |
let paremeterEncoding = URLEncoding.default | |
print("\(String.consoleSeperator)Webservice call for resource: \(resource.path)...") | |
guard isReachable else { | |
completion(.error(.notReachable)) | |
UIApplication.shared.isNetworkActivityIndicatorVisible = false | |
return nil | |
} | |
UIApplication.shared.isNetworkActivityIndicatorVisible = true | |
let dataRequest = WebResourceManager.sessionManager.request(url, method: resource.method.alamofireMethod, parameters: paremeter, encoding: paremeterEncoding, headers: header).responseData { (response) in | |
UIApplication.shared.isNetworkActivityIndicatorVisible = false | |
guard response.response?.url == url else { | |
if let error = response.result.error { | |
if error._code == NSURLErrorTimedOut { | |
completion(.error(AppError.requestTimedOut)) | |
} else { | |
completion(.error(AppError.serverError(error.localizedDescription))) | |
} | |
} | |
return | |
} | |
if let code = response.response?.statusCode { | |
if code == 401 { | |
completion(.error(.sessionExpired)) | |
return | |
} | |
print("Response Code = \(code)") | |
} | |
if let respondedError = response.result.error { | |
completion(.error(.serverError(respondedError.localizedDescription))) | |
return | |
} | |
if let data = response.result.value { | |
completion(A.decode(from: data)) | |
return | |
} else { | |
completion(.error(.canNotParse)) | |
} | |
}.responseString { (response) in | |
print("\(String.consoleSeperator)Webservice responded for Web Resource: \(resource) \nwith Response: \(response.debugDescription)") | |
} | |
let task = dataRequest.task | |
return RequestToken(task: task) | |
} | |
func uploadResource<A>(_ resource: WebResource<A>, completion: @escaping (Result<A>)->Void, progressCompletion: ((Double)->Void)?) where A: Decodable { | |
guard let url = resource.path.url else { | |
return | |
} | |
let parameter = resource.method.parameter | |
let header = resource.header | |
guard isReachable else { | |
UIApplication.shared.isNetworkActivityIndicatorVisible = false | |
completion(.error(.notReachable)) | |
return | |
} | |
print("Calling Webservice call for resource: \(resource.path)....") | |
UIApplication.shared.isNetworkActivityIndicatorVisible = true | |
Alamofire.upload(multipartFormData: { (multipartFormData) in | |
if let parameters = parameter { | |
for (key, value) in parameters { | |
let (url, mimeType) = MediaType.generateMimeType(key: key, value: value) | |
if let url = url { | |
multipartFormData.append(url, withName: key, fileName: url.fileName.appending(".").appending(url.pathExtension), mimeType: mimeType) | |
}else { | |
if let stringValue = value as? String, let data = stringValue.data(using: String.Encoding.utf8) { | |
multipartFormData.append(data, withName: key, mimeType: "text/plain") | |
} | |
} | |
} | |
} | |
}, to: url, headers: header) { (encodingResult) in | |
switch encodingResult { | |
case .success(let upload, _, _): | |
UIApplication.shared.isNetworkActivityIndicatorVisible = false | |
upload.uploadProgress(closure: {(progress: Progress) in | |
progressCompletion?(progress.fractionCompleted) | |
}) | |
upload.responseData(completionHandler: { (response) in | |
UIApplication.shared.isNetworkActivityIndicatorVisible = false | |
// print("\(String.consoleSeperator)Webservice responded for : \(resource) \nwith: \(response)") | |
print("\(String.consoleSeperator)Webservice responded for Web Resource: \(resource) \nwith Response: \(response.debugDescription)") | |
if let code = response.response?.statusCode { | |
if code == 401 { | |
completion(.error(.sessionExpired)) | |
return | |
} | |
} | |
if let data = response.result.value { | |
completion(A.decode(from: data)) | |
} else { | |
guard response.response?.url == url else { | |
return | |
} | |
if let error = response.result.error { | |
completion(.error(.serverError(error.localizedDescription))) | |
} | |
} | |
}) | |
case .failure(let error): | |
print("Error : \(error)") | |
} | |
} | |
} | |
} | |
enum MediaType: String { | |
case doc, dot, docx, | |
dotx, dotm, docm | |
case txt, rtf | |
case ppt, pot, pps, ppa, | |
pptx, potx, ppsx, ppam, pptm, | |
potm, ppsm | |
case xls, xlt, xla, xlsx, xltx, | |
xlsm, xltm, xlam, xlsb | |
case pdf | |
case jpg, png, jpeg | |
} | |
extension MediaType { | |
var mimeType: String { | |
switch self { | |
case .doc, .dot, .docx, | |
.dotx, .dotm, .docm: | |
return "application/msword" | |
case .txt, .rtf: | |
return "text/plain" | |
case .ppt, .pot, .pps, .ppa, | |
.pptx, .potx, .ppsx, .ppam, .pptm, | |
.potm, .ppsm: | |
return "application/vnd.openxmlformats-officedocument.presentationml.presentation" | |
case .pdf: | |
return "application/pdf" | |
case .xls, .xlt, .xla, .xlsx, .xltx, | |
.xlsm, .xltm, .xlam, .xlsb: | |
return "application/vnd.ms-excel" | |
case .png: | |
return "image/png" | |
case .jpg, .jpeg: | |
return "image/jpeg" | |
} | |
} | |
} | |
extension MediaType { | |
static func generateMimeType(key: String, value: Any) -> (url: URL?, mimeType: String) { | |
if let url = value as? URL { | |
guard let mediaType = MediaType(rawValue: url.pathExtension.lowercased()) else { | |
return (nil, "") | |
} | |
return (value as? URL, mediaType.mimeType) | |
} | |
return (nil, "") | |
} | |
} | |
extension String { | |
static var consoleSeperator: String { | |
return "-------------------------" | |
} | |
} | |
extension URL { | |
var fileName: String { | |
return self.deletingPathExtension().lastPathComponent | |
} | |
var fileExtension: String { | |
return self.pathExtension | |
} | |
var fileNameWithExtension: String { | |
return fileName.appending(".").appending(fileExtension) | |
} | |
var image: UIImage? { | |
if FileManager.default.fileExists(atPath: self.path) { | |
return UIImage(contentsOfFile: path) | |
} | |
return nil | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment