diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..ce28000 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,92 @@ +excluded: + - Carthage + - Pods +disabled_rules: + - file_header + - identifier_name +opt_in_rules: + - anyobject_protocol + - array_init + - attributes + - closure_end_indentation + - closure_spacing + - collection_alignment + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - discouraged_object_literal + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - explicit_init + - extension_access_modifier + - fallthrough + - fatal_error_message + - file_header + - file_name + - first_where + - flatmap_over_map_reduce + - identical_operands + - joined_default_parameter + - legacy_random + - let_var_whitespace + - last_where + - legacy_multiple + - literal_expression_end_indentation + - lower_acl_than_parent + - modifier_order + - nimble_operator + - nslocalizedstring_key + - number_separator +# - object_literal + - operator_usage_whitespace + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - single_test_class + - sorted_first_last +# - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces +# - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition + +force_cast: warning +force_try: + severity: error +line_length: + warning: 100 + ignores_comments: true + ignores_urls: true +reporter: "xcode" +number_separator: + minimum_length: 5 +file_name: + excluded: + - main.swift + - LinuxMain.swift +type_name: + min_length: 3 + max_length: 50 diff --git a/Example/AppDelegate.swift b/Example/AppDelegate.swift index 0e200f3..1947e2f 100644 --- a/Example/AppDelegate.swift +++ b/Example/AppDelegate.swift @@ -47,4 +47,3 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } } - diff --git a/Example/Extensions/UIButton+Extensions.swift b/Example/Extensions/UIButton+Extensions.swift index fc96d21..9dc1f12 100644 --- a/Example/Extensions/UIButton+Extensions.swift +++ b/Example/Extensions/UIButton+Extensions.swift @@ -10,7 +10,7 @@ import Foundation import UIKit extension UIButton { - + @discardableResult static func addToCenter( of view: UIView, @@ -22,14 +22,14 @@ extension UIButton { button.setTitle(title, for: .normal) button.addTarget(target, action: selector, for: .touchUpInside) button.setTitleColor(.systemBlue, for: .normal) - + view.addSubview(button) button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.centerXAnchor.constraint(equalTo: view.centerXAnchor), button.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) - + return button } } diff --git a/Example/SceneDelegate.swift b/Example/SceneDelegate.swift index 82c249e..4f024a1 100644 --- a/Example/SceneDelegate.swift +++ b/Example/SceneDelegate.swift @@ -22,7 +22,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let windowScene = (scene as? UIWindowScene) else { return } - + let window = UIWindow(windowScene: windowScene) window.rootViewController = UINavigationController( rootViewController: RootTableViewController() diff --git a/Example/Theme/UIColor+Colors.swift b/Example/Theme/UIColor+Colors.swift index afb38c9..de26267 100644 --- a/Example/Theme/UIColor+Colors.swift +++ b/Example/Theme/UIColor+Colors.swift @@ -12,14 +12,14 @@ import UIKit // MARK: - UIColor + Colors extension UIColor { - + /// Override `.red` color static let red = UIColor( - red: 231/255, green: 19/255, blue: 36/255, alpha: 1 + red: 231 / 255, green: 19 / 255, blue: 36 / 255, alpha: 1 ) /// Override `.darkGray` color static let darkGray = UIColor( - red: 109/255, green: 110/255, blue: 112/255, alpha: 1 + red: 109 / 255, green: 110 / 255, blue: 112 / 255, alpha: 1 ) } diff --git a/Example/Theme/UIImage+Images.swift b/Example/Theme/UIImage+Images.swift index 693ceeb..3b04b10 100644 --- a/Example/Theme/UIImage+Images.swift +++ b/Example/Theme/UIImage+Images.swift @@ -10,13 +10,13 @@ import Foundation import UIKit extension UIImage { - + /// Circular information i static let information = UIImage(named: "information-32") - + /// Simple black cross static let cross = UIImage(named: "cross-32") - + /// Donations static let donations = UIImage(named: "donations") } diff --git a/Example/ViewControllers/BadgeMessageViewController.swift b/Example/ViewControllers/BadgeMessageViewController.swift index d81a93e..c3b34ea 100644 --- a/Example/ViewControllers/BadgeMessageViewController.swift +++ b/Example/ViewControllers/BadgeMessageViewController.swift @@ -12,17 +12,17 @@ import MessageStackView /// `UIViewController` to demo posting a `BadgeMessage` on a `PostView` class BadgeMessageViewController: UIViewController { - + /// `PostView` for posting messages private lazy var postView = view.createPostView() - + /// Arrray of titles for `badgeMessages` to demo private let badgeTitles: [(title: String, subtitle: String)] = [ ("Badge Earned!", "That's a badge unlock"), ("Badge Earned Again!", "That's another badge unlock"), ("Badge Earned Again And Again!", "That's yet another badge unlock") ] - + /// `BadgeMessage`s from ``badgeTitles` private var badgeMessages: [BadgeMessage] { return badgeTitles.map { @@ -34,12 +34,12 @@ class BadgeMessageViewController: UIViewController { ) } } - + // MARK: - ViewController lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + UIButton.addToCenter( of: view, title: "Post with updated insets", @@ -50,26 +50,27 @@ class BadgeMessageViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + badgeMessages.forEach { postView.post(badgeMessage: $0) } } - + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - + postView.postManager.invalidate() } - + // MARK: - Actions - - @objc private func buttonTouchUpInside(_ sender: UIButton) { + + @objc + private func buttonTouchUpInside(_ sender: UIButton) { postView.edgeInsets = UIEdgeInsets( top: 3, left: 10, bottom: 3, right: 10 ) - - let _ = postView.post(badgeMessage: BadgeMessage( + + _ = postView.post(badgeMessage: BadgeMessage( title: "\(PostView.self) Insets Updated", subtitle: "The insets (margins) have been updated on the \(PostView.self)", image: .donations, diff --git a/Example/ViewControllers/MessageViewController.swift b/Example/ViewControllers/MessageViewController.swift index d3980b1..11d240a 100644 --- a/Example/ViewControllers/MessageViewController.swift +++ b/Example/ViewControllers/MessageViewController.swift @@ -10,7 +10,7 @@ import UIKit import MessageStackView class MessageViewController: UIViewController { - + private lazy var messageStackView: MessageStackView = { let messageStackView: MessageStackView = view.createMessageStackView() messageStackView.messageConfiguation = MessageConfiguration( @@ -21,7 +21,7 @@ class MessageViewController: UIViewController { ) return messageStackView }() - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) @@ -31,7 +31,7 @@ class MessageViewController: UIViewController { subtitle: "This is a subtitle, with a left image", leftImage: .information ), dismissAfter: 6) - + // Post another message after delay DispatchQueue.main.asyncAfterNow(time: .seconds(2)) { [weak self] in guard let self = self else { return } @@ -44,11 +44,11 @@ class MessageViewController: UIViewController { ), dismissAfter: 8 ) - + messageView.rightImageViewSize = CGSize(width: 10, height: 10) self.addTapToRemoveGesture(to: messageView) } - + // Post a custom view after delay DispatchQueue.main.asyncAfterNow(time: .seconds(4)) { [weak self] in let view = CustomView() @@ -56,7 +56,7 @@ class MessageViewController: UIViewController { self?.addTapToRemoveGesture(to: view) } } - + /// Add ability to remove `view` from `messageStackView` by tap /// /// - Parameter view: `UIView` @@ -68,21 +68,22 @@ class MessageViewController: UIViewController { // MARK: - CustomView -fileprivate class CustomView: UIView { - +private class CustomView: UIView { + init() { super.init(frame: .zero) backgroundColor = .blue - + translatesAutoresizingMaskIntoConstraints = false let heightConstraint = heightAnchor.constraint(equalToConstant: 50) - + // Allow the height to be defined by the stackView during animation heightConstraint.priority = .init(999) - + heightConstraint.isActive = true } - + + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/Example/ViewControllers/NoInternetTabBarController.swift b/Example/ViewControllers/NoInternetTabBarController.swift index 94b0478..0003ad4 100644 --- a/Example/ViewControllers/NoInternetTabBarController.swift +++ b/Example/ViewControllers/NoInternetTabBarController.swift @@ -13,17 +13,17 @@ import MessageStackView class NoInternetTabBarController: UITabBarController { // MARK: - Init - + init() { super.init(nibName: nil, bundle: nil) - + let icons: [UITabBarItem.SystemItem] = [ .bookmarks, .contacts, .downloads, .favorites ] - + viewControllers = icons.enumerated().map { UINavigationController( rootViewController: TabViewController( @@ -32,35 +32,36 @@ class NoInternetTabBarController: UITabBarController { ) ) } - + viewControllers?.append(UINavigationController( rootViewController: TabConnectivityViewController() )) } - + + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - ViewController lifecycle - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + let manager = ConnectivityManager.shared.messageManager manager.startObserving() manager.message.subtitle = "Sorry, please check your connection and try again" } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - + ConnectivityManager.shared.messageManager.stopObserving() } } @@ -68,34 +69,35 @@ class NoInternetTabBarController: UITabBarController { // MARK: - TabViewController class TabViewController: UIViewController { - + init(item: UITabBarItem.SystemItem, index: Int) { super.init(nibName: nil, bundle: nil) tabBarItem = UITabBarItem(tabBarSystemItem: item, tag: index) } - + + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white - + UIButton.addToCenter( of: view, title: "tableView", target: self, selector: #selector(tableViewButtonTouchUpInside) ) - + UIButton.addToCenter( of: view, title: "alert", target: self, selector: #selector(alertButtonTouchUpInside) ).transform = CGAffineTransform(translationX: 0, y: 30) - + navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .cancel, @@ -103,20 +105,22 @@ class TabViewController: UIViewController { action: #selector(cancelBarButtonItemTouchUpInside) ) } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } - + // MARK: - UIControlEvent - - @objc private func cancelBarButtonItemTouchUpInside( + + @objc + private func cancelBarButtonItemTouchUpInside( _ sender: UIBarButtonItem - ){ + ) { presentingViewController?.dismiss(animated: true) } - - @objc private func tableViewButtonTouchUpInside(_ sender: UIButton) { + + @objc + private func tableViewButtonTouchUpInside(_ sender: UIButton) { let viewController = UITableViewController() viewController.hidesBottomBarWhenPushed = true navigationController?.pushViewController( @@ -124,19 +128,20 @@ class TabViewController: UIViewController { animated: true ) } - - @objc private func alertButtonTouchUpInside(_ sender: UIButton) { + + @objc + private func alertButtonTouchUpInside(_ sender: UIButton) { let alertController = UIAlertController( title: "Alert title", message: "Alert Message", preferredStyle: .alert ) - + alertController.addAction(UIAlertAction( title: "Cancel", style: .cancel )) - + present(alertController, animated: true) } } @@ -144,53 +149,54 @@ class TabViewController: UIViewController { // MARK: - TabConnectivityViewController class TabConnectivityViewController: ConnectivityViewController { - + /// Post a `Message` on the `messageStackView` only one, flag to determine if /// the message has already been posted private var didPostMessage = false - + /// `Timer` for posting delayed message weak var timer: Timer? - + /// Root `TabConnectivityViewController`. /// The root can push another `TabConnectivityViewController` with a different /// `MessageLayout` private var isRoot = false - + /// `MessageLayout` override var messageLayout: MessageLayout { return isRoot ? .top : .bottom } - + // MARK: - Init convenience init() { self.init(isRoot: true) } - + private init(isRoot: Bool) { self.isRoot = isRoot - + super.init(nibName: nil, bundle: nil) - + tabBarItem.image = .add tabBarItem.title = "Connect" } - + + @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + deinit { timer?.invalidate() } - + // MARK: - ViewController lifecycle - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white - + guard isRoot else { return } UIButton.addToCenter( of: view, @@ -199,41 +205,42 @@ class TabConnectivityViewController: ConnectivityViewController { selector: #selector(buttonTouchUpInside) ) } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + guard !didPostMessage else { return } - + let messageView = messageStackView.post(message: Message( title: "Hello there!", subtitle: "Welcome to \(Self.self)", leftImage: .noInternet ), dismissAfter: nil) messageView.configureNoInternet() - + messageStackView.postManager.gestureManager .addTapToRemoveGesture(to: messageView) - + timer = Timer.scheduledTimer( withTimeInterval: 3, repeats: false - ) { [weak self] timer in + ) { [weak self] _ in self?.messageStackView.post(message: Message( title: "Another message", subtitle: "This is another message posted later" )) } - + didPostMessage = true } - + // MARK: - Actions - - @objc private func buttonTouchUpInside(_ sender: UIButton) { + + @objc + private func buttonTouchUpInside(_ sender: UIButton) { let viewController = TabConnectivityViewController(isRoot: false) viewController.hidesBottomBarWhenPushed = true - + navigationController?.pushViewController( viewController, animated: true diff --git a/Example/ViewControllers/RootTableViewController.swift b/Example/ViewControllers/RootTableViewController.swift index 01c1e07..b9989dd 100644 --- a/Example/ViewControllers/RootTableViewController.swift +++ b/Example/ViewControllers/RootTableViewController.swift @@ -17,7 +17,7 @@ typealias ExampleElement = ( /// `UITableViewController` to choose an example `UIViewController` class RootTableViewController: UITableViewController { - + /// `ExampleElement` private lazy var exampleElements: [ExampleElement] = [ ("Window", WindowViewController.self, false), @@ -26,12 +26,12 @@ class RootTableViewController: UITableViewController { ("No Internet", NoInternetTabBarController.self, true), ("Shadow", ShadowViewController.self, false) ] - + // MARK: - ViewController lifecycle override func viewDidLoad() { super.viewDidLoad() - + tableView.register( UITableViewCell.self, forCellReuseIdentifier: "\(UITableViewCell.self)" @@ -62,7 +62,7 @@ class RootTableViewController: UITableViewController { withIdentifier: "\(UITableViewCell.self)", for: indexPath ) - + cell.textLabel?.font = UIFont.systemFont( ofSize: 22, weight: .semibold @@ -71,22 +71,21 @@ class RootTableViewController: UITableViewController { cell.textLabel?.text = element(at: indexPath).title return cell } - + // MARK: - UITableViewDelegate - + override func tableView( _ tableView: UITableView, didSelectRowAt indexPath: IndexPath ) { let element = self.element(at: indexPath) - + let viewController = element.type.init() viewController.view.backgroundColor = .white - + if element.present { viewController.modalPresentationStyle = .fullScreen present(viewController, animated: true) - } else { navigationController?.pushViewController( viewController, @@ -96,7 +95,7 @@ class RootTableViewController: UITableViewController { } // MARK: - Element - + /// `ExampleElement` at `indexPath` /// - Parameter indexPath: `IndexPath` private func element(at indexPath: IndexPath) -> ExampleElement { diff --git a/Example/ViewControllers/ShadowViewController.swift b/Example/ViewControllers/ShadowViewController.swift index 6b00b1e..77162de 100644 --- a/Example/ViewControllers/ShadowViewController.swift +++ b/Example/ViewControllers/ShadowViewController.swift @@ -11,86 +11,86 @@ import UIKit import MessageStackView class ShadowViewController: UIViewController { - + /// `UIStackView` container of views private(set) lazy var stackView: UIStackView = { let stackView = UIStackView() - + stackView.axis = .horizontal stackView.distribution = .fillEqually stackView.alignment = .top stackView.spacing = 0 - + return stackView }() - + /// Recommended `ShadowView` approach private(set) lazy var view1: UIView = { let view1 = ShadowView() view1.backgroundColor = .green - + // NeuomorphicShadow can be done before `viewDidlayoutSubviews()` // and setting corners view1.setNeuomorphicShadow() setupCorners(view1) return view1 }() - + /// Non `ShadowView` with `createSubview` as `true` private(set) lazy var view2: UIView = { let view2 = UIView() view2.backgroundColor = .gray setupCorners(view2) - + // NeuomorphicShadow can be done before `viewDidlayoutSubviews()` // but not setting corners view2.setNeuomorphicShadow(createSubview: true) return view2 }() - + /// Non `ShadowView` with `createSubview` as `false` private(set) lazy var view3: UIView = { let view3 = UIView() view3.backgroundColor = .purple setupCorners(view3) - + // NeuomorphicShadow xan not be done before `viewDidlayoutSubviews()` // or setting corners return view3 }() - + // MARK: - ViewController lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + addSubviews() constrain() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + // Ensure `viewDidLayoutSubviews` updates with latest subview frames? view.layoutIfNeeded() } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - + // We need `view3`'s frame to have updated here view3.setNeuomorphicShadow(createSubview: false) } - + // MARK: - Subviews - + private func setupCorners(_ view: UIView) { view.layer.cornerRadius = 10 if #available(iOS 13, *) { view.layer.cornerCurve = .continuous } } - + private func addSubviews() { view.addSubview(stackView) stackView.addArrangedSubview(SampleView( @@ -103,7 +103,7 @@ class ShadowViewController: UIViewController { view: view3, text: "\(UIView.self) neuomorphic w/o subview" )) } - + private func constrain() { stackView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ @@ -126,60 +126,61 @@ class ShadowViewController: UIViewController { // MARK: - SampleView private class SampleView: UIView { - + private(set) lazy var stackView: UIStackView = { let stackView = UIStackView() - + stackView.axis = .vertical stackView.distribution = .fill stackView.alignment = .center stackView.spacing = 5 - + return stackView }() - + private(set) lazy var label: UILabel = { let label = UILabel() - + label.text = "" label.font = UIFont.systemFont(ofSize: 12, weight: .regular) label.textColor = UIColor.black label.numberOfLines = 0 label.lineBreakMode = .byWordWrapping label.textAlignment = .center - + return label }() - + init (view: UIView, text: String) { super.init(frame: .zero) setup(withView: view, text: text) } - + + @available(*, unavailable) required init? (coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setup(withView view: UIView, text: String) { addSubview(stackView) stackView.addArrangedSubview(view) stackView.addArrangedSubview(label) - + label.text = text - + stackView.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false label.translatesAutoresizingMaskIntoConstraints = false - + NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: leadingAnchor), stackView.topAnchor.constraint(equalTo: topAnchor), stackView.trailingAnchor.constraint(equalTo: trailingAnchor), stackView.bottomAnchor.constraint(equalTo: bottomAnchor), - + view.widthAnchor.constraint(equalToConstant: 80), view.heightAnchor.constraint(equalTo: view.widthAnchor), - + label.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.8) ]) } diff --git a/Example/ViewControllers/WindowViewController.swift b/Example/ViewControllers/WindowViewController.swift index fd8ffb7..1c810f3 100644 --- a/Example/ViewControllers/WindowViewController.swift +++ b/Example/ViewControllers/WindowViewController.swift @@ -11,12 +11,12 @@ import UIKit import MessageStackView class WindowViewController: UIViewController { - + // MARK: - ViewController lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + // `UIButton` to pop this `UIViewController` on // the `navigationController` UIButton.addToCenter( @@ -25,7 +25,7 @@ class WindowViewController: UIViewController { target: self, selector: #selector(popButtonTouchUpInside) ) - + // `UIButton` to present another `UIViewController` on top // of this one UIButton.addToCenter( @@ -34,7 +34,7 @@ class WindowViewController: UIViewController { target: self, selector: #selector(presentButtonTouchUpInside) ).transform = CGAffineTransform(translationX: 0, y: 30) - + // `UIButton` to set another `UIViewController` as the // `rootViewController` on the window UIButton.addToCenter( @@ -44,10 +44,10 @@ class WindowViewController: UIViewController { selector: #selector(keyWindowButtonTouchUpInside) ).transform = CGAffineTransform(translationX: 0, y: 60) } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + UIApplication.shared.postView.post(badgeMessage: BadgeMessage( title: "This is a window notification", @@ -57,14 +57,16 @@ class WindowViewController: UIViewController { ) ) } - + // MARK: - Actions - - @objc private func popButtonTouchUpInside(_ sender: UIButton) { + + @objc + private func popButtonTouchUpInside(_ sender: UIButton) { navigationController?.popViewController(animated: true) } - - @objc private func presentButtonTouchUpInside(_ sender: UIButton) { + + @objc + private func presentButtonTouchUpInside(_ sender: UIButton) { let viewController = UIViewController() viewController.view.backgroundColor = UIColor.gray viewController.navigationItem.rightBarButtonItem = UIBarButtonItem( @@ -72,38 +74,40 @@ class WindowViewController: UIViewController { target: self, action: #selector(cancelBarButtonItemTouchUpInside) ) - + let navigationController = UINavigationController( rootViewController: viewController ) - + present(navigationController, animated: true) } - - @objc private func keyWindowButtonTouchUpInside(_ sender: UIButton) { + + @objc + private func keyWindowButtonTouchUpInside(_ sender: UIButton) { let connectedScenes = UIApplication.shared.connectedScenes guard let windowScene = connectedScenes.first as? UIWindowScene, let sceneDelegate = windowScene.delegate as? SceneDelegate else { return } - + let oldWindow = sceneDelegate.window let viewController = WindowRootViewController() - + let newWindow = UIWindow(windowScene: windowScene) newWindow.rootViewController = viewController sceneDelegate.window = newWindow newWindow.makeKeyAndVisible() - + viewController.completion = { [weak newWindow] in newWindow?.resignKey() - + sceneDelegate.window = oldWindow oldWindow?.makeKeyAndVisible() } } - - @objc private func cancelBarButtonItemTouchUpInside(_ sender: UIBarButtonItem) { + + @objc + private func cancelBarButtonItemTouchUpInside(_ sender: UIBarButtonItem) { dismiss(animated: true) } } @@ -111,13 +115,13 @@ class WindowViewController: UIViewController { // MARK: - WindowRootViewController class WindowRootViewController: UIViewController { - + var completion: (() -> Void)? - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white - + UIButton.addToCenter( of: view, title: "Resign Key Window", @@ -125,8 +129,9 @@ class WindowRootViewController: UIViewController { selector: #selector(buttonTouchUpInside) ) } - - @objc private func buttonTouchUpInside(_ sender: UIButton) { + + @objc + private func buttonTouchUpInside(_ sender: UIButton) { completion?() } } diff --git a/MessageStackView.xcodeproj/project.pbxproj b/MessageStackView.xcodeproj/project.pbxproj index 2b04461..64936c3 100644 --- a/MessageStackView.xcodeproj/project.pbxproj +++ b/MessageStackView.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ B81AEE5024FFF27A0068CE23 /* ShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE4F24FFF27A0068CE23 /* ShadowLayer.swift */; }; B81AEE5224FFFB2E0068CE23 /* ParentShadowLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */; }; B81AEE5425003F270068CE23 /* ShadowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81AEE5325003F270068CE23 /* ShadowViewController.swift */; }; + B872BE8725A3874C0031D619 /* UIImageView+Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */; }; + B872BE9125A387570031D619 /* UILabel+Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = B872BE9025A387570031D619 /* UILabel+Hidden.swift */; }; B875C98925126EB200FA05B5 /* UIViewController+System.swift in Sources */ = {isa = PBXBuildFile; fileRef = B875C98825126EB200FA05B5 /* UIViewController+System.swift */; }; B875C98F251270A200FA05B5 /* SystemViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B875C98E251270A200FA05B5 /* SystemViewControllerTests.swift */; }; B87CC94D24C9FB80002B697C /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87CC94B24C9F5A3002B697C /* Array+Extensions.swift */; }; @@ -40,7 +42,7 @@ B87CC98B24CB02F8002B697C /* ConnectivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87CC98A24CB02F8002B697C /* ConnectivityViewController.swift */; }; B87CC99124CB2370002B697C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B87CC98E24CB21E1002B697C /* Assets.xcassets */; }; B87CC99324CB48DD002B697C /* ConnectivityMessageable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87CC99224CB48DD002B697C /* ConnectivityMessageable.swift */; }; - B87CC99524CB4A0E002B697C /* NoInternet+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87CC99424CB4A0D002B697C /* NoInternet+UI.swift */; }; + B87CC99524CB4A0E002B697C /* MessageView+NoInternet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87CC99424CB4A0D002B697C /* MessageView+NoInternet.swift */; }; B87CC99D24CC263E002B697C /* NoInternetTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87CC99C24CC263E002B697C /* NoInternetTabBarController.swift */; }; B87CC9A024CC2829002B697C /* UIButton+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B87CC99F24CC2829002B697C /* UIButton+Extensions.swift */; }; B8BB088224C4BC46000E2E87 /* RootTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB087424C4BC46000E2E87 /* RootTableViewController.swift */; }; @@ -71,7 +73,6 @@ OBJ_102 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* UIEdgeInsets+Extensions.swift */; }; OBJ_103 /* UIView+Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* UIView+Animation.swift */; }; OBJ_104 /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* UIView+Extensions.swift */; }; - OBJ_105 /* UIView+Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* UIView+Hidden.swift */; }; OBJ_106 /* UIView+SafeArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_28 /* UIView+SafeArea.swift */; }; OBJ_107 /* UIView+Shadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* UIView+Shadow.swift */; }; OBJ_108 /* UIView+ShadowComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* UIView+ShadowComponents.swift */; }; @@ -89,7 +90,7 @@ OBJ_120 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_46 /* MessageView.swift */; }; OBJ_121 /* MessageViewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_47 /* MessageViewable.swift */; }; OBJ_122 /* Poster+Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_48 /* Poster+Message.swift */; }; - OBJ_123 /* Message+MessageConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_50 /* Message+MessageConfigurable.swift */; }; + OBJ_123 /* MessageView+MessageConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_50 /* MessageView+MessageConfigurable.swift */; }; OBJ_124 /* MessageConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_51 /* MessageConfigurable.swift */; }; OBJ_125 /* MessageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_52 /* MessageConfiguration.swift */; }; OBJ_126 /* PostAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_54 /* PostAnimation.swift */; }; @@ -171,6 +172,8 @@ B81AEE4F24FFF27A0068CE23 /* ShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowLayer.swift; sourceTree = ""; }; B81AEE5124FFFB2E0068CE23 /* ParentShadowLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParentShadowLayer.swift; sourceTree = ""; }; B81AEE5325003F270068CE23 /* ShadowViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowViewController.swift; sourceTree = ""; }; + B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Hidden.swift"; sourceTree = ""; }; + B872BE9025A387570031D619 /* UILabel+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Hidden.swift"; sourceTree = ""; }; B875C98825126EB200FA05B5 /* UIViewController+System.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+System.swift"; sourceTree = ""; }; B875C98E251270A200FA05B5 /* SystemViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemViewControllerTests.swift; sourceTree = ""; }; B87CC94B24C9F5A3002B697C /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = ""; }; @@ -181,7 +184,7 @@ B87CC98A24CB02F8002B697C /* ConnectivityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityViewController.swift; sourceTree = ""; }; B87CC98E24CB21E1002B697C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B87CC99224CB48DD002B697C /* ConnectivityMessageable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectivityMessageable.swift; sourceTree = ""; }; - B87CC99424CB4A0D002B697C /* NoInternet+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NoInternet+UI.swift"; sourceTree = ""; }; + B87CC99424CB4A0D002B697C /* MessageView+NoInternet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageView+NoInternet.swift"; sourceTree = ""; }; B87CC99C24CC263E002B697C /* NoInternetTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoInternetTabBarController.swift; sourceTree = ""; }; B87CC99F24CC2829002B697C /* UIButton+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIButton+Extensions.swift"; sourceTree = ""; }; B8BB086D24C4BC23000E2E87 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -223,7 +226,6 @@ OBJ_24 /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; OBJ_25 /* UIView+Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Animation.swift"; sourceTree = ""; }; OBJ_26 /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; - OBJ_27 /* UIView+Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Hidden.swift"; sourceTree = ""; }; OBJ_28 /* UIView+SafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SafeArea.swift"; sourceTree = ""; }; OBJ_29 /* UIView+Shadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Shadow.swift"; sourceTree = ""; }; OBJ_30 /* UIView+ShadowComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+ShadowComponents.swift"; sourceTree = ""; }; @@ -241,7 +243,7 @@ OBJ_46 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; OBJ_47 /* MessageViewable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewable.swift; sourceTree = ""; }; OBJ_48 /* Poster+Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Poster+Message.swift"; sourceTree = ""; }; - OBJ_50 /* Message+MessageConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+MessageConfigurable.swift"; sourceTree = ""; }; + OBJ_50 /* MessageView+MessageConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageView+MessageConfigurable.swift"; sourceTree = ""; }; OBJ_51 /* MessageConfigurable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageConfigurable.swift; sourceTree = ""; }; OBJ_52 /* MessageConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageConfiguration.swift; sourceTree = ""; }; OBJ_54 /* PostAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostAnimation.swift; sourceTree = ""; }; @@ -311,6 +313,15 @@ path = Shadow; sourceTree = ""; }; + B872BE8525A3873F0031D619 /* Hidden */ = { + isa = PBXGroup; + children = ( + B872BE8625A3874C0031D619 /* UIImageView+Hidden.swift */, + B872BE9025A387570031D619 /* UILabel+Hidden.swift */, + ); + path = Hidden; + sourceTree = ""; + }; B87CC94E24CAE52A002B697C /* Reachability */ = { isa = PBXGroup; children = ( @@ -321,7 +332,7 @@ B8BCDA8224CD8E920067F26C /* ConnectivityManager+MessageManager.swift */, B87CC99224CB48DD002B697C /* ConnectivityMessageable.swift */, B87CC98A24CB02F8002B697C /* ConnectivityViewController.swift */, - B87CC99424CB4A0D002B697C /* NoInternet+UI.swift */, + B87CC99424CB4A0D002B697C /* MessageView+NoInternet.swift */, ); path = Reachability; sourceTree = ""; @@ -412,6 +423,7 @@ OBJ_18 /* Extensions */ = { isa = PBXGroup; children = ( + B872BE8525A3873F0031D619 /* Hidden */, OBJ_19 /* CALayer+Animation.swift */, OBJ_20 /* CGRect+Extensions.swift */, OBJ_21 /* DispatchQueue+Extensions.swift */, @@ -420,7 +432,6 @@ OBJ_24 /* UIEdgeInsets+Extensions.swift */, OBJ_25 /* UIView+Animation.swift */, OBJ_26 /* UIView+Extensions.swift */, - OBJ_27 /* UIView+Hidden.swift */, OBJ_28 /* UIView+SafeArea.swift */, OBJ_29 /* UIView+Shadow.swift */, OBJ_30 /* UIView+ShadowComponents.swift */, @@ -481,7 +492,7 @@ OBJ_49 /* MessageConfiguration */ = { isa = PBXGroup; children = ( - OBJ_50 /* Message+MessageConfigurable.swift */, + OBJ_50 /* MessageView+MessageConfigurable.swift */, OBJ_51 /* MessageConfigurable.swift */, OBJ_52 /* MessageConfiguration.swift */, ); @@ -657,6 +668,7 @@ isa = PBXNativeTarget; buildConfigurationList = OBJ_87 /* Build configuration list for PBXNativeTarget "MessageStackView" */; buildPhases = ( + B872BE7C25A37F160031D619 /* Swift Lint */, B87CC95624CAEAD9002B697C /* Headers */, OBJ_90 /* Sources */, OBJ_143 /* Frameworks */, @@ -710,7 +722,7 @@ attributes = { LastSwiftMigration = 9999; LastSwiftUpdateCheck = 1160; - LastUpgradeCheck = 9999; + LastUpgradeCheck = 1230; ORGANIZATIONNAME = "3 SIDED CUBE APP PRODUCTIONS LTD"; TargetAttributes = { B8BB086024C4BC23000E2E87 = { @@ -764,6 +776,27 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + B872BE7C25A37F160031D619 /* Swift Lint */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Swift Lint"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ B8BB086124C4BC23000E2E87 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -807,14 +840,16 @@ files = ( OBJ_91 /* TimeInterval+Defaults.swift in Sources */, B81AEE5224FFFB2E0068CE23 /* ParentShadowLayer.swift in Sources */, + B872BE9125A387570031D619 /* UILabel+Hidden.swift in Sources */, OBJ_92 /* CenterConstraints.swift in Sources */, OBJ_93 /* Constrainable.swift in Sources */, OBJ_94 /* EdgeConstraints.swift in Sources */, OBJ_95 /* MessageLayout.swift in Sources */, - B87CC99524CB4A0E002B697C /* NoInternet+UI.swift in Sources */, + B87CC99524CB4A0E002B697C /* MessageView+NoInternet.swift in Sources */, OBJ_96 /* SizeConstraints.swift in Sources */, OBJ_97 /* CALayer+Animation.swift in Sources */, B875C98925126EB200FA05B5 /* UIViewController+System.swift in Sources */, + B872BE8725A3874C0031D619 /* UIImageView+Hidden.swift in Sources */, OBJ_98 /* CGRect+Extensions.swift in Sources */, OBJ_99 /* DispatchQueue+Extensions.swift in Sources */, OBJ_100 /* String+Extensions.swift in Sources */, @@ -824,7 +859,6 @@ B8BCDA8C24CDB55B0067F26C /* UIViewController+Lifecycle.m in Sources */, B8BCDA8524CD8EB20067F26C /* UIApplication+KeyWindow.swift in Sources */, OBJ_104 /* UIView+Extensions.swift in Sources */, - OBJ_105 /* UIView+Hidden.swift in Sources */, B8E1CEE92500DA37002BFE77 /* UIColor+Hex.swift in Sources */, OBJ_106 /* UIView+SafeArea.swift in Sources */, OBJ_107 /* UIView+Shadow.swift in Sources */, @@ -850,7 +884,7 @@ OBJ_120 /* MessageView.swift in Sources */, OBJ_121 /* MessageViewable.swift in Sources */, OBJ_122 /* Poster+Message.swift in Sources */, - OBJ_123 /* Message+MessageConfigurable.swift in Sources */, + OBJ_123 /* MessageView+MessageConfigurable.swift in Sources */, OBJ_124 /* MessageConfigurable.swift in Sources */, B81AEE4C24FFEE520068CE23 /* FloatingPoint+Equals.swift in Sources */, OBJ_125 /* MessageConfiguration.swift in Sources */, @@ -1075,15 +1109,15 @@ OBJ_157 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = MessageStackView.xcodeproj/MessageStackViewTests_Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CFLAGS = "$(inherited)"; @@ -1092,7 +1126,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = MessageStackViewTests; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 2.0; }; name = Debug; @@ -1100,15 +1134,15 @@ OBJ_158 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CLANG_ENABLE_MODULES = YES; - EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = MessageStackView.xcodeproj/MessageStackViewTests_Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_CFLAGS = "$(inherited)"; @@ -1117,7 +1151,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = MessageStackViewTests; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 2.0; }; name = Release; @@ -1126,17 +1160,44 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_NS_ASSERTIONS = YES; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "SWIFT_PACKAGE=1", "DEBUG=1", ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; @@ -1153,15 +1214,41 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = s; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "SWIFT_PACKAGE=1", ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.10; OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1186,7 +1273,7 @@ ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; MACOSX_DEPLOYMENT_TARGET = 10.10; MARKETING_VERSION = 2.0.0; @@ -1203,7 +1290,7 @@ SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGET_NAME = MessageStackView; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 2.0; }; name = Debug; @@ -1221,7 +1308,7 @@ ); HEADER_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; MACOSX_DEPLOYMENT_TARGET = 10.10; MARKETING_VERSION = 2.0.0; @@ -1237,7 +1324,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_VERSION = 5.0; TARGET_NAME = MessageStackView; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 12.0; WATCHOS_DEPLOYMENT_TARGET = 2.0; }; name = Release; diff --git a/MessageStackView.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/MessageStackView.xcodeproj/xcshareddata/xcschemes/Example.xcscheme index fdcd4d4..7e869d2 100644 --- a/MessageStackView.xcodeproj/xcshareddata/xcschemes/Example.xcscheme +++ b/MessageStackView.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -1,6 +1,6 @@ @@ -50,7 +50,7 @@ diff --git a/MessageStackView.xcodeproj/xcshareddata/xcschemes/MessageStackViewPackageTests.xcscheme b/MessageStackView.xcodeproj/xcshareddata/xcschemes/MessageStackViewPackageTests.xcscheme index ea17c49..0aea7e1 100644 --- a/MessageStackView.xcodeproj/xcshareddata/xcschemes/MessageStackViewPackageTests.xcscheme +++ b/MessageStackView.xcodeproj/xcshareddata/xcschemes/MessageStackViewPackageTests.xcscheme @@ -1,6 +1,6 @@ CenterConstraints { translatesAutoresizingMaskIntoConstraints = false - + // Create `NSLayoutConstraint`s to the center anchors let centerConstraints = CenterConstraints( centerX: centerXAnchor.constraint(equalTo: view.centerXAnchor), centerY: centerYAnchor.constraint(equalTo: view.centerYAnchor) ) - + // Activate `NSLayoutConstraint`s if required if activate { centerConstraints.activate() } - + return centerConstraints } } diff --git a/Sources/Constraints/Constrainable.swift b/Sources/Constraints/Constrainable.swift index dee3f25..3034e7b 100644 --- a/Sources/Constraints/Constrainable.swift +++ b/Sources/Constraints/Constrainable.swift @@ -11,7 +11,7 @@ import UIKit /// Entity which has an Array of `NSLayoutConstraint`s protocol Constrainable { - + /// Array of `NSLayoutConstraint`s to activate var constraints: [NSLayoutConstraint] { get } } @@ -19,7 +19,7 @@ protocol Constrainable { // MARK: - Constrainable + Extensions extension Constrainable { - + /// Activate `constraints` func activate() { NSLayoutConstraint.activate(constraints) diff --git a/Sources/Constraints/EdgeConstraints.swift b/Sources/Constraints/EdgeConstraints.swift index 2f9c9bd..a54922d 100644 --- a/Sources/Constraints/EdgeConstraints.swift +++ b/Sources/Constraints/EdgeConstraints.swift @@ -19,28 +19,28 @@ import UIKit /// When talking about `UIEdgeInsets` for trailing and bottom, the value of `c` would refer to /// the opposite direction struct EdgeConstraints: Constrainable { - + /// Leading anchor var leading: NSLayoutConstraint - + /// Top anchor var top: NSLayoutConstraint - + /// Trailing anchor var trailing: NSLayoutConstraint - + /// Bottom anchor var bottom: NSLayoutConstraint - + // MARK: - Constrainable - + /// All `NSLayoutConstraint`s of `EdgeConstraints` var constraints: [NSLayoutConstraint] { return [leading, top, trailing, bottom] } - + // MARK: - UIEdgeInsets - + /// Constants of `constraints` var insets: UIEdgeInsets { get { @@ -63,7 +63,7 @@ struct EdgeConstraints: Constrainable { // MARK: - UIView + EdgeConstraints extension UIView { - + /// Construct `EdgeConstraints` `NSLayoutConstraint`s from /// `self` to `view` /// @@ -77,7 +77,7 @@ extension UIView { activate: Bool = true ) -> EdgeConstraints { translatesAutoresizingMaskIntoConstraints = false - + // Create `NSLayoutConstraint`s to the edge anchors var edgeConstraints = EdgeConstraints( leading: leadingAnchor.constraint(equalTo: view.leadingAnchor), @@ -85,15 +85,15 @@ extension UIView { trailing: trailingAnchor.constraint(equalTo: view.trailingAnchor), bottom: bottomAnchor.constraint(equalTo: view.bottomAnchor) ) - + // Set `UIEdgeInsets` as provided edgeConstraints.insets = insets - + // Activate `NSLayoutConstraint`s if required if activate { edgeConstraints.activate() } - + return edgeConstraints } } diff --git a/Sources/Constraints/MessageLayout.swift b/Sources/Constraints/MessageLayout.swift index 0d84966..cd06ef4 100644 --- a/Sources/Constraints/MessageLayout.swift +++ b/Sources/Constraints/MessageLayout.swift @@ -13,10 +13,10 @@ import UIKit /// - Note: /// `MessageLayout` is not required, one may opt to constrain explicitly instead. public enum MessageLayout: Int { - + /// Constrain a `UIView` to the top of `UIView` case top - + /// Constrain a `UIView` to the bottom of another `UIView` case bottom } @@ -37,7 +37,7 @@ public extension MessageLayout { ) { superview.addSubview(subview) subview.translatesAutoresizingMaskIntoConstraints = false - + switch self { case .top: constrain(subview, to: superview, including: [ @@ -53,7 +53,7 @@ public extension MessageLayout { ], safeAnchors: safeAnchors) } } - + /// Constrain given `constraints` + leading and width anchor of `subview` to `superview` /// - Parameters: /// - subview: `UIView` to add to `superview` @@ -64,7 +64,7 @@ public extension MessageLayout { to superview: UIView, including constraints: [NSLayoutConstraint], safeAnchors: Bool - ){ + ) { NSLayoutConstraint.activate(constraints + [ subview.leadingAnchor.constraint( equalTo: superview.leadingAnchor(safe: safeAnchors) @@ -79,7 +79,7 @@ public extension MessageLayout { // MARK: - UIView + MessageLayout public extension UIView { - + /// Layout `self` with `layout` (a common `MessageLayout` use case). /// /// - Note: @@ -97,12 +97,12 @@ public extension UIView { ) { // Remove from previous layout tree if exists removeFromSuperview() - + // Constrain `self` layout.constrain( subview: self, to: view, safeAnchors: constrainToSafeArea ) - + // Trigger a layout cycle view.setNeedsLayout() } diff --git a/Sources/Constraints/SizeConstraints.swift b/Sources/Constraints/SizeConstraints.swift index 15f6981..d36e46a 100644 --- a/Sources/Constraints/SizeConstraints.swift +++ b/Sources/Constraints/SizeConstraints.swift @@ -10,29 +10,29 @@ import UIKit /// `SizeConstraints` for the width and height `NSLayoutConstraint`s on a `UIView` struct SizeConstraints: Constrainable { - + /// Width `NSLayoutConstraint` var width: NSLayoutConstraint - + /// Height `NSLayoutConstraint` var height: NSLayoutConstraint - + // MARK: - Setters - + /// Set the `constant` on the `width` /// /// - Parameter value: `width` value func setWidth(_ value: CGFloat) { width.constant = value } - + /// Set the `constant` on the `height` /// /// - Parameter constant: `height` value func setHeight(_ value: CGFloat) { height.constant = value } - + /// Set the `constant` on the `width` and `height` /// /// - Parameter size: `CGSize` @@ -40,9 +40,9 @@ struct SizeConstraints: Constrainable { setWidth(size.width) setHeight(size.width) } - + // MARK: - Constrainable - + /// The `width` and `height` `NSLayoutConstraint`s var constraints: [NSLayoutConstraint] { return [width, height] @@ -52,7 +52,7 @@ struct SizeConstraints: Constrainable { // MARK: - UIView + SizeConstraints extension UIView { - + /// Construct `SizeConstraints` `NSLayoutConstraint`s on /// `self` to with constants of the given `size` /// @@ -65,16 +65,16 @@ extension UIView { activate: Bool = true ) -> SizeConstraints { translatesAutoresizingMaskIntoConstraints = false - + let sizeConstraints = SizeConstraints( width: widthAnchor.constraint(equalToConstant: size.width), height: heightAnchor.constraint(equalToConstant: size.height) ) - + if activate { sizeConstraints.activate() } - + return sizeConstraints } } diff --git a/Sources/Extensions/Array+Extensions.swift b/Sources/Extensions/Array+Extensions.swift index c957af3..ba1d74f 100644 --- a/Sources/Extensions/Array+Extensions.swift +++ b/Sources/Extensions/Array+Extensions.swift @@ -8,7 +8,7 @@ import Foundation public extension Array where Element: Equatable { - + /// Get the next `Element` after the first instance equal to `element`, where equal to is /// defined by `Equatable` /// @@ -21,7 +21,7 @@ public extension Array where Element: Equatable { guard newIndex < endIndex else { return nil } return self[newIndex] } - + /// Get the previous `Element` before the first instance equal to `element`, where equal to is /// defined by `Equatable` /// @@ -33,5 +33,4 @@ public extension Array where Element: Equatable { let newIndex = self.index(before: index) return self[newIndex] } - } diff --git a/Sources/Extensions/CALayer+Animation.swift b/Sources/Extensions/CALayer+Animation.swift index 5e872df..a4b416c 100644 --- a/Sources/Extensions/CALayer+Animation.swift +++ b/Sources/Extensions/CALayer+Animation.swift @@ -12,7 +12,7 @@ import UIKit // MARK: - CALayer extension CALayer { - + /// Animate transform from `from` to `to` /// - Parameters: /// - duration: Animation duration @@ -31,13 +31,13 @@ extension CALayer { animation.fromValue = from.array animation.toValue = to.array animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - + // Change the actual data value in the layer to the final value. transform = CATransform3DScale(transform, to.x, to.y, to.z) - + return animation } - + /// Animate opacity from `from` to `to` /// - Parameters: /// - duration: Animation duration @@ -48,13 +48,13 @@ extension CALayer { from: Float = 0, to: Float = 1 ) -> CABasicAnimation { - + let keyPath = #keyPath(opacity) let animation = CABasicAnimation(keyPath: keyPath) animation.duration = duration animation.fromValue = from animation.toValue = to - + // Change the actual data value in the layer to the final value. opacity = to diff --git a/Sources/Extensions/CGRect+Extensions.swift b/Sources/Extensions/CGRect+Extensions.swift index 43be22d..095f5f0 100644 --- a/Sources/Extensions/CGRect+Extensions.swift +++ b/Sources/Extensions/CGRect+Extensions.swift @@ -10,7 +10,7 @@ import Foundation import UIKit extension CGRect { - + /// `CRect` with `.zero` origin and `self.size` set to the given `size` /// - Parameter size: `CGSize` init (size: CGSize) { @@ -24,7 +24,7 @@ extension CGRect { func inset(by value: CGFloat) -> CGRect { return self.inset(by: UIEdgeInsets(value: value)) } - + /// `CGRect` centered about `self` with size `size` /// - Parameter size: Size of square rect func centered(size: CGFloat) -> CGRect { @@ -35,14 +35,14 @@ extension CGRect { height: size ) } - + /// Center `CGPoint` defined as: /// - `x` equals half the `width` and /// - `y` equals half the `height` var center: CGPoint { return CGPoint(x: width * 0.5, y: height * 0.5) } - + /// "Circle" `CGRect` at the given `center` with `radius`. /// Of course the `CGRect` is a square by definition /// - Parameters: diff --git a/Sources/Extensions/DispatchQueue+Extensions.swift b/Sources/Extensions/DispatchQueue+Extensions.swift index 282393d..fed2820 100644 --- a/Sources/Extensions/DispatchQueue+Extensions.swift +++ b/Sources/Extensions/DispatchQueue+Extensions.swift @@ -9,7 +9,7 @@ import Foundation public extension DispatchQueue { - + /// Execute `closure` after `time` from `.now()` /// - Parameters: /// - time: `DispatchTimeInterval` @@ -18,12 +18,9 @@ public extension DispatchQueue { time: DispatchTimeInterval, closure: @escaping () -> Void ) { - DispatchQueue.main.asyncAfter( - deadline: .now() + time, - execute: closure - ) + asyncAfter(deadline: .now() + time, execute: closure) } - + /// Execute `closure` on main thread or immediately if already on it. /// - Parameter closure: Closure to execute static func executeOnMain(execute closure: @escaping () -> Void) { diff --git a/Sources/Extensions/FloatingPoint+Equals.swift b/Sources/Extensions/FloatingPoint+Equals.swift index 7164f19..688e45d 100644 --- a/Sources/Extensions/FloatingPoint+Equals.swift +++ b/Sources/Extensions/FloatingPoint+Equals.swift @@ -9,7 +9,7 @@ import Foundation extension FloatingPoint { - + /// Is equal to `other` within precision of `epsilon` /// /// - Parameters: diff --git a/Sources/Extensions/Hidden/UIImageView+Hidden.swift b/Sources/Extensions/Hidden/UIImageView+Hidden.swift new file mode 100644 index 0000000..9e48596 --- /dev/null +++ b/Sources/Extensions/Hidden/UIImageView+Hidden.swift @@ -0,0 +1,20 @@ +// +// UIImageView+Hidden.swift +// MessageStackView +// +// Created by Ben Shutt on 04/01/2021. +// Copyright © 2021 3 SIDED CUBE APP PRODUCTIONS LTD. All rights reserved. +// + +import Foundation +import UIKit + +extension UIImageView { + + /// Set the `UIImageView.image` and update `isHidden` by nullability of`image` + /// - Parameter image: `UIImage` to set + func setImageAndHidden(_ image: UIImage?) { + self.image = image + isHidden = image == nil + } +} diff --git a/Sources/Extensions/UIView+Hidden.swift b/Sources/Extensions/Hidden/UILabel+Hidden.swift similarity index 53% rename from Sources/Extensions/UIView+Hidden.swift rename to Sources/Extensions/Hidden/UILabel+Hidden.swift index a7b1ff6..5d52d8b 100644 --- a/Sources/Extensions/UIView+Hidden.swift +++ b/Sources/Extensions/Hidden/UILabel+Hidden.swift @@ -1,30 +1,16 @@ // -// UIView+Hidden.swift +// UILabel+Hidden.swift // MessageStackView // -// Created by Ben Shutt on 03/07/2020. -// Copyright © 2020 3 SIDED CUBE APP PRODUCTIONS LTD. All rights reserved. +// Created by Ben Shutt on 04/01/2021. +// Copyright © 2021 3 SIDED CUBE APP PRODUCTIONS LTD. All rights reserved. // import Foundation import UIKit -// MARK: - UIImageView + Image + Hidden - -extension UIImageView { - - /// Set the `UIImageView.image` and update `isHidden` by nullability of`image` - /// - Parameter image: `UIImage` to set - func setImageAndHidden(_ image: UIImage?) { - self.image = image - isHidden = image == nil - } -} - -// MARK: - UILabel + Text + Hidden - extension UILabel { - + /// Set the `UILabel.text` and update `isHidden` by nullability of `text` /// - Parameters: /// - text: `String` text to set diff --git a/Sources/Extensions/String+Extensions.swift b/Sources/Extensions/String+Extensions.swift index c6b1850..e271d3c 100644 --- a/Sources/Extensions/String+Extensions.swift +++ b/Sources/Extensions/String+Extensions.swift @@ -9,7 +9,7 @@ import Foundation extension String { - + /// Shorthand to trim `.whitespacesAndNewlines` characters in a `String` var trimmed: String { return trimmingCharacters(in: .whitespacesAndNewlines) diff --git a/Sources/Extensions/UIApplication+KeyWindow.swift b/Sources/Extensions/UIApplication+KeyWindow.swift index d2b953c..0c433f5 100644 --- a/Sources/Extensions/UIApplication+KeyWindow.swift +++ b/Sources/Extensions/UIApplication+KeyWindow.swift @@ -10,14 +10,15 @@ import Foundation import UIKit public extension UIApplication { - + /// First `UIWindow` where `isKeyWindow` + /// /// - Note: - /// Named as such due to naming conflicts. TODO: Look to resolve + /// Named as such due to common naming conflicts. var appKeyWindow: UIWindow? { return windows.first { $0.isKeyWindow } ?? windows.first } - + /// `visibleViewController` on the `rootViewController` of `keyWindow` var visibleViewController: UIViewController? { guard let rootViewController = appKeyWindow?.rootViewController else { @@ -32,7 +33,7 @@ public extension UIApplication { // MARK: - UIViewController + VisibleViewController public extension UIViewController { - + /// Visible `UIViewController` from `viewController` /// /// - Parameters: @@ -43,32 +44,32 @@ public extension UIViewController { from viewController: UIViewController, includeSystemViewControllers: Bool = false ) -> UIViewController { - + // splitViewController if let splitViewController = viewController as? UISplitViewController, let firstViewController = splitViewController.viewControllers.first { return visibleViewController(from: firstViewController) } - + // navigationController if let navigationController = viewController as? UINavigationController, let lastViewController = navigationController.viewControllers.last { return visibleViewController(from: lastViewController) } - + // tabBarController if let tabController = viewController as? UITabBarController { if let selectedViewController = tabController.selectedViewController { return visibleViewController(from: selectedViewController) } } - + // presentedViewController if let presentedViewController = viewController.presentedViewController, (includeSystemViewControllers || !presentedViewController.isSystemViewController) { return visibleViewController(from: presentedViewController) } - + return viewController } } diff --git a/Sources/Extensions/UIApplication+StatusBar.swift b/Sources/Extensions/UIApplication+StatusBar.swift index d02818f..ad296b0 100644 --- a/Sources/Extensions/UIApplication+StatusBar.swift +++ b/Sources/Extensions/UIApplication+StatusBar.swift @@ -16,7 +16,7 @@ extension UIApplication { // If not iOS 13 simply return `statusBarFrame` return statusBarFrame } - + // Otherwise find the key window and get frame from the scene return appKeyWindow?.windowScene?.statusBarManager? .statusBarFrame ?? .zero diff --git a/Sources/Extensions/UIColor+Hex.swift b/Sources/Extensions/UIColor+Hex.swift index aa0d7c3..323b5d1 100644 --- a/Sources/Extensions/UIColor+Hex.swift +++ b/Sources/Extensions/UIColor+Hex.swift @@ -10,15 +10,15 @@ import Foundation import UIKit extension UIColor { - + convenience init (hexString: String) { let hexString = hexString.trimmed let scanner = Scanner(string: hexString) - + if hexString.hasPrefix("#") { scanner.scanLocation = 1 } - + var color: UInt32 = 0 scanner.scanHexInt32(&color) @@ -31,19 +31,21 @@ extension UIColor { let green = CGFloat(g) / 255.0 let blue = CGFloat(b) / 255.0 - self.init(red:red, green:green, blue:blue, alpha:1) + self.init(red: red, green: green, blue: blue, alpha: 1) } - + var hexString: String { var r: CGFloat = 0 var g: CGFloat = 0 var b: CGFloat = 0 var a: CGFloat = 0 - + getRed(&r, green: &g, blue: &b, alpha: &a) - - let rgb: Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0 - - return String(format:"#%06x", rgb) + + let rgb: Int = (Int)(r * 255) << 16 | + (Int)( g * 255) << 8 | + (Int)( b * 255) << 0 + + return String(format: "#%06x", rgb) } } diff --git a/Sources/Extensions/UIEdgeInsets+Extensions.swift b/Sources/Extensions/UIEdgeInsets+Extensions.swift index 5ca6789..b60c2d3 100644 --- a/Sources/Extensions/UIEdgeInsets+Extensions.swift +++ b/Sources/Extensions/UIEdgeInsets+Extensions.swift @@ -10,7 +10,7 @@ import Foundation import UIKit extension UIEdgeInsets { - + /// Set all properties: /// `top`, `left`, `bottom`, `right` /// to the same given `value` diff --git a/Sources/Extensions/UIView+Animation.swift b/Sources/Extensions/UIView+Animation.swift index 7bc4b13..a9040fd 100644 --- a/Sources/Extensions/UIView+Animation.swift +++ b/Sources/Extensions/UIView+Animation.swift @@ -10,7 +10,7 @@ import Foundation import UIKit extension UIView { - + /// Transform `self` in `x` and `y` by the given `scale` /// - Parameters: /// - scale: Transform scale @@ -34,4 +34,3 @@ extension UIView { layer.add(animation, forKey: #keyPath(layer.transform)) } } - diff --git a/Sources/Extensions/UIView+Extensions.swift b/Sources/Extensions/UIView+Extensions.swift index cfc5097..352f767 100644 --- a/Sources/Extensions/UIView+Extensions.swift +++ b/Sources/Extensions/UIView+Extensions.swift @@ -10,7 +10,7 @@ import Foundation import UIKit extension UIView { - + /// Find the first superview (recursive) of type `T`. /// - Parameters: /// - includeSelf: If `self` is of type `T` then return `self` @@ -18,10 +18,10 @@ extension UIView { if includeSelf, let view = self as? T { return view } - + return superview?.firstSuperviewOfType(includeSelf: true) } - + /// Find the first subview (recursive) of type `T`. /// - Parameters: /// - includeSelf: If `self` is of type `T` then return `self` @@ -29,16 +29,16 @@ extension UIView { if includeSelf, let view = self as? T { return view } - + for subview in subviews { if let view: T = subview.firstSubviewOfType(includeSelf: true) { return view } } - + return nil } - + /// Bring `self` to front of `superview` `subviews` stack func bringToFront() { superview?.bringSubviewToFront(self) diff --git a/Sources/Extensions/UIView+SafeArea.swift b/Sources/Extensions/UIView+SafeArea.swift index 256b491..437c958 100644 --- a/Sources/Extensions/UIView+SafeArea.swift +++ b/Sources/Extensions/UIView+SafeArea.swift @@ -12,7 +12,7 @@ import UIKit /// SafeArea support for pre iOS 11 extension UIView { - + /// If the safe area is available, then the `safeAreaLayoutGuide.topAnchor`, /// otherwise the `topAnchor` var safeTopAnchor: NSLayoutYAxisAnchor { @@ -22,27 +22,27 @@ extension UIView { return self.topAnchor } } - + /// If the safe area is available, then the `safeAreaLayoutGuide.leadingAnchor`, /// otherwise the `leadingAnchor` var safeLeadingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *){ + if #available(iOS 11.0, *) { return self.safeAreaLayoutGuide.leadingAnchor } else { return self.leadingAnchor } } - + /// If the safe area is available, then the `safeAreaLayoutGuide.trailingAnchor`, /// otherwise the `trailingAnchor` var safeTrailingAnchor: NSLayoutXAxisAnchor { - if #available(iOS 11.0, *){ + if #available(iOS 11.0, *) { return self.safeAreaLayoutGuide.trailingAnchor } else { return self.trailingAnchor } } - + /// If the safe area is available, then the `safeAreaLayoutGuide.bottomAnchor`, /// otherwise the `bottomAnchor` var safeBottomAnchor: NSLayoutYAxisAnchor { @@ -52,21 +52,21 @@ extension UIView { return self.bottomAnchor } } - + /// If the safe area is available, then the `safeAreaLayoutGuide.widthAnchor`, /// otherwise the `widthAnchor` var safeWidthAnchor: NSLayoutDimension { - if #available(iOS 11.0, *){ + if #available(iOS 11.0, *) { return self.safeAreaLayoutGuide.widthAnchor } else { return self.widthAnchor } } - + /// If the safe area is available, then the `safeAreaLayoutGuide.heightAnchor`, /// otherwise the `heightAnchor` var safeHeightAnchor: NSLayoutDimension { - if #available(iOS 11.0, *){ + if #available(iOS 11.0, *) { return self.safeAreaLayoutGuide.heightAnchor } else { return self.heightAnchor @@ -77,37 +77,37 @@ extension UIView { // MARK: - UIView + Anchor extension UIView { - + /// `leadingAnchor` /// - Parameter safe: `Bool` Use safe anchor func leadingAnchor(safe: Bool) -> NSLayoutXAxisAnchor { return safe ? safeLeadingAnchor : leadingAnchor } - + /// `trailingAnchor` /// - Parameter safe: `Bool` Use safe anchor func trailingAnchor(safe: Bool) -> NSLayoutXAxisAnchor { return safe ? safeTrailingAnchor : trailingAnchor } - + /// `topAnchor` /// - Parameter safe: `Bool` Use safe anchor func topAnchor(safe: Bool) -> NSLayoutYAxisAnchor { return safe ? safeTopAnchor : topAnchor } - + /// `bottomAnchor` /// - Parameter safe: `Bool` Use safe anchor func bottomAnchor(safe: Bool) -> NSLayoutYAxisAnchor { return safe ? safeBottomAnchor : bottomAnchor } - + /// `widthAnchor` /// - Parameter safe: `Bool` Use safe anchor func widthAnchor(safe: Bool) -> NSLayoutDimension { return safe ? safeWidthAnchor : widthAnchor } - + /// `heightAnchor` /// - Parameter safe: `Bool` Use safe anchor func heightAnchor(safe: Bool) -> NSLayoutDimension { diff --git a/Sources/Extensions/UIView+Shadow.swift b/Sources/Extensions/UIView+Shadow.swift index 8274b98..ef2a16f 100644 --- a/Sources/Extensions/UIView+Shadow.swift +++ b/Sources/Extensions/UIView+Shadow.swift @@ -11,7 +11,7 @@ import UIKit // MARK: - UIView + Shadow extension UIView { - + /// Add shadow below a `UIView` /// - Parameter setMask: Update `layer.masksToBounds` and `clipsToBounds` func addShadowBelow(setMask: Bool = true) { @@ -19,7 +19,7 @@ extension UIView { layer.masksToBounds = false clipsToBounds = false } - + shadowComponents = ShadowComponents( radius: 1, opacity: 0.6, @@ -27,7 +27,7 @@ extension UIView { offset: CGSize(width: 0, height: 1) ) } - + /// Reset `UIView` shadow properties back to default /// - Parameter setMask: Update `layer.masksToBounds` and `clipsToBounds` func removeShadow(setMask: Bool = true) { @@ -42,18 +42,18 @@ extension UIView { // MARK: - CALayer + RoundedShadow extension CALayer { - + /// Set the `layer` corner radius func updateCornerRadius(_ cornerRadius: CGFloat) { self.cornerRadius = cornerRadius allowsEdgeAntialiasing = true } - + /// Configure the `layer` shadow func updateRoundedShadowPath() { shadowPath = roundedPath.cgPath } - + /// `UIBezierPath` to draw the shadow var roundedPath: UIBezierPath { return UIBezierPath( diff --git a/Sources/Extensions/UIView+ShadowComponents.swift b/Sources/Extensions/UIView+ShadowComponents.swift index 8b9aa35..c28fd78 100644 --- a/Sources/Extensions/UIView+ShadowComponents.swift +++ b/Sources/Extensions/UIView+ShadowComponents.swift @@ -10,19 +10,19 @@ import UIKit /// Component representation of all properties required to render a shadow in UIKit public struct ShadowComponents { - + /// The blur radius of the shadow public let radius: CGFloat - + /// The opacity of the shadow public let opacity: Float - + /// The color of the shadow public let color: UIColor - + /// The offset of the shadow public let offset: CGSize - + /// Default public memberwise initialiser /// - Parameters: /// - radius: The blur radius of the shadow @@ -40,7 +40,7 @@ public struct ShadowComponents { self.color = color self.offset = offset } - + /// Default shadow properties on `CALayer` from UIKit /// - Note: This won't show any shadow public static let `default` = ShadowComponents( @@ -49,7 +49,7 @@ public struct ShadowComponents { color: .black, offset: CGSize(width: 0, height: -3) ) - + /// Default shadow gray public static let defaultBlack = ShadowComponents( radius: 3, @@ -62,15 +62,8 @@ public struct ShadowComponents { // MARK: - CALayer + ShadowComponents public extension CALayer { - + var shadowComponents: ShadowComponents? { - set { - guard let newValue = newValue else { - setShadowComponents(.default) - return - } - setShadowComponents(newValue) - } get { guard let shadowColor = shadowColor else { return nil @@ -83,8 +76,15 @@ public extension CALayer { offset: shadowOffset ) } + set { + guard let newValue = newValue else { + setShadowComponents(.default) + return + } + setShadowComponents(newValue) + } } - + /// Set properties on the `CALayer` given by `components` /// - Parameter components: `ShadowComponents` private func setShadowComponents(_ components: ShadowComponents) { @@ -98,7 +98,7 @@ public extension CALayer { // MARK: - UIView + ShadowComponents public extension UIView { - + var shadowComponents: ShadowComponents? { get { return layer.shadowComponents diff --git a/Sources/Extensions/UIViewController+System.swift b/Sources/Extensions/UIViewController+System.swift index 11dc2dc..babcc82 100644 --- a/Sources/Extensions/UIViewController+System.swift +++ b/Sources/Extensions/UIViewController+System.swift @@ -15,7 +15,7 @@ import EventKitUI import ContactsUI extension UIViewController { - + /// Array of "system" `UIViewController`s. /// We define "system" here as an Apple `UIViewController` which mostly defines /// it's own UI and behaviour. It wouldn't be common to subclass @@ -37,7 +37,7 @@ extension UIViewController { // Bluetooth, NFC? ] } - + /// Is the `type(of: self)` in `systemViewControllerTypes` var isSystemViewController: Bool { return Self.systemViewControllerTypes.contains { type(of: self) == $0 } diff --git a/Sources/Function/Function.swift b/Sources/Function/Function.swift index 50f33b9..8e30c80 100644 --- a/Sources/Function/Function.swift +++ b/Sources/Function/Function.swift @@ -20,4 +20,3 @@ protocol Function { /// - Returns: `f(x)` in `Y`, the range of `f` func value(for x: ValueType) -> ValueType } - diff --git a/Sources/Function/GrowFunction.swift b/Sources/Function/GrowFunction.swift index 8dedf1e..5e531da 100644 --- a/Sources/Function/GrowFunction.swift +++ b/Sources/Function/GrowFunction.swift @@ -17,28 +17,28 @@ import UIKit /// Use case `x` in `[0, X]` struct GrowFunction: Function { typealias ValueType = CGFloat - + /// Asymptotic value as `f` converges on `inf` let M: CGFloat = 1 - + /// Approximate "end" for domain, `x in [0, X]` let X: CGFloat = 1 - + /// Alpha exponential scale factor private var alpha: CGFloat { - + // The delta from `X` to attain delta of `M` ("close to `M`") let xDelta: ValueType = 0.1 let x = X - xDelta - + // The delta from `M` at `x` (`f(x) = M - yDelta`) let yDelta: ValueType = 0.01 - + return (-1 / x) * log(yDelta / M) } - + // MARK: - Function - + /// `f(x) = M(1 - exp(-alpha * x))` /// - Parameter x: `x` in the domain of `f` func value(for x: CGFloat) -> CGFloat { diff --git a/Sources/Function/ScaleFunction.swift b/Sources/Function/ScaleFunction.swift index 5e7bb6e..81270f9 100644 --- a/Sources/Function/ScaleFunction.swift +++ b/Sources/Function/ScaleFunction.swift @@ -18,38 +18,38 @@ import Foundation /// over a time (`T`). struct ScaleFunction: Function { typealias ValueType = TimeInterval - + /// Value in the domain where the `Function` attains `Y` let T: ValueType - + /// Value in the range to attain. /// E.g. a scale after a given time let Y: ValueType - + /// Default memberwise initializer /// - Parameters: /// - T: `ValueType` /// - Y: `ValueType` - public init( + init( T: ValueType, Y: ValueType - ){ + ) { self.T = T self.Y = Y } - + /// Gradient of linear function var m: ValueType { return (Y - 1) / T } - + /// Value of linear function at `0` var c: ValueType { return 1 } - + // MARK: - Function - + func value(for x: ValueType) -> ValueType { return m * x + c } diff --git a/Sources/Function/ShakeFunction.swift b/Sources/Function/ShakeFunction.swift index 2c29df0..63ee0ce 100644 --- a/Sources/Function/ShakeFunction.swift +++ b/Sources/Function/ShakeFunction.swift @@ -23,32 +23,32 @@ struct ShakeFunction: Function { /// Maximum function value let Y: ValueType - + /// A `sin(x)` curve repeats after a given cycle, define the number of cycles before `T` let N: Int - - public init( + + init( T: ValueType, Y: ValueType, N: Int - ){ + ) { self.T = T self.Y = Y self.N = N } - + /// Function scale factor var alpha: Double { return Y / (T * T) } - + /// Cycle time var C: TimeInterval { return T / TimeInterval(N) } - + // MARK: - Function - + func value(for x: Double) -> Double { return alpha * x * x * sin(2 * Double.pi * x / C) } diff --git a/Sources/Messages/BadgeMessage/BadgeMessage.swift b/Sources/Messages/BadgeMessage/BadgeMessage.swift index 04481ff..b074676 100644 --- a/Sources/Messages/BadgeMessage/BadgeMessage.swift +++ b/Sources/Messages/BadgeMessage/BadgeMessage.swift @@ -11,21 +11,21 @@ import UIKit /// Model structure for the view `BadgeMessageView` public struct BadgeMessage { - + /// `String` text for the `titleLabel` public var title: String? - + /// `String` subtitle for the `subtitleLabel` public var subtitle: String? - + /// - `UIImage` for the `badgeView` image /// - `UIImage` for the `backgroundImageView` public var image: UIImage? - + /// - Fill `UIColor` for the `badgeView` /// - `tintColor` for the `backgroundImageView` public var fillColor: UIColor - + /// Default public memberwise initializer /// - Parameters: /// - title: `String?` @@ -37,7 +37,7 @@ public struct BadgeMessage { subtitle: String?, image: UIImage?, fillColor: UIColor - ){ + ) { self.title = title self.subtitle = subtitle self.image = image diff --git a/Sources/Messages/BadgeMessage/BadgeMessageView+BadgeMessage.swift b/Sources/Messages/BadgeMessage/BadgeMessageView+BadgeMessage.swift index a3ed047..fda84b6 100644 --- a/Sources/Messages/BadgeMessage/BadgeMessageView+BadgeMessage.swift +++ b/Sources/Messages/BadgeMessage/BadgeMessageView+BadgeMessage.swift @@ -11,20 +11,20 @@ import UIKit /// View-model applying model `BadgeMessage` to view `BadgeMessageView` extension BadgeMessageView: BadgeMessageViewable { - + /// Apply `badgeMessage` model to `self` (`UIView`) /// - Parameter badgeMessage: `BadgeMessage` public func set(badgeMessage: BadgeMessage) { // title titleLabel.text = badgeMessage.title - + // subtitle subtitleLabel.text = badgeMessage.subtitle - + // fillColor badgeView.fillColor = badgeMessage.fillColor backgroundImageView.tintColor = badgeMessage.fillColor - + // image let image = badgeMessage.image?.withRenderingMode(.alwaysTemplate) badgeView.image = image diff --git a/Sources/Messages/BadgeMessage/BadgeMessageView.swift b/Sources/Messages/BadgeMessage/BadgeMessageView.swift index 8b0ba6d..7960246 100644 --- a/Sources/Messages/BadgeMessage/BadgeMessageView.swift +++ b/Sources/Messages/BadgeMessage/BadgeMessageView.swift @@ -12,34 +12,34 @@ import UIKit /// A `UIView` often toasted from the top of a screen to detail a message. /// This is a more comprehensive `MessageView`. open class BadgeMessageView: UIView { - + /// Fixed constants for `BadgeMessageView` private struct Constants { - + /// Corner radius of the `BadgeMessageView` static let cornerRadius: CGFloat = 16 - + /// `UIImage` for the cancel `UIButton` static let cancelImage: UIImage? = .iconClose - + /// `CGFloat` inset value for the `UIButton` static let buttonInset = UIEdgeInsets(value: 10) - + /// `UIEdgeInsets` of the `horizontalStackView` from self static let horizontalStackViewInsets = UIEdgeInsets( top: 20, left: 15, bottom: 20, right: 15 - buttonInset.right ) - + /// Fixed `CGSize` of the `badgeView` static let badgeViewSize = CGSize(width: 48, height: 48) - + /// `CALayer` `opacity` for `backgroundImageView` static let backgroundImageViewOpacity: Float = 0.06 - + /// Translate the `backgroundImageView` in X by this amount static let backgroundImageViewTranslationX: CGFloat = -8 } - + /// Overrideable default shadow components for `BadgeMessageView` public static var shadowComponents = ShadowComponents( radius: 8, @@ -49,14 +49,14 @@ open class BadgeMessageView: UIView { ) // MARK: - Subviews - + /// `ShadowView` container `UIView` of `containerView` so `containerView` /// can clip subview content but we can also apply shadow private(set) lazy var shadowView: UIView = { let shadowView = UIView() return shadowView }() - + /// Subview of `self` but container (super) `UIView` for all other subviews. /// This is so this `UIView` can clip content but we can apply shadow on `self`. private(set) lazy var containerView: UIView = { @@ -65,7 +65,7 @@ open class BadgeMessageView: UIView { view.clipsToBounds = true return view }() - + /// Root, horizontally aligned `UIStackView` public private(set) lazy var horizontalStackView: UIStackView = { let stackView = UIStackView() @@ -75,7 +75,7 @@ open class BadgeMessageView: UIView { stackView.spacing = 22 return stackView }() - + /// `UIImageView` behind the `badgeView` for a background effect public private(set) lazy var backgroundImageView: UIImageView = { let imageView = UIImageView() @@ -88,10 +88,10 @@ open class BadgeMessageView: UIView { ) return imageView }() - + /// `BadgeView` at the leading edge of the `horizontalStackView` public private(set) lazy var badgeView = BadgeView() - + /// Vertically aligned `UIStackView` for the `titleLabel` and `subtitleLabel`. /// Central between `badgeView` and `button`. public private(set) lazy var verticalStackView: UIStackView = { @@ -102,7 +102,7 @@ open class BadgeMessageView: UIView { stackView.spacing = 4 return stackView }() - + /// `UILabel` at the top of `verticalStackView` public private(set) lazy var titleLabel: UILabel = { let label = UILabel.default @@ -110,12 +110,12 @@ open class BadgeMessageView: UIView { label.font = UIFont.systemFont(ofSize: 18, weight: .heavy) return label }() - + /// `UILabel` at the bottom of `verticalStackView` public private(set) lazy var subtitleLabel: UILabel = { return UILabel.default }() - + /// `UIButton` at the trailing edge of the `stackView` public private(set) lazy var button: UIButton = { let button = UIButton() @@ -124,9 +124,9 @@ open class BadgeMessageView: UIView { button.contentEdgeInsets = Constants.buttonInset return button }() - + // MARK: - containerViewInsets - + /// `UIEdgeInsets` of `containerViewEdgeConstraints` public var containerViewInsets: UIEdgeInsets { get { @@ -136,37 +136,37 @@ open class BadgeMessageView: UIView { containerViewEdgeConstraints.insets = newValue } } - + /// `EdgeConstraints` constraining `containerView` to `self` private var containerViewEdgeConstraints: EdgeConstraints! - + // MARK: - Init - + public convenience init() { self.init(frame: .zero) } - - public override init(frame: CGRect) { + + override public init(frame: CGRect) { super.init(frame: frame) setup() } - + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + private func setup() { // backgroundColor backgroundColor = .clear clipsToBounds = false - + // Add subviews to `self` and add constraints addSubviewsAndConstrain() - + // layer updateCornersAndShadow() - + // Add target for dismiss on `.touchUpInside` button.addTarget( self, @@ -174,55 +174,55 @@ open class BadgeMessageView: UIView { for: .touchUpInside ) } - + // MARK: - Constraints - + private func addSubviewsAndConstrain() { // verticalStackView verticalStackView.addArrangedSubview(titleLabel) verticalStackView.addArrangedSubview(subtitleLabel) - + // horizontalStackView horizontalStackView.addArrangedSubview(badgeView) horizontalStackView.addArrangedSubview(verticalStackView) horizontalStackView.addArrangedSubview(button) - + // containerView containerView.addSubview(backgroundImageView) containerView.addSubview(horizontalStackView) - + // shadowView shadowView.addSubview(containerView) - + // self addSubview(shadowView) - + // Constrain addConstraints() } - + private func addConstraints() { // shadowView shadowView.edgeConstraints(to: containerView) - + // containerView containerViewEdgeConstraints = containerView.edgeConstraints(to: self) - + // horizontalStackView horizontalStackView.edgeConstraints( to: containerView, insets: Constants.horizontalStackViewInsets ) - + // badgeView let size = badgeView.sizeConstraints(size: Constants.badgeViewSize) - + // backgroundImageView backgroundImageView.sizeConstraints(size: CGSize( width: 2 * size.width.constant, height: 2 * size.height.constant )) - + NSLayoutConstraint.activate([ // backgroundImageView backgroundImageView.centerXAnchor.constraint( @@ -232,7 +232,7 @@ open class BadgeMessageView: UIView { equalTo: badgeView.centerYAnchor ) ]) - + // button let axes: [NSLayoutConstraint.Axis] = [.vertical, .horizontal] axes.forEach { @@ -241,42 +241,43 @@ open class BadgeMessageView: UIView { button.setContentCompressionResistancePriority(priority, for: $0) } } - + // MARK: - Layout - - public override func layoutSubviews() { + + override public func layoutSubviews() { super.layoutSubviews() updateCornersAndShadow() } - + /// Update cornerRadius and shadow private func updateCornersAndShadow() { let layers = [layer, containerView.layer, shadowView.layer] - + // cornerRadius layers.forEach { $0.updateCornerRadius(Constants.cornerRadius) } - + if #available(iOS 13.0, *) { layers.forEach { $0.cornerCurve = .continuous } } - + shadowView.layer.setSketchShadow(Self.shadowComponents.sketchShadow) } - + // MARK: - UIControlEvent - + /// On `sender` `.touchUpInside`, find the first `Poster` superview and call /// remove /// - Parameter sender: `UIButton` - @objc private func buttonTouchUpInside(_ sender: UIButton) { + @objc + private func buttonTouchUpInside(_ sender: UIButton) { let poster: Poster? = sender.firstSuperviewOfType() let request = poster?.postManager.currentPostRequests .first { $0.view == self } - + if let postRequest = request { poster?.postManager.remove(postRequest: postRequest) } diff --git a/Sources/Messages/BadgeMessage/BadgeMessageViewable.swift b/Sources/Messages/BadgeMessage/BadgeMessageViewable.swift index 753d950..a2e769c 100644 --- a/Sources/Messages/BadgeMessage/BadgeMessageViewable.swift +++ b/Sources/Messages/BadgeMessage/BadgeMessageViewable.swift @@ -10,7 +10,7 @@ import Foundation /// Handle a `BadgeMessage` public protocol BadgeMessageViewable { - + /// Apply the given `BadgeMessage` model /// - Parameter badgeMessage: `BadgeMessage` func set(badgeMessage: BadgeMessage) diff --git a/Sources/Messages/BadgeMessage/Poster+BadgeMessage.swift b/Sources/Messages/BadgeMessage/Poster+BadgeMessage.swift index 0e7c087..447de32 100644 --- a/Sources/Messages/BadgeMessage/Poster+BadgeMessage.swift +++ b/Sources/Messages/BadgeMessage/Poster+BadgeMessage.swift @@ -10,7 +10,7 @@ import Foundation import UIKit public extension Poster { - + /// Post a `badgeMessage` /// /// - Parameters: @@ -23,20 +23,20 @@ public extension Poster { dismissAfter: TimeInterval? = .defaultMessageDismiss, animated: PostAnimation = .both ) -> BadgeMessageView { - + // Create a `BadgeMessageView` let badgeMessageView = BadgeMessageView() - + // Apply `badgeMessage` badgeMessageView.set(badgeMessage: badgeMessage) - + // Post the view postManager.post(postRequest: PostRequest( view: badgeMessageView, dismissAfter: dismissAfter, animated: animated )) - + return badgeMessageView } } diff --git a/Sources/Messages/Message/Message.swift b/Sources/Messages/Message/Message.swift index 2791326..122b7d3 100644 --- a/Sources/Messages/Message/Message.swift +++ b/Sources/Messages/Message/Message.swift @@ -10,19 +10,19 @@ import UIKit /// Message model to post public struct Message { - + /// Title of the message - public var title: String? = nil - + public var title: String? + /// Subtitle of the message - public var subtitle: String? = nil - + public var subtitle: String? + /// Left `UIImage` of the message - public var leftImage: UIImage? = nil - + public var leftImage: UIImage? + /// Right `UIImage` of the message - public var rightImage: UIImage? = nil - + public var rightImage: UIImage? + /// Default public memberwise initializer. /// - Parameters: /// - title: `String` title of the message @@ -34,7 +34,7 @@ public struct Message { subtitle: String? = nil, leftImage: UIImage? = nil, rightImage: UIImage? = nil - ){ + ) { self.title = title self.subtitle = subtitle self.leftImage = leftImage diff --git a/Sources/Messages/Message/MessageView+Message.swift b/Sources/Messages/Message/MessageView+Message.swift index 763c20a..647bf51 100644 --- a/Sources/Messages/Message/MessageView+Message.swift +++ b/Sources/Messages/Message/MessageView+Message.swift @@ -11,20 +11,20 @@ import UIKit /// View-model applying model `Message` to view `MessageView` extension MessageView: MessageViewable { - + /// Apply `Message` to `MessageView` /// - Parameter message: `Message` public func set(message: Message) { - + // Title titleLabel.setTextAndHidden(message.title) - + // Detail subtitleLabel.setTextAndHidden(message.subtitle) - + // Left Image leftImageView.setImageAndHidden(message.leftImage) - + // Right Image rightImageView.setImageAndHidden(message.rightImage) } diff --git a/Sources/Messages/Message/MessageView.swift b/Sources/Messages/Message/MessageView.swift index 20de188..dd839cc 100644 --- a/Sources/Messages/Message/MessageView.swift +++ b/Sources/Messages/Message/MessageView.swift @@ -16,46 +16,46 @@ import UIKit /// - Title `UILabel` /// - Detail `UILabel` open class MessageView: UIView { - + /// Constants for the `MessageView` public struct Constants { - + /// Size of the `UIImageView` - static let imageViewSize: CGSize = CGSize(width: 20, height: 20) - + static let imageViewSize = CGSize(width: 20, height: 20) + /// Inset of the `UIStackView` relative to the `MessageView` static let stackViewInsets = UIEdgeInsets( top: 10, left: 16, bottom: 10, right: 16 ) - + /// Vertical `UIStackView` spacing static let verticalStackViewSpacing: CGFloat = 5 - + /// Horizontal `UIStackView` spacing static let horizontalStackViewSpacing: CGFloat = 10 - + /// Default tint color for view static let defaultTintColor = UIColor.darkGray - + /// Default font for `titleLabel` static let titleFont = UIFont.systemFont(ofSize: 14, weight: .semibold) - + /// Default font for `detailLabel` static let detailFont = UIFont.systemFont(ofSize: 12, weight: .regular) } - + /// Container view for other subviews of `MessageView`. /// Required for shadow: for smooth animations we need to clip subview content (`clipsToBounds = true` ), /// but this would remove shadow. So clip this `containerView`, but allow out of bounds content on the `MessageView` public private(set) lazy var containerView: UIView = { let view = UIView() view.backgroundColor = .clear - + /// Hide subview content when the height of this view is being animated view.clipsToBounds = true return view }() - + /// `UIStackView` subview of `MessageView` to position `UIImageView` and `UILabel` public private(set) lazy var horizontalStackView: UIStackView = { let stackView: UIStackView = .defaultStackView @@ -64,19 +64,19 @@ open class MessageView: UIView { stackView.spacing = Constants.horizontalStackViewSpacing return stackView }() - + /// `UIImageView`, first arrangedSubview of the horizontally oriented `UIStackView` public private(set) lazy var leftImageView: UIImageView = { return .defaultImageView }() - + /// `UIStackView` subview of `MessageView` to position `UIImageView` and `UILabel` public private(set) lazy var verticalStackView: UIStackView = { let stackView: UIStackView = .defaultStackView stackView.spacing = Constants.verticalStackViewSpacing return stackView }() - + /// `UILabel`, last arrangedSubview of the horizontally oriented `UIStackView` public private(set) lazy var titleLabel: UILabel = { let label: UILabel = .default @@ -84,7 +84,7 @@ open class MessageView: UIView { label.textColor = .black return label }() - + /// `UILabel`, last arrangedSubview of the horizontally oriented `UIStackView` public private(set) lazy var subtitleLabel: UILabel = { let label: UILabel = .default @@ -92,59 +92,59 @@ open class MessageView: UIView { label.textColor = .darkGray return label }() - + /// `UIImageView`, first arrangedSubview of the horizontally oriented `UIStackView` public private(set) lazy var rightImageView: UIImageView = { return .defaultImageView }() - + /// `CGSize` of the `leftImageView` public var leftImageViewSize: CGSize = Constants.imageViewSize { didSet { leftImageSizeConstaints.setSize(leftImageViewSize) } } - + /// `CGSize` of the `rightImageView` public var rightImageViewSize: CGSize = Constants.imageViewSize { didSet { rightImageSizeConstaints.setSize(rightImageViewSize) } } - + /// `NSLayoutConstraints` setting the `SizeConstraints` on the `leftImageView` private var leftImageSizeConstaints: SizeConstraints! - + /// `NSLayoutConstraints` setting the `SizeConstraints` on the `rightImageView` private var rightImageSizeConstaints: SizeConstraints! - + // MARK: - Init - + /// Create with default frame public convenience init() { self.init(frame: .zero) } - + /// Create with explicit frame - `CGRect` - public override init(frame: CGRect) { + override public init(frame: CGRect) { super.init(frame: frame) setup() } - + /// Create with coder - `NSCoder` public required init?(coder: NSCoder) { super.init(coder: coder) setup() } - + // MARK: - Setup - + /// Add subviews and constrain internal func setup() { - + /// Explicity set default tintColor tintColor = Constants.defaultTintColor - + // Add subviews addSubview(containerView) containerView.addSubview(horizontalStackView) @@ -153,17 +153,17 @@ open class MessageView: UIView { horizontalStackView.addArrangedSubview(rightImageView) verticalStackView.addArrangedSubview(titleLabel) verticalStackView.addArrangedSubview(subtitleLabel) - + // Set translatesAutoresizingMaskIntoConstraints containerView.translatesAutoresizingMaskIntoConstraints = false horizontalStackView.translatesAutoresizingMaskIntoConstraints = false leftImageView.translatesAutoresizingMaskIntoConstraints = false rightImageView.translatesAutoresizingMaskIntoConstraints = false - + // Set hugging and compressionResistance subtitleLabel.setContentHuggingPriority(.init(200), for: .vertical) subtitleLabel.setContentCompressionResistancePriority(.init(700), for: .vertical) - + // Constrain var constraints = containerView.edgeConstraints(to: self).constraints @@ -171,7 +171,7 @@ open class MessageView: UIView { let horizontalStackViewBottom = horizontalStackView.bottomAnchor.constraint( equalTo: containerView.bottomAnchor, constant: -Constants.stackViewInsets.bottom) horizontalStackViewBottom.priority = .init(999) - + constraints += [ horizontalStackView.leadingAnchor.constraint( equalTo: containerView.leadingAnchor, constant: Constants.stackViewInsets.left), @@ -183,16 +183,16 @@ open class MessageView: UIView { ] leftImageSizeConstaints = leftImageView.sizeConstraints(size: Constants.imageViewSize) constraints += leftImageSizeConstaints?.constraints ?? [] - + rightImageSizeConstaints = rightImageView.sizeConstraints(size: Constants.imageViewSize) constraints += rightImageSizeConstaints?.constraints ?? [] - + NSLayoutConstraint.activate(constraints) } - + // MARK: - Override - - public override var tintColor: UIColor! { + + override public var tintColor: UIColor! { didSet { leftImageView.tintColor = tintColor titleLabel.textColor = tintColor @@ -205,7 +205,7 @@ open class MessageView: UIView { // MARK: - UIStackView + Default private extension UIStackView { - + /// `UIStackView` setting default properties static var defaultStackView: UIStackView { let stackView = UIStackView() @@ -220,7 +220,7 @@ private extension UIStackView { // MARK: - UIImageView + Default private extension UIImageView { - + /// `UIImageView` setting default properties static var defaultImageView: UIImageView { let imageView = UIImageView() diff --git a/Sources/Messages/Message/MessageViewable.swift b/Sources/Messages/Message/MessageViewable.swift index 3e7286e..a43f57b 100644 --- a/Sources/Messages/Message/MessageViewable.swift +++ b/Sources/Messages/Message/MessageViewable.swift @@ -10,7 +10,7 @@ import Foundation /// Handle a `Message` public protocol MessageViewable { - + /// Apply the given `Message` model /// - Parameter message: `Message` func set(message: Message) diff --git a/Sources/Messages/Message/Poster+Message.swift b/Sources/Messages/Message/Poster+Message.swift index 65d2204..cac281b 100644 --- a/Sources/Messages/Message/Poster+Message.swift +++ b/Sources/Messages/Message/Poster+Message.swift @@ -11,8 +11,8 @@ import UIKit // MARK: - Post -extension Poster { - +public extension Poster { + /// Post a `Message` creating a `MessageView` /// /// - Parameters: @@ -22,25 +22,25 @@ extension Poster { /// /// - Returns: The `MessageView` created and added to the `MessageStackView` @discardableResult - public func post( + func post( message: Message, dismissAfter: TimeInterval? = .defaultMessageDismiss, animated: PostAnimation = .default ) -> MessageView { - + // Create a `MessageView` let messageView = MessageView() - + // Apply message messageView.set(message: message) - + // Post the view postManager.post(postRequest: PostRequest( view: messageView, dismissAfter: dismissAfter, animated: animated )) - + return messageView } } diff --git a/Sources/Messages/MessageConfiguration/MessageConfigurable.swift b/Sources/Messages/MessageConfiguration/MessageConfigurable.swift index 7bf06c6..ebf24ab 100644 --- a/Sources/Messages/MessageConfiguration/MessageConfigurable.swift +++ b/Sources/Messages/MessageConfiguration/MessageConfigurable.swift @@ -10,7 +10,7 @@ import UIKit /// Apply a `MessageConfiguration` public protocol MessageConfigurable { - + /// Allow a `MessageConfiguration` to be applied /// /// - Parameter configuration: The `MessageConfiguration` diff --git a/Sources/Messages/MessageConfiguration/MessageConfiguration.swift b/Sources/Messages/MessageConfiguration/MessageConfiguration.swift index 9bb7b58..e0403bc 100644 --- a/Sources/Messages/MessageConfiguration/MessageConfiguration.swift +++ b/Sources/Messages/MessageConfiguration/MessageConfiguration.swift @@ -10,7 +10,7 @@ import UIKit /// Define a default configuration to look and feel public struct MessageConfiguration { - + /// A standard/default `MessageConfiguration` public static var `default` = MessageConfiguration( backgroundColor: .defaultBackgroundColor, @@ -18,19 +18,19 @@ public struct MessageConfiguration { shadow: true, applyToAll: false ) - + /// Background color of `MessageView`s public var backgroundColor: UIColor? - + /// Tint color of `MessageView`s public var tintColor: UIColor? - + /// If shadow is added below `MessageView` public var shadow: Bool - + /// If this configuration updates, should previously posted `MessageView`s be updated public var applyToAll: Bool - + /// Public default memberwise initializer. /// - Parameters: /// - backgroundColor: `UIColor` @@ -42,7 +42,7 @@ public struct MessageConfiguration { tintColor: UIColor? = nil, shadow: Bool = false, applyToAll: Bool = false - ){ + ) { self.backgroundColor = backgroundColor self.tintColor = tintColor self.shadow = shadow @@ -53,12 +53,12 @@ public struct MessageConfiguration { // MARK: - MessageConfiguration + UIColor public extension UIColor { - + /// Default background color of a `MessageView` static let defaultBackgroundColor = UIColor( red255: 245, green255: 245, blue255: 245 ) - + /// Default tint color of a `MessageView` static let defaultTintColor: UIColor = .darkGray } diff --git a/Sources/Messages/MessageConfiguration/Message+MessageConfigurable.swift b/Sources/Messages/MessageConfiguration/MessageView+MessageConfigurable.swift similarity index 91% rename from Sources/Messages/MessageConfiguration/Message+MessageConfigurable.swift rename to Sources/Messages/MessageConfiguration/MessageView+MessageConfigurable.swift index 846347e..a0dc132 100644 --- a/Sources/Messages/MessageConfiguration/Message+MessageConfigurable.swift +++ b/Sources/Messages/MessageConfiguration/MessageView+MessageConfigurable.swift @@ -1,5 +1,5 @@ // -// Message+MessageConfigurable.swift +// MessageView+MessageConfigurable.swift // MessageStackView // // Created by Ben Shutt on 03/07/2020. @@ -10,21 +10,21 @@ import Foundation import UIKit extension MessageView: MessageConfigurable { - + /// Apply a `MessageConfiguration` /// - Parameter configuration: `MessageConfiguration` public func set(configuration: MessageConfiguration) { - + // backgroundColor if let backgroundColor = configuration.backgroundColor { self.backgroundColor = backgroundColor } - + // tintColor if let tintColor = configuration.tintColor { self.tintColor = tintColor } - + // shadow if configuration.shadow { addShadowBelow() diff --git a/Sources/Models/PostAnimation.swift b/Sources/Models/PostAnimation.swift index 32f2d90..2d74ca3 100644 --- a/Sources/Models/PostAnimation.swift +++ b/Sources/Models/PostAnimation.swift @@ -11,7 +11,7 @@ import Foundation /// Alow the caller to control the animation when posting a message public struct PostAnimation: OptionSet { public let rawValue: Int - + /// Override access modifier to public /// - Parameter rawValue: `Int` public init(rawValue: Int) { @@ -20,21 +20,21 @@ public struct PostAnimation: OptionSet { /// Animate only on post public static let onPost = PostAnimation(rawValue: 1 << 0) - + /// Animate only on remove public static let onRemove = PostAnimation(rawValue: 1 << 1) - + /// Animate both on post and remove public static let both: PostAnimation = [.onPost, .onRemove] - + /// Don't animate on either post or remove public static let none: PostAnimation = [] } // MARK: PostAnimation + Default -extension PostAnimation { - +public extension PostAnimation { + /// Default `PostAnimation` - public static let `default`: PostAnimation = .both + static let `default`: PostAnimation = .both } diff --git a/Sources/Models/Queue.swift b/Sources/Models/Queue.swift index 6f9a333..76760e5 100644 --- a/Sources/Models/Queue.swift +++ b/Sources/Models/Queue.swift @@ -10,16 +10,16 @@ import Foundation /// ArrayDeque structure struct Queue { - + /// `Array` of `Element`s in the `Queue` private var elements: [Element] = [] - + /// Append `element` to the end of the `Queue` /// - Parameter element: `Element` to append mutating func enqueue(_ element: Element) { elements.append(element) } - + /// Remove the `Element` from the front of the queue mutating func dequeue() -> Element? { guard !elements.isEmpty else { @@ -27,22 +27,22 @@ struct Queue { } return elements.removeFirst() } - + /// `Element` at the front of the `Queue` var head: Element? { return elements.first } - + /// Element at the end of the `Queue` var tail: Element? { return elements.last } - + /// Number of elements in the `Queue` var count: Int { return elements.count } - + /// Returns `true` if the `Queue` doesn't have any elements. /// I.e. `elements` is empty. var isEmpty: Bool { @@ -55,7 +55,7 @@ struct Queue { extension Queue: Sequence { typealias Iterator = Array.Iterator typealias Element = Element - + __consuming func makeIterator() -> Array.Iterator { return elements.makeIterator() } diff --git a/Sources/Models/Vector3.swift b/Sources/Models/Vector3.swift index 60d4dd9..ed4a4e1 100644 --- a/Sources/Models/Vector3.swift +++ b/Sources/Models/Vector3.swift @@ -10,13 +10,13 @@ import Foundation /// 3 dimensional vector with values in X, Y, and Z struct Vector3 where Element: FloatingPoint { - + /// X coordinate value var x: Element - + /// Y coordinate value var y: Element - + /// Z coordinate value var z: Element } @@ -44,11 +44,11 @@ extension Vector3: CustomStringConvertible { // MARK: - AdditiveArithmetic extension Vector3: AdditiveArithmetic { - + static var zero: Vector3 { return Vector3(x: 0, y: 0, z: 0) } - + static func + (lhs: Vector3, rhs: Vector3) -> Vector3 { return Vector3( x: lhs.x + rhs.x, @@ -56,7 +56,7 @@ extension Vector3: AdditiveArithmetic { z: lhs.z + rhs.z ) } - + static func - (lhs: Vector3, rhs: Vector3) -> Vector3 { return Vector3( x: lhs.x - rhs.x, @@ -69,7 +69,7 @@ extension Vector3: AdditiveArithmetic { // MARK: - Extensions extension Vector3 { - + /// `Array` for `x`, `y`, `z` var array: [Element] { return [x, y, z] diff --git a/Sources/PostManager/PostGestureManager.swift b/Sources/PostManager/PostGestureManager.swift index b03261b..4e3c10b 100644 --- a/Sources/PostManager/PostGestureManager.swift +++ b/Sources/PostManager/PostGestureManager.swift @@ -10,18 +10,18 @@ import Foundation import UIKit /// Delegate callbacks for `PostGestureManager` -protocol PostGestureManagerDelegate: class { - +protocol PostGestureManagerDelegate: AnyObject { + func postGestureManager( _ manager: PostGestureManager, didRequestToRemoveView view: UIView ) - + func postGestureManager( _ manager: PostGestureManager, didStartPanGestureForView view: UIView ) - + func postGestureManager( _ manager: PostGestureManager, didEndPanGestureForView view: UIView @@ -33,24 +33,24 @@ protocol PostGestureManagerDelegate: class { /// E.g. The user pans a view off the top wanting for it to be dimissed. /// E.g. T user tapped to dismiss public class PostGestureManager { - + /// `PostGestureManagerDelegate` weak var delegate: PostGestureManagerDelegate? - + /// Map `UIView`s to their "tap to dismiss" `UITapGestureRecognizer` private var tapGestureMap = [UIView: UITapGestureRecognizer]() - + /// Map `UIView`s to their "pan to dismiss" `UIPanGestureRecognizer` private var panGestureMap = [UIView: UIPanGestureRecognizer]() - + // MARK: - Deinit - + deinit { invalidate() } - + // MARK: - Invalidate - + func invalidate() { tapGestureMap.forEach { $0.key.removeGestureRecognizer($0.value) @@ -58,13 +58,13 @@ public class PostGestureManager { panGestureMap.forEach { $0.key.removeGestureRecognizer($0.value) } - + tapGestureMap = [:] panGestureMap = [:] } - + // MARK: - Tap - + /// Add `UITapGestureRecognizer` to `view` to remove it from `self` /// - Parameter view: `UIView` to add gesture to public func addTapToRemoveGesture(to view: UIView) { @@ -78,28 +78,29 @@ public class PostGestureManager { tapGestureMap[view] = tap view.addGestureRecognizer(tap) } - + /// Remove `UITapGestureRecognizer` from `view` /// - Parameter view: `UIView` to remove gesture from public func removeTapToRemoveGesture(from view: UIView) { guard let tap = tapGestureMap[view] else { return } - + tapGestureMap[view] = nil view.removeGestureRecognizer(tap) } - + /// When a `UIView` wants to be dismissed on tap. /// - Parameter sender: `UITapGestureRecognizer` - @objc private func viewTapped(_ sender: UITapGestureRecognizer) { + @objc + private func viewTapped(_ sender: UITapGestureRecognizer) { if sender.state == .ended, let view = sender.view { requestRemove(view: view) } } - + // MARK: - Pan - + /// Add `UIPanGestureRecognizer` to `view` to remove it from `self` /// - Parameter view: `UIView` to add gesture to public func addPanToRemoveGesture(to view: UIView) { @@ -113,24 +114,25 @@ public class PostGestureManager { panGestureMap[view] = tap view.addGestureRecognizer(tap) } - + /// Remove `UIPanGestureRecognizer` from `view` /// - Parameter view: `UIView` to remove gesture from public func removePanToRemoveGesture(from view: UIView) { guard let tap = panGestureMap[view] else { return } - + panGestureMap[view] = nil view.removeGestureRecognizer(tap) } - + /// When a `UIView` wants to be dismissed on pan. /// - Parameter sender: `UIPanGestureRecognizer` - @objc private func viewPanned(_ sender: UIPanGestureRecognizer) { + @objc + private func viewPanned(_ sender: UIPanGestureRecognizer) { guard let view = sender.view else { return } let state = sender.state - + // Trigger delegate callback based for `boundaryState` switch state.boundaryState { case .started: @@ -144,10 +146,10 @@ public class PostGestureManager { default: break } - + // Handle pan based on state switch state { - + // Pan did update case .changed: view.transform = CGAffineTransform( @@ -155,37 +157,37 @@ public class PostGestureManager { y: min(0, sender.translation(in: view.superview).y) ) return - + // Pan did finish case .ended: let translateY = view.transform.ty let centerY = view.center.y - + let finalY = centerY + translateY if finalY < 0 { requestRemove(view: view) return } - + // Other default: break } - + UIView.animate(withDuration: .animationDuration) { view.transform = .identity } } - + // MARK: - Remove - + /// `UIView` should be removed based on gesture /// - Parameter view: `UIView` to remove private func requestRemove(view: UIView) { remove(view: view) delegate?.postGestureManager(self, didRequestToRemoveView: view) } - + /// Remove references to gestures for `view` /// - Parameter view: `UIView` func remove(view: UIView) { @@ -197,17 +199,18 @@ public class PostGestureManager { // MARK: - UIGestureRecognizer.State + BoundaryState private extension UIGestureRecognizer.State { - + /// `State` can be started or finished - @frozen enum BoundaryState { - + @frozen + enum BoundaryState { + /// `State` when the gesture started case started - + /// `State` when the gesture end case ended } - + /// `BoundaryState` var boundaryState: BoundaryState? { switch self { diff --git a/Sources/PostManager/PostManager.swift b/Sources/PostManager/PostManager.swift index dd92f57..081e5e6 100644 --- a/Sources/PostManager/PostManager.swift +++ b/Sources/PostManager/PostManager.swift @@ -22,13 +22,13 @@ import UIKit /// The actual `UIView` posting and removing should be handled by the `UIViewPoster` passed /// into `PostManager` in the initializer. public class PostManager { - + /// `UIViewPoster` to handle the post and remove of a `PostRequest` private weak var poster: UIViewPoster? - + /// `PostManagerDelegate` for `PostManager` delegate callabacks public weak var delegate: PostManagerDelegate? - + /// `PostGestureManager` to manage `UIGestureRecognizer` actions /// to remove a `UIView` public private(set) lazy var gestureManager: PostGestureManager = { @@ -36,16 +36,16 @@ public class PostManager { gestureManager.delegate = self return gestureManager }() - + /// Map a posted `UIView` to a `Timer`s which triggers the removal of that /// `UIView` if it has a finite lifetime private var timerMap = [UIView: Timer]() - + /// `Queue` of `PostRequest`s when `isSerialQueue` is `true`. /// If `isSerialQueue` is set to `false`, `queue` is emptied and posts all /// pending `PostRequest`s. private(set) var queue = Queue() - + /// Should the posted `UIView`s be handled by a serial `Queue`, i.e. one at a time. /// If `true`, queued `PostRequest`s are stored in `queue`. public var isSerialQueue: Bool = true { @@ -56,25 +56,25 @@ public class PostManager { } } } - + /// Currently executing `PostRequest`s public private(set) var currentPostRequests: [PostRequest] = [] // MARK: - Init - + /// Initializer with `poster` /// - Parameter poster: `UIViewPoster` internal init (poster: UIViewPoster) { self.poster = poster } - + /// Invalidate properties deinit { invalidate() } - + // MARK: - Invalidate - + /// Invalidate: /// - Remove `PostRequest`s /// - Clear `queue` @@ -82,18 +82,18 @@ public class PostManager { public func invalidate() { // Invalidate `gestureManager` gestureManager.invalidate() - + // Remove `currentPostRequest` currentPostRequests.forEach { remove(view: $0.view, animated: false) // force animated false } currentPostRequests = [] - + // Empty and remove `queue` while let request = queue.dequeue() { remove(view: request.view, animated: false) // force animated false } - + // Invalidate timers // These should theoretically be cleaned up from the remove methods, // but invalidate anyway to handle unexpected manipulation from caller @@ -102,22 +102,22 @@ public class PostManager { } timerMap = [UIView: Timer]() } - + // MARK: - Active - + /// Is the `PostManager` posting or scheduled to post public var isActive: Bool { // A post is showing atm - let isPosting = currentPostRequests.count > 0 - + let isPosting = !currentPostRequests.isEmpty + // A post is scheduled to show in the future - let isQueued = queue.count > 0 - + let isQueued = !queue.isEmpty + return isPosting || isQueued } - + // MARK: - Post - + /// Post the given `postRequest`. /// /// If `isSerialQueue`, ensure there is not a `currentPostRequest` otherwise @@ -129,37 +129,37 @@ public class PostManager { guard poster?.shouldPost(view: postRequest.view) ?? true else { return } - + // Add to queue if `isSerialQueue` and there is a `currentPostRequest` - guard !isSerialQueue || currentPostRequests.count == 0 else { + guard !isSerialQueue || currentPostRequests.isEmpty else { queue.enqueue(postRequest) return } - + // Set `currentPostRequest` currentPostRequests.append(postRequest) - + // Get `view` from the `postRequest` let view = postRequest.view - + // Remove from previous layout tree if it exists view.removeFromSuperview() - + // Execute `delegate` will post if `poster` is a `PostManagerDelegate` (poster as? PostManagerDelegate)?.postManager(self, willPost: view) - + // Execute `delegate` will post delegate?.postManager(self, willPost: view) - + // Inform `poster` to post poster?.post( view: view, animated: postRequest.animated.contains(.onPost), completion: { self.completePost(postRequest: postRequest) - }) + }) } - + /// Invoke on the completion of `poster` posting a `view` of a `postRequest`. /// E.g. after an animation has completed. /// @@ -167,47 +167,47 @@ public class PostManager { private func completePost(postRequest: PostRequest) { // Start dismiss timer if appropriate startTimer(for: postRequest) - + // `view` of the `postRequest` let view = postRequest.view - + // Execute `delegate` did post if `poster` is a `PostManagerDelegate` (poster as? PostManagerDelegate)?.postManager(self, didPost: view) - + // Execute `delegate` did post delegate?.postManager(self, didPost: view) } - + /// Setup dismiss `Timer` for `postRequest` /// - Parameter postRequest: `PostRequest` private func startTimer(for postRequest: PostRequest) { // Time after post to dismiss, zero or `nil` to mean do not dismiss let dismissAfterTimeInterval = max(0, postRequest.dismissAfter ?? 0) - + // Stop here if the caller does not want to dismiss this message guard dismissAfterTimeInterval > 0 else { return } - + // `UIView` posted let view = postRequest.view - + // Should we animate the removal let animateRemove = postRequest.animated.contains(.onRemove) - + // Schedule timer to fire `remove` call timerMap[view] = Timer.scheduledTimer( withTimeInterval: dismissAfterTimeInterval, repeats: false - ) { [weak view, weak self] timer in + ) { [weak view, weak self] _ in if let view = view, let self = self { self.remove(view: view, animated: animateRemove) } } } - + // MARK: - Remove - + /// Remove the given `postRequest` /// - Parameter postRequest: `PostRequest` public func remove(postRequest: PostRequest) { @@ -216,7 +216,7 @@ public class PostManager { animated: postRequest.animated.contains(.onRemove) ) } - + /// Remove (unpost) a posted `view` invalidatating appropriate properties. /// - Parameters: /// - view: `UIView` to remove @@ -224,46 +224,46 @@ public class PostManager { public func remove(view: UIView, animated: Bool = true) { // Remove gestures gestureManager.remove(view: view) - + // Remove pending removal timer timerMap[view]?.invalidate() timerMap[view] = nil - + // Ensure the context of the subview is still valid guard poster?.shouldRemove(view: view) ?? true else { return } - + // Execute `delegate` will remove if `poster` is a `PostManagerDelegate` (poster as? PostManagerDelegate)?.postManager(self, willRemove: view) - + // Execute `delegate` will remove delegate?.postManager(self, willRemove: view) - + // Inform `poster` to remove poster?.remove(view: view, animated: animated) { self.completeRemove(view: view) } } - + /// Invoke on the completion of `poster` removing `view`. /// - Parameter view: `UIView` private func completeRemove(view: UIView) { // No current currentPostRequests.removeAll { $0.view == view } - + // Execute `delegate` did remove if `poster` is a `PostManagerDelegate` (poster as? PostManagerDelegate)?.postManager(self, didRemove: view) - + // Execute `delegate` did remove delegate?.postManager(self, didRemove: view) - + // If the queue is non-empty, post the next if let postRequest = queue.dequeue() { post(postRequest: postRequest) } } - + /// Remove the `currentPostRequest` public func removeCurrent() { currentPostRequests.forEach { @@ -276,7 +276,7 @@ public class PostManager { // MARK: - PostGestureManagerDelegate extension PostManager: PostGestureManagerDelegate { - + func postGestureManager( _ manager: PostGestureManager, didRequestToRemoveView view: UIView @@ -288,7 +288,7 @@ extension PostManager: PostGestureManagerDelegate { remove(view: request.view, animated: animated) } } - + func postGestureManager( _ manager: PostGestureManager, didStartPanGestureForView view: UIView @@ -297,7 +297,7 @@ extension PostManager: PostGestureManagerDelegate { timerMap[view]?.invalidate() timerMap[view] = nil } - + func postGestureManager( _ manager: PostGestureManager, didEndPanGestureForView view: UIView diff --git a/Sources/PostManager/PostManagerDelegate.swift b/Sources/PostManager/PostManagerDelegate.swift index 087b212..3198383 100644 --- a/Sources/PostManager/PostManagerDelegate.swift +++ b/Sources/PostManager/PostManagerDelegate.swift @@ -13,7 +13,7 @@ import UIKit /// Delegate methods for `PostManager` public protocol PostManagerDelegate: AnyObject { - + /// Called when a `view` will be posted /// - Parameters: /// - postManager: `PostManager` @@ -22,7 +22,7 @@ public protocol PostManagerDelegate: AnyObject { _ postManager: PostManager, willPost view: UIView ) - + /// Called when a `view` was posted /// - Parameters: /// - postManager: `PostManager` @@ -31,7 +31,7 @@ public protocol PostManagerDelegate: AnyObject { _ postManager: PostManager, didPost view: UIView ) - + /// Called when a `view` will be removed /// - Parameters: /// - postManager: `PostManager` @@ -40,7 +40,7 @@ public protocol PostManagerDelegate: AnyObject { _ postManager: PostManager, willRemove view: UIView ) - + /// Called when a `view` was removed /// - Parameters: /// - postManager: `PostManager` @@ -53,32 +53,32 @@ public protocol PostManagerDelegate: AnyObject { /// Provide default implementation of `PostManagerDelegate` optional methods public extension PostManagerDelegate { - + func postManager( _ postManager: PostManager, willPost view: UIView - ){ + ) { // by default do nothing } - + func postManager( _ postManager: PostManager, didPost view: UIView - ){ + ) { // by default do nothing } - + func postManager( _ postManager: PostManager, willRemove view: UIView - ){ + ) { // by default do nothing } - + func postManager( _ postManager: PostManager, didRemove view: UIView - ){ + ) { // by default do nothing } } diff --git a/Sources/PostManager/PostRequest.swift b/Sources/PostManager/PostRequest.swift index 633ccff..581a8dc 100644 --- a/Sources/PostManager/PostRequest.swift +++ b/Sources/PostManager/PostRequest.swift @@ -11,17 +11,17 @@ import UIKit /// Request a `UIView` to be posted, its lifetime, and how the animation should behave public struct PostRequest { - + /// A `UIView` to post public var view: UIView - + /// After how long after post should we dismiss `view`. /// A value `<= 0` or `nil` will not start a timer to dismiss `view`. public var dismissAfter: TimeInterval? - + /// Animate the posting of `view` public var animated: PostAnimation - + /// Default public memberwise initializer /// - Parameters: /// - view: `UIView` @@ -31,7 +31,7 @@ public struct PostRequest { view: UIView, dismissAfter: TimeInterval? = nil, animated: PostAnimation = .both - ){ + ) { self.view = view self.dismissAfter = dismissAfter self.animated = animated diff --git a/Sources/PostManager/Poster.swift b/Sources/PostManager/Poster.swift index 42d5374..577ba8e 100644 --- a/Sources/PostManager/Poster.swift +++ b/Sources/PostManager/Poster.swift @@ -11,7 +11,7 @@ import UIKit /// Entity with a `PostManager` public protocol Poster { - + /// Has reference to a `PostManager` var postManager: PostManager { get } } @@ -19,7 +19,7 @@ public protocol Poster { // MARK: - Poster + UIView public extension Poster { - + /// Post `view`, `dismissAfter`, `animated` onto the `postManager` /// - Parameters: /// - view: `UIView` @@ -29,7 +29,7 @@ public extension Poster { view: UIView, dismissAfter: TimeInterval? = .defaultDismiss, animated: PostAnimation = .default - ){ + ) { postManager.post(postRequest: PostRequest( view: view, dismissAfter: dismissAfter, diff --git a/Sources/PostManager/UIView+Poster.swift b/Sources/PostManager/UIView+Poster.swift index 53a0799..6afefc6 100644 --- a/Sources/PostManager/UIView+Poster.swift +++ b/Sources/PostManager/UIView+Poster.swift @@ -12,7 +12,7 @@ import UIKit // MARK: - UIView + Poster public extension UIView { - + /// Create a `T` and constrain inline with the given arguments /// /// - Parameters: @@ -21,7 +21,7 @@ public extension UIView { func createPosterView( layout: MessageLayout = .default, constrainToSafeArea: Bool = true - ) -> T where T : UIView, T : Poster { + ) -> T where T: UIView, T: Poster { let posterView = T() posterView.addTo( view: self, @@ -32,9 +32,9 @@ public extension UIView { posterView.layoutIfNeeded() return posterView } - + // MARK: - Instance - + /// `MessageStackView` or create and constrain on `view` /// /// - Parameters: @@ -51,7 +51,7 @@ public extension UIView { messageStackView.updateOrderForLayout(layout) return messageStackView } - + /// `PostView` or create and constrain on `view` /// - Parameters: /// - layout: `MessageLayout` @@ -70,7 +70,7 @@ public extension UIView { // MARK: - UIApplication + PostView public extension UIApplication { - + /// `UIApplication` shared singleton `PostView` var postView: PostView { return ApplicationPostView.shared @@ -80,7 +80,7 @@ public extension UIApplication { // MARK: - UIApplication + PostView public extension MessageLayout { - + /// The default `MessageLayout` static let `default`: MessageLayout = .top } diff --git a/Sources/PostManager/UIViewPoster.swift b/Sources/PostManager/UIViewPoster.swift index 234a458..3217809 100644 --- a/Sources/PostManager/UIViewPoster.swift +++ b/Sources/PostManager/UIViewPoster.swift @@ -11,10 +11,10 @@ import UIKit /// `UIViewPoster` is sent messages to post and remove `UIView`s. /// It should handle any animation and execute a given closure on completion. -internal protocol UIViewPoster: class { - +internal protocol UIViewPoster: AnyObject { + // MARK: - Post - + /// Post `view` with animation if `animated` /// /// - Parameters: @@ -24,14 +24,14 @@ internal protocol UIViewPoster: class { func post( view: UIView, animated: Bool, completion: @escaping () -> Void ) - + /// Should `view` be posted /// /// - Parameter view: `UIView` func shouldPost(view: UIView) -> Bool - + // MARK: - Remove - + /// Remove `view` with animation if `animated` /// /// - Parameters: @@ -41,7 +41,7 @@ internal protocol UIViewPoster: class { func remove( view: UIView, animated: Bool, completion: @escaping () -> Void ) - + /// Should `view` be removed /// /// - Parameter view: `UIView` @@ -51,11 +51,11 @@ internal protocol UIViewPoster: class { // MARK: - UIViewPoster + Default internal extension UIViewPoster { - + func shouldPost(view: UIView) -> Bool { return true // By default allow post } - + func shouldRemove(view: UIView) -> Bool { return true // By default allow remove } diff --git a/Sources/Reachability/ConnectivityManager+MessageManager.swift b/Sources/Reachability/ConnectivityManager+MessageManager.swift index 6d502d7..3e102e0 100644 --- a/Sources/Reachability/ConnectivityManager+MessageManager.swift +++ b/Sources/Reachability/ConnectivityManager+MessageManager.swift @@ -9,15 +9,15 @@ import Foundation import UIKit -extension ConnectivityManager { - +public extension ConnectivityManager { + /// Manage a `MessageStackView` to post "Not connected to internet" messages at the bottom /// of the `visibleViewController`s view. - public class MessageManager: ConnectivityMessageable, PostManagerDelegate { + class MessageManager: ConnectivityMessageable, PostManagerDelegate { /// `NSObjectProtocol` observing internet connection updates private var observer: NSObjectProtocol? - + /// `MessageStackView ` to post messages public private(set) lazy var messageStackView: MessageStackView = { let messageStackView = MessageStackView() @@ -25,14 +25,14 @@ extension ConnectivityManager { messageStackView.postManager.delegate = self return messageStackView }() - + /// `Message` to post when internet connectivity is lost public var message: Message = .noInternet { didSet { messageView?.set(message: message) } } - + /// `MessageView` posted when internet connectivity was lost /// /// - Warning: @@ -40,46 +40,46 @@ extension ConnectivityManager { /// if the `visibleViewController` conforms to `InternetConnectivityMessageable` /// then it will post on there instead private weak var messageView: MessageView? - + // MARK: - Init - + deinit { stopObserving() } - + // MARK: - Connection Observer - + /// Add observer to `ConnectivityManager` public func startObserving() { guard observer == nil else { return } - + NotificationCenter.default.addObserver( self, selector: #selector(viewWillDisappear), name: .viewWillDisappear, object: nil ) - + observer = ConnectivityManager.shared.addObserver { [weak self] state in self?.didUpdateState(state) } } - + /// Remove observer from `ConnectivityManager` public func stopObserving() { guard let observer = observer else { return } - + ConnectivityManager.shared.removeObserver(observer) - + NotificationCenter.default.removeObserver( self, name: .viewWillDisappear, object: nil ) } - + // MARK: - State - + /// `ConnectivityManager.State` did update /// - Parameter state: `ConnectivityManager.State` private func didUpdateState(_ state: ConnectivityManager.State) { @@ -89,26 +89,26 @@ extension ConnectivityManager { onDisconnected() } } - + // MARK: - Connection Lifecycle - + /// Handle internet connection disconnected private func onDisconnected() { invalidateMessageStackView() - + guard let visibleViewController = UIApplication.shared.visibleViewController, var visibleView = visibleViewController.view else { return } - + // If the `visibleViewController` conforms to `ConnectivityMessageable` // then send the message there! if let messageable = visibleViewController as? ConnectivityMessageable { post(to: messageable) return } - + // TODO: Can we avoid? Usually would have a `UITableViewController` // child inside a `UIViewController` parent. // For `UITableViewController`, add to `view` of `navigationController`. @@ -117,7 +117,7 @@ extension ConnectivityManager { visibleView = visibleViewController.navigationController?.view ?? visibleViewController.tabBarController?.view ?? visibleView } - + messageStackView.addTo( view: visibleView, layout: .bottom, @@ -125,20 +125,20 @@ extension ConnectivityManager { ) messageStackView.spaceViewHeight = visibleViewController.view.safeAreaInsets.bottom - + visibleView.layoutIfNeeded() messageStackView.layoutIfNeeded() - + post(to: self) } - + /// Handle internet connection connected private func onConnected() { removeMessageStackView(animated: true) } - + // MARK: - MessageStackView - + /// Remove the `messageStackView` from its superview /// - Parameter animated: `Bool` private func removeMessageStackView(animated: Bool) { @@ -147,28 +147,29 @@ extension ConnectivityManager { invalidateMessageStackView() return } - + messageStackView.postManager.remove( view: messageView, animated: animated ) } - + /// - Invalidate `postManager` of `messageStackView` /// - Remove `messageStackView` from its superview private func invalidateMessageStackView() { messageStackView.postManager.invalidate() messageStackView.removeFromSuperview() } - + /// `UIViewController` lifecycle event /// - Parameter sender: `Notification` - @objc private func viewWillDisappear(_ sender: Notification) { + @objc + private func viewWillDisappear(_ sender: Notification) { removeMessageStackView(animated: true) } - + // MARK: - Post - + /// Post internet connectivity lost on the given `messageable` /// /// - Parameter messageable: `ConnectivityMessageable` @@ -176,21 +177,21 @@ extension ConnectivityManager { guard messageable.messageManagerShouldPost(self) else { return } - + let messageView = messageable.messageStackView.post( message: messageable.message, dismissAfter: messageable.dismissAfter, animated: messageable.postAnimation ) - + messageView.configureNoInternet() self.messageView = messageView - + messageable.messageManager(self, didPostMessageView: messageView) } - + // MARK: - PostManagerDelegate - + public func postManager( _ postManager: PostManager, didRemove view: UIView @@ -204,7 +205,7 @@ extension ConnectivityManager { // MARK: - Message + ConnectivityManager.MessageManager public extension Message { - + /// Default `Message` to post when internet connectivity is lost static var noInternet: Message { return Message( diff --git a/Sources/Reachability/ConnectivityManager+Notification.swift b/Sources/Reachability/ConnectivityManager+Notification.swift index 0157025..b67d311 100644 --- a/Sources/Reachability/ConnectivityManager+Notification.swift +++ b/Sources/Reachability/ConnectivityManager+Notification.swift @@ -11,8 +11,8 @@ import Foundation // MARK: - Notification.Name public extension Notification.Name { - - /// `Notification.Name` for `ConnectivityManager` `Notfication` + + /// `Notification.Name` for `ConnectivityManager` `Notification` static let internetConnectivityChanged = Notification.Name( rawValue: "messageStackView.internetConnectivityChanged" ) @@ -25,24 +25,24 @@ public typealias StateClosure = (ConnectivityManager.State) -> Void /// `Notification` related `ConnectivityManager` functionality public extension ConnectivityManager { - + /// `String` key in the `Notification` `userInfo` to find `State` static let stateUserInfoKey = "messageStackView.connectivityManager.state" - + /// Post `Notification` on the `.default` `NotificationCenter` on /// `state` updated /// /// - Parameter state: `State` func postNotification(for state: State) { let key = ConnectivityManager.stateUserInfoKey - + NotificationCenter.default.post( name: .internetConnectivityChanged, object: self, userInfo: [key: state] ) } - + /// Add an observer to the `.default` `NotificationCenter` listening for /// internet connectivity `Notification`s. Execute `closure` on observation event. /// @@ -62,7 +62,7 @@ public extension ConnectivityManager { observers.append(observer) return observer } - + /// Remove `observer` from the `.default` `NotificationCenter` listening for /// internet connectivity `Notification`s /// @@ -74,7 +74,7 @@ public extension ConnectivityManager { name: .internetConnectivityChanged, object: self ) - + observers.removeAll { $0 === observer } } } diff --git a/Sources/Reachability/ConnectivityManager.swift b/Sources/Reachability/ConnectivityManager.swift index bf27629..e0bee6d 100644 --- a/Sources/Reachability/ConnectivityManager.swift +++ b/Sources/Reachability/ConnectivityManager.swift @@ -12,26 +12,26 @@ import Foundation /// Simple Swift wrapper of `Reachability`. /// Manage internet connection`Notification`s on connect and disconnect. public class ConnectivityManager { - + /// Internet connectivity state public enum State { - + /// Conencted to the internet with `NetworkStatus` case connected(NetworkStatus) - + /// Not connected to the internet case notConnected } - + /// Shared singleton `ConnectivityManager` instance public static let shared = ConnectivityManager() - + /// Is listening for internet notification events private var isListening = false - + /// `Reachability` instance for internet connection private lazy var reachability = Reachability.forInternetConnection() - + /// Observers listening on the `.default` `NotificationCenter` internal var observers: [NSObjectProtocol] = [] { didSet { @@ -45,34 +45,34 @@ public class ConnectivityManager { /// Log on `reachabilityChanged(_:)` `Notifications` public var logReachabilityChanged = false - + /// Disconnect when `observers` is empty public var stopOnEmptyObservers = true - + /// `MessageManager` instance. /// /// The `MessageManager` manages a `MessageStackView` to add and /// remove from the `visibleViewController` on Reachability internet connectivity /// events. public private(set) var messageManager = MessageManager() - + // MARK: - Init - + /// Default public initializer public init() { } - + deinit { stopListening() } - + // MARK: - Listen - + /// Start internet connectivity `Reachability` notifier and start listening for /// `.reachabilityChanged` `Notification`s public func startListening() { guard !isListening else { return } - + NotificationCenter.default.addObserver( self, selector: #selector(reachabilityChanged), @@ -80,46 +80,47 @@ public class ConnectivityManager { object: nil ) reachability?.startNotifier() - + isListening = true } - + /// Stop internet connectivity `Reachability` notifier and stop listening for /// `.reachabilityChanged` `Notification`s public func stopListening() { guard isListening else { return } - + reachability?.stopNotifier() NotificationCenter.default.removeObserver( self, name: .reachabilityChanged, object: nil ) - + isListening = false } - + // MARK: - Notification - + /// `sender` sent when `NetworkStatus` changed on `reachability` /// - Parameter sender: `Notification` - @objc private func reachabilityChanged(_ sender: Notification) { + @objc + private func reachabilityChanged(_ sender: Notification) { guard let reachability = sender.object as? Reachability else { return } - + let status = reachability.currentReachabilityStatus() let state = status.state - + if logReachabilityChanged { debugPrint("\(ConnectivityManager.self) \(#function) \(state)") } - + postNotification(for: state) } - + // MARK: - Observers - + /// Is the given `observer` contained in `observers` /// - Parameter observer: `NSObjectProtocol` public func isObserver(_ observer: NSObjectProtocol) -> Bool { @@ -130,7 +131,7 @@ public class ConnectivityManager { // MARK: - NetworkStatus + ConnectivityManager.State public extension NetworkStatus { - + /// `NetworkStatus` to `ConnectivityManager.State` var state: ConnectivityManager.State { switch self { @@ -140,7 +141,7 @@ public extension NetworkStatus { default: return .notConnected } } - + /// Description of the connection fileprivate var connectionDescription: String { switch self { @@ -155,7 +156,7 @@ public extension NetworkStatus { // MARK: - ConnectivityManager.State + CustomStringConvertible extension ConnectivityManager.State: CustomStringConvertible { - + public var description: String { switch self { case .connected(let networkStatus): @@ -169,7 +170,7 @@ extension ConnectivityManager.State: CustomStringConvertible { // MARK: - ConnectivityManager.State + Connected public extension ConnectivityManager.State { - + /// Is `ConnectivityManager.State` considered connected var isConnected: Bool { switch self { diff --git a/Sources/Reachability/ConnectivityMessageable.swift b/Sources/Reachability/ConnectivityMessageable.swift index be89fac..1774e90 100644 --- a/Sources/Reachability/ConnectivityMessageable.swift +++ b/Sources/Reachability/ConnectivityMessageable.swift @@ -16,27 +16,27 @@ import UIKit /// /// E.g. A `UIViewController` may already have a `MessageStackView` in its view hierarchy, /// so doesn't want another `MessageStackView` added by the shared `MessageManager` -public protocol ConnectivityMessageable: class { - +public protocol ConnectivityMessageable: AnyObject { + /// The `MessageStackView` to post `Message`s on var messageStackView: MessageStackView { get } - + /// `Message` to post on `messageStackView` when internet connectivity is lost var message: Message { get } - + /// `TimeInterval` `"dismissAfter"` argument when posting on `messageStackView` var dismissAfter: TimeInterval? { get } - + /// `PostAnimation` `"animated"` argument when posting on `messageStackView` var postAnimation: PostAnimation { get } - + /// Called when `messageManager` will post a `MessageView` on the `messageStackView` /// /// - Parameter messageManager: `ConnectivityManager.MessageManager` func messageManagerShouldPost( _ messageManager: ConnectivityManager.MessageManager ) -> Bool - + /// Called when `messageManager` posted `messageView` on the `messageStackView` /// /// - Parameters: @@ -51,28 +51,28 @@ public protocol ConnectivityMessageable: class { // MARK: - ConnectivityMessageable + Functionality public extension ConnectivityMessageable { - + var message: Message { return .noInternet } - + var dismissAfter: TimeInterval? { return .defaultMessageDismiss } - + var postAnimation: PostAnimation { return .default } - + func messageManagerShouldPost( _ messageManager: ConnectivityManager.MessageManager ) -> Bool { return true } - + func messageManager( _ messageManager: ConnectivityManager.MessageManager, didPostMessageView messageView: MessageView - ) { + ) { } } diff --git a/Sources/Reachability/ConnectivityViewController.swift b/Sources/Reachability/ConnectivityViewController.swift index f800669..fee5b04 100644 --- a/Sources/Reachability/ConnectivityViewController.swift +++ b/Sources/Reachability/ConnectivityViewController.swift @@ -18,21 +18,21 @@ open class ConnectivityViewController: UIViewController, ConnectivityMessageable open var messageLayout: MessageLayout { return .bottom } - + /// `MessageStackView` at the bottom of the screen public private(set) lazy var messageStackView = MessageStackView() - + // MARK: - Deinit - + deinit { messageStackView.postManager.invalidate() } - + // MARK: - ViewController lifecycle - open override func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() - + messageStackView.addTo( view: view, layout: messageLayout, @@ -42,13 +42,13 @@ open class ConnectivityViewController: UIViewController, ConnectivityMessageable view.setNeedsLayout() } - open override func viewSafeAreaInsetsDidChange() { + override open func viewSafeAreaInsetsDidChange() { super.viewSafeAreaInsetsDidChange() messageStackView.spaceViewHeight = safeAreaInset } - + // MARK: - SafeArea - + /// Safe area inset of `messageParentView` var safeAreaInset: CGFloat { switch messageLayout { diff --git a/Sources/Reachability/NoInternet+UI.swift b/Sources/Reachability/MessageView+NoInternet.swift similarity index 93% rename from Sources/Reachability/NoInternet+UI.swift rename to Sources/Reachability/MessageView+NoInternet.swift index 79ead3d..f25f2ac 100644 --- a/Sources/Reachability/NoInternet+UI.swift +++ b/Sources/Reachability/MessageView+NoInternet.swift @@ -1,5 +1,5 @@ // -// NoInternet+UI.swift +// MessageView+NoInternet.swift // MessageStackView // // Created by Ben Shutt on 24/07/2020. @@ -12,7 +12,7 @@ import UIKit // MARK: - MessageView + Configure public extension MessageView { - + /// Configure UI properties for the no internet `MessageView` func configureNoInternet() { backgroundColor = .darkGray @@ -25,8 +25,8 @@ public extension MessageView { // MARK: - UILabel + Configure -extension UILabel { - +private extension UILabel { + /// Configure UI properties for the no internet `UILabel` /// - Parameters: /// - size: `CGFloat` Size of the font diff --git a/Sources/Shadow/CALayer+SketchShadow.swift b/Sources/Shadow/CALayer+SketchShadow.swift index c207601..7bd4fe2 100644 --- a/Sources/Shadow/CALayer+SketchShadow.swift +++ b/Sources/Shadow/CALayer+SketchShadow.swift @@ -11,22 +11,22 @@ import UIKit /// Model based on Sketch properties. struct SketchShadow { - + /// Shadow color var color: UIColor - + /// Shadow opacity var alpha: Float - + /// Shadow offset in x var x: CGFloat - + /// Shadow offset in y var y: CGFloat - + /// Shadow blur (2 * shadow radius) var blur: CGFloat - + /// Shadow spread var spread: CGFloat } @@ -34,7 +34,7 @@ struct SketchShadow { // MARK: - ShadowComponents + SketchShadow extension ShadowComponents { - + /// `ShadowComponents` to `SketchShadow` var sketchShadow: SketchShadow { return SketchShadow( @@ -51,7 +51,7 @@ extension ShadowComponents { // MARK: - CALayer + SketchShadow extension CALayer { - + /// Apply `sketchShadow` to `CALayer` /// /// - Warning: diff --git a/Sources/Shadow/Corner.swift b/Sources/Shadow/Corner.swift index cc86f90..36e4f93 100644 --- a/Sources/Shadow/Corner.swift +++ b/Sources/Shadow/Corner.swift @@ -10,10 +10,10 @@ import Foundation /// Describes the corners of an entity public enum Corner { - + /// Entity is rounded by `value` but may not be `.circular` case rounded(_ cornerRadius: CGFloat) - + /// Entity is a circle case circular } @@ -21,7 +21,7 @@ public enum Corner { // MARK: - Corner + Functionality public extension Corner { - + /// `Corner` to `CALayerCornerCurve` @available(iOS 13, *) var cornerCurve: CALayerCornerCurve { @@ -35,10 +35,10 @@ public extension Corner { // MARK: - CALayer + Corner public extension CALayer { - + /// Precision epsilon when calculating `Corner` private static let epsilon: CGFloat = 0.0001 - + /// Calculate `Corner` from the relative values of: /// - Width /// - Height @@ -46,42 +46,42 @@ public extension CALayer { internal var calculatedCorner: Corner { let width = bounds.size.width let height = bounds.size.height - + // Is the width == height let widthEqualsHeight = width.isFloatingPointEqual( to: height, withPrecision: Self.epsilon ) - + // Is width * 0.5 == cornerRadius let halfWidthEqualsCornerRadius = (width * 0.5).isFloatingPointEqual( to: cornerRadius, withPrecision: Self.epsilon ) - + if widthEqualsHeight, halfWidthEqualsCornerRadius { return .circular } - + return .rounded(cornerRadius) } - + /// Apply `corner` to the `CALayer` instance /// - Parameter corner: `Corner` func setCorner(_ corner: Corner) { switch corner { - + // rounded case .rounded(let cornerRadius): self.cornerRadius = cornerRadius - + // circular case .circular: let width = bounds.size.width let height = bounds.size.height let size = min(width, height) - + cornerRadius = 0.5 * size } - + // cornerCurve if #available(iOS 13, *) { cornerCurve = corner.cornerCurve diff --git a/Sources/Shadow/NeuomorphicShadow.swift b/Sources/Shadow/NeuomorphicShadow.swift index b942255..7ea8f66 100644 --- a/Sources/Shadow/NeuomorphicShadow.swift +++ b/Sources/Shadow/NeuomorphicShadow.swift @@ -10,10 +10,10 @@ import Foundation /// Neuomorphic shadow styling to add to a `CALayer` public enum NeuomorphicShadow { - + /// Neuomorphic shadow centralised on view case center - + /// Neuomorphic shadow dropped below view case dropped } @@ -21,7 +21,7 @@ public enum NeuomorphicShadow { // MARK: - NeuomorphicShadow + ShadowComponents public extension NeuomorphicShadow { - + /// `ShadowComponents`s for the given `NeuomorphicShadow` var components: [ShadowComponents] { switch self { @@ -45,7 +45,7 @@ public extension NeuomorphicShadow { offset: .init(width: 0, height: 2) ) ] - + case .dropped: return [ .init( radius: 2, diff --git a/Sources/Shadow/ParentShadowLayer.swift b/Sources/Shadow/ParentShadowLayer.swift index 7010939..7072eb8 100644 --- a/Sources/Shadow/ParentShadowLayer.swift +++ b/Sources/Shadow/ParentShadowLayer.swift @@ -15,71 +15,71 @@ import UIKit /// This is required to handle the relevant lifecycle/observable methods onto the /// `ShadowLayer` sublayers. open class ParentShadowLayer: CALayer { - + // MARK: - Init - - public override init() { + + override public init() { super.init() setup() } - - public override init(layer: Any) { + + override public init(layer: Any) { super.init(layer: layer) setup() } - - required public init?(coder: NSCoder) { + + public required init?(coder: NSCoder) { super.init(coder: coder) setup() } - + func setup() { masksToBounds = false backgroundColor = UIColor.clear.cgColor } - + // MARK: - Observable Properties - + @available(iOS 13, *) - public override var cornerCurve: CALayerCornerCurve { + override public var cornerCurve: CALayerCornerCurve { didSet { forEachShadowLayer { $0.cornerCurve = cornerCurve } } } - - public override var cornerRadius: CGFloat { + + override public var cornerRadius: CGFloat { didSet { forEachShadowLayer { $0.cornerRadius = cornerRadius } } } - - public override var backgroundColor: CGColor? { + + override public var backgroundColor: CGColor? { didSet { forEachShadowLayer { $0.backgroundColor = backgroundColor } } } - + // MARK: - Sublayer - - open override func insertSublayer(_ layer: CALayer, at idx: UInt32) { + + override open func insertSublayer(_ layer: CALayer, at idx: UInt32) { super.insertSublayer(layer, at: idx) - + if let shadow = layer as? ShadowLayer { shadow.copySuperlayerPropertiesForShadow() } } - + // MARK: - Layout - - public override func layoutSublayers() { + + override public func layoutSublayers() { super.layoutSublayers() - + forEachShadowLayer { $0.frame = bounds } @@ -89,7 +89,7 @@ open class ParentShadowLayer: CALayer { // MARK: - CALayer + ShadowLayer extension CALayer { - + /// Iterate sublayers of type `ShadowLayer` /// - Parameter closure: Closure to execute func forEachSublayer( @@ -99,7 +99,7 @@ extension CALayer { .compactMap { $0 as? T } .forEach { closure($0) } } - + /// Iterate sublayers of type `ShadowLayer` /// - Parameter closure: Closure to execute func forEachShadowLayer(_ closure: (ShadowLayer) -> Void) { @@ -110,23 +110,23 @@ extension CALayer { // MARK: - CALayer + SuperlayerShadow extension CALayer { - + /// Copy shadow relevant properties of `superlayer` to `self` /// - Warning: These aren't the `ShadowComponents` func copySuperlayerPropertiesForShadow() { guard let superlayer = superlayer else { return } - + // cornerRadius cornerRadius = superlayer.cornerRadius - + // This is important because layers with no background colour // cannot render shadows // backgroundColor backgroundColor = superlayer.backgroundColor - + // frame frame = superlayer.bounds - + if #available(iOS 13, *) { // cornerCurve cornerCurve = superlayer.cornerCurve diff --git a/Sources/Shadow/ShadowLayer.swift b/Sources/Shadow/ShadowLayer.swift index ad3e660..b7b3acc 100644 --- a/Sources/Shadow/ShadowLayer.swift +++ b/Sources/Shadow/ShadowLayer.swift @@ -10,24 +10,24 @@ import Foundation /// Subclass of `CALayer` for explicit type internal class ShadowLayer: CALayer { - + // MARK: - Init - - public override init() { + + override internal init() { super.init() setup() } - - public override init(layer: Any) { + + override internal init(layer: Any) { super.init(layer: layer) setup() } - - required public init?(coder: NSCoder) { + + internal required init?(coder: NSCoder) { super.init(coder: coder) setup() } - + func setup() { masksToBounds = false backgroundColor = UIColor.clear.cgColor diff --git a/Sources/Shadow/ShadowView.swift b/Sources/Shadow/ShadowView.swift index 7d28e32..6c6ccbd 100644 --- a/Sources/Shadow/ShadowView.swift +++ b/Sources/Shadow/ShadowView.swift @@ -12,29 +12,29 @@ import UIKit /// A subclass of `UIView` which sets the layer to `ParentShadowLayer` which /// manages `ShadowLayer` sublayers open class ShadowView: UIView { - + // MARK: - Layer - - public override class var layerClass: AnyClass { + + override public class var layerClass: AnyClass { return ParentShadowLayer.self } - + // MARK: - Init - - convenience public init() { + + public convenience init() { self.init(frame: .zero) } - + override public init(frame: CGRect) { super.init(frame: frame) setup() } - - required public init?(coder aDecoder: NSCoder) { + + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + private func setup() { backgroundColor = .white clipsToBounds = false diff --git a/Sources/Shadow/UIView+ShadowLayer.swift b/Sources/Shadow/UIView+ShadowLayer.swift index 6c6cde2..961688c 100644 --- a/Sources/Shadow/UIView+ShadowLayer.swift +++ b/Sources/Shadow/UIView+ShadowLayer.swift @@ -10,7 +10,7 @@ import Foundation import UIKit public extension UIView { - + /// Set a Neuomorphic Shadow on the `UIView` instance /// /// - Parameters: @@ -22,14 +22,14 @@ public extension UIView { ) { setShadowComponents(shadow.components, createSubview: createSubview) } - + /// Remove sublayers of type `ShadowLayer` func removeShadowLayers() { layer.forEachShadowLayer { $0.removeFromSuperlayer() } } - + /// Add `ShadowLayer` sublayers mapped by the given `components` /// /// - Parameters: @@ -54,7 +54,7 @@ public extension UIView { ) { // view to add `ShadowLayer`s to var view = self - + // If `createSubview`, find or create a `ShadowView` to add the // `ShadowLayer`s to. // Setting this to `true` allows setting an `autoresizingMask` @@ -66,17 +66,17 @@ public extension UIView { insertSubview(shadowView, at: 0) } view = shadowView - + if shadowView != self { // Copy shadow relevant properties of superlayer to new // shadowView subview shadowView.layer.copySuperlayerPropertiesForShadow() } } - + // Remove `ShadowLayer`s view.removeShadowLayers() - + // Add `ShadowLayer`s components.forEach { let shadowLayer = ShadowLayer() diff --git a/Sources/Swizzling/UIViewController+Lifecycle.h b/Sources/Swizzling/UIViewController+Lifecycle.h index db91972..ca5b515 100644 --- a/Sources/Swizzling/UIViewController+Lifecycle.h +++ b/Sources/Swizzling/UIViewController+Lifecycle.h @@ -11,7 +11,9 @@ NS_ASSUME_NONNULL_BEGIN extern NSString *kViewWillAppearNotification; +extern NSString *kViewDidAppearNotification; extern NSString *kViewWillDisappearNotification; +extern NSString *kViewDidDisappearNotification; @interface UIViewController (Lifecycle) diff --git a/Sources/Swizzling/UIViewController+Lifecycle.m b/Sources/Swizzling/UIViewController+Lifecycle.m index f450de5..a8b3ed0 100644 --- a/Sources/Swizzling/UIViewController+Lifecycle.m +++ b/Sources/Swizzling/UIViewController+Lifecycle.m @@ -15,7 +15,9 @@ #define SWIZZLE(X, Y) [[SwizzledMethod alloc] initWithOriginal:(X) swizzled:(Y)] NSString *kViewWillAppearNotification = @"kViewWillAppearNotification"; +NSString *kViewDidAppearNotification = @"kViewDidAppearNotification"; NSString *kViewWillDisappearNotification = @"kViewWillDisappearNotification"; +NSString *kViewDidDisappearNotification = @"kViewDidDisappearNotification"; @implementation UIViewController (Lifecycle) @@ -55,7 +57,9 @@ + (void)load { + (NSArray *)swizzledMethods { return @[ SWIZZLE(@selector(viewWillAppear:), @selector(swizzled_viewWillAppear:)), - SWIZZLE(@selector(viewWillDisappear:), @selector(swizzled_viewWillDisappear:)) + SWIZZLE(@selector(viewDidAppear:), @selector(swizzled_viewDidAppear:)), + SWIZZLE(@selector(viewWillDisappear:), @selector(swizzled_viewWillDisappear:)), + SWIZZLE(@selector(viewDidDisappear:), @selector(swizzled_viewDidDisappear:)) ]; } @@ -72,6 +76,17 @@ - (void)swizzled_viewWillAppear:(BOOL)animated { [center postNotificationName:kViewWillAppearNotification object:self]; } +- (void)swizzled_viewDidAppear:(BOOL)animated { + [self swizzled_viewDidAppear:animated]; + + if (SWIZZLE_LOG) { + NSLog(@"%@ %@", NSStringFromSelector(_cmd), self); + } + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:kViewDidAppearNotification object:self]; +} + - (void)swizzled_viewWillDisappear:(BOOL)animated { [self swizzled_viewWillDisappear:animated]; @@ -83,4 +98,15 @@ - (void)swizzled_viewWillDisappear:(BOOL)animated { [center postNotificationName:kViewWillDisappearNotification object:self]; } +- (void)swizzled_viewDidDisappear:(BOOL)animated { + [self swizzled_viewDidDisappear:animated]; + + if (SWIZZLE_LOG) { + NSLog(@"%@ %@", NSStringFromSelector(_cmd), self); + } + + NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; + [center postNotificationName:kViewDidDisappearNotification object:self]; +} + @end diff --git a/Sources/Theme/UIColor+Colors.swift b/Sources/Theme/UIColor+Colors.swift index e521dba..2eb3ade 100644 --- a/Sources/Theme/UIColor+Colors.swift +++ b/Sources/Theme/UIColor+Colors.swift @@ -11,19 +11,19 @@ import UIKit /// Custom `UIColor`s used in the framework extension UIColor { - + /// Theme red color static let themeRed = UIColor(red255: 231, green255: 19, blue255: 36) - + /// Theme dark gray color static let themeDarkGray = UIColor(red255: 109, green255: 110, blue255: 112) - + /// Shadow color static let shadowGray = UIColor(red255: 63, green255: 63, blue255: 63) - + /// First part of neomorphic shadow static let neomorphicGray1 = UIColor(red255: 149, green255: 150, blue255: 177) - + /// Second part of neomorphic shadow static let neomorphicGray2 = UIColor(red255: 143, green255: 147, blue255: 150) } @@ -31,7 +31,7 @@ extension UIColor { // MARK: - UIColor + RGB-255 extension UIColor { - + /// Shorthand to create colors using 255 notation /// /// - Parameters: @@ -46,9 +46,9 @@ extension UIColor { alpha: CGFloat = 1 ) { self.init( - red: red/255, - green: green/255, - blue: blue/255, + red: red / 255, + green: green / 255, + blue: blue / 255, alpha: alpha ) } diff --git a/Sources/Theme/UIImage+Images.swift b/Sources/Theme/UIImage+Images.swift index 7d52b7d..da88880 100644 --- a/Sources/Theme/UIImage+Images.swift +++ b/Sources/Theme/UIImage+Images.swift @@ -11,10 +11,10 @@ import UIKit /// Custom `UIImage`s used in the framework public extension UIImage { - + /// Gray circle with a cross in the center static let iconClose = UIImage("iconClose") - + /// Icon for not connected to internet static let noInternet = UIImage("noInternet") } @@ -22,7 +22,7 @@ public extension UIImage { // MARK: - UIImage + Init private extension UIImage { - + /// Find a `UIImage` with name from this frameworks `Bundle` /// - Parameter name: Name of the `UIImage` convenience init?(_ name: String) { diff --git a/Sources/Theme/UILabel+Default.swift b/Sources/Theme/UILabel+Default.swift index ad2a713..14e8fba 100644 --- a/Sources/Theme/UILabel+Default.swift +++ b/Sources/Theme/UILabel+Default.swift @@ -12,7 +12,7 @@ import UIKit // MARK: - UILabel + Default extension UILabel { - + /// `UILabel` setting default properties static var `default`: UILabel { let label = UILabel() diff --git a/Sources/Views/Badges/BadgeContainerView.swift b/Sources/Views/Badges/BadgeContainerView.swift index 37201f1..a33d7b3 100644 --- a/Sources/Views/Badges/BadgeContainerView.swift +++ b/Sources/Views/Badges/BadgeContainerView.swift @@ -13,118 +13,118 @@ import UIKit /// - Note: /// Inherit `ShakeView` to add badge animations open class BadgeContainerView: UIView { - + /// Overrideable default shadow components for `BadgeContainerView` public static var shadowComponents: ShadowComponents = .defaultBlack - + /// Fixed constants private struct Constants { - + /// Instrinsic width and height of `BadgeContainerView` static let intrinsicSize: CGFloat = 150 } - + /// Amount the fill path is inset by from the `bounds` - public var containerBorderWidthScale: CGFloat = 1/75 { + public var containerBorderWidthScale: CGFloat = 1 / 75 { didSet { setNeedsDisplay() } } - + /// The corner radius of the rounded rect path - public var containerCornerRadiusScale: CGFloat = 7/30 { + public var containerCornerRadiusScale: CGFloat = 7 / 30 { didSet { setNeedsLayout() } } - + /// The color of the border public var containerBorderColor: UIColor = .white { didSet { setNeedsDisplay() } } - + /// The color to fill the view, rounded rect inset by `borderWidth` from `bounds` public var fillColor: UIColor = .themeRed { didSet { setNeedsDisplay() } } - + // MARK: - Computed - + /// Min of the `bounds` width and height private var size: CGFloat { return min(bounds.size.width, bounds.size.height) } - + /// Corner radius by scaling `size` according to `containerCornerRadiusScale` private var cornerRadius: CGFloat { return containerCornerRadiusScale * size } - + /// Border width by scaling `size` according to `containerBorderWidthScale` private var borderWidth: CGFloat { return containerBorderWidthScale * size } - + // MARK: - Init - + public convenience init() { self.init(frame: .zero) } - - public override init(frame: CGRect) { + + override public init(frame: CGRect) { super.init(frame: frame) setup() } - + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + private func setup() { backgroundColor = .clear shadowComponents = BadgeContainerView.shadowComponents updateLayer() } - + // MARK: - Lifecycle - - public override func layoutSubviews() { + + override public func layoutSubviews() { super.layoutSubviews() updateLayer() } - - public override var intrinsicContentSize: CGSize { + + override public var intrinsicContentSize: CGSize { return CGSize( width: Constants.intrinsicSize, height: Constants.intrinsicSize ) } - + // MARK: - Draw - - public override func draw(_ rect: CGRect) { + + override public func draw(_ rect: CGRect) { super.draw(rect) - + // Fill the whole bounds with the border color fillRoundedPath( rect: bounds, fillColor: containerBorderColor ) - + // On top of the whole bounds border color, fill the color fillRoundedPath( rect: bounds.inset(by: borderWidth), fillColor: fillColor ) } - + // MARK: - Path - + /// Round `rect` with `cornerRadius` /// - Parameter rect: `CGRect` to round private func roundedPath(for rect: CGRect) -> UIBezierPath { @@ -133,7 +133,7 @@ open class BadgeContainerView: UIView { cornerRadius: cornerRadius ) } - + /// Get `roundedRect(for:)` and fill with `fillColor` /// - Parameters: /// - rect: `CGRect` to round @@ -143,9 +143,9 @@ open class BadgeContainerView: UIView { fillColor.setFill() path.fill() } - + // MARK: - Layer - + /// Update `layer`: /// - cornerRadius /// - shadow diff --git a/Sources/Views/Badges/BadgeView.swift b/Sources/Views/Badges/BadgeView.swift index aa0b32d..e2621fa 100644 --- a/Sources/Views/Badges/BadgeView.swift +++ b/Sources/Views/Badges/BadgeView.swift @@ -10,14 +10,7 @@ import UIKit /// `UIView` for displaying a badge. open class BadgeView: BadgeContainerView { - - /// Fixed constants for `BadgeView` - private struct Constants { - - /// Width of `imageView` subview relative to `self` - static let imageViewWidthScale: CGFloat = 0.65 - } - + /// `UIImageView` subview public private(set) lazy var imageView: UIImageView = { let imageView = UIImageView() @@ -26,21 +19,42 @@ open class BadgeView: BadgeContainerView { imageView.contentMode = .scaleAspectFit return imageView }() - + + /// The constraint between the image view's width and the + /// container view's width. Can be adjusted to change how much + /// of the container the image fills + private var imageWidthConstraint: NSLayoutConstraint! + // MARK: - Computed - + + /// Width of `imageView` subview relative to `self` + public var imageSizeRatio: CGFloat = 0.65 { + didSet { + updateImageWidthConstraint() + setNeedsUpdateConstraints() + } + } + + /// Allows setting the rendering mode of the image view, + /// defaults to `.alwaysTemplate` + public var renderingMode: UIImage.RenderingMode = .alwaysTemplate { + didSet { + imageView.image = image?.withRenderingMode(renderingMode) + } + } + /// Shorthand for getting and setting `image` of `imageView` public var image: UIImage? { get { return imageView.image } set { - imageView.image = newValue?.withRenderingMode(.alwaysTemplate) + imageView.image = newValue?.withRenderingMode(renderingMode) } } - + /// Drive `UIImageView`s `tintColor` from `tintColor` of `self` - public override var tintColor: UIColor! { + override public var tintColor: UIColor! { get { return super.tintColor } @@ -51,34 +65,42 @@ open class BadgeView: BadgeContainerView { } // MARK: - Init - + public convenience init() { self.init(frame: .zero) } - - public override init(frame: CGRect) { + + override public init(frame: CGRect) { super.init(frame: frame) setup() } - + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + // MARK: - Setup - + private func setup() { addSubview(imageView) imageView.translatesAutoresizingMaskIntoConstraints = false + updateImageWidthConstraint() NSLayoutConstraint.activate([ imageView.centerXAnchor.constraint(equalTo: centerXAnchor), imageView.centerYAnchor.constraint(equalTo: centerYAnchor), - imageView.widthAnchor.constraint( - equalTo: widthAnchor, multiplier: - Constants.imageViewWidthScale - ), imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor) ]) } + + private func updateImageWidthConstraint() { + if let widthConstraint = imageWidthConstraint { + imageView.removeConstraint(widthConstraint) + } + imageWidthConstraint = imageView.widthAnchor.constraint( + equalTo: widthAnchor, + multiplier: imageSizeRatio + ) + imageWidthConstraint?.isActive = true + } } diff --git a/Sources/Views/Badges/ShakeView.swift b/Sources/Views/Badges/ShakeView.swift index f33cb61..b0738b4 100644 --- a/Sources/Views/Badges/ShakeView.swift +++ b/Sources/Views/Badges/ShakeView.swift @@ -11,22 +11,22 @@ import UIKit // MARK: - ShakeViewDelegate /// Touch animation callbacks for `ShakeView` -public protocol ShakeViewDelegate: class { - +public protocol ShakeViewDelegate: AnyObject { + /// Invoked when the touch animation starts /// - Parameter shakeView: `ShakeView` func shakeViewAnimationDidStart(_ shakeView: ShakeView) - + /// Invoked when the touch animation stops /// - Parameters: /// - shakeView: `ShakeView` /// - complete: Did the animation complete func shakeViewAnimationDidStop(_ shakeView: ShakeView, complete: Bool) - + /// Pulse animation will start /// - Parameter shakeView: `ShakeView` func shakeViewPulseWillStart(_ shakeView: ShakeView) - + /// Pulse animation did stop /// - Parameter shakeView: `ShakeView` func shakeViewPulseDidStop(_ shakeView: ShakeView) @@ -34,19 +34,19 @@ public protocol ShakeViewDelegate: class { /// Implementation defaults for `ShakeViewDelegate` public extension ShakeViewDelegate { - + func shakeViewAnimationDidStart(_ shakeView: ShakeView) { // do nothing } - + func shakeViewAnimationDidStop(_ shakeView: ShakeView, complete: Bool) { // do nothing } - + func shakeViewPulseWillStart(_ shakeView: ShakeView) { // do nothing } - + func shakeViewPulseDidStop(_ shakeView: ShakeView) { // do nothing } @@ -56,10 +56,10 @@ public extension ShakeViewDelegate { /// A `UIView` which "shakes" in response to touch and pulses when not touching public class ShakeView: UIView { - + /// Fixed constants in `ShakeView` private struct Constants { - + /// How long the user should hold their touch before the view pops/completes its animation static let animationDuration: TimeInterval = 2.5 @@ -69,54 +69,54 @@ public class ShakeView: UIView { /// Duration until the first pulse animation fires, from then wait`pulseDelay` static let firstPulseDelay: TimeInterval = 1 } - + // MARK: - Properties - + /// `ShakeViewDelegate` for touch animation related callbacks public weak var delegate: ShakeViewDelegate? /// `CADisplayLink` to sync selector when the screen updates private var displayLink: CADisplayLink? - + /// A `Timer` which pulses this view every `pulseInterval` (except the first case) private var pulseTimer: Timer? - + /// `Date` the touch animation started private var animationStart: Date! - + /// Have we done the first pulse animation - this will determine /// the `pulseTimer` `TimeInterval` private var isFirstPulse = true - + /// `ShakeFunction` to handle the rotation transform of the view (on touch) private static let shakeFunction = ShakeFunction( T: Constants.animationDuration, Y: TimeInterval.pi / 15, N: 15 ) - + /// `ScaleFunction` to handle the scale transform of the view (on touch) private static let scaleFunction = ScaleFunction( T: Constants.animationDuration, Y: 1.4 ) - + // MARK: - Pulse Animation - + /// Start the pulse animation private func startPulse() { isFirstPulse = true startPulseTimer() } - + /// Execute pulse animation and notify delegate private func executePulse() { delegate?.shakeViewPulseWillStart(self) - pulse { finished in + pulse { _ in self.delegate?.shakeViewPulseDidStop(self) } } - + /// Start the pulse animation timer private func startPulseTimer() { let timeInterval = isFirstPulse ? @@ -126,65 +126,65 @@ public class ShakeView: UIView { pulseTimer = Timer.scheduledTimer( withTimeInterval: timeInterval, repeats: true, - block: - { [weak self] timer in - guard let self = self else { return } - - self.executePulse() - if self.isFirstPulse { - timer.invalidate() - self.isFirstPulse = false - - self.startPulseTimer() + block: { [weak self] timer in + guard let self = self else { return } + + self.executePulse() + if self.isFirstPulse { + timer.invalidate() + self.isFirstPulse = false + + self.startPulseTimer() + } } - }) + ) } - + /// Stop the pulse animation private func stopPulse() { pulseTimer?.invalidate() } - + // MARK: - Invalidate - + /// Invalidate touch and pulse animations private func invalidate() { stopAnimation(complete: false) stopPulse() // Must be called after as the above starts the shake again! } - + // MARK: - Lifecycle - - public override func didMoveToSuperview() { + + override public func didMoveToSuperview() { super.didMoveToSuperview() startPulse() } - - public override func removeFromSuperview() { + + override public func removeFromSuperview() { super.removeFromSuperview() invalidate() } - + deinit { invalidate() } - + // MARK: - Touch Animation - + /// Start the touch animation private func startAnimation() { layer.removeAllAnimations() animationStart = Date() - + displayLink?.invalidate() displayLink = CADisplayLink(target: self, selector: #selector(step)) displayLink?.add(to: .current, forMode: .default) - + stopPulse() - + delegate?.shakeViewAnimationDidStart(self) } - + /// Stop touch animation and animate back to identity private func stopAnimation(complete: Bool) { if animationStart == nil { @@ -192,71 +192,69 @@ public class ShakeView: UIView { // ending the animation (e.g. after animation ended by duration) return } - + layer.removeAllAnimations() displayLink?.invalidate() animationStart = nil - + delegate?.shakeViewAnimationDidStop(self, complete: complete) - + UIView.animate(withDuration: 0.1) { self.transform = .identity } - + startPulse() } - + /// `CADisplayLink` step function - @objc private func step(displaylink: CADisplayLink) { + @objc + private func step(displaylink: CADisplayLink) { guard let animationStart = animationStart else { stopAnimation(complete: false) return } - + let elapsed = Date().timeIntervalSince(animationStart) if elapsed > Self.shakeFunction.T { stopAnimation(complete: true) return } - + let rotation = CGFloat(Self.shakeFunction.value(for: elapsed)) let scale = CGFloat(Self.scaleFunction.value(for: elapsed)) - + transform = CGAffineTransform.identity .rotated(by: rotation) .scaledBy(x: scale, y: scale) } -} - -// MARK: - Touch Events -extension ShakeView { + // MARK: - Touch Events - public override func touchesBegan( + override public func touchesBegan( _ touches: Set, with event: UIEvent? ) { super.touchesBegan(touches, with: event) startAnimation() } - - public override func touchesCancelled( + + override public func touchesCancelled( _ touches: Set, with event: UIEvent? ) { super.touchesCancelled(touches, with: event) stopAnimation(complete: false) } - - public override func touchesEnded( + + override public func touchesEnded( _ touches: Set, with event: UIEvent? ) { super.touchesEnded(touches, with: event) stopAnimation(complete: false) } - - public override func touchesMoved( + + override public func touchesMoved( _ touches: Set, with event: UIEvent? ) { diff --git a/Sources/Views/PostView/ApplicationPostView.swift b/Sources/Views/PostView/ApplicationPostView.swift index ed07794..771e7e2 100644 --- a/Sources/Views/PostView/ApplicationPostView.swift +++ b/Sources/Views/PostView/ApplicationPostView.swift @@ -10,51 +10,53 @@ import Foundation /// `PostView` singleton to to be added to the `UIApplication`'s key window class ApplicationPostView: PostView { - + /// Shared `PostView` singleton static let shared = ApplicationPostView() - + /// Override `removeFromSuperviewOnEmpty` forcing `true` override var removeFromSuperviewOnEmpty: Bool { get { return true } + // swiftlint:disable unused_setter_value set { } + // swiftlint:enable unused_setter_value } - + // MARK: - Init - + convenience init() { self.init(frame: .zero) } - + override init(frame: CGRect) { super.init(frame: frame) setup() } - + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + deinit { NotificationCenter.default.removeObserver( self, name: UIWindow.didBecomeKeyNotification, object: nil ) - + NotificationCenter.default.removeObserver( self, name: .viewWillAppear, object: nil ) } - + // MARK: - Setup - + private func setup() { NotificationCenter.default.addObserver( self, @@ -62,7 +64,7 @@ class ApplicationPostView: PostView { name: UIWindow.didBecomeKeyNotification, object: nil ) - + NotificationCenter.default.addObserver( self, selector: #selector(viewWillAppearNotification), @@ -70,9 +72,9 @@ class ApplicationPostView: PostView { object: nil ) } - + // MARK: - Functionality - + /// Add as subview to `UIApplication.shared.appKeyWindow` if required /// or bring to front of superview subviews hierarchy. /// @@ -83,16 +85,16 @@ class ApplicationPostView: PostView { if force { removeFromSuperview() } - + guard superview == nil else { bringToFront() return } - + guard let keyWindow = UIApplication.shared.appKeyWindow else { return } - + addTo( view: keyWindow, layout: .top, @@ -101,24 +103,26 @@ class ApplicationPostView: PostView { keyWindow.layoutIfNeeded() layoutIfNeeded() } - + // MARK: - Notification - - @objc private func windowDidBecomeKeyNotification( + + @objc + private func windowDidBecomeKeyNotification( _ sender: Notification - ){ + ) { guard postManager.isActive else { return } updateSuperviewIfRequired(force: true) } - - @objc private func viewWillAppearNotification( + + @objc + private func viewWillAppearNotification( _ sender: Notification - ){ + ) { bringToFront() } - + // MARK: - PostManagerDelegate - + override func postManager( _ postManager: PostManager, willPost view: UIView diff --git a/Sources/Views/PostView/MessageStackView.swift b/Sources/Views/PostView/MessageStackView.swift index 8a96294..5345989 100644 --- a/Sources/Views/PostView/MessageStackView.swift +++ b/Sources/Views/PostView/MessageStackView.swift @@ -10,40 +10,40 @@ import UIKit /// A `UIStackView` with a `PostManager` for posting, queueing and removing `UIView`s open class MessageStackView: UIStackView, Poster { - + /// How posted `UIView`s are ordered in the `arrangedSubviews`. public enum Order { - + /// Natural order of `UIStackView`s. Posted `UIView`s get appended to the /// `arrangedSubviews` array appearing below/after the previous. case `default` // topToBottom - + /// Reverese order of `UIStackView`s. Posted `UIView`s get inserted at the start of the /// `arrangedSubviews` array appearing above/before the previous. case reversed // bottomToTop } - + /// `PostManager` to manage posting, queueing, removing of `PostRequest`s public private(set) lazy var postManager: PostManager = { let postManager = PostManager(poster: self) postManager.isSerialQueue = false return postManager }() - + /// Default `MessageConfiguration` to apply to posted `UIView`s public var messageConfiguation = MessageConfiguration() { didSet { guard messageConfiguation.applyToAll else { return } - + // If the `messageConfiguation` has updated, update the current `UIView`s arrangedSubviewsExcludingSpace .compactMap { $0 as? MessageConfigurable } .forEach { $0.set(configuration: messageConfiguation) } } } - + /// Order of the posted `UIView`s in the `arrangedSubviews` /// /// When `.default`, `spaceView` will be the first `arrangedSubview`. @@ -53,9 +53,9 @@ open class MessageStackView: UIStackView, Poster { updateSpaceView(updateArrangedSubviews: true) } } - + // MARK: - Views - + /// This view is for smooth animations when there are no `arrangedSubviews` /// in the `UIStackView`. /// Otherwise the `UIStackView` can not determine its width/height. @@ -66,14 +66,14 @@ open class MessageStackView: UIStackView, Poster { view.backgroundColor = .clear return view }() - + /// `NSLayoutConstraint` setting the constant of the height on the `spaceView` internal lazy var spaceViewHeightConstraint: NSLayoutConstraint = { return spaceView.heightAnchor.constraint( equalToConstant: .leastNormalMagnitude ) }() - + /// Height of the `spaceViewHeightConstraint`. /// /// - Note: @@ -83,13 +83,13 @@ open class MessageStackView: UIStackView, Poster { /// it will also animate the height of the `spaceView` to zero. public var spaceViewHeight: CGFloat = 0 { didSet { - guard arrangedSubviewsExcludingSpace.count > 0 else { return } + guard !arrangedSubviewsExcludingSpace.isEmpty else { return } spaceViewHeightConstraint.constant = spaceViewHeight } } - + // MARK: - Init - + /// Default initializer public convenience init() { self.init(arrangedSubviews: []) @@ -97,31 +97,31 @@ open class MessageStackView: UIStackView, Poster { alignment = .fill distribution = .fill spacing = 0 - + // Configure `spaceView` updateSpaceView(updateArrangedSubviews: true) - + // Make sure `postManger` is instantiated. This is in case `deinit` is // is the first instance to reference `postManager`, lazily // instantiating it, referencing `self`, which is being // de-initialized... - let _ = self.postManager.isSerialQueue + _ = self.postManager.isSerialQueue } - + /// Invalidate on deinit deinit { postManager.invalidate() } - + // MARK: - ArrangedSubviews - + /// `arrangedSubviews` excluding `spaceView` public var arrangedSubviewsExcludingSpace: [UIView] { return arrangedSubviews.filter { $0 != spaceView } } - + // MARK: - Animation - + /// Show or hide the given `view` /// /// - Parameters: @@ -140,12 +140,12 @@ open class MessageStackView: UIStackView, Poster { completion() return } - + let spaceViewHeightAfterAnimation = animationWillStart(hidden: hidden) view.isHidden = !hidden layoutIfNeeded() superview?.layoutIfNeeded() - + UIView.animate(withDuration: .animationDuration, animations: { view.isHidden = hidden self.animationWillEnd(spaceViewHeightAfterAnimation) @@ -154,7 +154,7 @@ open class MessageStackView: UIStackView, Poster { completion() }) } - + /// Prepare `spaceView` if the animating is either: /// - adding the first arrangedSubview /// - removing the last arrangedSubview @@ -166,7 +166,7 @@ open class MessageStackView: UIStackView, Poster { guard arrangedSubviewsExcludingSpace.count == 1 else { return nil } - + let heightAfterAnimation: CGFloat if hidden { // Removing last arrangedSubview @@ -174,14 +174,14 @@ open class MessageStackView: UIStackView, Poster { } else { // Adding first arrangedSubview heightAfterAnimation = spaceViewHeight - + // Don't overrite spaceViewHeight spaceViewHeightConstraint.constant = .leastNormalMagnitude } - + return heightAfterAnimation } - + /// If `spaceViewHeightAfterAnimation` is defined, set the `spaceViewHeight` /// to this. /// - Parameter spaceViewHeightAfterAnimation: `CGFloat?` @@ -191,13 +191,13 @@ open class MessageStackView: UIStackView, Poster { guard let spaceViewHeight = spaceViewHeightAfterAnimation else { return } - + // Don't overrite spaceViewHeight spaceViewHeightConstraint.constant = spaceViewHeight } - + // MARK: - SpaceView - + /// Updates the `backgroundColor` of the `spaceView` based on the "next" arranged subview. /// If the `spaceView` is the first in the `arrangedSubviews`, then "next" is the subview /// after. Otherwise "next" is the subview before. @@ -212,7 +212,7 @@ open class MessageStackView: UIStackView, Poster { postArrangedSubview(view: spaceView, order: order.switched) constrainSpaceView() } - + let next: UIView? switch order { case .default: @@ -220,11 +220,11 @@ open class MessageStackView: UIStackView, Poster { case .reversed: next = arrangedSubviews.elementBeforeFirst(of: spaceView) } - + // Set backgroundColor equivalent to adjacent arranged subview spaceView.backgroundColor = next?.backgroundColor ?? .clear } - + /// Add `spaceView` to the `arrangedSubviews` private func constrainSpaceView() { spaceView.translatesAutoresizingMaskIntoConstraints = false @@ -232,9 +232,9 @@ open class MessageStackView: UIStackView, Poster { spaceViewHeightConstraint.isActive = true } } - + // MARK: - Order - + /// Update `order` given `layout` /// - Parameter layout: `MessageLayout` public func updateOrderForLayout(_ layout: MessageLayout) { @@ -243,16 +243,16 @@ open class MessageStackView: UIStackView, Poster { case .bottom: order = .reversed } } - + // MARK: - Post - + /// `postArrangedSubview(view:order:)` with `view` and `order` /// /// - Parameter view: `UIView` private func postArrangedSubview(view: UIView) { postArrangedSubview(view: view, order: order) } - + /// Post a `view` adding it to the `arrangedSubviews` /// /// - Parameters: @@ -271,13 +271,13 @@ open class MessageStackView: UIStackView, Poster { // MARK: - UIViewPoster extension MessageStackView: UIViewPoster { - + /// Only remove if `self` is the `view.superview` /// - Parameter view: `UIView` func shouldRemove(view: UIView) -> Bool { return view.superview == self } - + /// Post `view` /// /// - Note: @@ -297,16 +297,16 @@ extension MessageStackView: UIViewPoster { ) { postArrangedSubview(view: view) (view as? MessageConfigurable)?.set(configuration: messageConfiguation) - + // Update spaceView here too incase properties on adjacent // arrangedSubview has, since posting, changed DispatchQueue.main.async { self.updateSpaceView() } - + setView(view, hidden: false, animated: animated, completion: completion) } - + /// Remove posted `view` /// /// - Parameters: @@ -331,7 +331,7 @@ extension MessageStackView: UIViewPoster { // MARK: - Order + Extensions private extension MessageStackView.Order { - + /// Other `Order` (opposite direction) var switched: Self { switch self { diff --git a/Sources/Views/PostView/PostView.swift b/Sources/Views/PostView/PostView.swift index 6d1a7ad..fa7314b 100644 --- a/Sources/Views/PostView/PostView.swift +++ b/Sources/Views/PostView/PostView.swift @@ -11,19 +11,19 @@ import UIKit /// `UIView` to post `UIView`s on a `PostManager` in a serial manner open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { - + /// Fixed constants for `PostView` private struct Constants { - + /// `UIEdgeInsets` to inset subviews relative to `self` static let edgeInsets = UIEdgeInsets( top: 20, left: 10, bottom: 20, right: 10 ) } - + /// `UIEdgeInsets` of subviews from `self` public var edgeInsets: UIEdgeInsets = Constants.edgeInsets - + /// `PostManager` to manage posting, queueing, removing of `PostRequest`s public private(set) lazy var postManager: PostManager = { let postManager = PostManager(poster: self) @@ -36,39 +36,39 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { public var removeFromSuperviewOnEmpty = false // MARK: - Init - + public convenience init() { self.init(frame: .zero) } - - public override init(frame: CGRect) { + + override public init(frame: CGRect) { super.init(frame: frame) setup() } - + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } - + private func setup() { clipsToBounds = true - + // Make sure `postManger` is instantiated. This is in case `deinit` is // is the first instance to reference `postManager`, lazily // instantiating it, referencing `self`, which is being // de-initialized... - let _ = self.postManager.isSerialQueue + _ = self.postManager.isSerialQueue } - + // MARK: - IntrinsicContentSize - - open override var intrinsicContentSize: CGSize { + + override open var intrinsicContentSize: CGSize { return CGSize(width: UIView.noIntrinsicMetric, height: 0) } - + // MARK: - Animation - + /// Show or hide a subview (`view`) based on the `hidden`argument. /// The transition may be animated. /// @@ -98,13 +98,14 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { options: .curveEaseIn, animations: { self.setTransform(on: view, forHidden: hidden) - }) { _ in - completion() - } + }, completion: { _ in + completion() + } + ) } - + // MARK: - Transform - + /// Consider "hidden" views as transformed out of the bounds above. /// With `clipsToBounds = true` these views will not be visible. /// We can then "show" them by animating their transform back to `.identity` @@ -115,7 +116,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { private func setTransform(on view: UIView, forHidden hidden: Bool) { view.transform = hidden ? hiddenTranslationY(for: view) : .identity } - + /// `CGAffineTransform` to effectively "hide" a view off the top of `self`'s `bounds` /// /// - Note: @@ -132,32 +133,32 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { y: -(view.bounds.size.height + edgeInsets.top) ) } - + // MARK: - Subview - + /// Add posted `subview`, constraining accordingly and ensuring `transform` for animation /// - Parameter subview: `UIView` private func addPostSubview(_ subview: UIView) { addSubview(subview) subview.edgeConstraints(to: self, insets: edgeInsets) layoutIfNeeded() - + setTransform(on: subview, forHidden: true) } - + /// Remove a previously posted `subview` and ensure layout /// - Parameter subview: `UIView` private func removePostSubview(_ subview: UIView) { subview.removeFromSuperview() layoutIfNeeded() } - + // MARK: - UIViewPoster func shouldRemove(view: UIView) -> Bool { return view.superview == self } - + func post( view: UIView, animated: Bool, @@ -165,10 +166,10 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { ) { // Add `view` as a posted subview addPostSubview(view) - + // Add pan gesture by default postManager.gestureManager.addPanToRemoveGesture(to: view) - + // Execute hide/show with animation if required setView( view, @@ -177,7 +178,7 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { completion: completion ) } - + func remove( view: UIView, animated: Bool, @@ -192,7 +193,8 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { // Remove `view` as a posted subview self.removePostSubview(view) completion() - }) + } + ) } // MARK: - PostManagerDelegate @@ -200,9 +202,9 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { public func postManager( _ postManager: PostManager, willPost view: UIView - ){ + ) { } - + /// Called when a `view` was posted /// - Parameters: /// - postManager: `PostManager` @@ -210,9 +212,9 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { public func postManager( _ postManager: PostManager, didPost view: UIView - ){ + ) { } - + /// Called when a `view` will be removed /// - Parameters: /// - postManager: `PostManager` @@ -220,13 +222,13 @@ open class PostView: UIView, Poster, UIViewPoster, PostManagerDelegate { public func postManager( _ postManager: PostManager, willRemove view: UIView - ){ + ) { } - + public func postManager( _ postManager: PostManager, didRemove view: UIView - ){ + ) { // Should check to remove self guard removeFromSuperviewOnEmpty, !postManager.isActive else { return diff --git a/Sources/Views/PostViewController.swift b/Sources/Views/PostViewController.swift index 15a6d6e..4d17f47 100644 --- a/Sources/Views/PostViewController.swift +++ b/Sources/Views/PostViewController.swift @@ -11,13 +11,11 @@ import UIKit /// `UIViewController` with a `PostView` intended to be used as the /// `rootViewController` of a `PostWindow` -/// -/// - TODO: This file is a WIP class PostViewController: UIViewController { - + /// Shared `PostViewController` singleton instance static let shared = PostViewController() - + /// Add `PostViewController` to own `UIWindow` private(set) lazy var window: UIWindow = { let window = UIWindow(frame: UIScreen.main.bounds) @@ -26,18 +24,18 @@ class PostViewController: UIViewController { window.backgroundColor = .clear return window }() - + /// `PostView` added to the `.top` of `self` private(set) lazy var postView = PostView() - + // MARK: - ViewController lifecycle - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .clear window.isHidden = true - + postView.postManager.delegate = self postView.addTo( view: view, @@ -50,7 +48,7 @@ class PostViewController: UIViewController { // MARK: - PostManagerDelegate extension PostViewController: PostManagerDelegate { - + func postManager( _ postManager: PostManager, willPost view: UIView @@ -58,7 +56,7 @@ extension PostViewController: PostManagerDelegate { window.isHidden = false self.view.layoutIfNeeded() } - + func postManager( _ postManager: PostManager, didRemove view: UIView diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 6c1d780..5013a40 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,7 +1,10 @@ import XCTest - import MessageStackViewTests +// swiftlint:disable let_var_whitespace + var tests = [XCTestCaseEntry]() tests += MessageStackViewTests.allTests() XCTMain(tests) + +// swiftlint:enable let_var_whitespace diff --git a/Tests/MessageStackViewTests/ArrayExtensionTests.swift b/Tests/MessageStackViewTests/ArrayExtensionTests.swift index cdf5a32..c91aa38 100644 --- a/Tests/MessageStackViewTests/ArrayExtensionTests.swift +++ b/Tests/MessageStackViewTests/ArrayExtensionTests.swift @@ -2,75 +2,77 @@ import XCTest @testable import MessageStackView final class ArrayExtensionTests: XCTestCase { - + /// Default test elements private let items = ["A", "B", "C", "D", "E"] - + /// Single test element private let single = [17] - + /// Double test element private let double = [345, 92] - + // MARK: - Tests - + func testElementAfter() { XCTAssertEqual(items.elementAfterFirst(of: "A"), "B") XCTAssertEqual(items.elementAfterFirst(of: "B"), "C") XCTAssertEqual(items.elementAfterFirst(of: "C"), "D") XCTAssertEqual(items.elementAfterFirst(of: "D"), "E") XCTAssertNil(items.elementAfterFirst(of: "E")) - + XCTAssertNil(items.elementAfterFirst(of: UUID().uuidString)) } - + func testElementBefore() { XCTAssertNil(items.elementBeforeFirst(of: "A")) XCTAssertEqual(items.elementBeforeFirst(of: "B"), "A") XCTAssertEqual(items.elementBeforeFirst(of: "C"), "B") XCTAssertEqual(items.elementBeforeFirst(of: "D"), "C") XCTAssertEqual(items.elementBeforeFirst(of: "E"), "D") - + XCTAssertNil(items.elementBeforeFirst(of: UUID().uuidString)) } - + func testEmpty() { let empty: [String] = [] - + ["", "x"].forEach { XCTAssertNil(empty.elementBeforeFirst(of: $0)) XCTAssertNil(empty.elementAfterFirst(of: $0)) } } - + func testSingle() { guard let element = single.first else { XCTFail("\(#function) array invalid") return } - + XCTAssertNil(single.elementBeforeFirst(of: element)) XCTAssertNil(single.elementAfterFirst(of: element)) } - + func testDouble() { guard let first = double.first, let last = double.last else { XCTFail("\(#function) array invalid") return } - + XCTAssertNil(double.elementBeforeFirst(of: first)) XCTAssertEqual(double.elementAfterFirst(of: first), last) - + XCTAssertEqual(double.elementBeforeFirst(of: last), first) XCTAssertNil(double.elementAfterFirst(of: last)) } + // swiftlint:disable empty_xctest_method static var allTests = [ ("testElementAfter", testElementAfter), ("testElementBefore", testElementBefore), ("testEmpty", testEmpty), ("testSingle", testSingle), - ("testDouble", testDouble), + ("testDouble", testDouble) ] + // swiftlint:enable empty_xctest_method } diff --git a/Tests/MessageStackViewTests/SystemViewControllerTests.swift b/Tests/MessageStackViewTests/SystemViewControllerTests.swift index e212272..ef62234 100644 --- a/Tests/MessageStackViewTests/SystemViewControllerTests.swift +++ b/Tests/MessageStackViewTests/SystemViewControllerTests.swift @@ -31,11 +31,19 @@ class SystemViewControllerTests: XCTestCase { } XCTAssertTrue(UIImagePickerController().isSystemViewController) XCTAssertTrue(EKEventEditViewController().isSystemViewController) - XCTAssertTrue(UIActivityViewController(activityItems: [], applicationActivities: nil).isSystemViewController) + XCTAssertTrue( + UIActivityViewController( + activityItems: [], + applicationActivities: nil).isSystemViewController + ) XCTAssertTrue(CNContactViewController().isSystemViewController) - XCTAssertTrue(UIDocumentPickerViewController(documentTypes: [".txt"], in: .open).isSystemViewController) + XCTAssertTrue( + UIDocumentPickerViewController( + documentTypes: [".txt"], in: .open + ).isSystemViewController + ) XCTAssertTrue(AVPlayerViewController().isSystemViewController) - + XCTAssertFalse(UIViewController().isSystemViewController) XCTAssertFalse(UITableViewController().isSystemViewController) XCTAssertFalse(CustomViewController().isSystemViewController) @@ -50,7 +58,7 @@ class CustomViewController: UIViewController { // MARK: - Extensions private extension URL { - + /// A sample `URL` static let sample = URL(string: "https://3sidedcube.com/")! } diff --git a/Tests/MessageStackViewTests/XCTestManifests.swift b/Tests/MessageStackViewTests/XCTestManifests.swift index 2b94ef8..d8cf027 100644 --- a/Tests/MessageStackViewTests/XCTestManifests.swift +++ b/Tests/MessageStackViewTests/XCTestManifests.swift @@ -3,7 +3,7 @@ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ - testCase(MessageStackViewTests.allTests), + testCase(MessageStackViewTests.allTests) ] } #endif