├── .assets ├── demo_activity.png ├── demo_background_styles.png ├── demo_close_button.png ├── demo_screenshots.png ├── demo_segue.png ├── demo_thumbnail.png ├── demo_tint_color.png ├── page_customization.png └── twitter_badge.svg ├── .generate-docs.sh ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── .travis.yml ├── BLTNBoard.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── BLTNBoard.xcscheme ├── BulletinBoard.podspec ├── BulletinBoard.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Configs └── BLTNBoard.plist ├── Example ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x-1.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x-1.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x-1.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ ├── Icon-App-83.5x83.5@2x.png │ │ └── Icon-App-iTunes.png │ ├── Cats │ │ ├── Contents.json │ │ ├── cat_img_1.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_4.jpg │ │ ├── cat_img_10.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_10.jpg │ │ ├── cat_img_11.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_11.jpg │ │ ├── cat_img_12.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_12.jpg │ │ ├── cat_img_13.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_13.jpg │ │ ├── cat_img_14.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_14.jpg │ │ ├── cat_img_15.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_15.jpg │ │ ├── cat_img_16.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_16.jpg │ │ ├── cat_img_2.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_6.jpg │ │ ├── cat_img_3.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_3.jpg │ │ ├── cat_img_4.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_1.jpg │ │ ├── cat_img_5.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_5.jpg │ │ ├── cat_img_6.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_2.jpg │ │ ├── cat_img_7.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_7.jpg │ │ ├── cat_img_8.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_8.jpg │ │ └── cat_img_9.imageset │ │ │ ├── Contents.json │ │ │ └── cat_img_9.jpg │ ├── Contents.json │ ├── Dogs │ │ ├── Contents.json │ │ ├── dog_img_1.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_1.jpg │ │ ├── dog_img_10.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_10.jpg │ │ ├── dog_img_11.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_11.jpg │ │ ├── dog_img_12.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_12.jpg │ │ ├── dog_img_13.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_13.jpg │ │ ├── dog_img_14.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_14.jpg │ │ ├── dog_img_15.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_15.jpg │ │ ├── dog_img_16.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_16.jpg │ │ ├── dog_img_2.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_2.jpg │ │ ├── dog_img_3.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_3.jpg │ │ ├── dog_img_4.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_4.jpg │ │ ├── dog_img_5.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_5.jpg │ │ ├── dog_img_6.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_6.jpg │ │ ├── dog_img_7.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_7.jpg │ │ ├── dog_img_8.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_8.jpg │ │ └── dog_img_9.imageset │ │ │ ├── Contents.json │ │ │ └── dog_img_9.jpg │ ├── IntroCompletion.imageset │ │ ├── Contents.json │ │ ├── IntroCompletion.png │ │ ├── IntroCompletion@2x.png │ │ └── IntroCompletion@3x.png │ ├── LocationPrompt.imageset │ │ ├── Contents.json │ │ ├── LocationPrompt.png │ │ ├── LocationPrompt@2x.png │ │ └── LocationPrompt@3x.png │ ├── NotificationPrompt.imageset │ │ ├── Contents.json │ │ ├── NotificationPrompt.png │ │ ├── NotificationPrompt@2x.png │ │ └── NotificationPrompt@3x.png │ └── RoundedIcon.imageset │ │ ├── Contents.json │ │ ├── RoundedIcon.png │ │ ├── RoundedIcon@2x.png │ │ └── RoundedIcon@3x.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ ├── Main-ObjC.storyboard │ └── Main-Swift.storyboard ├── Configs │ ├── Info-ObjC.plist │ └── Info.plist ├── CustomBulletins │ ├── CollectionUtilities.swift │ ├── DatePickerBulletinItem.swift │ ├── FeedbackGenerators.swift │ ├── FeedbackPageBulletinItem.swift │ ├── Info.plist │ ├── PetSelectorBulletinPage.swift │ ├── PetValidationBulletinItem.swift │ └── TextFieldBulletinPage.swift ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── BB-ObjC.xcscheme │ │ └── BB-Swift.xcscheme ├── InstanimalIcon.sketch ├── ObjC │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Bulletin │ │ ├── BackgroundViewStyle.h │ │ ├── BackgroundViewStyle.m │ │ ├── BulletinDataSource.h │ │ └── BulletinDataSource.m │ ├── RootViewController.h │ ├── RootViewController.m │ ├── Supporting Files │ │ ├── CollectionDataSource.h │ │ ├── CollectionDataSource.m │ │ ├── PermissionsManager.h │ │ ├── PermissionsManager.m │ │ ├── SelectionFeedbackGenerator.h │ │ ├── SelectionFeedbackGenerator.m │ │ ├── SuccessFeedbackGenerator.h │ │ └── SuccessFeedbackGenerator.m │ └── main.m ├── Swift │ ├── AppDelegate.swift │ ├── Bulletin │ │ ├── BackgroundStyles.swift │ │ └── BulletinDataSource.swift │ ├── Supporting Files │ │ └── PermissionsManager.swift │ └── ViewController.swift ├── de.lproj │ ├── LaunchScreen.strings │ ├── Main-ObjC.strings │ └── Main-Swift.strings └── fr.lproj │ ├── LaunchScreen.strings │ ├── Main-ObjC.strings │ └── Main-Swift.strings ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── BLTNItemManager.swift ├── Deprecations.swift ├── InterfaceBuilder │ ├── BLTNBackgroundViewStyle.swift │ ├── BLTNContainerView.swift │ ├── BLTNHighlightButtonWrapper.swift │ ├── BLTNInterfaceBuilder.swift │ ├── BLTNItemAppearance.swift │ ├── BLTNSpacing.swift │ ├── BLTNTitleLabelContainer.swift │ ├── BLTNViewPosition.swift │ ├── HighlightButton.swift │ └── UIButton+BackgroundColor.swift ├── Models │ ├── BLTNActionItem.swift │ ├── BLTNItem.swift │ └── BLTNPageItem.swift └── Support │ ├── Animations │ ├── AnimationChain.swift │ ├── BulletinDismissAnimationController.swift │ ├── BulletinPresentationAnimationController.swift │ └── BulletinSwipeInteractionController.swift │ ├── BLTNBoardSwiftSupport.h │ ├── BulletinViewController.swift │ ├── Helpers │ ├── BLTNItemManager+Helpers.swift │ └── UIColor+Luminance.swift │ └── Views │ └── Internal │ ├── ActivityIndicator.swift │ ├── BulletinBackgroundView.swift │ ├── BulletinCloseButton.swift │ └── ContinuousCorners │ ├── ContinuousMaskLayer.swift │ ├── RoundedViewProtocol.swift │ └── UIView+RoundedView.swift └── guides ├── Getting Started.md └── Migrating To V2.md /.assets/demo_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/demo_activity.png -------------------------------------------------------------------------------- /.assets/demo_background_styles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/demo_background_styles.png -------------------------------------------------------------------------------- /.assets/demo_close_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/demo_close_button.png -------------------------------------------------------------------------------- /.assets/demo_screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/demo_screenshots.png -------------------------------------------------------------------------------- /.assets/demo_segue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/demo_segue.png -------------------------------------------------------------------------------- /.assets/demo_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/demo_thumbnail.png -------------------------------------------------------------------------------- /.assets/demo_tint_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/demo_tint_color.png -------------------------------------------------------------------------------- /.assets/page_customization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/.assets/page_customization.png -------------------------------------------------------------------------------- /.assets/twitter_badge.svg: -------------------------------------------------------------------------------- 1 | ContactContact@_alexaubry@_alexaubry -------------------------------------------------------------------------------- /.generate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | MODULE_VERSION=$1 5 | OUTPUT=$2 6 | SWIFT_VERSION="5.1" 7 | AUTHOR="Alexis Aubry" 8 | AUTHOR_URL="https://twitter.com/_alexaubry" 9 | MODULE_NAME="BLTNBoard" 10 | COPYRIGHT="Copyright © 2017 - present $AUTHOR. Available under the MIT License." 11 | GITHUB_URL="https://github.com/alexaubry/BulletinBoard" 12 | GH_PAGES_URL="https://alexisakers.github.io/BulletinBoard" 13 | 14 | bundle exec jazzy \ 15 | --swift-version $SWIFT_VERSION \ 16 | -a "$AUTHOR" \ 17 | -u "$AUTHOR_URL" \ 18 | -m "$MODULE_NAME" \ 19 | --module-version "$MODULE_VERSION" \ 20 | --copyright "$COPYRIGHT" \ 21 | -g "$GITHUB_URL" \ 22 | --github-file-prefix "$GITHUB_URL/tree/master" \ 23 | -r "$GH_PAGES_URL" \ 24 | -o "$OUTPUT" \ 25 | --min-acl public \ 26 | --use-safe-filenames \ 27 | --exclude="Sources/Support/*.swift" \ 28 | --documentation="guides/*.md" 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve BulletinBoard 4 | 5 | --- 6 | 7 | **Problem Description:** 8 | 9 | **Steps to reproduce:** 10 | 11 | **Environment:** 12 | - Device: [e.g. iPhone 6] 13 | - OS: [e.g. iOS 8.1] 14 | - Version of BulletinBoard: [e.g. 2.0] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Checklist 4 | - [ ] I've tested my changes. 5 | - [ ] I've read the [Contribution Guidelines](https://github.com/alexaubry/BulletinBoard/blob/master/CONTRIBUTING.md). 6 | - [ ] I've updated the documentation if necessary. 7 | 8 | ### Motivation and Context 9 | 10 | 11 | 12 | 13 | 14 | ### Description 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | 3 | .DS_Store 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | Packages/ 36 | Package.pins 37 | .build/ 38 | 39 | # Carthage 40 | Carthage/Build 41 | Carthage/Checkouts 42 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode10 2 | language: objective-c 3 | 4 | env: 5 | global: 6 | - WORKSPACE="BulletinBoard.xcworkspace" 7 | matrix: 8 | - DESTINATION="platform=iOS Simulator,name=iPhone 6,OS=9.0" PLATFORM="iOS" 9 | - DESTINATION="platform=iOS Simulator,name=iPhone 6,OS=11.0" PLATFORM="iOS" 10 | 11 | before_install: 12 | - brew update 13 | - brew outdated carthage || brew upgrade carthage 14 | - gem install xcpretty 15 | 16 | before_script: 17 | - open -b com.apple.iphonesimulator 18 | 19 | script: 20 | - xcodebuild -workspace "$WORKSPACE" -list 21 | # Build Framework 22 | - set -o pipefail && xcodebuild clean build -workspace "$WORKSPACE" -scheme "BLTNBoard" -destination "$DESTINATION" | xcpretty 23 | # Build Demo Project 24 | - set -o pipefail && xcodebuild clean build -workspace "$WORKSPACE" -scheme "BB-Swift" -destination "$DESTINATION" | xcpretty 25 | - set -o pipefail && xcodebuild clean build -workspace "$WORKSPACE" -scheme "BB-ObjC" -destination "$DESTINATION" | xcpretty 26 | # Build Project with Package Managers 27 | - carthage build --platform $PLATFORM --no-skip-current 28 | - pod lib lint 29 | -------------------------------------------------------------------------------- /BLTNBoard.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BLTNBoard.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BLTNBoard.xcodeproj/xcshareddata/xcschemes/BLTNBoard.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /BulletinBoard.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "BulletinBoard" 3 | s.version = "5.0.0-rc.1" 4 | s.summary = "Generate and Display Bottom Card Interfaces for iOS" 5 | s.description = <<-DESC 6 | 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. 7 | It has an interface similar to the cards displayed by iOS for AirPods, Apple TV configuration and NFC tag scanning. 8 | It has built-in support for accessibility features such as VoiceOver and Switch Control. 9 | DESC 10 | s.homepage = "https://github.com/alexaubry/BulletinBoard" 11 | s.license = { :type => "MIT", :file => "LICENSE" } 12 | s.author = { "Alexis Aubry" => "me@alexaubry.fr" } 13 | s.social_media_url = "https://twitter.com/_alexaubry" 14 | s.ios.deployment_target = "11.0" 15 | s.source = { :git => "https://github.com/alexaubry/BulletinBoard.git", :tag => s.version.to_s } 16 | s.source_files = "Sources/**/*" 17 | s.private_header_files = "Sources/Support/**/*.h" 18 | s.frameworks = "UIKit" 19 | s.documentation_url = "https://alexisakers.github.io/BulletinBoard" 20 | s.module_name = "BLTNBoard" 21 | s.swift_version = "5.0" 22 | end 23 | -------------------------------------------------------------------------------- /BulletinBoard.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BulletinBoard.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # _BulletinBoard_ Changelog 2 | ## Unreleased 3 | 4 | ## 🔖 v5.0.0 5 | ### Changes 6 | - Require iOS 11.0 7 | - Support for Swift Package Manager 8 | 9 | ### Fixes 10 | - Fix the background view origin when presenting 11 | [#183](https://github.com/alexaubry/BulletinBoard/pull/183) 12 | 13 | ## 🔖 v4.1.2 14 | ### Fixes 15 | - Fix crash for iOS 11 and under 16 | [#177](https://github.com/alexaubry/BulletinBoard/issues/177) 17 | 18 | ## 🔖 v4.1.1 19 | ### Changes 20 | - Do not use external resources for close button 21 | 22 | ### Fixes 23 | - Fix for iPad split view bug 24 | [#173](https://github.com/alexaubry/BulletinBoard/pull/173) 25 | 26 | ## 🔖 v4.1.0 27 | ### New Features 28 | - iOS 13 Dark Mode support 29 | [#170](https://github.com/alexaubry/BulletinBoard/issues/170) 30 | - Add mechanism to pop to item 31 | [#165](https://github.com/alexaubry/BulletinBoard/pull/165) 32 | 33 | ### Fixes 34 | - Remove testing dependencies from the Cartfile 35 | [#166](https://github.com/alexaubry/BulletinBoard/pull/166) 36 | 37 | ## 🔖 v4.0.0 38 | ### Fixes 39 | - Upgrade to Swift 5 40 | 41 | ## 🔖 v3.0.0 42 | 43 | ### New Features 44 | 45 | - Add `isShowingBulletin` property 46 | - Add `willDisplay` method to BLTNItem 47 | - Add option to show the bulletin above the whole application 48 | 49 | ### Fixes 50 | 51 | - Upgrade to Swift 4.2 52 | - Fix frozen dismissal after initial interaction 53 | 54 | ## 🔖 v2.0.2 55 | 56 | - Fix setters and retain semantics 57 | - Add workaround to allow static library usage 58 | - Fix Swift version in Podspec for compatibility with Xcode 10 59 | 60 | ## 🔖 v2.0.1 61 | 62 | - Add missing resources to Podspec (this caused a crash) 63 | 64 | ## 🔖 v2.0.0 65 | 66 | ### New Features 67 | 68 | - Make PageBulletinItem more open to customization: if you create custom pages, you no longer need to recreate the standard components yourself 69 | - Customize fonts and more colors 70 | - Customize status bar colors 71 | - Customize bulletin background color 72 | - Customize corner radius 73 | - Customize padding between screen and bulletin 74 | - Hide the activity indicator without changing the current item 75 | - Annotate library to support Objective-C apps 76 | - Handle keyboard frame updates (support for text fields) 77 | - Support for tinting images with template rendering mode 78 | - Allow customization of the background view 79 | - Add text field as a standard control 80 | - Show activity indicator immediately after item is presented 81 | - Callback for configuration and presentation from BulletinItem 82 | 83 | ### User-Facing Changes 84 | 85 | - On iPad, the bulletin will be presented at the center of the screen and can only be dismissed by a tap (no swipe) 86 | - The item will not be dismissed on swipe unless the user lifts their finger from the screen 87 | - Use screen corner radius on iPhone X 88 | 89 | ### Bug fixes 90 | 91 | - Fix dismiss tap background gesture being called for touches inside the content view 92 | - Fix width contraint not being respected for regular layouts 93 | - Fix iTunes Connect rejection bug due to LLVM code coverage 94 | - Fix action button not being hidden when changing the item 95 | - Fix dismissal handler not being called 96 | - Fix controls inside the card not receiving `touchesEnded` events 97 | - Fix cropped bulletin when presenting above split view controller 98 | - Correctly reset non-dismissable cards position when swipe ends 99 | - Fix Auto Layout conflicts during transitions 100 | - Fix crash when reusing bulletin manager 101 | 102 | ### Library 103 | 104 | - Split `BulletinInterfaceFactory` in two more open classes: `BulletinAppearance` for appearance customization, and `BulletinInterfaceBuilder` for interface components creation 105 | - Create `ActionBulletinItem` as a root bulletin item for items with buttons. Handles button creation and tap events. Views above and below buttons are customizable 106 | - Add example of a collection view bulletin item 107 | - Remove `HighlightButton` from public API 108 | - Various gardening operations to make comments and code more clear 109 | 110 | ## 🔖 v1.3.0 111 | 112 | - Add customizable bulletin backgrounds 113 | - Refactor swipe-to-dismiss: use animation controllers 114 | - Add interactive dismissal (animated background blur radius / opacity) 115 | - Improve iPhone X support: display a blurred bar at the bottom of the safe area to highlight the home indicator 116 | - Simplify layout 117 | - Various documentation and codebase improvements 118 | 119 | ## 🔖 v1.2.0 120 | 121 | - Dismiss the bulletin by swiping down 122 | - Support Swift 3.2 123 | 124 | ## 🔖 v1.1.0 125 | 126 | - Add Accessibility technologies support (VoiceOver, Switch Control) - thanks @lennet! 127 | - Add an optional activity indicator before transitions 128 | - Improve memory management and fix retain cycles/leaks 129 | 130 | ## 🔖 v1.0.0 131 | 132 | - Inital Release 133 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at me@alexaubry.fr. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to _BulletinBoard_ 2 | 3 | The following is a set of guidelines for contributing to _BulletinBoard_ on GitHub. 4 | 5 | > Above all, thank you for your interest in the project and for taking the time to contribute! 👍 6 | 7 | ## I want to report a problem or ask a question 8 | 9 | Before submitting a new GitHub issue, please make sure to 10 | 11 | - Check out the [documentation](https://alexisakers.github.io/BulletinBoard). 12 | - Read the usage guide on [the README](https://github.com/alexaubry/BulletinBoard/#usage). 13 | - Search for [existing GitHub issues](https://github.com/alexaubry/BulletinBoard/issues). 14 | 15 | If the above doesn't help, please [submit an issue](https://github.com/alexaubry/BulletinBoard/issues) on GitHub. 16 | 17 | ## I want to contribute to _BulletinBoard_ 18 | 19 | ### Prerequisites 20 | 21 | To develop _BulletinBoard_, you will need to use an Xcode version compatible with the Swift version specified in the [README](https://github.com/alexaubry/BulletinBoard/#requirements). 22 | 23 | ### Checking out the repository 24 | 25 | We use gitflow for PRs. The `main` branch contains the state of the latest released version. `develop` contains the changes from the current unreleased state. You create your PRs againts the `develop` branch. 26 | 27 | - Click the “Fork” button in the upper right corner of repo 28 | - Clone your fork: 29 | - `git clone https://github.com//BulletinBoard.git` 30 | - Create a new branch to work on: 31 | - `git checkout -b ` 32 | - A good name for a branch describes the thing you’ll be working on, e.g. `voice-over`, `fix-font-size`, etc. 33 | 34 | That’s it! Now you’re ready to work on _BulletinBoard_. Open the `BulletinBoard.xcworkspace` workspace to start coding. 35 | 36 | ### Things to keep in mind 37 | 38 | - Please do not change the minimum iOS version 39 | - Always document new public methods and properties 40 | 41 | ### Testing your local changes 42 | 43 | Before opening a pull request, please make sure your changes don't break things. 44 | 45 | - The framework and example project should build without warnings 46 | - The example project should run without issues. 47 | 48 | ### Submitting the PR 49 | 50 | When the coding is done and you’ve finished testing your changes, you are ready to submit the PR to the [main repo](https://github.com/alexaubry/BulletinBoard), towards the `develop` branch. Some best practices are: 51 | 52 | - Use a descriptive title 53 | - Link the issues that are related to your PR in the body 54 | 55 | After you open your PR, please update the CHANGELOG under the "Unreleased" tab with a link to your changes, under the appropriate section, and following this format: 56 | 57 | ``` 58 | - (Your changes, usually your PR title) 59 | [#XXX](https://github.com/alexaubry/BulletinBoard/pulls/XXX) 60 | ``` 61 | 62 | The sections are: 63 | - `### New Features` 64 | - `### Changed Behavior` 65 | - `### Fixes` 66 | 67 | If you don't see the section under Unreleased, you can add it. 68 | 69 | ## Code of Conduct 70 | 71 | Help us keep _BulletinBoard_ open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md). 72 | 73 | ## License 74 | 75 | This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file. 76 | 77 | _These contribution guidelines were adapted from [_fastlane_](https://github.com/fastlane/fastlane) guides._ 78 | -------------------------------------------------------------------------------- /Configs/BLTNBoard.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2017 Alexis Aubry. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-40x40@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@3x.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-60x60@2x.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@3x.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "Icon-App-20x20@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@2x-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-29x29@1x.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@2x-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-40x40@1x.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@2x-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-76x76@1x.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@2x.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-83.5x83.5@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "Icon-App-iTunes.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/AppIcon.appiconset/Icon-App-iTunes.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_4.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_1.imageset/cat_img_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_1.imageset/cat_img_4.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_10.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_10.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_10.imageset/cat_img_10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_10.imageset/cat_img_10.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_11.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_11.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_11.imageset/cat_img_11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_11.imageset/cat_img_11.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_12.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_12.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_12.imageset/cat_img_12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_12.imageset/cat_img_12.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_13.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_13.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_13.imageset/cat_img_13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_13.imageset/cat_img_13.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_14.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_14.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_14.imageset/cat_img_14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_14.imageset/cat_img_14.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_15.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_15.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_15.imageset/cat_img_15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_15.imageset/cat_img_15.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_16.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_16.imageset/cat_img_16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_16.imageset/cat_img_16.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_6.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_2.imageset/cat_img_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_2.imageset/cat_img_6.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_3.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_3.imageset/cat_img_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_3.imageset/cat_img_3.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_1.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_4.imageset/cat_img_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_4.imageset/cat_img_1.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_5.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_5.imageset/cat_img_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_5.imageset/cat_img_5.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_6.imageset/cat_img_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_6.imageset/cat_img_2.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_7.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_7.imageset/cat_img_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_7.imageset/cat_img_7.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_8.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_8.imageset/cat_img_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_8.imageset/cat_img_8.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cat_img_9.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Cats/cat_img_9.imageset/cat_img_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Cats/cat_img_9.imageset/cat_img_9.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_1.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_1.imageset/dog_img_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_1.imageset/dog_img_1.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_10.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_10.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_10.imageset/dog_img_10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_10.imageset/dog_img_10.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_11.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_11.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_11.imageset/dog_img_11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_11.imageset/dog_img_11.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_12.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_12.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_12.imageset/dog_img_12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_12.imageset/dog_img_12.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_13.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_13.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_13.imageset/dog_img_13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_13.imageset/dog_img_13.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_14.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_14.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_14.imageset/dog_img_14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_14.imageset/dog_img_14.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_15.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_15.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_15.imageset/dog_img_15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_15.imageset/dog_img_15.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_16.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_16.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_16.imageset/dog_img_16.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_16.imageset/dog_img_16.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_2.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_2.imageset/dog_img_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_2.imageset/dog_img_2.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_3.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_3.imageset/dog_img_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_3.imageset/dog_img_3.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_4.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_4.imageset/dog_img_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_4.imageset/dog_img_4.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_5.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_5.imageset/dog_img_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_5.imageset/dog_img_5.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_6.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_6.imageset/dog_img_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_6.imageset/dog_img_6.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_7.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_7.imageset/dog_img_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_7.imageset/dog_img_7.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_8.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_8.imageset/dog_img_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_8.imageset/dog_img_8.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dog_img_9.jpg", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Dogs/dog_img_9.imageset/dog_img_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/Dogs/dog_img_9.imageset/dog_img_9.jpg -------------------------------------------------------------------------------- /Example/Assets.xcassets/IntroCompletion.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "IntroCompletion.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "IntroCompletion@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "IntroCompletion@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | }, 23 | "properties" : { 24 | "template-rendering-intent" : "template" 25 | } 26 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/IntroCompletion.imageset/IntroCompletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/IntroCompletion.imageset/IntroCompletion.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/IntroCompletion.imageset/IntroCompletion@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/IntroCompletion.imageset/IntroCompletion@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/IntroCompletion.imageset/IntroCompletion@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/IntroCompletion.imageset/IntroCompletion@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/LocationPrompt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LocationPrompt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LocationPrompt@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LocationPrompt@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/LocationPrompt.imageset/LocationPrompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/LocationPrompt.imageset/LocationPrompt.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/LocationPrompt.imageset/LocationPrompt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/LocationPrompt.imageset/LocationPrompt@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/LocationPrompt.imageset/LocationPrompt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/LocationPrompt.imageset/LocationPrompt@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/NotificationPrompt.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "NotificationPrompt.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "NotificationPrompt@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "NotificationPrompt@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/NotificationPrompt.imageset/NotificationPrompt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/NotificationPrompt.imageset/NotificationPrompt.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/NotificationPrompt.imageset/NotificationPrompt@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/NotificationPrompt.imageset/NotificationPrompt@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/NotificationPrompt.imageset/NotificationPrompt@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/NotificationPrompt.imageset/NotificationPrompt@3x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/RoundedIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "RoundedIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "RoundedIcon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "RoundedIcon@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/RoundedIcon.imageset/RoundedIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/RoundedIcon.imageset/RoundedIcon.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/RoundedIcon.imageset/RoundedIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/RoundedIcon.imageset/RoundedIcon@2x.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/RoundedIcon.imageset/RoundedIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/Assets.xcassets/RoundedIcon.imageset/RoundedIcon@3x.png -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main-ObjC.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main-Swift.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /Example/Configs/Info-ObjC.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PetBoard C 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSLocationWhenInUseUsageDescription 26 | We can use your location to customize your feed. NB: PetBoard is a demo app. No location data will actually be used. 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main-ObjC 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortraitUpsideDown 38 | UIInterfaceOrientationPortrait 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/Configs/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PetBoard S 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSLocationWhenInUseUsageDescription 26 | We can use your location to customize your feed. NB: PetBoard is a demo app. No location data will actually be used. 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main-Swift 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortraitUpsideDown 38 | UIInterfaceOrientationPortrait 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Example/CustomBulletins/CollectionUtilities.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | extension Notification.Name { 9 | 10 | /** 11 | * The favorite tab index did change. 12 | * 13 | * The user info dictionary contains the following values: 14 | * 15 | * - `"Index"` = an integer with the new favorite tab index. 16 | */ 17 | 18 | public static let FavoriteTabIndexDidChange = Notification.Name("PetBoardFavoriteTabIndexDidChangeNotification") 19 | } 20 | 21 | extension UserDefaults { 22 | public var favoriteTabIndex: Int { 23 | get { integer(forKey: "BLTNBoard.FavoriteTabIndex") } 24 | set { set(newValue, forKey: "BLTNBoard.FavoriteTabIndex") } 25 | } 26 | } 27 | 28 | 29 | /** 30 | * A data provider for a collection view. 31 | */ 32 | 33 | public enum CollectionDataSource: String { 34 | case cat, dog 35 | 36 | /// Get the image at the given index. 37 | public func image(at index: Int) -> UIImage { 38 | let name = "\(rawValue)_img_\(index + 1)" 39 | return UIImage(named: name)! 40 | } 41 | 42 | /// The number of images on the data set. 43 | public var numberOfImages: Int { 44 | return 16 45 | } 46 | } 47 | 48 | // MARK: - ImageCollectionViewCell 49 | 50 | /** 51 | * A collection view cell that displays an image. 52 | */ 53 | 54 | @objc public class ImageCollectionViewCell: UICollectionViewCell { 55 | 56 | @objc public let imageView = UIImageView() 57 | 58 | override init(frame: CGRect) { 59 | super.init(frame: frame) 60 | initialize() 61 | } 62 | 63 | required init?(coder aDecoder: NSCoder) { 64 | super.init(coder: aDecoder) 65 | initialize() 66 | } 67 | 68 | private func initialize() { 69 | 70 | imageView.translatesAutoresizingMaskIntoConstraints = false 71 | imageView.contentMode = .scaleAspectFill 72 | 73 | contentView.addSubview(imageView) 74 | imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true 75 | imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true 76 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true 77 | contentView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor).isActive = true 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Example/CustomBulletins/DatePickerBulletinItem.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | import BLTNBoard 8 | 9 | /** 10 | * A bulletin item that demonstrates how to integrate a date picker inside a bulletin item. 11 | */ 12 | 13 | @objc public class DatePickerBLTNItem: BLTNPageItem { 14 | public lazy var datePicker = UIDatePicker() 15 | 16 | /** 17 | * Display the date picker under the description label. 18 | */ 19 | 20 | override public func makeViewsUnderDescription(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView]? { 21 | datePicker.datePickerMode = .date 22 | return [datePicker] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Example/CustomBulletins/FeedbackGenerators.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A 3D Touch selection feedback generator wrapper that uses the API only when available. 10 | */ 11 | 12 | class SelectionFeedbackGenerator { 13 | 14 | private let anyObject: AnyObject? 15 | 16 | init() { 17 | 18 | if #available(iOS 10, *) { 19 | anyObject = UISelectionFeedbackGenerator() 20 | } else { 21 | anyObject = nil 22 | } 23 | 24 | } 25 | 26 | func prepare() { 27 | 28 | if #available(iOS 10, *) { 29 | (anyObject as! UISelectionFeedbackGenerator).prepare() 30 | } 31 | 32 | } 33 | 34 | func selectionChanged() { 35 | 36 | if #available(iOS 10, *) { 37 | (anyObject as! UISelectionFeedbackGenerator).selectionChanged() 38 | } 39 | 40 | } 41 | 42 | } 43 | 44 | /** 45 | * A 3D Touch success feedback generator wrapper that uses the API only when available. 46 | */ 47 | 48 | class SuccessFeedbackGenerator { 49 | 50 | private let anyObject: AnyObject? 51 | 52 | init() { 53 | 54 | if #available(iOS 10, *) { 55 | anyObject = UINotificationFeedbackGenerator() 56 | } else { 57 | anyObject = nil 58 | } 59 | 60 | } 61 | 62 | func prepare() { 63 | 64 | if #available(iOS 10, *) { 65 | (anyObject as! UINotificationFeedbackGenerator).prepare() 66 | } 67 | 68 | } 69 | 70 | func success() { 71 | 72 | if #available(iOS 10, *) { 73 | (anyObject as! UINotificationFeedbackGenerator).notificationOccurred(.success) 74 | } 75 | 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Example/CustomBulletins/FeedbackPageBulletinItem.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | import BLTNBoard 8 | 9 | /** 10 | * A subclass of page bulletin item that plays an haptic feedback when the buttons are pressed. 11 | * 12 | * This class demonstrates how to override `PageBLTNItem` to customize button tap handling. 13 | */ 14 | 15 | @objc public class FeedbackPageBLTNItem: BLTNPageItem { 16 | 17 | private let feedbackGenerator = SelectionFeedbackGenerator() 18 | 19 | override public func actionButtonTapped(sender: UIButton) { 20 | 21 | // Play an haptic feedback 22 | 23 | feedbackGenerator.prepare() 24 | feedbackGenerator.selectionChanged() 25 | 26 | // Call super 27 | 28 | super.actionButtonTapped(sender: sender) 29 | 30 | } 31 | 32 | override public func alternativeButtonTapped(sender: UIButton) { 33 | 34 | // Play an haptic feedback 35 | 36 | feedbackGenerator.prepare() 37 | feedbackGenerator.selectionChanged() 38 | 39 | // Call super 40 | 41 | super.alternativeButtonTapped(sender: sender) 42 | 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Example/CustomBulletins/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/CustomBulletins/PetSelectorBulletinPage.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | import BLTNBoard 8 | 9 | /** 10 | * An item that displays a choice with two buttons. 11 | * 12 | * This item demonstrates how to create a page bulletin item with a custom interface, and changing the 13 | * next item based on user interaction. 14 | */ 15 | 16 | @objc public class PetSelectorBulletinPage: FeedbackPageBLTNItem { 17 | private var catButtonContainer: UIButton! 18 | private var dogButtonContainer: UIButton! 19 | private var selectionFeedbackGenerator = SelectionFeedbackGenerator() 20 | 21 | let completionHandler: (BLTNItem) -> Void 22 | 23 | @objc public init(completionHandler: @escaping (BLTNItem) -> Void) { 24 | self.completionHandler = completionHandler 25 | super.init(title: "Choose your Favorite") 26 | } 27 | 28 | // MARK: - BLTNItem 29 | 30 | /** 31 | * Called by the manager when the item is about to be removed from the bulletin. 32 | * 33 | * Use this function as an opportunity to do any clean up or remove tap gesture recognizers / 34 | * button targets from your views to avoid retain cycles. 35 | */ 36 | 37 | public override func tearDown() { 38 | catButtonContainer?.removeTarget(self, action: nil, for: .touchUpInside) 39 | dogButtonContainer?.removeTarget(self, action: nil, for: .touchUpInside) 40 | } 41 | 42 | /** 43 | * Called by the manager to build the view hierachy of the bulletin. 44 | * 45 | * We need to return the view in the order we want them displayed. You should use a 46 | * `BulletinInterfaceFactory` to generate standard views, such as title labels and buttons. 47 | */ 48 | 49 | public override func makeViewsUnderDescription(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView]? { 50 | 51 | let favoriteTabIndex = UserDefaults.standard.favoriteTabIndex 52 | 53 | // Pets Stack 54 | 55 | // We add choice cells to a group stack because they need less spacing 56 | let petsStack = interfaceBuilder.makeGroupStack(spacing: 16) 57 | 58 | // Cat Button 59 | 60 | let catButtonContainer = createChoiceCell(dataSource: .cat, isSelected: favoriteTabIndex == 0) 61 | catButtonContainer.addTarget(self, action: #selector(catButtonTapped), for: .touchUpInside) 62 | petsStack.addArrangedSubview(catButtonContainer) 63 | 64 | self.catButtonContainer = catButtonContainer 65 | 66 | // Dog Button 67 | 68 | let dogButtonContainer = createChoiceCell(dataSource: .dog, isSelected: favoriteTabIndex == 1) 69 | dogButtonContainer.addTarget(self, action: #selector(dogButtonTapped), for: .touchUpInside) 70 | petsStack.addArrangedSubview(dogButtonContainer) 71 | 72 | self.dogButtonContainer = dogButtonContainer 73 | 74 | return [petsStack] 75 | 76 | } 77 | 78 | // MARK: - Custom Views 79 | 80 | /** 81 | * Creates a custom choice cell. 82 | */ 83 | 84 | func createChoiceCell(dataSource: CollectionDataSource, isSelected: Bool) -> UIButton { 85 | 86 | let emoji: String 87 | let animalType: String 88 | 89 | switch dataSource { 90 | case .cat: 91 | emoji = "🐱" 92 | animalType = "Cats" 93 | case .dog: 94 | emoji = "🐶" 95 | animalType = "Dogs" 96 | } 97 | 98 | let button = UIButton(type: .system) 99 | button.setTitle(emoji + " " + animalType, for: .normal) 100 | button.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold) 101 | button.contentHorizontalAlignment = .center 102 | button.accessibilityLabel = animalType 103 | 104 | if isSelected { 105 | button.accessibilityTraits.insert(.selected) 106 | } else { 107 | button.accessibilityTraits.remove(.selected) 108 | } 109 | 110 | button.layer.cornerRadius = 12 111 | button.layer.borderWidth = 2 112 | 113 | button.setContentHuggingPriority(.defaultHigh, for: .horizontal) 114 | 115 | let heightConstraint = button.heightAnchor.constraint(equalToConstant: 55) 116 | heightConstraint.priority = .defaultHigh 117 | heightConstraint.isActive = true 118 | 119 | let buttonColor = isSelected ? appearance.actionButtonColor : .lightGray 120 | button.layer.borderColor = buttonColor.cgColor 121 | button.setTitleColor(buttonColor, for: .normal) 122 | button.layer.borderColor = buttonColor.cgColor 123 | 124 | if isSelected { 125 | next = PetValidationBLTNItem(dataSource: dataSource, animalType: animalType.lowercased(), validationHandler: completionHandler) 126 | } 127 | 128 | return button 129 | 130 | } 131 | 132 | // MARK: - Touch Events 133 | 134 | /// Called when the cat button is tapped. 135 | @objc func catButtonTapped() { 136 | 137 | // Play haptic feedback 138 | 139 | selectionFeedbackGenerator.prepare() 140 | selectionFeedbackGenerator.selectionChanged() 141 | 142 | // Update UI 143 | 144 | let catButtonColor = appearance.actionButtonColor 145 | catButtonContainer?.layer.borderColor = catButtonColor.cgColor 146 | catButtonContainer?.setTitleColor(catButtonColor, for: .normal) 147 | catButtonContainer?.accessibilityTraits.insert(.selected) 148 | 149 | let dogButtonColor = UIColor.lightGray 150 | dogButtonContainer?.layer.borderColor = dogButtonColor.cgColor 151 | dogButtonContainer?.setTitleColor(dogButtonColor, for: .normal) 152 | dogButtonContainer?.accessibilityTraits.remove(.selected) 153 | 154 | // Send a notification to inform observers of the change 155 | 156 | NotificationCenter.default.post(name: .FavoriteTabIndexDidChange, 157 | object: self, 158 | userInfo: ["Index": 0]) 159 | 160 | // Set the next item 161 | 162 | next = PetValidationBLTNItem(dataSource: .cat, animalType: "cats", validationHandler: completionHandler) 163 | } 164 | 165 | /// Called when the dog button is tapped. 166 | @objc func dogButtonTapped() { 167 | 168 | // Play haptic feedback 169 | 170 | selectionFeedbackGenerator.prepare() 171 | selectionFeedbackGenerator.selectionChanged() 172 | 173 | // Update UI 174 | 175 | let catButtonColor = UIColor.lightGray 176 | catButtonContainer?.layer.borderColor = catButtonColor.cgColor 177 | catButtonContainer?.setTitleColor(catButtonColor, for: .normal) 178 | catButtonContainer?.accessibilityTraits.remove(.selected) 179 | 180 | let dogButtonColor = appearance.actionButtonColor 181 | dogButtonContainer?.layer.borderColor = dogButtonColor.cgColor 182 | dogButtonContainer?.setTitleColor(dogButtonColor, for: .normal) 183 | dogButtonContainer?.accessibilityTraits.insert(.selected) 184 | 185 | // Send a notification to inform observers of the change 186 | 187 | NotificationCenter.default.post(name: .FavoriteTabIndexDidChange, 188 | object: self, 189 | userInfo: ["Index": 1]) 190 | 191 | // Set the next item 192 | next = PetValidationBLTNItem(dataSource: .dog, animalType: "dogs", validationHandler: completionHandler) 193 | } 194 | 195 | override public func actionButtonTapped(sender: UIButton) { 196 | // Play haptic feedback 197 | selectionFeedbackGenerator.prepare() 198 | selectionFeedbackGenerator.selectionChanged() 199 | 200 | // Ask the manager to present the next item. 201 | manager?.displayNextItem() 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /Example/CustomBulletins/PetValidationBulletinItem.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | import BLTNBoard 8 | 9 | /** 10 | * A bulletin page that allows the user to validate its selection. 11 | * 12 | * This item demonstrates popping to the previous item, and including a collection view inside the page. 13 | */ 14 | 15 | @objc public class PetValidationBLTNItem: FeedbackPageBLTNItem { 16 | 17 | let dataSource: CollectionDataSource 18 | let animalType: String 19 | let validationHandler: (BLTNItem) -> Void 20 | 21 | let selectionFeedbackGenerator = SelectionFeedbackGenerator() 22 | let successFeedbackGenerator = SuccessFeedbackGenerator() 23 | 24 | init(dataSource: CollectionDataSource, animalType: String, validationHandler: @escaping (BLTNItem) -> Void) { 25 | self.dataSource = dataSource 26 | self.animalType = animalType 27 | self.validationHandler = validationHandler 28 | super.init(title: "Choose your Favorite") 29 | 30 | isDismissable = false 31 | descriptionText = "You chose \(animalType) as your favorite animal type. Here are a few examples of posts in this category." 32 | actionButtonTitle = "Validate" 33 | alternativeButtonTitle = "Change" 34 | 35 | } 36 | 37 | // MARK: - Interface 38 | 39 | var collectionView: UICollectionView? 40 | 41 | override public func makeViewsUnderDescription(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView]? { 42 | 43 | let flowLayout = UICollectionViewFlowLayout() 44 | flowLayout.scrollDirection = .vertical 45 | flowLayout.minimumInteritemSpacing = 1 46 | 47 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) 48 | collectionView.backgroundColor = .white 49 | 50 | let collectionWrapper = interfaceBuilder.wrapView(collectionView, width: nil, height: 256, position: .pinnedToEdges) 51 | 52 | self.collectionView = collectionView 53 | collectionView.register(ImageCollectionViewCell.self, forCellWithReuseIdentifier: "cell") 54 | collectionView.dataSource = self 55 | collectionView.delegate = self 56 | 57 | return [collectionWrapper] 58 | 59 | } 60 | 61 | override public func tearDown() { 62 | super.tearDown() 63 | collectionView?.dataSource = nil 64 | collectionView?.delegate = nil 65 | } 66 | 67 | // MARK: - Touch Events 68 | 69 | override public func actionButtonTapped(sender: UIButton) { 70 | 71 | // > Play Haptic Feedback 72 | 73 | selectionFeedbackGenerator.prepare() 74 | selectionFeedbackGenerator.selectionChanged() 75 | 76 | // > Display the loading indicator 77 | 78 | manager?.displayActivityIndicator() 79 | 80 | // > Wait for a "task" to complete before displaying the next item 81 | 82 | let delay = DispatchTime.now() + .seconds(2) 83 | 84 | DispatchQueue.main.asyncAfter(deadline: delay) { 85 | // Play success haptic feedback 86 | self.successFeedbackGenerator.prepare() 87 | self.successFeedbackGenerator.success() 88 | 89 | // Display next item 90 | self.validationHandler(self) 91 | } 92 | } 93 | 94 | public override func alternativeButtonTapped(sender: UIButton) { 95 | 96 | // Play selection haptic feedback 97 | 98 | selectionFeedbackGenerator.prepare() 99 | selectionFeedbackGenerator.selectionChanged() 100 | 101 | // Display previous item 102 | 103 | manager?.popItem() 104 | 105 | } 106 | 107 | } 108 | 109 | // MARK: - Collection View 110 | 111 | extension PetValidationBLTNItem: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { 112 | 113 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 114 | return 1 115 | } 116 | 117 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 118 | return 9 119 | } 120 | 121 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 122 | 123 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ImageCollectionViewCell 124 | cell.imageView.image = dataSource.image(at: indexPath.row) 125 | cell.imageView.contentMode = .scaleAspectFill 126 | cell.imageView.clipsToBounds = true 127 | 128 | return cell 129 | 130 | } 131 | 132 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 133 | 134 | let squareSideLength = (collectionView.frame.width / 3) - 3 135 | return CGSize(width: squareSideLength, height: squareSideLength) 136 | 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /Example/CustomBulletins/TextFieldBulletinPage.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | import BLTNBoard 8 | 9 | /** 10 | * An item that displays a text field. 11 | * 12 | * This item demonstrates how to create a bulletin item with a text field and how it will behave 13 | * when the keyboard is visible. 14 | */ 15 | 16 | @objc public class TextFieldBulletinPage: FeedbackPageBLTNItem { 17 | 18 | @objc public var textField: UITextField! 19 | 20 | @objc public var textInputHandler: ((TextFieldBulletinPage, String?) -> Void)? = nil 21 | 22 | override public func makeViewsUnderDescription(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView]? { 23 | textField = interfaceBuilder.makeTextField(placeholder: "First and Last Name", returnKey: .done, delegate: self) 24 | return [textField] 25 | } 26 | 27 | override public func tearDown() { 28 | super.tearDown() 29 | textField?.delegate = nil 30 | } 31 | 32 | override public func actionButtonTapped(sender: UIButton) { 33 | textField.resignFirstResponder() 34 | super.actionButtonTapped(sender: sender) 35 | } 36 | 37 | } 38 | 39 | // MARK: - UITextFieldDelegate 40 | 41 | extension TextFieldBulletinPage: UITextFieldDelegate { 42 | 43 | @objc open func isInputValid(text: String?) -> Bool { 44 | 45 | if text == nil || text!.isEmpty { 46 | return false 47 | } 48 | 49 | return true 50 | 51 | } 52 | 53 | public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { 54 | return true 55 | } 56 | 57 | public func textFieldShouldReturn(_ textField: UITextField) -> Bool { 58 | textField.resignFirstResponder() 59 | return true 60 | } 61 | 62 | public func textFieldDidEndEditing(_ textField: UITextField) { 63 | 64 | if isInputValid(text: textField.text) { 65 | textInputHandler?(self, textField.text) 66 | } else { 67 | descriptionLabel!.textColor = .red 68 | descriptionLabel!.text = "You must enter some text to continue." 69 | textField.backgroundColor = UIColor.red.withAlphaComponent(0.3) 70 | } 71 | 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/BB-ObjC.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/BB-Swift.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Example/InstanimalIcon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexaubry/BulletinBoard/7b35bb98d776f869851f345696215def30814de2/Example/InstanimalIcon.sketch -------------------------------------------------------------------------------- /Example/ObjC/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | 8 | @interface AppDelegate : UIResponder 9 | 10 | @property (strong, nonatomic) UIWindow *window; 11 | 12 | @end 13 | 14 | -------------------------------------------------------------------------------- /Example/ObjC/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | #import "AppDelegate.h" 7 | 8 | @interface AppDelegate () 9 | 10 | @end 11 | 12 | @implementation AppDelegate 13 | 14 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 15 | return YES; 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Example/ObjC/Bulletin/BackgroundViewStyle.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | @import BLTNBoard; 8 | 9 | /** 10 | * A background view style. 11 | */ 12 | 13 | @interface BackgroundViewStyle : NSObject 14 | 15 | /// The name of the style. 16 | @property (nonatomic, copy) NSString *name; 17 | 18 | /// The raw style to use. 19 | @property (nonatomic) BLTNBackgroundViewStyle *style; 20 | 21 | /// All the styles. 22 | @property (class, copy, readonly) NSArray *allStyles; 23 | 24 | /// The default style. 25 | @property (class, readonly) BackgroundViewStyle *defaultStyle; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Example/ObjC/Bulletin/BackgroundViewStyle.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | #import "BackgroundViewStyle.h" 7 | 8 | @implementation BackgroundViewStyle 9 | 10 | -(instancetype)initWithName:(NSString *)name style:(BLTNBackgroundViewStyle *)style { 11 | self = [super init]; 12 | if (self) { 13 | self.name = name; 14 | self.style = style; 15 | } 16 | return self; 17 | }; 18 | 19 | + (NSArray *)allStyles 20 | { 21 | NSMutableArray *styles = [NSMutableArray array]; 22 | 23 | BackgroundViewStyle *none = [[BackgroundViewStyle alloc] initWithName:@"None" 24 | style:[BLTNBackgroundViewStyle none]]; 25 | 26 | BackgroundViewStyle *dimmed = [[BackgroundViewStyle alloc] initWithName:@"Dimmed" 27 | style:[BLTNBackgroundViewStyle dimmed]]; 28 | 29 | [styles addObject:none]; 30 | [styles addObject:dimmed]; 31 | 32 | if (@available(iOS 10.0, *)) { 33 | 34 | BackgroundViewStyle *extraLight = [[BackgroundViewStyle alloc] initWithName:@"Light" 35 | style:[BLTNBackgroundViewStyle blurredLight]]; 36 | 37 | BackgroundViewStyle *light = [[BackgroundViewStyle alloc] initWithName:@"Extra Light" 38 | style:[BLTNBackgroundViewStyle blurredExtraLight]]; 39 | 40 | BackgroundViewStyle *dark = [[BackgroundViewStyle alloc] initWithName:@"Dark" 41 | style:[BLTNBackgroundViewStyle blurredDark]]; 42 | 43 | BackgroundViewStyle *extraDark = [[BackgroundViewStyle alloc] initWithName:@"Extra Dark" 44 | style:[BLTNBackgroundViewStyle blurredWithStyle:3 isDark:YES]]; 45 | 46 | [styles addObject:extraLight]; 47 | [styles addObject:light]; 48 | [styles addObject:dark]; 49 | [styles addObject:extraDark]; 50 | } 51 | 52 | return (NSArray *)styles; 53 | } 54 | 55 | + (BackgroundViewStyle *)defaultStyle 56 | { 57 | return [[BackgroundViewStyle alloc] initWithName:@"Dimmed" 58 | style:[BLTNBackgroundViewStyle dimmed]]; 59 | } 60 | 61 | @end 62 | -------------------------------------------------------------------------------- /Example/ObjC/Bulletin/BulletinDataSource.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | @import BLTNBoard; 8 | 9 | /** 10 | * A set of tools to interact with the demo data. 11 | * 12 | * This demonstrates how to create and configure bulletin items. 13 | */ 14 | 15 | @interface BulletinDataSource : NSObject 16 | 17 | /// The current favorite tab index. 18 | @property (class) NSInteger favoriteTabIndex; 19 | 20 | /// Whether user completed setup. 21 | @property (class) BOOL userDidCompleteSetup; 22 | 23 | /// Whether to use the Avenir font. 24 | @property (class) BOOL useAvenirFont; 25 | 26 | /// The name of the current font. 27 | @property (class, copy, readonly) NSString *currentFontName; 28 | 29 | #pragma mark Pages 30 | 31 | /** 32 | * Create the introduction page. 33 | * 34 | * This creates a `FeedbackPageBLTNItem` with: a title, an image, a description text and 35 | * and action button. 36 | * 37 | * The action button presents the next item (the textfield page). 38 | */ 39 | 40 | +(BLTNPageItem *)makeIntroPage; 41 | 42 | /** 43 | * Create the location page. 44 | * 45 | * This creates a `PageBLTNItem` with: a title, an image, a description text, and an action 46 | * button. The item can be dismissed. The tint color of the action button is customized. 47 | * 48 | * The action button dismisses the bulletin. The alternative button pops to the root item. 49 | */ 50 | 51 | +(BLTNPageItem *)makeCompletionPage; 52 | 53 | @end 54 | 55 | #pragma mark Notifications 56 | 57 | /** 58 | * The setup did complete. 59 | * 60 | * The user info dictionary is empty. 61 | */ 62 | 63 | extern NSString *const SetupDidCompleteNotificationName; 64 | 65 | /** 66 | * The favorite tab index did change. 67 | * 68 | * The user info dictionary contains the following values: 69 | * 70 | * - `"Index"` = an integer with the new favorite tab index. 71 | */ 72 | 73 | extern NSString *const FavoriteTabIndexDidChangeNotificationName; 74 | -------------------------------------------------------------------------------- /Example/ObjC/RootViewController.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | @import BLTNBoard; 8 | 9 | /** 10 | * A view controller displaying a set of images. 11 | * 12 | * This demonstrates how to set up a bulletin manager and present the bulletin. 13 | */ 14 | 15 | @interface RootViewController : UIViewController 16 | 17 | @property (nonatomic, weak) IBOutlet UIBarButtonItem *styleButtonItem; 18 | @property (nonatomic, weak) IBOutlet UISegmentedControl *segmentedControl; 19 | @property (nonatomic, weak) IBOutlet UIBarButtonItem *showIntoButtonItem; 20 | @property (nonatomic, weak) IBOutlet UICollectionView *collectionView; 21 | 22 | - (IBAction)styleButtonTapped:(id)sender; 23 | - (IBAction)showIntroButtonTapped:(id)sender; 24 | - (IBAction)tabIndexChanged:(UISegmentedControl *)sender; 25 | 26 | @end 27 | 28 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/CollectionDataSource.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | 8 | /** 9 | * A data provider for a collection view. 10 | */ 11 | 12 | @interface CollectionDataSource : NSObject 13 | 14 | /// The number of images on the data set. 15 | @property (nonatomic, readonly) NSInteger numberOfImages; 16 | 17 | /// The name of the pet. 18 | @property (nonatomic, copy, readonly) NSString *petName; 19 | 20 | /// The pluralized name of the pet. 21 | @property (nonatomic, copy, readonly) NSString *pluralizedPetName; 22 | 23 | /// The emoji for the animal. 24 | @property (nonatomic, copy, readonly) NSString *emoji; 25 | 26 | /// Get the image at the given index. 27 | - (UIImage *)imageAtIndex:(NSInteger)index; 28 | 29 | @end 30 | 31 | @interface DogCollectionDataSource : CollectionDataSource 32 | @end 33 | 34 | @interface CatCollectionDataSource : CollectionDataSource 35 | @end 36 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/CollectionDataSource.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | #import "CollectionDataSource.h" 7 | 8 | @implementation CollectionDataSource 9 | 10 | - (NSInteger)numberOfImages 11 | { 12 | return 16; 13 | } 14 | 15 | - (NSString *)petName 16 | { 17 | @throw [self requireConcreteImplementation]; 18 | } 19 | 20 | - (NSString *)pluralizedPetName 21 | { 22 | @throw [self requireConcreteImplementation]; 23 | } 24 | 25 | 26 | -(NSString *)emoji 27 | { 28 | @throw [self requireConcreteImplementation]; 29 | } 30 | 31 | - (UIImage *)imageAtIndex:(NSInteger)index 32 | { 33 | NSString *name = [NSString stringWithFormat:@"%@_img_%lx", [self petName], (unsigned long)index + 1]; 34 | return [UIImage imageNamed:name]; 35 | } 36 | 37 | - (NSException *)requireConcreteImplementation 38 | { 39 | return [NSException exceptionWithName:NSInternalInconsistencyException 40 | reason:@"Please use a concrete sublclass of CollectionDataSource" 41 | userInfo:NULL]; 42 | } 43 | 44 | @end 45 | 46 | @implementation DogCollectionDataSource 47 | 48 | - (NSString *)petName { 49 | return @"dog"; 50 | } 51 | 52 | - (NSString *)pluralizedPetName 53 | { 54 | return @"Dogs"; 55 | } 56 | 57 | - (NSString *)emoji 58 | { 59 | return @"🐶"; 60 | } 61 | 62 | @end 63 | 64 | @implementation CatCollectionDataSource 65 | 66 | - (NSString *)petName { 67 | return @"cat"; 68 | } 69 | 70 | - (NSString *)pluralizedPetName 71 | { 72 | return @"Cats"; 73 | } 74 | 75 | - (NSString *)emoji 76 | { 77 | return @"🐱"; 78 | } 79 | 80 | @end 81 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/PermissionsManager.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | @import CoreLocation; 8 | 9 | /** 10 | * Manages the permissions of the app. 11 | */ 12 | 13 | @interface PermissionsManager : NSObject 14 | 15 | /** 16 | * Requests permission for system features. 17 | */ 18 | 19 | + (PermissionsManager*)sharedManager; 20 | 21 | /// Show the notification permission prompt. 22 | - (void)requestLocalNotifications; 23 | 24 | /// Show the location permission prompt. 25 | - (void)requestWhenInUseLocation; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/PermissionsManager.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | #import 7 | #import "PermissionsManager.h" 8 | 9 | @interface PermissionsManager () 10 | 11 | @property (nonatomic, strong) CLLocationManager *locationManager; 12 | 13 | @end 14 | 15 | @implementation PermissionsManager 16 | 17 | + (PermissionsManager*)sharedManager 18 | { 19 | static PermissionsManager *manager; 20 | static dispatch_once_t onceToken; 21 | 22 | dispatch_once(&onceToken, ^{ 23 | manager = [[PermissionsManager alloc] init]; 24 | }); 25 | 26 | return manager; 27 | } 28 | 29 | - (instancetype)init 30 | { 31 | self = [super init]; 32 | if (self) { 33 | self.locationManager = [[CLLocationManager alloc] init]; 34 | } 35 | return self; 36 | } 37 | 38 | -(void)requestLocalNotifications 39 | { 40 | UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionBadge | UNAuthorizationOptionSound; 41 | [UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) { 42 | // no-op 43 | }]; 44 | } 45 | 46 | -(void)requestWhenInUseLocation 47 | { 48 | [self.locationManager requestWhenInUseAuthorization]; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/SelectionFeedbackGenerator.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | 8 | /** 9 | * A 3D Touch selection feedback generator wrapper that uses the API only when available. 10 | */ 11 | 12 | @interface SelectionFeedbackGenerator : NSObject 13 | 14 | /** 15 | * Prepares the taptic engine. 16 | */ 17 | 18 | - (void)prepare; 19 | 20 | /** 21 | * Plays a selection change haptic feedback. 22 | */ 23 | 24 | - (void)selectionChanged; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/SelectionFeedbackGenerator.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | #import "SelectionFeedbackGenerator.h" 7 | 8 | @interface SelectionFeedbackGenerator () 9 | 10 | @property (nonatomic, strong, nullable) NSObject *feedbackGenerator; 11 | 12 | @end 13 | 14 | @implementation SelectionFeedbackGenerator 15 | 16 | - (instancetype)init 17 | { 18 | self = [super init]; 19 | if (self) { 20 | if (@available(iOS 10.0, *)) { 21 | self.feedbackGenerator = [[UISelectionFeedbackGenerator alloc] init]; 22 | } 23 | } 24 | return self; 25 | } 26 | 27 | - (void)prepare { 28 | if (@available(iOS 10.0, *)) { 29 | [((UISelectionFeedbackGenerator *)self.feedbackGenerator) prepare]; 30 | } 31 | } 32 | 33 | - (void)selectionChanged { 34 | if (@available(iOS 10.0, *)) { 35 | [((UISelectionFeedbackGenerator *)self.feedbackGenerator) selectionChanged]; 36 | } 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/SuccessFeedbackGenerator.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | 8 | /** 9 | * A 3D Touch success feedback generator wrapper that uses the API only when available. 10 | */ 11 | 12 | @interface SuccessFeedbackGenerator : NSObject 13 | 14 | /** 15 | * Prepares the taptic engine. 16 | */ 17 | 18 | - (void)prepare; 19 | 20 | /** 21 | * Plays a success haptic feedback. 22 | */ 23 | 24 | - (void)notifySuccess; 25 | 26 | @end 27 | 28 | -------------------------------------------------------------------------------- /Example/ObjC/Supporting Files/SuccessFeedbackGenerator.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | #import "SuccessFeedbackGenerator.h" 7 | 8 | @interface SuccessFeedbackGenerator () 9 | 10 | @property (nonatomic, strong, nullable) NSObject *feedbackGenerator; 11 | 12 | @end 13 | 14 | @implementation SuccessFeedbackGenerator 15 | 16 | - (instancetype)init 17 | { 18 | self = [super init]; 19 | if (self) { 20 | if (@available(iOS 10.0, *)) { 21 | self.feedbackGenerator = [[UINotificationFeedbackGenerator alloc] init]; 22 | } 23 | 24 | } 25 | return self; 26 | } 27 | 28 | - (void)prepare 29 | { 30 | if (@available(iOS 10.0, *)) { 31 | [((UINotificationFeedbackGenerator *)self.feedbackGenerator) prepare]; 32 | } 33 | } 34 | 35 | - (void)notifySuccess 36 | { 37 | if (@available(iOS 10.0, *)) { 38 | [((UINotificationFeedbackGenerator *)self.feedbackGenerator) notificationOccurred:UINotificationFeedbackTypeSuccess]; 39 | } 40 | } 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /Example/ObjC/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | @import UIKit; 7 | #import "AppDelegate.h" 8 | 9 | int main(int argc, char * argv[]) { 10 | @autoreleasepool { 11 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Example/Swift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | var window: UIWindow? 11 | } 12 | -------------------------------------------------------------------------------- /Example/Swift/Bulletin/BackgroundStyles.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import BLTNBoard 3 | 4 | /** 5 | * Returns a list of all the background styles. 6 | */ 7 | 8 | func BackgroundStyles() -> [(name: String, style: BLTNBackgroundViewStyle)] { 9 | 10 | var styles: [(name: String, style: BLTNBackgroundViewStyle)] = [ 11 | ("None", .none), 12 | ("Dimmed", .dimmed) 13 | ] 14 | 15 | if #available(iOS 10, *) { 16 | styles.append(("Extra Light", .blurredExtraLight)) 17 | styles.append(("Light", .blurredLight)) 18 | styles.append(("Dark", .blurredDark)) 19 | styles.append(("Extra Dark", .blurred(style: UIBlurEffect.Style(rawValue: 3)!, isDark: true))) 20 | } 21 | 22 | return styles 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Example/Swift/Supporting Files/PermissionsManager.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | import CoreLocation 8 | 9 | /** 10 | * Requests permission for system features. 11 | */ 12 | 13 | class PermissionsManager { 14 | 15 | static let shared = PermissionsManager() 16 | 17 | let locationManager = CLLocationManager() 18 | 19 | func requestLocalNotifications() { 20 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in 21 | // no-op 22 | } 23 | } 24 | 25 | func requestWhenInUseLocation() { 26 | locationManager.requestWhenInUseAuthorization() 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Example/de.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Example/de.lproj/Main-ObjC.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIBarButtonItem"; title = "Show Intro"; ObjectID = "EpR-v7-nqd"; */ 3 | "EpR-v7-nqd.title" = "Show Intro"; 4 | 5 | /* Class = "UIBarButtonItem"; title = "%@STYLE%@"; ObjectID = "euD-bA-1s9"; */ 6 | "euD-bA-1s9.title" = "%@STYLE%@"; 7 | 8 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[0] = "Cats"; ObjectID = "nJY-e8-Bxo"; */ 9 | "nJY-e8-Bxo.segmentTitles[0]" = "Cats"; 10 | 11 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[1] = "Dogs"; ObjectID = "nJY-e8-Bxo"; */ 12 | "nJY-e8-Bxo.segmentTitles[1]" = "Dogs"; 13 | -------------------------------------------------------------------------------- /Example/de.lproj/Main-Swift.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIBarButtonItem"; title = "Show Intro"; ObjectID = "EpR-v7-nqd"; */ 3 | "EpR-v7-nqd.title" = "Show Intro"; 4 | 5 | /* Class = "UIBarButtonItem"; title = "%@STYLE%@"; ObjectID = "euD-bA-1s9"; */ 6 | "euD-bA-1s9.title" = "%@STYLE%@"; 7 | 8 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[0] = "Cats"; ObjectID = "nJY-e8-Bxo"; */ 9 | "nJY-e8-Bxo.segmentTitles[0]" = "Cats"; 10 | 11 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[1] = "Dogs"; ObjectID = "nJY-e8-Bxo"; */ 12 | "nJY-e8-Bxo.segmentTitles[1]" = "Dogs"; 13 | -------------------------------------------------------------------------------- /Example/fr.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Example/fr.lproj/Main-ObjC.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIBarButtonItem"; title = "Show Intro"; ObjectID = "EpR-v7-nqd"; */ 3 | "EpR-v7-nqd.title" = "Show Intro"; 4 | 5 | /* Class = "UIBarButtonItem"; title = "%@STYLE%@"; ObjectID = "euD-bA-1s9"; */ 6 | "euD-bA-1s9.title" = "%@STYLE%@"; 7 | 8 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[0] = "Cats"; ObjectID = "nJY-e8-Bxo"; */ 9 | "nJY-e8-Bxo.segmentTitles[0]" = "Cats"; 10 | 11 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[1] = "Dogs"; ObjectID = "nJY-e8-Bxo"; */ 12 | "nJY-e8-Bxo.segmentTitles[1]" = "Dogs"; 13 | -------------------------------------------------------------------------------- /Example/fr.lproj/Main-Swift.strings: -------------------------------------------------------------------------------- 1 | 2 | /* Class = "UIBarButtonItem"; title = "Show Intro"; ObjectID = "EpR-v7-nqd"; */ 3 | "EpR-v7-nqd.title" = "Show Intro"; 4 | 5 | /* Class = "UIBarButtonItem"; title = "%@STYLE%@"; ObjectID = "euD-bA-1s9"; */ 6 | "euD-bA-1s9.title" = "%@STYLE%@"; 7 | 8 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[0] = "Cats"; ObjectID = "nJY-e8-Bxo"; */ 9 | "nJY-e8-Bxo.segmentTitles[0]" = "Cats"; 10 | 11 | /* Class = "UISegmentedControl"; nJY-e8-Bxo.segmentTitles[1] = "Dogs"; ObjectID = "nJY-e8-Bxo"; */ 12 | "nJY-e8-Bxo.segmentTitles[1]" = "Dogs"; 13 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "jazzy" 5 | gem "cocoapods" 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.1) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.7.0) 11 | public_suffix (>= 2.0.2, < 5.0) 12 | algoliasearch (1.27.1) 13 | httpclient (~> 2.8, >= 2.8.3) 14 | json (>= 1.5.1) 15 | atomos (0.1.3) 16 | babosa (1.0.3) 17 | claide (1.0.3) 18 | cocoapods (1.8.1) 19 | activesupport (>= 4.0.2, < 5) 20 | claide (>= 1.0.2, < 2.0) 21 | cocoapods-core (= 1.8.1) 22 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 23 | cocoapods-downloader (>= 1.2.2, < 2.0) 24 | cocoapods-plugins (>= 1.0.0, < 2.0) 25 | cocoapods-search (>= 1.0.0, < 2.0) 26 | cocoapods-stats (>= 1.0.0, < 2.0) 27 | cocoapods-trunk (>= 1.4.0, < 2.0) 28 | cocoapods-try (>= 1.1.0, < 2.0) 29 | colored2 (~> 3.1) 30 | escape (~> 0.0.4) 31 | fourflusher (>= 2.3.0, < 3.0) 32 | gh_inspector (~> 1.0) 33 | molinillo (~> 0.6.6) 34 | nap (~> 1.0) 35 | ruby-macho (~> 1.4) 36 | xcodeproj (>= 1.11.1, < 2.0) 37 | cocoapods-core (1.8.1) 38 | activesupport (>= 4.0.2, < 6) 39 | algoliasearch (~> 1.0) 40 | concurrent-ruby (~> 1.1) 41 | fuzzy_match (~> 2.0.4) 42 | nap (~> 1.0) 43 | cocoapods-deintegrate (1.0.4) 44 | cocoapods-downloader (1.2.2) 45 | cocoapods-plugins (1.0.0) 46 | nap 47 | cocoapods-search (1.0.0) 48 | cocoapods-stats (1.1.0) 49 | cocoapods-trunk (1.4.1) 50 | nap (>= 0.8, < 2.0) 51 | netrc (~> 0.11) 52 | cocoapods-try (1.1.0) 53 | colored (1.2) 54 | colored2 (3.1.2) 55 | commander-fastlane (4.4.6) 56 | highline (~> 1.7.2) 57 | concurrent-ruby (1.1.5) 58 | declarative (0.0.10) 59 | declarative-option (0.1.0) 60 | digest-crc (0.4.1) 61 | domain_name (0.5.20190701) 62 | unf (>= 0.0.5, < 1.0.0) 63 | dotenv (2.7.5) 64 | emoji_regex (1.0.1) 65 | escape (0.0.4) 66 | excon (0.67.0) 67 | faraday (0.15.4) 68 | multipart-post (>= 1.2, < 3) 69 | faraday-cookie_jar (0.0.6) 70 | faraday (>= 0.7.4) 71 | http-cookie (~> 1.0.0) 72 | faraday_middleware (0.13.1) 73 | faraday (>= 0.7.4, < 1.0) 74 | fastimage (2.1.7) 75 | fastlane (2.133.0) 76 | CFPropertyList (>= 2.3, < 4.0.0) 77 | addressable (>= 2.3, < 3.0.0) 78 | babosa (>= 1.0.2, < 2.0.0) 79 | bundler (>= 1.12.0, < 3.0.0) 80 | colored 81 | commander-fastlane (>= 4.4.6, < 5.0.0) 82 | dotenv (>= 2.1.1, < 3.0.0) 83 | emoji_regex (>= 0.1, < 2.0) 84 | excon (>= 0.45.0, < 1.0.0) 85 | faraday (< 0.16.0) 86 | faraday-cookie_jar (~> 0.0.6) 87 | faraday_middleware (< 0.16.0) 88 | fastimage (>= 2.1.0, < 3.0.0) 89 | gh_inspector (>= 1.1.2, < 2.0.0) 90 | google-api-client (>= 0.21.2, < 0.24.0) 91 | google-cloud-storage (>= 1.15.0, < 2.0.0) 92 | highline (>= 1.7.2, < 2.0.0) 93 | json (< 3.0.0) 94 | jwt (~> 2.1.0) 95 | mini_magick (>= 4.9.4, < 5.0.0) 96 | multi_xml (~> 0.5) 97 | multipart-post (~> 2.0.0) 98 | plist (>= 3.1.0, < 4.0.0) 99 | public_suffix (~> 2.0.0) 100 | rubyzip (>= 1.3.0, < 2.0.0) 101 | security (= 0.1.3) 102 | simctl (~> 1.6.3) 103 | slack-notifier (>= 2.0.0, < 3.0.0) 104 | terminal-notifier (>= 2.0.0, < 3.0.0) 105 | terminal-table (>= 1.4.5, < 2.0.0) 106 | tty-screen (>= 0.6.3, < 1.0.0) 107 | tty-spinner (>= 0.8.0, < 1.0.0) 108 | word_wrap (~> 1.0.0) 109 | xcodeproj (>= 1.8.1, < 2.0.0) 110 | xcpretty (~> 0.3.0) 111 | xcpretty-travis-formatter (>= 0.0.3) 112 | ffi (1.11.1) 113 | fourflusher (2.3.1) 114 | fuzzy_match (2.0.4) 115 | gh_inspector (1.1.3) 116 | google-api-client (0.23.9) 117 | addressable (~> 2.5, >= 2.5.1) 118 | googleauth (>= 0.5, < 0.7.0) 119 | httpclient (>= 2.8.1, < 3.0) 120 | mime-types (~> 3.0) 121 | representable (~> 3.0) 122 | retriable (>= 2.0, < 4.0) 123 | signet (~> 0.9) 124 | google-cloud-core (1.3.1) 125 | google-cloud-env (~> 1.0) 126 | google-cloud-env (1.2.1) 127 | faraday (~> 0.11) 128 | google-cloud-storage (1.16.0) 129 | digest-crc (~> 0.4) 130 | google-api-client (~> 0.23) 131 | google-cloud-core (~> 1.2) 132 | googleauth (>= 0.6.2, < 0.10.0) 133 | googleauth (0.6.7) 134 | faraday (~> 0.12) 135 | jwt (>= 1.4, < 3.0) 136 | memoist (~> 0.16) 137 | multi_json (~> 1.11) 138 | os (>= 0.9, < 2.0) 139 | signet (~> 0.7) 140 | highline (1.7.10) 141 | http-cookie (1.0.3) 142 | domain_name (~> 0.5) 143 | httpclient (2.8.3) 144 | i18n (0.9.5) 145 | concurrent-ruby (~> 1.0) 146 | jazzy (0.11.2) 147 | cocoapods (~> 1.5) 148 | mustache (~> 1.1) 149 | open4 150 | redcarpet (~> 3.4) 151 | rouge (>= 2.0.6, < 4.0) 152 | sassc (~> 2.1) 153 | sqlite3 (~> 1.3) 154 | xcinvoke (~> 0.3.0) 155 | json (2.2.0) 156 | jwt (2.1.0) 157 | liferaft (0.0.6) 158 | memoist (0.16.0) 159 | mime-types (3.3) 160 | mime-types-data (~> 3.2015) 161 | mime-types-data (3.2019.0904) 162 | mini_magick (4.9.5) 163 | minitest (5.12.2) 164 | molinillo (0.6.6) 165 | multi_json (1.13.1) 166 | multi_xml (0.6.0) 167 | multipart-post (2.0.0) 168 | mustache (1.1.0) 169 | nanaimo (0.2.6) 170 | nap (1.1.0) 171 | naturally (2.2.0) 172 | netrc (0.11.0) 173 | open4 (1.3.4) 174 | os (1.0.1) 175 | plist (3.5.0) 176 | public_suffix (2.0.5) 177 | redcarpet (3.5.0) 178 | representable (3.0.4) 179 | declarative (< 0.1.0) 180 | declarative-option (< 0.2.0) 181 | uber (< 0.2.0) 182 | retriable (3.1.2) 183 | rouge (2.0.7) 184 | ruby-macho (1.4.0) 185 | rubyzip (1.3.0) 186 | sassc (2.2.1) 187 | ffi (~> 1.9) 188 | security (0.1.3) 189 | signet (0.11.0) 190 | addressable (~> 2.3) 191 | faraday (~> 0.9) 192 | jwt (>= 1.5, < 3.0) 193 | multi_json (~> 1.10) 194 | simctl (1.6.6) 195 | CFPropertyList 196 | naturally 197 | slack-notifier (2.3.2) 198 | sqlite3 (1.4.1) 199 | terminal-notifier (2.0.0) 200 | terminal-table (1.8.0) 201 | unicode-display_width (~> 1.1, >= 1.1.1) 202 | thread_safe (0.3.6) 203 | tty-cursor (0.7.0) 204 | tty-screen (0.7.0) 205 | tty-spinner (0.9.1) 206 | tty-cursor (~> 0.7) 207 | tzinfo (1.2.5) 208 | thread_safe (~> 0.1) 209 | uber (0.1.0) 210 | unf (0.1.4) 211 | unf_ext 212 | unf_ext (0.0.7.6) 213 | unicode-display_width (1.6.0) 214 | word_wrap (1.0.0) 215 | xcinvoke (0.3.0) 216 | liferaft (~> 0.0.6) 217 | xcodeproj (1.12.0) 218 | CFPropertyList (>= 2.3.3, < 4.0) 219 | atomos (~> 0.1.3) 220 | claide (>= 1.0.2, < 2.0) 221 | colored2 (~> 3.1) 222 | nanaimo (~> 0.2.6) 223 | xcpretty (0.3.0) 224 | rouge (~> 2.0.7) 225 | xcpretty-travis-formatter (1.0.0) 226 | xcpretty (~> 0.2, >= 0.0.7) 227 | 228 | PLATFORMS 229 | ruby 230 | 231 | DEPENDENCIES 232 | cocoapods 233 | fastlane 234 | jazzy 235 | 236 | BUNDLED WITH 237 | 1.16.6 238 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Alexis Aubry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "BLTNBoard", 6 | platforms: [.iOS(.v11)], 7 | products: [ 8 | .library(name: "BLTNBoard", targets: ["BLTNBoard"]), 9 | ], 10 | dependencies: [], 11 | targets: [ 12 | .target( 13 | name: "BLTNBoard", 14 | dependencies: [], 15 | path: "Sources" 16 | ), 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BulletinBoard 2 | 3 | [![Version](https://img.shields.io/cocoapods/v/BulletinBoard.svg?style=flat)](https://cocoapods.org/pods/BulletinBoard) 4 | [![License](https://img.shields.io/cocoapods/l/BulletinBoard.svg?style=flat)](https://cocoapods.org/pods/BulletinBoard) 5 | [![Platform](https://img.shields.io/cocoapods/p/BulletinBoard.svg?style=flat)](https://cocoapods.org/pods/BulletinBoard) 6 | [![Documentation](https://img.shields.io/badge/Documentation-available-blue.svg)](https://alexisakers.github.io/BulletinBoard) 7 | [![Contact: @_alexaubry](https://raw.githubusercontent.com/alexaubry/BulletinBoard/main/.assets/twitter_badge.svg?sanitize=true)](https://twitter.com/_alexaubry) 8 | 9 | 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. 10 | 11 | It has an interface similar to the cards displayed by iOS for AirPods, Apple TV/HomePod configuration and NFC tag scanning. It supports both the iPhone, iPhone X and the iPad. 12 | 13 | It has built-in support for accessibility features such as VoiceOver and Switch Control. 14 | 15 | Here are some screenshots showing what you can build with BulletinBoard: 16 | 17 | ![Demo Screenshots](https://raw.githubusercontent.com/alexaubry/BulletinBoard/main/.assets/demo_screenshots.png) 18 | 19 | ## Requirements 20 | 21 | - Xcode 11 and later 22 | - iOS 9 and later 23 | - Swift 5.1 and later (also works with Objective-C). 24 | 25 | ## Demo 26 | 27 | A demo project is included in the `BulletinBoard` workspace. It demonstrates how to: 28 | 29 | - integrate the library (setup, data flow) 30 | - create standard page cards 31 | - create custom page subclasses to add features 32 | - create custom cards from scratch 33 | 34 | Two demo targets are available: 35 | 36 | - `BB-Swift` (demo written in Swift) 37 | - `BB-ObjC` (demo written in Objective-C) 38 | 39 | Build and run the scheme for your favorite language to open the demo app. 40 | 41 | ## Installation 42 | 43 | ### Swift Package Manager 44 | 45 | To install BulletinBoard using the [Swift Package Manager](https://swift.org/package-manager/), add this dependency to your `Package.swift` file: 46 | 47 | ~~~swift 48 | .package(url: "https://github.com/alexaubry/BulletinBoard.git", from: "5.0.0") 49 | ~~~ 50 | 51 | ### CocoaPods 52 | 53 | To install BulletinBoard using [CocoaPods](https://cocoapods.org), add this line to your `Podfile`: 54 | 55 | ~~~ruby 56 | pod 'BulletinBoard' 57 | ~~~ 58 | 59 | ### Carthage 60 | 61 | To install BulletinBoard using [Carthage](https://github.com/Carthage/Carthage), add this line to your `Cartfile`: 62 | 63 | ~~~ 64 | github "alexaubry/BulletinBoard" 65 | ~~~ 66 | 67 | ## Documentation 68 | 69 | - The full library documentation is available [here](https://alexisakers.github.io/BulletinBoard). 70 | - To learn how to start using `BulletinBoard`, check out our [Getting Started](https://alexisakers.github.io/BulletinBoard/getting-started.html) guide. 71 | 72 | ## Contributing 73 | 74 | Thank you for your interest in the project! Contributions are welcome and appreciated. 75 | 76 | Make sure to read these guides before getting started: 77 | 78 | - [Code of Conduct](https://github.com/alexaubry/BulletinBoard/blob/master/CODE_OF_CONDUCT.md) 79 | - [Contribution Guidelines](https://github.com/alexaubry/BulletinBoard/blob/master/CONTRIBUTING.md) 80 | 81 | ## Author 82 | 83 | Written by Alexis Aubry. You can [find me on Twitter](https://twitter.com/_alexaubry). 84 | 85 | ## License 86 | 87 | BulletinBoard is available under the MIT license. See the [LICENSE](LICENSE) file for more info. 88 | -------------------------------------------------------------------------------- /Sources/Deprecations.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | @available(*, unavailable, renamed: "BLTNItem") 9 | @objc public protocol BulletinItem {} 10 | 11 | @available(*, unavailable, renamed: "BLTNItemManager") 12 | @objc public class BulletinManager: NSObject {} 13 | 14 | @available(*, unavailable, renamed: "BLTNActionItem") 15 | @objc public class ActionBulletinItem: NSObject {} 16 | 17 | @available(*, unavailable, renamed: "BLTNPageItem") 18 | @objc public class PageBulletinItem: NSObject {} 19 | 20 | @available(*, unavailable, message: "To specify the appearance, use BLTNItemAppearance. To create standard views, use BLTNInterfaceBuilder.") 21 | @objc public class BulletinInterfaceFactory: NSObject {} 22 | 23 | @available(*, unavailable, renamed: "BLTNSpacing") 24 | @objc public class BulletinPadding: NSObject {} 25 | 26 | @available(*, unavailable, renamed: "BLTNBackgroundViewStyle") 27 | @objc public class BulletinBackgroundViewStyle: NSObject {} 28 | 29 | @available(*, unavailable, renamed: "BLTNHighlightButtonWrapper") 30 | @objc public class HighlightButtonWrapper: UIView {} 31 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/BLTNBackgroundViewStyle.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * The types of background used to cover the content behind the bulletins. 10 | */ 11 | 12 | @objc public class BLTNBackgroundViewStyle: NSObject { 13 | public enum Style { 14 | case none 15 | case dimmed 16 | case blurred(style: UIBlurEffect.Style, isDark: Bool) 17 | 18 | public var isDark: Bool { 19 | switch self { 20 | case .none, .dimmed: return true 21 | case .blurred(_, let isDarkBlur): return isDarkBlur 22 | } 23 | } 24 | } 25 | 26 | public let rawValue: Style 27 | 28 | init(rawValue: Style) { 29 | self.rawValue = rawValue 30 | } 31 | 32 | @available(*, unavailable, message: "Use one of the presets to create a backrgound style object.") 33 | override init() { 34 | fatalError("BLTNBackgroundViewStyle.init is unavailable. Use one of the presets instead.") 35 | } 36 | } 37 | 38 | // MARK: - Presets 39 | 40 | extension BLTNBackgroundViewStyle { 41 | /// The background content is not covered. 42 | @objc public static let none = BLTNBackgroundViewStyle(rawValue: .none) 43 | 44 | /** 45 | * The background is covered with a semi-transparent view similar to the view displayed behind 46 | * UIKit alerts and action sheets. 47 | */ 48 | 49 | @objc public static let dimmed = BLTNBackgroundViewStyle(rawValue: .dimmed) 50 | 51 | /** 52 | * The background is blurred with the specified effect. 53 | * 54 | * Available on iOS 10.0 and later. 55 | * 56 | * - parameter style: The style of blur to use to cover the background. 57 | * - parameter isDark: Whether the blur effect is dark. 58 | */ 59 | 60 | @available(iOS 10, *) 61 | @objc public static func blurred(style: UIBlurEffect.Style, isDark: Bool) -> BLTNBackgroundViewStyle { 62 | return BLTNBackgroundViewStyle(rawValue: .blurred(style: style, isDark: isDark)) 63 | } 64 | 65 | /// The background blurred with a light style. 66 | @available(iOS 10, *) 67 | @objc public static let blurredLight: BLTNBackgroundViewStyle = .blurred(style: .light, isDark: false) 68 | 69 | /// The background blurred with an extra light style. 70 | @available(iOS 10, *) 71 | @objc public static let blurredExtraLight: BLTNBackgroundViewStyle = .blurred(style: .extraLight, isDark: false) 72 | 73 | /// The background blurred with a dark style. 74 | @available(iOS 10, *) 75 | @objc public static let blurredDark: BLTNBackgroundViewStyle = .blurred(style: .dark, isDark: true) 76 | } 77 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/BLTNContainerView.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A view that contains another view without intrinsic content size. 10 | * 11 | * The intrinsic content size is provided by this view, with the `contentSize` property. 12 | * 13 | * You should not add subviews directly. Instead, call `setChildView(childView:constraintsBuilder:)` 14 | * to specify the view that should be displayed and position it with Auto Layout. 15 | */ 16 | 17 | @objc public class BLTNContainerView: UIView { 18 | 19 | /// The size of the content displayed in this view. 20 | @objc public var contentSize: CGSize = .zero 21 | 22 | /** 23 | * Adds the child view and configures the constraints. 24 | * - parameter childView: The view to display inside the fixed-size container. 25 | * - parameter constraintsBuilder: The block of code to executed for adding constaints to position 26 | * the child view. 27 | */ 28 | 29 | @objc public func setChildView(_ childView: UIView, constraintsBuilder: @escaping (BLTNContainerView, UIView) -> Void) { 30 | currentChildView?.removeFromSuperview() 31 | currentChildView = childView 32 | addSubview(childView) 33 | childView.translatesAutoresizingMaskIntoConstraints = false 34 | constraintsBuilder(self, childView) 35 | } 36 | 37 | // MARK: - Utilties 38 | 39 | private var currentChildView: UIView? 40 | 41 | public override var intrinsicContentSize: CGSize { 42 | return contentSize 43 | } 44 | 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/BLTNHighlightButtonWrapper.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A view that wraps a HighlightButton. 10 | * 11 | * A wrapper is required to avoid alpha animation issues when unhighlighting the button and performing 12 | * a bulletin transition. 13 | */ 14 | 15 | @objc public class BLTNHighlightButtonWrapper: UIView { 16 | 17 | /// The underlying button. 18 | @objc public let button: UIButton 19 | 20 | public required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) is unavailable. Use init(button:) instead.") 22 | } 23 | 24 | init(button: HighlightButton) { 25 | 26 | self.button = button 27 | super.init(frame: .zero) 28 | 29 | addSubview(button) 30 | button.translatesAutoresizingMaskIntoConstraints = false 31 | button.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 32 | button.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true 33 | button.topAnchor.constraint(equalTo: topAnchor).isActive = true 34 | button.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 35 | 36 | } 37 | 38 | public override var intrinsicContentSize: CGSize { 39 | return button.intrinsicContentSize 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/BLTNItemAppearance.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * An object that defines the appearance of bulletin items. 10 | */ 11 | 12 | @objc public class BLTNItemAppearance: NSObject { 13 | 14 | // MARK: - Color Customization 15 | 16 | /// The tint color to apply to the action button (default `.link` on iOS 13 and `.blue` on older systems). 17 | @objc public var actionButtonColor: UIColor = { 18 | if #available(iOS 13.0, *) { 19 | return .link 20 | } else { 21 | return #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1) 22 | } 23 | }() 24 | 25 | /// The button image to apply to the action button 26 | @objc public var actionButtonImage: UIImage? 27 | 28 | /// The title color to apply to action button (default white). 29 | @objc public var actionButtonTitleColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) 30 | 31 | /// The border color to apply to action button. 32 | @objc public var actionButtonBorderColor: UIColor? = nil 33 | 34 | /// The border width to apply to action button. 35 | @objc public var actionButtonBorderWidth: CGFloat = 1.0 36 | 37 | /// The title color to apply to the alternative button (default `.link` on iOS 13 and `.blue` on older systems). 38 | @objc public var alternativeButtonTitleColor: UIColor = { 39 | if #available(iOS 13.0, *) { 40 | return .link 41 | } else { 42 | return #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1) 43 | } 44 | }() 45 | 46 | /// The border color to apply to the alternative button. 47 | @objc public var alternativeButtonBorderColor: UIColor? = nil 48 | 49 | /// The border width to apply to the alternative button. 50 | @objc public var alternativeButtonBorderWidth: CGFloat = 1.0 51 | 52 | /// The tint color to apply to the imageView (if image rendered in template mode, default `.link` on iOS 13 and `.blue` on older systems). 53 | @objc public var imageViewTintColor: UIColor = { 54 | if #available(iOS 13.0, *) { 55 | return .link 56 | } else { 57 | return #colorLiteral(red: 0, green: 0.4784313725, blue: 1, alpha: 1) 58 | } 59 | }() 60 | 61 | /// The color of title text labels (default `.secondaryLabel` on iOS 13 and light gray on older systems). 62 | @objc public var titleTextColor: UIColor = { 63 | if #available(iOS 13.0, *) { 64 | return .secondaryLabel 65 | } else { 66 | return #colorLiteral(red: 0.568627451, green: 0.5647058824, blue: 0.5725490196, alpha: 1) 67 | } 68 | }() 69 | 70 | /// The color of description text labels (default `.label` on iOS 13 and black on older systems). 71 | @objc public var descriptionTextColor: UIColor = { 72 | if #available(iOS 13.0, *) { 73 | return .label 74 | } else { 75 | return #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) 76 | } 77 | }() 78 | 79 | // MARK: - Corner Radius Customization 80 | 81 | /// The corner radius of the action button (default 12). 82 | @objc public var actionButtonCornerRadius: CGFloat = 12 83 | 84 | /// The corner radius of the alternative button (default 12). 85 | @objc public var alternativeButtonCornerRadius: CGFloat = 12 86 | 87 | // MARK: - Font Customization 88 | 89 | /// An optional custom font to use for the title label. Set this to nil to use the system font. 90 | @objc public var titleFontDescriptor: UIFontDescriptor? 91 | 92 | /// An optional custom font to use for the description label. Set this to nil to use the system font. 93 | @objc public var descriptionFontDescriptor: UIFontDescriptor? 94 | 95 | /// An optional custom font to use for the buttons. Set this to nil to use the system font. 96 | @objc public var buttonFontDescriptor: UIFontDescriptor? 97 | 98 | /** 99 | * Whether the description text should be displayed with a smaller font. 100 | * 101 | * You should set this to `true` if your text is long (more that two sentences). 102 | */ 103 | 104 | @objc public var shouldUseCompactDescriptionText: Bool = false 105 | 106 | 107 | // MARK: - Font Constants 108 | 109 | /// The font size of title elements (default 30). 110 | @objc public var titleFontSize: CGFloat = 30 111 | 112 | /// The font size of description labels (default 20). 113 | @objc public var descriptionFontSize: CGFloat = 20 114 | 115 | /// The font size of compact description labels (default 15). 116 | @objc public var compactDescriptionFontSize: CGFloat = 15 117 | 118 | /// The font size of action buttons (default 17). 119 | @objc public var actionButtonFontSize: CGFloat = 17 120 | 121 | /// The font size of alternative buttons (default 15). 122 | @objc public var alternativeButtonFontSize: CGFloat = 15 123 | 124 | } 125 | 126 | // MARK: - Font Factories 127 | 128 | extension BLTNItemAppearance { 129 | 130 | /** 131 | * Creates the font for title labels. 132 | */ 133 | 134 | @objc public func makeTitleFont() -> UIFont { 135 | 136 | if let titleFontDescriptor = self.titleFontDescriptor { 137 | return UIFont(descriptor: titleFontDescriptor, size: titleFontSize) 138 | } else { 139 | return UIFont.systemFont(ofSize: titleFontSize, weight: .medium) 140 | } 141 | 142 | } 143 | 144 | /** 145 | * Creates the font for description labels. 146 | */ 147 | 148 | @objc public func makeDescriptionFont() -> UIFont { 149 | 150 | let size = shouldUseCompactDescriptionText ? compactDescriptionFontSize : descriptionFontSize 151 | 152 | if let descriptionFontDescriptor = self.descriptionFontDescriptor { 153 | return UIFont(descriptor: descriptionFontDescriptor, size: size) 154 | } else { 155 | return UIFont.systemFont(ofSize: size) 156 | } 157 | 158 | } 159 | 160 | /** 161 | * Creates the font for action buttons. 162 | */ 163 | 164 | @objc public func makeActionButtonFont() -> UIFont { 165 | 166 | if let buttonFontDescriptor = self.buttonFontDescriptor { 167 | return UIFont(descriptor: buttonFontDescriptor, size: actionButtonFontSize) 168 | } else { 169 | return UIFont.systemFont(ofSize: actionButtonFontSize, weight: .semibold) 170 | } 171 | 172 | } 173 | 174 | /** 175 | * Creates the font for alternative buttons. 176 | */ 177 | 178 | @objc public func makeAlternativeButtonFont() -> UIFont { 179 | 180 | if let buttonFontDescriptor = self.buttonFontDescriptor { 181 | return UIFont(descriptor: buttonFontDescriptor, size: alternativeButtonFontSize) 182 | } else { 183 | return UIFont.systemFont(ofSize: alternativeButtonFontSize, weight: .semibold) 184 | } 185 | 186 | } 187 | 188 | } 189 | 190 | // MARK: - Status Bar 191 | 192 | /** 193 | * Styles of status bar to use with bulletin items. 194 | */ 195 | 196 | @objc public enum BLTNStatusBarAppearance: Int { 197 | 198 | /// The status bar is hidden. 199 | case hidden 200 | 201 | /// The color of the status bar is determined automatically. This is the default style. 202 | case automatic 203 | 204 | /// Style to use with dark backgrounds. 205 | case lightContent 206 | 207 | /// Style to use with light backgrounds. 208 | case darkContent 209 | 210 | } 211 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/BLTNSpacing.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * Represents a spacing value. 10 | */ 11 | 12 | @objc public class BLTNSpacing: NSObject { 13 | public let rawValue: CGFloat 14 | 15 | init(rawValue: CGFloat) { 16 | self.rawValue = rawValue 17 | } 18 | 19 | /// A custom spacing. 20 | /// - parameter value: The spacing to apply. 21 | @objc public class func custom(_ value: CGFloat) -> BLTNSpacing { 22 | return BLTNSpacing(rawValue: value) 23 | } 24 | 25 | /// No spacing is applied. (value: 0) 26 | /// - note: If you use this spacing, corner radii will be ignored. 27 | @objc public class var none: BLTNSpacing { 28 | return BLTNSpacing(rawValue: 0) 29 | } 30 | 31 | /// A compact spacing. (value: 6) 32 | @objc public class var compact: BLTNSpacing { 33 | return BLTNSpacing(rawValue: 6) 34 | } 35 | 36 | /// The standard spacing. (value: 12) 37 | @objc public class var regular: BLTNSpacing { 38 | return BLTNSpacing(rawValue: 12) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/BLTNTitleLabelContainer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A view that contains a title label. 10 | */ 11 | 12 | @objc public class BLTNTitleLabelContainer: UIView { 13 | 14 | /// The label contained in the view. 15 | @objc public let label: UILabel 16 | 17 | // MARK: - Initialization 18 | 19 | @objc init(label: UILabel, horizontalInset: CGFloat) { 20 | self.label = label 21 | super.init(frame: .zero) 22 | configureSubviews(horizontalInset: horizontalInset) 23 | } 24 | 25 | required public init?(coder aDecoder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | private func configureSubviews(horizontalInset: CGFloat) { 30 | 31 | addSubview(label) 32 | label.translatesAutoresizingMaskIntoConstraints = false 33 | 34 | label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalInset).isActive = true 35 | label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalInset).isActive = true 36 | label.topAnchor.constraint(equalTo: topAnchor).isActive = true 37 | label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 38 | 39 | } 40 | 41 | public override var intrinsicContentSize: CGSize { 42 | return label.intrinsicContentSize 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/BLTNViewPosition.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import Foundation 7 | 8 | /** 9 | * Describes the position of a view inside of its parent container. 10 | */ 11 | 12 | @objc public enum BLTNViewPosition: Int { 13 | 14 | /// The view is centered in its parent container. 15 | case centered 16 | 17 | /// The view is pinned to the four edges of its parent container. 18 | case pinnedToEdges 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/HighlightButton.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A button that provides a visual feedback when the user interacts with it. 10 | * 11 | * This style of button works best with a solid background color. Use the `setBackgroundColor` 12 | * function on `UIButton` to set one. 13 | */ 14 | 15 | class HighlightButton: UIButton { 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | configureHighlighting() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | super.init(coder: aDecoder) 23 | configureHighlighting() 24 | } 25 | 26 | private func configureHighlighting() { 27 | addTarget(self, action: #selector(highlight), for: [.touchUpInside, .touchDragEnter]) 28 | addTarget(self, action: #selector(unhighlight), for: [.touchUpInside, .touchDragExit]) 29 | } 30 | 31 | @objc private func highlight() { 32 | let animations = { 33 | self.alpha = 0.5 34 | } 35 | 36 | UIView.transition(with: self, duration: 0.1, animations: animations) 37 | } 38 | 39 | @objc private func unhighlight() { 40 | let animations = { 41 | self.alpha = 1 42 | } 43 | 44 | UIView.transition(with: self, duration: 0.1, animations: animations) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/InterfaceBuilder/UIButton+BackgroundColor.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | extension UIButton { 9 | 10 | /** 11 | * Sets a solid background color for the button. 12 | */ 13 | 14 | func setBackgroundColor(_ color: UIColor, forState controlState: UIControl.State) { 15 | 16 | UIGraphicsBeginImageContext(CGSize(width: 1, height: 1)) 17 | UIGraphicsGetCurrentContext()?.setFillColor(color.cgColor) 18 | UIGraphicsGetCurrentContext()?.fill(CGRect(x: 0, y: 0, width: 1, height: 1)) 19 | let colorImage = UIGraphicsGetImageFromCurrentImageContext() 20 | UIGraphicsEndImageContext() 21 | setBackgroundImage(colorImage, for: controlState) 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Models/BLTNItem.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /** 4 | * An item that can be displayed inside a bulletin card. 5 | */ 6 | 7 | @objc open class BLTNItem: NSObject { 8 | 9 | // MARK: - Configuration 10 | 11 | /** 12 | * The current object managing the item. 13 | * 14 | * This property is set when the item is currently being displayed. It will be set to `nil` when 15 | * the item is removed from view. 16 | * 17 | * When implementing `BLTNItem`, you should mark this property `weak` to avoid retain cycles. 18 | */ 19 | 20 | @objc public internal(set) weak var manager: BLTNItemManager? 21 | 22 | /** 23 | * Whether the page can be dismissed. 24 | * 25 | * The default value is `true`, which means the user will be able to dismiss the bulletin by tapping outside 26 | * of the card or by swiping down. 27 | * 28 | * You should set it to `true` for the last item you want to display, or for items that start an optional flow 29 | * (ex: a purchase). 30 | */ 31 | 32 | @objc open var isDismissable: Bool = true 33 | 34 | /** 35 | * Whether the page can be dismissed with a close button. 36 | * 37 | * The default value is `true`. The user will be able to dismiss the bulletin by tapping on a button 38 | * in the corner of the screen. 39 | * 40 | * You should set it to `false` if the interface of the bulletin already has buttons to dismiss the item, 41 | * such as an action button. 42 | */ 43 | 44 | @objc open var requiresCloseButton: Bool = true 45 | 46 | /** 47 | * Whether the card should start with an activity indicator. 48 | * 49 | * Set this value to `false` to display the elements right away. If you set it to `true`, 50 | * you'll need to call `manager?.hideActivityIndicator()` to show the UI. 51 | */ 52 | 53 | @objc open var shouldStartWithActivityIndicator: Bool = false 54 | 55 | /** 56 | * Whether the item should move with the keyboard. 57 | * 58 | * You must set it to `true` if the item displays a text field. You can set it to `false` if you 59 | * don't want the bulletin to move when system alerts containing a text field (ex: iTunes login) 60 | * are displayed. 61 | */ 62 | 63 | @objc open var shouldRespondToKeyboardChanges: Bool = true 64 | 65 | /** 66 | * The item to display after this one. 67 | * 68 | * If you set this value, you'll be able to call `manager?.displayNextItem()` to push the next item to 69 | * the stack. 70 | */ 71 | 72 | @objc(nextItem) open var next: BLTNItem? 73 | 74 | // MARK: - Event Handlers 75 | 76 | /** 77 | * The block of code to execute when the bulletin item is presented. This is called after the 78 | * bulletin is moved onto the view. 79 | * 80 | * - parameter item: The item that is being presented. 81 | */ 82 | 83 | @objc open var presentationHandler: ((BLTNItem) -> Void)? 84 | 85 | /** 86 | * The block of code to execute when the bulletin item is dismissed. This is called when the bulletin 87 | * is moved out of view. 88 | * 89 | * You can leave it `nil` if `isDismissable` is set to false. 90 | */ 91 | 92 | @objc open var dismissalHandler: ((BLTNItem) -> Void)? 93 | 94 | // MARK: - Interface 95 | 96 | /** 97 | * Creates the list of views to display inside the bulletin card. 98 | * 99 | * The views will be arranged vertically, in the order they are stored in the return array. 100 | */ 101 | 102 | open func makeArrangedSubviews() -> [UIView] { 103 | return [] 104 | } 105 | 106 | /** 107 | * Called by the manager when the item was added to the bulletin. 108 | * 109 | * Use this function to configure your managed views, and allocate any resources required 110 | * for this item. 111 | */ 112 | 113 | open func setUp() { 114 | // no-op 115 | } 116 | 117 | /** 118 | * Called by the manager when the item was removed from the bulletin. 119 | * 120 | * Use this function to remove any button target or gesture recognizers from your managed views, and 121 | * deallocate any resources created for this item that are no longer needed. 122 | */ 123 | 124 | open func tearDown() { 125 | // no-op 126 | } 127 | 128 | /** 129 | * Called by the manager when bulletin item is about to be pushed onto the view. 130 | */ 131 | 132 | open func willDisplay() { 133 | // no-op 134 | } 135 | 136 | /** 137 | * Called by the manager when bulletin item is pushed onto the view. 138 | */ 139 | 140 | open func onDisplay() { 141 | presentationHandler?(self) 142 | } 143 | 144 | /** 145 | * Called by the manager when bulletin item is dismissed. This is called after the bulletin 146 | * is moved out of view. 147 | */ 148 | 149 | open func onDismiss() { 150 | dismissalHandler?(self) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Sources/Support/Animations/AnimationChain.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import Foundation 7 | import UIKit 8 | 9 | // MARK: AnimationChain 10 | 11 | /** 12 | * A sequence of animations where animations are executed the one after the other. 13 | * 14 | * Animations are represented by `AnimationPhase` objects, that contain the code of the animation, 15 | * its duration relative to the chain duration, their curve and their individual completion handlers. 16 | */ 17 | 18 | public class AnimationChain { 19 | 20 | /// The total duration of the animation chain. 21 | public let duration: TimeInterval 22 | 23 | /// The initial delay before the animation chain starts. 24 | public var initialDelay: TimeInterval = 0 25 | 26 | /// The code to execute after animation chain is executed. 27 | public var completionHandler: () -> Void 28 | 29 | /// Whether the chain is being run. 30 | public private(set) var isRunning: Bool = false 31 | 32 | // MARK: Initialization 33 | 34 | private var animations: [AnimationPhase] = [] 35 | private var didFinishFirstAnimation: Bool = false 36 | 37 | /** 38 | * Creates an animation chain with the specified duration. 39 | */ 40 | 41 | public init(duration: TimeInterval) { 42 | self.duration = duration 43 | self.completionHandler = {} 44 | } 45 | 46 | // MARK: - Interacting with the Chain 47 | 48 | /** 49 | * Add an animation at the end of the chain. 50 | * 51 | * You cannot add animations if the chain is running. 52 | * 53 | * - parameter animation: The animation phase to add. 54 | */ 55 | 56 | public func add(_ animation: AnimationPhase) { 57 | precondition(!isRunning, "Cannot add an animation to the chain because it is already performing.") 58 | animations.append(animation) 59 | } 60 | 61 | /** 62 | * Starts the animation chain. 63 | */ 64 | 65 | public func start() { 66 | 67 | precondition(!isRunning, "Animation chain already running.") 68 | 69 | isRunning = true 70 | performNextAnimation() 71 | 72 | } 73 | 74 | private func performNextAnimation() { 75 | 76 | guard animations.count > 0 else { 77 | completeGroup() 78 | return 79 | } 80 | 81 | let animation = animations.removeFirst() 82 | 83 | let duration = animation.relativeDuration * self.duration 84 | let options = UIView.AnimationOptions(rawValue: UInt(animation.curve.rawValue << 16)) 85 | let delay: TimeInterval = didFinishFirstAnimation ? 0 : initialDelay 86 | 87 | UIView.animate(withDuration: duration, delay: delay, options: options, animations: animation.block) { _ in 88 | 89 | self.didFinishFirstAnimation = true 90 | 91 | animation.completionHandler() 92 | self.performNextAnimation() 93 | 94 | } 95 | 96 | } 97 | 98 | private func completeGroup() { 99 | isRunning = false 100 | completionHandler() 101 | } 102 | 103 | } 104 | 105 | // MARK: - AnimationPhase 106 | 107 | /** 108 | * A member of an `AnimationChain`, representing a single animation. 109 | * 110 | * Set the `block` property to a block containing the animations. Set the `completionHandler` with 111 | * a block to execute at the end of the animation. The default values do nothing. 112 | */ 113 | 114 | public class AnimationPhase { 115 | 116 | /** 117 | * The duration of the animation, relative to the total duration of the chain. 118 | * 119 | * Must be between 0 and 1. 120 | */ 121 | 122 | public let relativeDuration: TimeInterval 123 | 124 | /** 125 | * The animation curve. 126 | */ 127 | 128 | public let curve: UIView.AnimationCurve 129 | 130 | /** 131 | * The animation code. 132 | */ 133 | 134 | public var block: () -> Void 135 | 136 | /** 137 | * A block to execute at the end of the animation. 138 | */ 139 | 140 | public var completionHandler: () -> Void 141 | 142 | // MARK: Initialization 143 | 144 | /** 145 | * Creates an animtion phase object. 146 | * 147 | * - parameter relativeDuration: The duration of the animation, as a fraction of the total chain 148 | * duration. Must be between 0 and 1. 149 | * - parameter curve: The animation curve 150 | */ 151 | 152 | public init(relativeDuration: TimeInterval, curve: UIView.AnimationCurve) { 153 | 154 | self.relativeDuration = relativeDuration 155 | self.curve = curve 156 | 157 | self.block = {} 158 | self.completionHandler = {} 159 | 160 | } 161 | 162 | } 163 | 164 | -------------------------------------------------------------------------------- /Sources/Support/Animations/BulletinDismissAnimationController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | /** 4 | * The animation controller for bulletin dismissal. 5 | * 6 | * It moves the card out of the screen, fades out the background view and removes it from the hierarchy 7 | * on completion. 8 | */ 9 | 10 | class BulletinDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning { 11 | 12 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 13 | return 0.3 14 | } 15 | 16 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 17 | 18 | guard let fromVC = transitionContext.viewController(forKey: .from) as? BulletinViewController else { 19 | transitionContext.completeTransition(false) 20 | return 21 | } 22 | 23 | let rootView = fromVC.view! 24 | let contentView = fromVC.contentView 25 | let backgroundView = fromVC.backgroundView! 26 | let activityIndicatorView = fromVC.activityIndicator 27 | let snapshotActivityIndicator = ActivityIndicator() 28 | snapshotActivityIndicator.startAnimating() 29 | 30 | // Take Snapshot 31 | 32 | guard let snapshot = contentView.snapshotView(afterScreenUpdates: true) else { 33 | transitionContext.completeTransition(false) 34 | return 35 | } 36 | 37 | snapshotActivityIndicator.translatesAutoresizingMaskIntoConstraints = false 38 | 39 | snapshot.addSubview(snapshotActivityIndicator) 40 | snapshotActivityIndicator.topAnchor.constraint(equalTo: snapshot.topAnchor).isActive = true 41 | snapshotActivityIndicator.leftAnchor.constraint(equalTo: snapshot.leftAnchor).isActive = true 42 | snapshotActivityIndicator.rightAnchor.constraint(equalTo: snapshot.rightAnchor).isActive = true 43 | snapshotActivityIndicator.bottomAnchor.constraint(equalTo: snapshot.bottomAnchor).isActive = true 44 | 45 | if #available(iOS 13.0, *) { 46 | snapshotActivityIndicator.style = UIActivityIndicatorView.Style.large 47 | } else { 48 | snapshotActivityIndicator.style = .whiteLarge 49 | } 50 | snapshotActivityIndicator.color = .black 51 | snapshotActivityIndicator.isUserInteractionEnabled = false 52 | 53 | snapshotActivityIndicator.alpha = activityIndicatorView.alpha 54 | 55 | rootView.insertSubview(snapshot, aboveSubview: contentView) 56 | snapshot.frame = contentView.frame 57 | contentView.isHidden = true 58 | activityIndicatorView.isHidden = true 59 | 60 | fromVC.prepareForDismissal(displaying: snapshot) 61 | 62 | // Animate dismissal 63 | 64 | let duration = transitionDuration(using: transitionContext) 65 | let options = UIView.AnimationOptions(rawValue: 6 << 16) 66 | 67 | let animations = { 68 | snapshot.frame.origin.y = rootView.frame.maxY + 12 69 | backgroundView.hide() 70 | } 71 | 72 | UIView.animate(withDuration: duration, delay: 0, options: options, animations: animations) { finished in 73 | 74 | let isCancelled = transitionContext.transitionWasCancelled 75 | 76 | if !isCancelled { 77 | fromVC.view.removeFromSuperview() 78 | } else { 79 | contentView.isHidden = false 80 | activityIndicatorView.isHidden = false 81 | snapshot.removeFromSuperview() 82 | } 83 | 84 | transitionContext.completeTransition(!isCancelled) 85 | 86 | } 87 | 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /Sources/Support/Animations/BulletinPresentationAnimationController.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * The animation controller for bulletin presentation. 10 | * 11 | * It moves the card on screen, creates and fades in the background view. 12 | */ 13 | 14 | class BulletinPresentationAnimationController: NSObject, UIViewControllerAnimatedTransitioning { 15 | 16 | let style: BLTNBackgroundViewStyle 17 | 18 | init(style: BLTNBackgroundViewStyle) { 19 | self.style = style 20 | } 21 | 22 | // MARK: - Transition 23 | 24 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 25 | return 0.3 26 | } 27 | 28 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 29 | 30 | guard let toVC = transitionContext.viewController(forKey: .to) as? BulletinViewController else { 31 | return 32 | } 33 | 34 | let containerView = transitionContext.containerView 35 | 36 | // Fix the frame (Needed for iPad app running in split view) 37 | // (Convert the "from" view's frame coordinates to the container view's coordinate system) 38 | if let fromView = transitionContext.viewController(forKey: .from)?.view { 39 | let fromFrame = containerView.convert(fromView.frame, from: fromView) 40 | toVC.view.frame = fromFrame 41 | } 42 | 43 | let rootView = toVC.view! 44 | let contentView = toVC.contentView 45 | let backgroundView = toVC.backgroundView! 46 | 47 | // Add root view 48 | 49 | containerView.addSubview(rootView) 50 | 51 | // Prepare background view 52 | 53 | rootView.insertSubview(backgroundView, at: 0) 54 | backgroundView.leadingAnchor.constraint(equalTo: rootView.leadingAnchor).isActive = true 55 | backgroundView.trailingAnchor.constraint(equalTo: rootView.trailingAnchor).isActive = true 56 | backgroundView.topAnchor.constraint(equalTo: rootView.topAnchor).isActive = true 57 | backgroundView.bottomAnchor.constraint(equalTo: rootView.bottomAnchor).isActive = true 58 | 59 | rootView.setNeedsLayout() 60 | contentView.setNeedsLayout() 61 | 62 | rootView.layoutIfNeeded() 63 | contentView.layoutIfNeeded() 64 | backgroundView.layoutIfNeeded() 65 | 66 | // Animate presentation 67 | 68 | let duration = transitionDuration(using: transitionContext) 69 | let options = UIView.AnimationOptions(rawValue: 7 << 16) 70 | 71 | let animations = { 72 | toVC.moveIntoPlace() 73 | backgroundView.show() 74 | } 75 | 76 | UIView.animate(withDuration: duration, delay: 0, options: options, animations: animations) { _ in 77 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 78 | } 79 | 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Sources/Support/BLTNBoardSwiftSupport.h: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | // Workaround needed to allow static library usage through Cocoapods. 7 | // https://github.com/CocoaPods/CocoaPods/issues/7594 8 | // https://github.com/mxcl/PromiseKit/issues/825 9 | #if __has_include("BLTNBoard-Swift.h") 10 | #import "BLTNBoard-Swift.h" 11 | #else 12 | #import 13 | #endif 14 | -------------------------------------------------------------------------------- /Sources/Support/Helpers/BLTNItemManager+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BLTNItemManager+Helpers.swift 3 | // BLTNBoard 4 | // 5 | // Created by Alexis Aubry on 6/1/20. 6 | // Copyright © 2020 Bulletin. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension BLTNItemManager { 12 | public func displayActivityIndicator() { 13 | displayActivityIndicator(color: nil) 14 | } 15 | 16 | public func present(_ viewController: UIViewController, animated: Bool) -> Void { 17 | present(viewController, animated: animated, completion: nil) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Support/Helpers/UIColor+Luminance.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | extension UIColor { 9 | 10 | var luminance: CGFloat { 11 | 12 | var red: CGFloat = 0 13 | var green: CGFloat = 0 14 | var blue: CGFloat = 0 15 | 16 | getRed(&red, green: &green, blue: &blue, alpha: nil) 17 | return 0.2126 * red + 0.7152 * green + 0.0722 * blue 18 | 19 | } 20 | 21 | var needsDarkText: Bool { 22 | return luminance > sqrt(1.05 * 0.05) - 0.05 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Sources/Support/Views/Internal/ActivityIndicator.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A view that contains an activity indicator. The indicator is centered inside the view. 10 | */ 11 | 12 | class ActivityIndicator: UIView { 13 | 14 | private let activityIndicatorView = UIActivityIndicatorView() 15 | 16 | // MARK: - Lifecycle 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | initialize() 21 | } 22 | 23 | required init?(coder aDecoder: NSCoder) { 24 | super.init(coder: aDecoder) 25 | initialize() 26 | } 27 | 28 | private func initialize() { 29 | 30 | activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false 31 | addSubview(activityIndicatorView) 32 | 33 | activityIndicatorView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 34 | activityIndicatorView.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 35 | 36 | } 37 | 38 | // MARK: - Activity Indicator 39 | 40 | /// Starts the animation of the activity indicator. 41 | func startAnimating() { 42 | activityIndicatorView.startAnimating() 43 | } 44 | 45 | /// Stops the animation of the activity indicator. 46 | func stopAnimating() { 47 | activityIndicatorView.stopAnimating() 48 | } 49 | 50 | /// The color of the activity indicator. 51 | var color: UIColor? { 52 | get { 53 | return activityIndicatorView.color 54 | } 55 | set { 56 | activityIndicatorView.color = newValue 57 | } 58 | } 59 | 60 | /// The style of the activity indicator. 61 | var style: UIActivityIndicatorView.Style { 62 | get { 63 | return activityIndicatorView.style 64 | } 65 | set { 66 | activityIndicatorView.style = newValue 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Support/Views/Internal/BulletinBackgroundView.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * The view to display behind the bulletin. 10 | */ 11 | 12 | class BulletinBackgroundView: UIView { 13 | 14 | let style: BLTNBackgroundViewStyle 15 | 16 | // MARK: - Content View 17 | 18 | enum ContentView { 19 | 20 | case dim(UIView, CGFloat) 21 | case blur(UIVisualEffectView, UIBlurEffect) 22 | 23 | var instance: UIView { 24 | switch self { 25 | case .dim(let dimmingView, _): 26 | return dimmingView 27 | case .blur(let blurView, _): 28 | return blurView 29 | } 30 | } 31 | 32 | } 33 | 34 | private(set) var contentView: ContentView! 35 | 36 | // MARK: - Initialization 37 | 38 | init(style: BLTNBackgroundViewStyle) { 39 | self.style = style 40 | super.init(frame: .zero) 41 | initialize() 42 | } 43 | 44 | override init(frame: CGRect) { 45 | style = .dimmed 46 | super.init(frame: frame) 47 | initialize() 48 | } 49 | 50 | required init?(coder aDecoder: NSCoder) { 51 | style = .dimmed 52 | super.init(coder: aDecoder) 53 | initialize() 54 | } 55 | 56 | private func initialize() { 57 | 58 | translatesAutoresizingMaskIntoConstraints = false 59 | 60 | func makeDimmingView() -> UIView { 61 | 62 | let dimmingView = UIView() 63 | dimmingView.alpha = 0.0 64 | dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) 65 | dimmingView.translatesAutoresizingMaskIntoConstraints = false 66 | 67 | return dimmingView 68 | 69 | } 70 | 71 | switch style.rawValue { 72 | case .none: 73 | 74 | let dimmingView = makeDimmingView() 75 | 76 | addSubview(dimmingView) 77 | contentView = .dim(dimmingView, 0.0) 78 | 79 | case .dimmed: 80 | 81 | let dimmingView = makeDimmingView() 82 | 83 | addSubview(dimmingView) 84 | contentView = .dim(dimmingView, 1.0) 85 | 86 | case .blurred(let blurredBackground, _): 87 | 88 | let blurEffect = UIBlurEffect(style: blurredBackground) 89 | let blurEffectView = UIVisualEffectView(effect: nil) 90 | blurEffectView.translatesAutoresizingMaskIntoConstraints = false 91 | 92 | addSubview(blurEffectView) 93 | contentView = .blur(blurEffectView, blurEffect) 94 | 95 | } 96 | 97 | let contentViewInstance = contentView.instance 98 | 99 | contentViewInstance.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true 100 | contentViewInstance.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true 101 | contentViewInstance.topAnchor.constraint(equalTo: topAnchor).isActive = true 102 | contentViewInstance.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 103 | 104 | } 105 | 106 | // MARK: - Interactions 107 | 108 | /// Shows the background view. Animatable. 109 | func show() { 110 | 111 | switch contentView! { 112 | case .dim(let dimmingView, let maxAlpha): 113 | dimmingView.alpha = maxAlpha 114 | 115 | case .blur(let blurView, let blurEffect): 116 | blurView.effect = blurEffect 117 | } 118 | 119 | } 120 | 121 | /// Hides the background view. Animatable. 122 | func hide() { 123 | 124 | switch contentView! { 125 | case .dim(let dimmingView, _): 126 | dimmingView.alpha = 0 127 | 128 | case .blur(let blurView, _): 129 | blurView.effect = nil 130 | } 131 | 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /Sources/Support/Views/Internal/BulletinCloseButton.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A button to close the bulletin. 10 | */ 11 | 12 | class BulletinCloseButton: UIControl { 13 | private let backgroundContainer = UIView() 14 | private let closeGlyph = UIImageView() 15 | 16 | // MARK: - Initialization 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | configureSubviews() 21 | configureConstraints() 22 | configureHighlighting() 23 | } 24 | 25 | required init?(coder aDecoder: NSCoder) { 26 | super.init(coder: aDecoder) 27 | configureSubviews() 28 | configureConstraints() 29 | configureHighlighting() 30 | } 31 | 32 | private func configureSubviews() { 33 | 34 | // Content 35 | 36 | isAccessibilityElement = true 37 | accessibilityLabel = Bundle.UIKitCore.localizedString(forKey: "Close", value: "Close", table: nil) 38 | 39 | // Layout 40 | addSubview(backgroundContainer) 41 | addSubview(closeGlyph) 42 | 43 | backgroundContainer.layer.cornerRadius = 14 44 | 45 | closeGlyph.image = UIImage.closeButton.withRenderingMode(.alwaysTemplate) 46 | closeGlyph.contentMode = .scaleAspectFit 47 | closeGlyph.clipsToBounds = true 48 | 49 | backgroundContainer.isUserInteractionEnabled = false 50 | closeGlyph.isUserInteractionEnabled = false 51 | 52 | } 53 | 54 | private func configureConstraints() { 55 | 56 | backgroundContainer.translatesAutoresizingMaskIntoConstraints = false 57 | closeGlyph.translatesAutoresizingMaskIntoConstraints = false 58 | 59 | backgroundContainer.widthAnchor.constraint(equalToConstant: 28).isActive = true 60 | backgroundContainer.heightAnchor.constraint(equalToConstant: 28).isActive = true 61 | backgroundContainer.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 62 | backgroundContainer.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 63 | 64 | closeGlyph.widthAnchor.constraint(equalToConstant: 12).isActive = true 65 | closeGlyph.heightAnchor.constraint(equalToConstant: 12).isActive = true 66 | closeGlyph.centerXAnchor.constraint(equalTo: backgroundContainer.centerXAnchor).isActive = true 67 | closeGlyph.centerYAnchor.constraint(equalTo: backgroundContainer.centerYAnchor).isActive = true 68 | 69 | } 70 | 71 | // MARK: - Customization 72 | 73 | func updateColors(isDarkBackground: Bool) { 74 | if isDarkBackground { 75 | backgroundContainer.backgroundColor = #colorLiteral(red: 0.9529411765, green: 0.9607843137, blue: 0.9607843137, alpha: 1) 76 | closeGlyph.tintColor = #colorLiteral(red: 0.3764705882, green: 0.3921568627, blue: 0.431372549, alpha: 1) 77 | } else { 78 | backgroundContainer.backgroundColor = #colorLiteral(red: 0.3764705882, green: 0.3921568627, blue: 0.431372549, alpha: 1) 79 | closeGlyph.tintColor = #colorLiteral(red: 0.9529411765, green: 0.9607843137, blue: 0.9607843137, alpha: 1) 80 | } 81 | } 82 | 83 | // MARK: - Highlighting 84 | 85 | private func configureHighlighting() { 86 | addTarget(self, action: #selector(highlight), for: [.touchUpInside, .touchDragEnter]) 87 | addTarget(self, action: #selector(unhighlight), for: [.touchUpInside, .touchDragExit]) 88 | } 89 | 90 | @objc private func highlight() { 91 | let animations = { 92 | self.alpha = 0.5 93 | } 94 | 95 | UIView.transition(with: self, duration: 0.1, animations: animations) 96 | } 97 | 98 | @objc func unhighlight() { 99 | let animations = { 100 | self.alpha = 1 101 | } 102 | 103 | UIView.transition(with: self, duration: 0.1, animations: animations) 104 | } 105 | } 106 | 107 | extension Bundle { 108 | fileprivate static var UIKitCore: Bundle { 109 | if #available(iOS 12, *) { 110 | return Bundle(identifier: "com.apple.UIKitCore")! 111 | } else { 112 | return Bundle(for: UIApplication.self) 113 | } 114 | } 115 | } 116 | 117 | extension UIImage { 118 | fileprivate static var closeButton: UIImage { 119 | let shape = UIBezierPath() 120 | shape.move(to: CGPoint(x: 0.93, y: 30.21)) 121 | shape.addCurve(to: CGPoint(x: 0.97, y: 35.02), controlPoint1: CGPoint(x: -0.28, y: 31.44), controlPoint2: CGPoint(x: -0.35, y: 33.72)) 122 | shape.addCurve(to: CGPoint(x: 5.78, y: 35.06), controlPoint1: CGPoint(x: 2.29, y: 36.34), controlPoint2: CGPoint(x: 4.55, y: 36.3)) 123 | shape.addLine(to: CGPoint(x: 18.01, y: 22.84)) 124 | shape.addLine(to: CGPoint(x: 30.21, y: 35.04)) 125 | shape.addCurve(to: CGPoint(x: 35, y: 34.99), controlPoint1: CGPoint(x: 31.49, y: 36.34), controlPoint2: CGPoint(x: 33.7, y: 36.32)) 126 | shape.addCurve(to: CGPoint(x: 35.05, y: 30.21), controlPoint1: CGPoint(x: 36.33, y: 33.69), controlPoint2: CGPoint(x: 36.33, y: 31.48)) 127 | shape.addLine(to: CGPoint(x: 22.84, y: 18.01)) 128 | shape.addLine(to: CGPoint(x: 35.05, y: 5.79)) 129 | shape.addCurve(to: CGPoint(x: 35, y: 1), controlPoint1: CGPoint(x: 36.33, y: 4.51), controlPoint2: CGPoint(x: 36.33, y: 2.3)) 130 | shape.addCurve(to: CGPoint(x: 30.21, y: 0.95), controlPoint1: CGPoint(x: 33.7, y: -0.32), controlPoint2: CGPoint(x: 31.49, y: -0.32)) 131 | shape.addLine(to: CGPoint(x: 18.01, y: 13.15)) 132 | shape.addLine(to: CGPoint(x: 5.78, y: 0.93)) 133 | shape.addCurve(to: CGPoint(x: 0.97, y: 0.98), controlPoint1: CGPoint(x: 4.55, y: -0.28), controlPoint2: CGPoint(x: 2.27, y: -0.35)) 134 | shape.addCurve(to: CGPoint(x: 0.93, y: 5.79), controlPoint1: CGPoint(x: -0.33, y: 2.3), controlPoint2: CGPoint(x: -0.28, y: 4.55)) 135 | shape.addLine(to: CGPoint(x: 13.15, y: 18.01)) 136 | shape.addLine(to: CGPoint(x: 0.93, y: 30.21)) 137 | shape.close() 138 | 139 | let size = CGSize(width: 36, height: 36) 140 | UIGraphicsBeginImageContext(size) 141 | 142 | defer { 143 | UIGraphicsEndImageContext() 144 | } 145 | 146 | UIColor.black.setFill() 147 | shape.fill() 148 | 149 | return UIGraphicsGetImageFromCurrentImageContext()! 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/Support/Views/Internal/ContinuousCorners/ContinuousMaskLayer.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A shape layer that animates its path inside a block. 10 | */ 11 | 12 | private class AnimatingShapeLayer: CAShapeLayer { 13 | 14 | override class func defaultAction(forKey event: String) -> CAAction? { 15 | 16 | if event == "path" { 17 | return CABasicAnimation(keyPath: event) 18 | } else { 19 | return super.defaultAction(forKey: event) 20 | } 21 | 22 | } 23 | 24 | } 25 | 26 | /** 27 | * A layer whose corners are rounded with a continuous mask (“squircle“). 28 | */ 29 | 30 | class ContinuousMaskLayer: CALayer { 31 | 32 | /// The corner radius. 33 | var continuousCornerRadius: CGFloat = 0 { 34 | didSet { 35 | refreshMask() 36 | } 37 | } 38 | 39 | /// The corners to round. 40 | var roundedCorners: UIRectCorner = .allCorners { 41 | didSet { 42 | refreshMask() 43 | } 44 | } 45 | 46 | // MARK: - Initialization 47 | 48 | override init(layer: Any) { 49 | super.init(layer: layer) 50 | } 51 | 52 | override init() { 53 | super.init() 54 | self.mask = AnimatingShapeLayer() 55 | } 56 | 57 | required init?(coder aDecoder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | // MARK: - Layout 62 | 63 | override func layoutSublayers() { 64 | super.layoutSublayers() 65 | refreshMask() 66 | } 67 | 68 | private func refreshMask() { 69 | 70 | guard let mask = mask as? CAShapeLayer else { 71 | return 72 | } 73 | 74 | let radii = CGSize(width: continuousCornerRadius, height: continuousCornerRadius) 75 | let roundedPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: roundedCorners, cornerRadii: radii) 76 | 77 | mask.path = roundedPath.cgPath 78 | 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /Sources/Support/Views/Internal/ContinuousCorners/RoundedViewProtocol.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A view with rounded corners. Adopt this protocol if your view's layer is a `ContinuousMaskLayer`. 10 | * This protocol provides utilities to easily change the rounded corners. 11 | * 12 | * You need to override `+ (Class *)layerClass` on `UIView` before conforming to this protocol. 13 | */ 14 | 15 | protocol RoundedViewProtocol: NSObjectProtocol { 16 | var layer: CALayer { get } 17 | } 18 | 19 | extension RoundedViewProtocol { 20 | 21 | /// The corner radius of the view. 22 | var cornerRadius: CGFloat { 23 | get { 24 | return roundedLayer.continuousCornerRadius 25 | } 26 | set { 27 | roundedLayer.continuousCornerRadius = newValue 28 | } 29 | } 30 | 31 | /// The corners to round. 32 | var roundedCorners: UIRectCorner { 33 | get { 34 | return roundedLayer.roundedCorners 35 | } 36 | set { 37 | roundedLayer.roundedCorners = newValue 38 | } 39 | } 40 | 41 | private var roundedLayer: ContinuousMaskLayer { 42 | return layer as! ContinuousMaskLayer 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Sources/Support/Views/Internal/ContinuousCorners/UIView+RoundedView.swift: -------------------------------------------------------------------------------- 1 | /** 2 | * BulletinBoard 3 | * Copyright (c) 2017 - present Alexis Aubry. Licensed under the MIT license. 4 | */ 5 | 6 | import UIKit 7 | 8 | /** 9 | * A view with rounded corners. 10 | */ 11 | 12 | class RoundedView: UIView, RoundedViewProtocol { 13 | 14 | override class var layerClass: AnyClass { 15 | return ContinuousMaskLayer.self 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /guides/Migrating To V2.md: -------------------------------------------------------------------------------- 1 | # Migrating to _BulletinBoard_ v2 2 | 3 | _BulletinBoard_ v2 was released on May 28 2018. This version contains lots of new feature such as extended customizability, and provides a more refined developer experience. To view all the new features, please see the [release notes](https://github.com/alexaubry/BulletinBoard/releases/tag/2.0.0). 4 | 5 | Before updating, please be aware that numerous source breaking changes have been made in this version. We wrote this document to help you with the migration task. If you encounter an issue not mentioned in this document, please open an issue on [GitHub](https://github.com/alexaubry/BulletinBoard/issues); we will be happy to help you. 6 | 7 | ## Imports 8 | 9 | For compatibility reasons, the module was renamed to `BLTNBoard`. You will need to update all your import declarations: 10 | 11 | ### Swift 12 | 13 | ~~~swift 14 | // import BulletinBoard 15 | import BLTNBoard 16 | ~~~ 17 | 18 | ### Objective-C 19 | 20 | ~~~objc 21 | // #import 22 | #import 23 | ~~~ 24 | 25 | ## Renamed classes 26 | 27 | Most types have been renamed to adopt the `BLTN` prefix. See the table below for a comparison of old and new names. 28 | 29 | | Name in <= v1.3.0 | Name in v2.0.0 | 30 | |----------------------|-------------------| 31 | | BulletinItem | BLTNItem | 32 | | BulletinManager | BLTNItemManager | 33 | | PageBulletinItem | BLTNPageItem | 34 | | BulletinPadding | BLTNSpacing | 35 | | BulletinBackgroundViewStyle | BLTNBackgroundViewStyle | 36 | | HighlightButtonWrapper | BLTNHighlightButtonWrapper | 37 | 38 | ### Changes to interface factory 39 | 40 | The `BulletinIterfaceFactory` class was split into two entities: `BLTNItemAppearance` and `BLTNInterfaceBuilder`. 41 | 42 | `BLTNActionItem` and its subclasses vend a `BLTNItemAppearance` through their `appearance` property. You can change the values of this object when configuring pages to change the appearance. 43 | 44 | When the item calls `makeContentViewsWithInterfaceBuilder`, it creates an iterface builder for subclasses to use when creating the content. If your custom item is not based on `BLTNActionItem`, you can also create a custom `BLTNInterfaceBuilder` yourself. 45 | 46 | ## Presenting bulletins 47 | 48 | You don't need to manually call `prepare` before presenting the bulletin anymore. 49 | 50 | Replace: 51 | 52 | ~~~swift 53 | bulletinManager.prepare() 54 | bulletinManager.presentBulletin(above: self) 55 | ~~~ 56 | 57 | By: 58 | 59 | ~~~swift 60 | bulletinManager.showBulletin(above: self) 61 | ~~~ 62 | 63 | ## Creating custom items 64 | 65 | The flow for creating custom items was revamped. If you create a page that contains buttons and text, you can subclass `BLTNPageItem` and implement one of these methods to provide the views to add: 66 | 67 | - `makeHeaderViewsWithInterfaceBuilder:` 68 | - `makeViewsUnderTitleWithInterfaceBuilder:` 69 | - `makeViewsUnderImageWithInterfaceBuilder:` 70 | - `makeViewsUnderDescriptionWithInterfaceBuilder:` 71 | - `makeFooterViewsWithInterfaceBuilder:` 72 | 73 | These allow you to create the views you need in addition to the standard controls, without having to recreate those; as it was required in the previous version. 74 | 75 | ### Example 76 | 77 | This is how you would create a page with a text field. 78 | 79 | ~~~swift 80 | class TextFieldBulletinPage: FeedbackPageBLTNItem { 81 | 82 | var textField: UITextField! 83 | 84 | override func makeViewsUnderDescription(with interfaceBuilder: BLTNInterfaceBuilder) -> [UIView]? { 85 | textField = interfaceBuilder.makeTextField(placeholder: "First and Last Name", returnKey: .done, delegate: self) 86 | return [textField] 87 | } 88 | 89 | } 90 | 91 | let page = TextFieldBulletinPage(title: "Enter your Name") 92 | page.descriptionText = "This will be displayed on your profile page." 93 | page.actionButtonTitle = "Save" 94 | ~~~ 95 | --------------------------------------------------------------------------------