├── .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 |
--------------------------------------------------------------------------------
/.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 | [](https://cocoapods.org/pods/BulletinBoard)
4 | [](https://cocoapods.org/pods/BulletinBoard)
5 | [](https://cocoapods.org/pods/BulletinBoard)
6 | [](https://alexisakers.github.io/BulletinBoard)
7 | [](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 | 
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 |
--------------------------------------------------------------------------------