diff --git a/.travis.yml b/.travis.yml index 26d2c8a..4e28177 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,11 @@ before_script: script: # Build Framework - - set -o pipefail && xcodebuild clean build -workspace "$WORKSPACE" -scheme "BulletinBoard" -destination "$DESTINATION" | xcpretty + - set -o pipefail && xcodebuild clean build -workspace "$WORKSPACE" -scheme "BulletinBoard" -destination "$DESTINATION" SWIFT_VERSION="3.0" | xcpretty + - set -o pipefail && xcodebuild clean build -workspace "$WORKSPACE" -scheme "BulletinBoard" -destination "$DESTINATION" SWIFT_VERSION="4.0" | xcpretty # Build Demo Project - set -o pipefail && xcodebuild clean build -workspace "$WORKSPACE" -scheme "Instanimal" -destination "$DESTINATION" | xcpretty # Build Project with Package Managers - carthage build --platform $PLATFORM --no-skip-current - - pod lib lint + - pod lib lint --swift-version=3.0 + - pod lib lint --swift-version=4.0 diff --git a/BulletinBoard.podspec b/BulletinBoard.podspec index a6d75fa..3355ced 100644 --- a/BulletinBoard.podspec +++ b/BulletinBoard.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "BulletinBoard" - s.version = "1.1.0" + s.version = "1.2.0" s.summary = "Generate and Display Bottom Card Interfaces for iOS" s.description = <<-DESC BulletinBoard is an iOS library that generates and manages contextual cards displayed at the bottom of the screen. It is especially well suited for quick user interactions such as onboarding screens or configuration. diff --git a/BulletinBoard.xcodeproj/project.pbxproj b/BulletinBoard.xcodeproj/project.pbxproj index 0b62666..75d0511 100644 --- a/BulletinBoard.xcodeproj/project.pbxproj +++ b/BulletinBoard.xcodeproj/project.pbxproj @@ -237,7 +237,7 @@ SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -286,8 +286,9 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_SWIFT3_OBJC_INFERENCE = Off; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 3.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7d6cd69 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# _BulletinBoard_ Changelog + +## 🔖 v1.2.0 + +- Dismiss the bulletin by swiping down +- Support Swift 3.2 + +## 🔖 v1.1.0 + +- Add Accessibility technologies support (VoiceOver, Switch Control) - thanks @lennet! +- Add an optional activity indicator before transitions +- Improve memory management and fix retain cycles/leaks + +## 🔖 v1.0.0 + +- Inital Release \ No newline at end of file diff --git a/README.md b/README.md index 09de7c7..c9b8663 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Here are some screenshots showing what you can build with BulletinBoard: ## Requirements - iOS 9 and later -- Swift 4 +- Swift 3.2 and later ## Demo @@ -236,9 +236,9 @@ This creates the following interaction: ![Activity Indicator](https://raw.githubusercontent.com/alexaubry/BulletinBoard/master/.assets/demo_activity.png) -## Automatic Dismissal +## Dismissal -If you set the `isDismissable` property to `true`, the user will be able to dismiss the bulletin by tapping outside of the card. +If you set the `isDismissable` property to `true`, the user will be able to dismiss the bulletin by tapping outside of the card or by swiping the card down. You should set this property to `true` for the last item. diff --git a/Sources/BulletinInterfaceFactory.swift b/Sources/BulletinInterfaceFactory.swift index fc73746..305eeb5 100644 --- a/Sources/BulletinInterfaceFactory.swift +++ b/Sources/BulletinInterfaceFactory.swift @@ -66,7 +66,7 @@ public class BulletinInterfaceFactory { titleLabel.numberOfLines = 1 titleLabel.adjustsFontSizeToFitWidth = true - titleLabel.font = UIFont.systemFont(ofSize: titleFontSize, weight: .medium) + titleLabel.font = UIFont.systemFont(ofSize: titleFontSize, weight: UIFontWeightMedium) return titleLabel @@ -112,12 +112,12 @@ public class BulletinInterfaceFactory { actionButton.autoresizingMask = .flexibleWidth actionButton.setTitle(title, for: .normal) - actionButton.titleLabel?.font = UIFont.systemFont(ofSize: actionButtonFontSize, weight: .semibold) + actionButton.titleLabel?.font = UIFont.systemFont(ofSize: actionButtonFontSize, weight: UIFontWeightSemibold) actionButton.layer.cornerRadius = 12 actionButton.clipsToBounds = true - let actionContainer = ContainerView(actionButton) + let actionContainer = ContainerView(actionButton) actionContainer.heightAnchor.constraint(equalToConstant: 55).isActive = true return actionContainer @@ -161,3 +161,10 @@ public class BulletinInterfaceFactory { } } + +// MARK: - Swift Compatibility + +#if swift(>=4.0) +private let UIFontWeightMedium = UIFont.Weight.medium +private let UIFontWeightSemibold = UIFont.Weight.semibold +#endif diff --git a/Sources/BulletinManager.swift b/Sources/BulletinManager.swift index 11bb88f..7e6f411 100644 --- a/Sources/BulletinManager.swift +++ b/Sources/BulletinManager.swift @@ -181,10 +181,13 @@ public final class BulletinManager: NSObject, UIViewControllerTransitioningDeleg /** * Dismisses the bulletin and clears the current page. - * - parameter animated: Whether to animate dismissal. + * + * - parameter animated: Whether to animate dismissal. Defaults to `true`. + * - parameter completion: An optional block to execute after dismissal. Default to `nil`. */ - public func dismissBulletin( animated: Bool) { + public func dismissBulletin(animated: Bool = true, + completion: (() -> Void)? = nil) { precondition(Thread.isMainThread) precondition(isPrepared, "You must call the `prepare` function before interacting with the bulletin.") @@ -192,13 +195,17 @@ public final class BulletinManager: NSObject, UIViewControllerTransitioningDeleg currentItem.tearDown() currentItem.manager = nil - viewController.dismiss(animated: true) { + viewController.dismiss(animated: animated) { + + completion?() for arrangedSubview in self.viewController.contentStackView.arrangedSubviews { self.viewController.contentStackView.removeArrangedSubview(arrangedSubview) arrangedSubview.removeFromSuperview() } + self.viewController.resetContentView() + } currentItem = rootItem @@ -212,7 +219,7 @@ public final class BulletinManager: NSObject, UIViewControllerTransitioningDeleg } - /// :nodoc: + /// Returns the presentation controller for the bulletin view controller. public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { diff --git a/Sources/BulletinViewController.swift b/Sources/BulletinViewController.swift index 19ebae1..1f5b91d 100644 --- a/Sources/BulletinViewController.swift +++ b/Sources/BulletinViewController.swift @@ -9,7 +9,7 @@ import UIKit * A view controller that displays a card at the bottom of the screen. */ -final class BulletinViewController: UIViewController { +final class BulletinViewController: UIViewController, UIGestureRecognizerDelegate { /** * The stack view displaying the content of the card. @@ -50,6 +50,8 @@ final class BulletinViewController: UIViewController { // MARK: - Private Interface Elements + private var panGesture: UIPanGestureRecognizer! + private let contentView = UIView() private let activityIndicator = ActivityIndicator() @@ -90,10 +92,10 @@ final class BulletinViewController: UIViewController { centerXConstraint = contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor) minWidthConstraint = contentView.widthAnchor.constraint(equalToConstant: 444) - minWidthConstraint.priority = .defaultHigh + minWidthConstraint.priority = UILayoutPriorityDefaultHigh let maxWidthConstraint = contentView.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, constant: -24) - maxWidthConstraint.priority = .required + maxWidthConstraint.priority = UILayoutPriorityRequired maxWidthConstraint.isActive = true containerBottomConstraint = contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor) @@ -115,11 +117,11 @@ final class BulletinViewController: UIViewController { let topConstraint = contentView.topAnchor.constraint(equalTo: contentStackView.topAnchor, constant: -24) topConstraint.isActive = true - topConstraint.priority = .defaultHigh + topConstraint.priority = UILayoutPriorityDefaultHigh let minYConstraint = contentView.topAnchor.constraint(greaterThanOrEqualTo: topLayoutGuide.bottomAnchor) minYConstraint.isActive = true - minYConstraint.priority = .required + minYConstraint.priority = UILayoutPriorityRequired contentStackView.axis = .vertical contentStackView.alignment = .fill @@ -137,12 +139,22 @@ final class BulletinViewController: UIViewController { activityIndicator.activityIndicatorViewStyle = .whiteLarge activityIndicator.color = .black + activityIndicator.isUserInteractionEnabled = true activityIndicator.alpha = 0 contentView.backgroundColor = #colorLiteral(red: 0.9921568627, green: 1, blue: 1, alpha: 1) setUpLayout(with: traitCollection) + // Pan Gesture + + panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)) + panGesture.maximumNumberOfTouches = 1 + panGesture.cancelsTouchesInView = false + panGesture.delegate = self + + contentView.addGestureRecognizer(panGesture) + } // MARK: - Layout @@ -239,4 +251,74 @@ final class BulletinViewController: UIViewController { return dismissIfPossible() } + // MARK: - Pan Gesture + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { + return !(touch.view is UIControl) + } + + @objc private func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) { + + switch gestureRecognizer.state { + case .began: + gestureRecognizer.setTranslation(.zero, in: contentView) + + case .changed: + let translation = gestureRecognizer.translation(in: contentView) + updateContentView(forVerticalTranslation: translation.y) + + case .ended: + + let translation = gestureRecognizer.translation(in: contentView) + let dismissThreshold = 1/2 * contentView.frame.height + + guard translation.y >= dismissThreshold && isDismissable else { + + UIView.animate(withDuration: 0.25) { + self.contentView.transform = .identity + } + + return + + } + + dismissIfPossible() + + default: + break + + } + + } + + private func updateContentView(forVerticalTranslation translation: CGFloat) { + + let translationFactor: CGFloat = translation < 0 ? 1/2 : 2/3 + + let contentViewTranslation: CGFloat + + if translation < 0 || !(isDismissable) { + + let frictionTranslation = 30 * atan(translation/120) + translation/10 + contentViewTranslation = frictionTranslation * translationFactor + + } else { + contentViewTranslation = translation * translationFactor + } + + contentView.transform = CGAffineTransform(translationX: 0, y: contentViewTranslation) + + } + + func resetContentView() { + contentView.transform = .identity + } + } + +// MARK: - Swift Compatibility + +#if swift(>=4.0) +private let UILayoutPriorityRequired = UILayoutPriority.required +private let UILayoutPriorityDefaultHigh = UILayoutPriority.defaultHigh +#endif diff --git a/Sources/PageBulletinItem.swift b/Sources/PageBulletinItem.swift index 76ace91..4eacb00 100644 --- a/Sources/PageBulletinItem.swift +++ b/Sources/PageBulletinItem.swift @@ -51,13 +51,33 @@ open class PageBulletinItem: BulletinItem { // MARK: - BulletinItem - /// :nodoc: + /** + * The current object managing the item. + * + * This property is set when the item is currently being displayed. It will be set to `nil` when + * the item is removed from view. + */ + public weak var manager: BulletinManager? - /// :nodoc: + /** + * Whether the page can be dismissed. + * + * If you set this value to `true`, the user will be able to dismiss the bulletin by tapping outside + * the card. + * + * You should set it to `true` for the last item you want to display. + */ + public var isDismissable: Bool = false - /// :nodoc: + /** + * The item to display after this one. + * + * If you set this value, you'll be able to call `displayNextItem()` to present the next item to + * the stack. + */ + public var nextItem: BulletinItem? = nil @@ -122,7 +142,12 @@ open class PageBulletinItem: BulletinItem { // MARK: - View Management - /// :nodoc: + /** + * Creates the list of views to display on the bulletin. + * + * This is an implementation detail of `BulletinItem` and you should not call it directly. + */ + public func makeArrangedSubviews() -> [UIView] { var arrangedSubviews = [UIView]() @@ -190,7 +215,13 @@ open class PageBulletinItem: BulletinItem { } - /// :nodoc: + /** + * Called by the manager when the item was removed from the bulletin view. Use this function + * to remove any button target or gesture recognizers from your managed views. + * + * This is an implementation detail of `BulletinItem` and you should not call it directly. + */ + public func tearDown() { actionButton?.contentView.removeTarget(self, action: nil, for: .touchUpInside) alternativeButton?.removeTarget(self, action: nil, for: .touchUpInside)