Skip to content

Commit aabffb9

Browse files
committedJun 25, 2017
Add center presentation style
1 parent 0f324aa commit aabffb9

34 files changed

+1273
-690
lines changed
 

‎CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4+
## [3.4.0](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.4.0)
5+
6+
### Features
7+
* Added `.center` presentation style with a physics-based dismissal gesture.
8+
* Added `.custom(animator:)` presentation style, where you provide an instance of the `Animator` protocol. The `TopBottomAnimation` and `CenterAnimation` animations both implement `Animator` and may be subclassed (configuration options will be added in a future release). `PhysicsPanHandler` class to provide a physics-based dismissal gesture.
9+
* Added `.centered` message view layout with elements centered and arranged vertically.
10+
* Added `configureBackgroundView(width:)` and `configureBackgroundView(sideMargin:)` convenience methods to `MessageView`.
11+
412
## [3.3.4](https://github.com/SwiftKickMobile/SwiftMessages/releases/tag/3.3.4)
513

614
### Features

‎Demo/Demo.xcodeproj/project.pbxproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@
212212
);
213213
runOnlyForDeploymentPostprocessing = 0;
214214
shellPath = /bin/sh;
215-
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
215+
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
216216
showEnvVarsInLog = 0;
217217
};
218218
/* End PBXShellScriptBuildPhase section */

‎Demo/Demo/AppDelegate.swift

-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
3838
func applicationWillTerminate(_ application: UIApplication) {
3939
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
4040
}
41-
42-
4341
}
4442

‎Demo/Demo/Base.lproj/Main.storyboard

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="JQZ-C5-7mw">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="JQZ-C5-7mw">
33
<device id="retina4_0" orientation="portrait">
44
<adaptation id="fullscreen"/>
55
</device>
66
<dependencies>
77
<deployment identifier="iOS"/>
8-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
8+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
99
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
1010
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
1111
</dependencies>
@@ -36,7 +36,7 @@
3636
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
3737
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
3838
<prototypes>
39-
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="TitleBody" rowHeight="80" id="2n5-7h-3B5" userLabel="TitleBodyCell" customClass="TitleBodyCell" customModule="Demo" customModuleProvider="target">
39+
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="TitleBody" rowHeight="80" id="2n5-7h-3B5" userLabel="TitleBody Cell" customClass="TitleBodyCell" customModule="Demo" customModuleProvider="target">
4040
<rect key="frame" x="0.0" y="28" width="320" height="80"/>
4141
<autoresizingMask key="autoresizingMask"/>
4242
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="2n5-7h-3B5" id="Q5r-8D-38q">
@@ -71,7 +71,7 @@
7171
<outlet property="titleLabel" destination="Icl-Ci-lfe" id="XnB-LL-hx6"/>
7272
</connections>
7373
</tableViewCell>
74-
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Explore" rowHeight="80" id="4Pm-kC-YGr" userLabel="ExploreCell" customClass="TitleBodyCell" customModule="Demo" customModuleProvider="target">
74+
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Explore" rowHeight="80" id="4Pm-kC-YGr" userLabel="Explore Cell" customClass="TitleBodyCell" customModule="Demo" customModuleProvider="target">
7575
<rect key="frame" x="0.0" y="108" width="320" height="80"/>
7676
<autoresizingMask key="autoresizingMask"/>
7777
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4Pm-kC-YGr" id="LmG-UL-Bu8">
@@ -144,10 +144,11 @@
144144
<nil key="highlightedColor"/>
145145
</label>
146146
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="Agu-Vo-ckI">
147-
<rect key="frame" x="18" y="29" width="119" height="29"/>
147+
<rect key="frame" x="18" y="29" width="179" height="29"/>
148148
<segments>
149149
<segment title="Top"/>
150150
<segment title="Bottom"/>
151+
<segment title="Center"/>
151152
</segments>
152153
</segmentedControl>
153154
</subviews>
@@ -691,7 +692,7 @@
691692
</tableViewController>
692693
<placeholder placeholderIdentifier="IBFirstResponder" id="skR-IF-bf8" userLabel="First Responder" sceneMemberID="firstResponder"/>
693694
</objects>
694-
<point key="canvasLocation" x="2340.8000000000002" y="303.14842578710648"/>
695+
<point key="canvasLocation" x="2115" y="304"/>
695696
</scene>
696697
<!--Demo-->
697698
<scene sceneID="TNg-VF-1BI">

‎Demo/Demo/ExploreViewController.swift

+5-3
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate {
1818
let view: MessageView
1919
switch layout.selectedSegmentIndex {
2020
case 1:
21-
view = MessageView.viewFromNib(layout: .CardView)
21+
view = MessageView.viewFromNib(layout: .cardView)
2222
case 2:
23-
view = MessageView.viewFromNib(layout: .TabView)
23+
view = MessageView.viewFromNib(layout: .tabView)
2424
case 3:
25-
view = MessageView.viewFromNib(layout: .StatusLine)
25+
view = MessageView.viewFromNib(layout: .statusLine)
2626
default:
2727
view = try! SwiftMessages.viewFromNib()
2828
}
@@ -85,6 +85,8 @@ class ExploreViewController: UITableViewController, UITextFieldDelegate {
8585
switch presentationStyle.selectedSegmentIndex {
8686
case 1:
8787
config.presentationStyle = .bottom
88+
case 2:
89+
config.presentationStyle = .center
8890
default:
8991
break
9092
}

‎Demo/Demo/ViewController.swift

+23-10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ViewController: UITableViewController {
1616
.titleBody(title: "ANY VIEW", body: "Any view, no matter how cute, can be displayed as a message.", function: ViewController.demoAnyView),
1717
.titleBody(title: "CUSTOMIZE", body: "Easily customize by copying one of the SwiftMessages nib files into your project as a starting point. Then order some tacos.", function: ViewController.demoCustomNib),
1818
.explore,
19+
.titleBody(title: "CENTERED", body: "Show cenetered messages with a fun, physics-based dismissal gesture.", function: ViewController.demoCentered),
1920
]
2021

2122
/*
@@ -55,12 +56,12 @@ class ViewController: UITableViewController {
5556

5657
static func demoBasics() -> Void {
5758

58-
let error = MessageView.viewFromNib(layout: .TabView)
59+
let error = MessageView.viewFromNib(layout: .tabView)
5960
error.configureTheme(.error)
6061
error.configureContent(title: "Error", body: "Something is horribly wrong!")
6162
error.button?.setTitle("Stop", for: .normal)
6263

63-
let warning = MessageView.viewFromNib(layout: .CardView)
64+
let warning = MessageView.viewFromNib(layout: .cardView)
6465
warning.configureTheme(.warning)
6566
warning.configureDropShadow()
6667

@@ -70,31 +71,31 @@ class ViewController: UITableViewController {
7071
var warningConfig = SwiftMessages.defaultConfig
7172
warningConfig.presentationContext = .window(windowLevel: UIWindowLevelStatusBar)
7273

73-
let success = MessageView.viewFromNib(layout: .CardView)
74+
let success = MessageView.viewFromNib(layout: .cardView)
7475
success.configureTheme(.success)
7576
success.configureDropShadow()
7677
success.configureContent(title: "Success", body: "Something good happened!")
7778
success.button?.isHidden = true
7879
var successConfig = SwiftMessages.defaultConfig
79-
successConfig.presentationStyle = .bottom
80+
successConfig.presentationStyle = .center
8081
successConfig.presentationContext = .window(windowLevel: UIWindowLevelNormal)
8182

82-
let info = MessageView.viewFromNib(layout: .MessageView)
83+
let info = MessageView.viewFromNib(layout: .messageView)
8384
info.configureTheme(.info)
8485
info.button?.isHidden = true
8586
info.configureContent(title: "Info", body: "This is a very lengthy and informative info message that wraps across multiple lines and grows in height as needed.")
8687
var infoConfig = SwiftMessages.defaultConfig
8788
infoConfig.presentationStyle = .bottom
8889
infoConfig.duration = .seconds(seconds: 0.25)
8990

90-
let status = MessageView.viewFromNib(layout: .StatusLine)
91+
let status = MessageView.viewFromNib(layout: .statusLine)
9192
status.backgroundView.backgroundColor = UIColor.purple
9293
status.bodyLabel?.textColor = UIColor.white
9394
status.configureContent(body: "A tiny line of text covering the status bar.")
9495
var statusConfig = SwiftMessages.defaultConfig
9596
statusConfig.presentationContext = .window(windowLevel: UIWindowLevelStatusBar)
9697

97-
let status2 = MessageView.viewFromNib(layout: .StatusLine)
98+
let status2 = MessageView.viewFromNib(layout: .statusLine)
9899
status2.backgroundView.backgroundColor = UIColor.orange
99100
status2.bodyLabel?.textColor = UIColor.white
100101
status2.configureContent(body: "Switched to light status bar!")
@@ -138,8 +139,20 @@ class ViewController: UITableViewController {
138139
SwiftMessages.show(config: config, view: view)
139140
}
140141

141-
static func demoExplore() {
142-
142+
static func demoCentered() {
143+
let messageView: MessageView = MessageView.viewFromNib(layout: .centeredView)
144+
messageView.configureBackgroundView(width: 250)
145+
messageView.configureContent(title: "Hey There!", body: "Please try swiping to dismiss this message.", iconImage: nil, iconText: "🦄", buttonImage: nil, buttonTitle: "No Thanks") { _ in
146+
SwiftMessages.hide()
147+
}
148+
messageView.backgroundView.backgroundColor = UIColor.init(white: 0.97, alpha: 1)
149+
messageView.backgroundView.layer.cornerRadius = 12
150+
var config = SwiftMessages.defaultConfig
151+
config.presentationStyle = .center
152+
config.duration = .forever
153+
config.dimMode = .blur(style: .dark, alpha: 1, interactive: true)
154+
config.presentationContext = .window(windowLevel: UIWindowLevelStatusBar)
155+
SwiftMessages.show(config: config, view: messageView)
143156
}
144157
}
145158

@@ -149,7 +162,7 @@ enum Item {
149162

150163
case titleBody(title: String, body: String, function: Function)
151164
case explore
152-
165+
153166
func dequeueCell(_ tableView: UITableView) -> UITableViewCell {
154167
switch self {
155168
case .titleBody(let data):

‎Demo/Podfile.lock

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PODS:
2-
- SwiftMessages (3.3.0)
2+
- SwiftMessages (3.4.0)
33

44
DEPENDENCIES:
55
- SwiftMessages (from `../`)
@@ -9,8 +9,8 @@ EXTERNAL SOURCES:
99
:path: "../"
1010

1111
SPEC CHECKSUMS:
12-
SwiftMessages: 1e6f8140374c014befafcbf3149da86b323b0575
12+
SwiftMessages: dd4ce974e4b8f20e4cde4c77a33577c6ae5e17a0
1313

1414
PODFILE CHECKSUM: 6431c980c9207084d738b6ba87b2101dd9eb5097
1515

16-
COCOAPODS: 1.2.0
16+
COCOAPODS: 1.2.1

‎Demo/Pods/Local Podspecs/SwiftMessages.podspec.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/Pods/Manifest.lock

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/Pods/Pods.xcodeproj/project.pbxproj

+306-264
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/Pods/Target Support Files/Pods-Demo/Pods-Demo-resources.sh

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/Pods/Target Support Files/Pods-Demo/Pods-Demo.debug.xcconfig

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/Pods/Target Support Files/Pods-Demo/Pods-Demo.release.xcconfig

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/Pods/Target Support Files/SwiftMessages/Info.plist

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/Pods/Target Support Files/SwiftMessages/ResourceBundle-SwiftMessages-Info.plist

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Demo/demo.png

120 KB
Loading

‎Design/SwiftMessagesDesign.sketch

920 KB
Binary file not shown.

‎README.md

+4-16
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
[![Platform](https://img.shields.io/cocoapods/p/SwiftMessages.svg?style=flat)](http://cocoadocs.org/docsets/SwiftMessages)
77
[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
88

9-
SwiftMessages is a message bar library for iOS. It's very flexible. And written in Swift.
9+
SwiftMessages is a message view library for iOS. It's very flexible. And written in Swift.
1010

11-
Message bars can be displayed across the top or bottom of the screen, over or under the status bar, or behind navigation bars and tab bars. There's an interactive dismiss gesture. You can dim the background if you like. And much more!
11+
Message views can be displayed at the top, bottom, or center of the screen, over or under the status bar, or behind navigation bars and tab bars. There's an interactive dismiss gesture. You can dim the background if you like. And a lot more!
1212

13-
In addition to numerous configuration options, SwiftMessages provides several attractive layouts and themes. But SwiftMessages was also built to be designer-friendly, which means you can fully and easily customize the view:
13+
In addition to the numerous configuration options, SwiftMessages provides several good-looking layouts and themes. But SwiftMessages is also designer-friendly, which means you can fully and easily customize the view:
1414

1515
* Copy one of the included nib files into your project and change it.
1616
* Subclass `MessageView` and add elements, etc.
@@ -35,14 +35,8 @@ Add one of the following lines to your Podfile depending on your Swift version:
3535
````ruby
3636
# Swift 3.0 - Xcode 8
3737
pod 'SwiftMessages'
38-
39-
# Swift 2.3 - Xcode 8
40-
pod 'SwiftMessages', '~> 2.0.0'
41-
42-
# Swift 2.2 - Xcode 7.3.1
43-
pod 'SwiftMessages', '~> 1.1.4'
4438
````
45-
__Note that Swift 2.3 and Swift 3.0 require minimum CocoaPods version 1.1.0__.
39+
__Note that the minimum CocoaPods version is 1.1.0__.
4640

4741
### Carthage
4842

@@ -51,12 +45,6 @@ Add one of the following lines to your Cartfile depending on your Swift version:
5145
````ruby
5246
# Swift 3.0 - Xcode 8
5347
github "SwiftKickMobile/SwiftMessages"
54-
55-
# Swift 2.3 - Xcode 8
56-
github "SwiftKickMobile/SwiftMessages" ~> 2.0.0
57-
58-
# Swift 2.2 - Xcode 7.3.1
59-
github "SwiftKickMobile/SwiftMessages" ~> 1.1.4
6048
````
6149

6250
## Usage

‎SwiftMessages.podspec

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
Pod::Spec.new do |spec|
22
spec.name = 'SwiftMessages'
3-
spec.version = '3.3.4'
3+
spec.version = '3.4.0'
44
spec.license = { :type => 'MIT' }
55
spec.homepage = 'https://github.com/SwiftKickMobile/SwiftMessages'
66
spec.authors = { 'Timothy Moose' => 'tim@swiftkick.it' }
77
spec.summary = 'A very flexible message bar for iOS written in Swift.'
8-
spec.source = {:git => 'https://github.com/SwiftKickMobile/SwiftMessages.git', :tag => '3.3.4'}
8+
spec.source = {:git => 'https://github.com/SwiftKickMobile/SwiftMessages.git', :tag => '3.4.0'}
99
spec.platform = :ios, '8.0'
1010
spec.ios.deployment_target = '8.0'
1111
spec.source_files = 'SwiftMessages/**/*.swift'

‎SwiftMessages.xcodeproj/project.pbxproj

+32
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
2244656E1EF1D66800C50413 /* CenterAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2244656D1EF1D66800C50413 /* CenterAnimation.swift */; };
11+
2298C2051EE47DC900E2DDC1 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2041EE47DC900E2DDC1 /* Weak.swift */; };
12+
2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2061EE480D000E2DDC1 /* Animator.swift */; };
13+
2298C2091EE486E300E2DDC1 /* TopBottomAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */; };
14+
22DFC9161EFF30F6001B1CA1 /* CenteredView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */; };
15+
22DFC9181F00674E001B1CA1 /* PhysicsPanHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */; };
1016
22E01F641E74EC8B00ACE19A /* MaskingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E01F631E74EC8B00ACE19A /* MaskingView.swift */; };
1117
22E307FF1E74C5B100E35893 /* AccessibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22E307FE1E74C5B100E35893 /* AccessibleMessage.swift */; };
1218
86589D471D64B6E40041676C /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86589D461D64B6E40041676C /* BaseView.swift */; };
@@ -46,6 +52,12 @@
4652
/* End PBXContainerItemProxy section */
4753

4854
/* Begin PBXFileReference section */
55+
2244656D1EF1D66800C50413 /* CenterAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CenterAnimation.swift; sourceTree = "<group>"; };
56+
2298C2041EE47DC900E2DDC1 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
57+
2298C2061EE480D000E2DDC1 /* Animator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = "<group>"; };
58+
2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopBottomAnimation.swift; sourceTree = "<group>"; };
59+
22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = CenteredView.xib; path = Resources/CenteredView.xib; sourceTree = "<group>"; };
60+
22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhysicsPanHandler.swift; sourceTree = "<group>"; };
4961
22E01F631E74EC8B00ACE19A /* MaskingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MaskingView.swift; sourceTree = "<group>"; };
5062
22E307FE1E74C5B100E35893 /* AccessibleMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessibleMessage.swift; sourceTree = "<group>"; };
5163
862C0C6A1D58E93300D06168 /* SwiftMessages.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; fileEncoding = 4; path = SwiftMessages.podspec; sourceTree = SOURCE_ROOT; };
@@ -97,6 +109,16 @@
97109
/* End PBXFrameworksBuildPhase section */
98110

99111
/* Begin PBXGroup section */
112+
2244656C1EF1D62700C50413 /* Animations */ = {
113+
isa = PBXGroup;
114+
children = (
115+
2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */,
116+
2244656D1EF1D66800C50413 /* CenterAnimation.swift */,
117+
22DFC9171F00674E001B1CA1 /* PhysicsPanHandler.swift */,
118+
);
119+
name = Animations;
120+
sourceTree = "<group>";
121+
};
100122
862C0CD81D5A396900D06168 /* Resources */ = {
101123
isa = PBXGroup;
102124
children = (
@@ -105,6 +127,7 @@
105127
86BBA8F81D5E01FC00FE8F16 /* CardView.xib */,
106128
86589D901D692B1B0041676C /* TabView.xib */,
107129
862C0CDB1D5A397F00D06168 /* StatusLine.xib */,
130+
22DFC9151EFF30F6001B1CA1 /* CenteredView.xib */,
108131
862C0CE21D5A3A0D00D06168 /* MessageViewIOS8.xib */,
109132
);
110133
name = Resources;
@@ -121,6 +144,8 @@
121144
22E307FE1E74C5B100E35893 /* AccessibleMessage.swift */,
122145
86AAF82A1D580DD70031EE32 /* Error.swift */,
123146
86DBE0031D75BE800071E51D /* Array+Utils.swift */,
147+
2298C2061EE480D000E2DDC1 /* Animator.swift */,
148+
2298C2041EE47DC900E2DDC1 /* Weak.swift */,
124149
);
125150
name = Base;
126151
sourceTree = "<group>";
@@ -163,6 +188,7 @@
163188
864495581D4FA0AD0056EB2A /* SwiftMessages.swift */,
164189
867E21821D4D025200594A41 /* MessageView.swift */,
165190
862C0CD81D5A396900D06168 /* Resources */,
191+
2244656C1EF1D62700C50413 /* Animations */,
166192
864495571D4F7C490056EB2A /* Base */,
167193
867E218E1D4D3DFD00594A41 /* Internal */,
168194
86B48B031D5A41E500063E2B /* Support */,
@@ -290,6 +316,7 @@
290316
E6E49F941D70A395006CB883 /* Images.xcassets in Resources */,
291317
86589D911D692B1C0041676C /* TabView.xib in Resources */,
292318
E6E49F921D70A349006CB883 /* StatusLine.xib in Resources */,
319+
22DFC9161EFF30F6001B1CA1 /* CenteredView.xib in Resources */,
293320
);
294321
runOnlyForDeploymentPostprocessing = 0;
295322
};
@@ -310,16 +337,21 @@
310337
86BBA8FC1D5E03F100FE8F16 /* MessageView.swift in Sources */,
311338
86BBA9061D5E040C00FE8F16 /* Identifiable.swift in Sources */,
312339
86BBA9011D5E040600FE8F16 /* PassthroughWindow.swift in Sources */,
340+
2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */,
313341
86BBA9031D5E040600FE8F16 /* UIViewController+Utils.swift in Sources */,
314342
22E01F641E74EC8B00ACE19A /* MaskingView.swift in Sources */,
343+
2298C2051EE47DC900E2DDC1 /* Weak.swift in Sources */,
315344
86BBA9001D5E040600FE8F16 /* PassthroughView.swift in Sources */,
345+
22DFC9181F00674E001B1CA1 /* PhysicsPanHandler.swift in Sources */,
346+
2244656E1EF1D66800C50413 /* CenterAnimation.swift in Sources */,
316347
22E307FF1E74C5B100E35893 /* AccessibleMessage.swift in Sources */,
317348
86BBA9041D5E040600FE8F16 /* NSBundle+Utils.swift in Sources */,
318349
86BBA8FD1D5E03F800FE8F16 /* SwiftMessages.swift in Sources */,
319350
86BBA9021D5E040600FE8F16 /* WindowViewController.swift in Sources */,
320351
86BBA8FF1D5E040600FE8F16 /* Presenter.swift in Sources */,
321352
86BBA9051D5E040C00FE8F16 /* Theme.swift in Sources */,
322353
86BBA9081D5E040C00FE8F16 /* Error.swift in Sources */,
354+
2298C2091EE486E300E2DDC1 /* TopBottomAnimation.swift in Sources */,
323355
86589D471D64B6E40041676C /* BaseView.swift in Sources */,
324356
86BBA9071D5E040C00FE8F16 /* MarginAdjustable.swift in Sources */,
325357
867BED211D622793005212E3 /* BackgroundViewable.swift in Sources */,

‎SwiftMessages/Animator.swift

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// Animator.swift
3+
// SwiftMessages
4+
//
5+
// Created by Timothy Moose on 6/4/17.
6+
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public typealias AnimationCompletion = (_ completed: Bool) -> Void
12+
13+
public protocol AnimationDelegate: class {
14+
func hide(animator: Animator)
15+
func panStarted(animator: Animator)
16+
func panEnded(animator: Animator)
17+
}
18+
19+
public class AnimationContext {
20+
21+
public let messageView: UIView
22+
public let containerView: UIView
23+
public let behindStatusBar: Bool
24+
public let interactiveHide: Bool
25+
26+
init(messageView: UIView, containerView: UIView, behindStatusBar: Bool, interactiveHide: Bool) {
27+
self.messageView = messageView
28+
self.containerView = containerView
29+
self.behindStatusBar = behindStatusBar
30+
self.interactiveHide = interactiveHide
31+
}
32+
}
33+
34+
public protocol Animator: class {
35+
36+
weak var delegate: AnimationDelegate? { get set }
37+
38+
func show(context: AnimationContext, completion: @escaping AnimationCompletion)
39+
40+
func hide(context: AnimationContext, completion: @escaping AnimationCompletion)
41+
}

‎SwiftMessages/BaseView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ extension BaseView {
168168
layer.masksToBounds = false
169169
updateShadowPath()
170170
}
171-
171+
172172
private func updateShadowPath() {
173173
layer.shadowPath = UIBezierPath(roundedRect: layer.bounds, cornerRadius: layer.cornerRadius).cgPath
174174
}

‎SwiftMessages/CenterAnimation.swift

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//
2+
// CenterAnimation.swift
3+
// SwiftMessages
4+
//
5+
// Created by Timothy Moose on 6/14/17.
6+
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
public class CenterAnimation: NSObject, Animator {
12+
13+
public weak var delegate: AnimationDelegate?
14+
weak var messageView: UIView?
15+
weak var containerView: UIView?
16+
17+
init(delegate: AnimationDelegate) {
18+
self.delegate = delegate
19+
}
20+
21+
public func show(context: AnimationContext, completion: @escaping AnimationCompletion) {
22+
install(context: context)
23+
showAnimation(context: context, completion: completion)
24+
}
25+
26+
public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) {
27+
if panHandler?.isOffScreen ?? false {
28+
context.messageView.alpha = 0
29+
panHandler?.state?.stop()
30+
}
31+
let view = context.messageView
32+
CATransaction.begin()
33+
CATransaction.setCompletionBlock {
34+
view.alpha = 1
35+
view.transform = CGAffineTransform.identity
36+
completion(true)
37+
}
38+
UIView.animate(withDuration: 0.15, delay: 0, options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], animations: {
39+
view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
40+
}, completion: nil)
41+
UIView.animate(withDuration: 0.15, delay: 0, options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], animations: {
42+
view.alpha = 0
43+
}, completion: nil)
44+
CATransaction.commit()
45+
}
46+
47+
func install(context: AnimationContext) {
48+
let view = context.messageView
49+
let container = context.containerView
50+
messageView = view
51+
containerView = container
52+
view.translatesAutoresizingMaskIntoConstraints = false
53+
container.addSubview(view)
54+
let centerX = NSLayoutConstraint(item: view, attribute: .centerX, relatedBy: .equal, toItem: container, attribute: .centerX, multiplier: 1.00, constant: 0.0)
55+
let centerY = NSLayoutConstraint(item: view, attribute: .centerY, relatedBy: .equal, toItem: container, attribute: .centerY, multiplier: 1.00, constant: 0.0)
56+
let leftMargin = NSLayoutConstraint(item: view, attribute: .left, relatedBy: .equal, toItem: container, attribute: .left, multiplier: 1.00, constant: 00.0)
57+
container.addConstraints([centerX, centerY, leftMargin])
58+
container.layoutIfNeeded()
59+
installInteractive(context: context)
60+
}
61+
62+
func showAnimation(context: AnimationContext, completion: @escaping AnimationCompletion) {
63+
let view = context.messageView
64+
view.alpha = 0.25
65+
view.transform = CGAffineTransform(scaleX: 0.6, y: 0.6)
66+
CATransaction.begin()
67+
CATransaction.setCompletionBlock {
68+
completion(true)
69+
}
70+
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: {
71+
view.transform = CGAffineTransform.identity
72+
}, completion: nil)
73+
UIView.animate(withDuration: 0.15, delay: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: {
74+
view.alpha = 1
75+
}, completion: nil)
76+
CATransaction.commit()
77+
}
78+
79+
var panHandler: PhysicsPanHandler?
80+
81+
func installInteractive(context: AnimationContext) {
82+
guard context.interactiveHide else { return }
83+
panHandler = PhysicsPanHandler(context: context, animator: self)
84+
}
85+
}
86+

‎SwiftMessages/MaskingView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class MaskingView: PassthroughView {
1717
didSet {
1818
oldValue?.removeFromSuperview()
1919
if let view = backgroundView {
20+
view.isUserInteractionEnabled = false
2021
view.frame = bounds
2122
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
2223
addSubview(view)

‎SwiftMessages/MessageView.swift

+44-6
Original file line numberDiff line numberDiff line change
@@ -144,30 +144,36 @@ extension MessageView {
144144
The standard message view that stretches across the full width of the
145145
container view.
146146
*/
147-
case MessageView = "MessageView"
147+
case messageView = "MessageView"
148148

149149
/**
150150
A floating card-style view with rounded corners.
151151
*/
152-
case CardView = "CardView"
152+
case cardView = "CardView"
153153

154154
/**
155155
Like `CardView` with one end attached to the super view.
156156
*/
157-
case TabView = "TabView"
157+
case tabView = "TabView"
158158

159159
/**
160160
A 20pt tall view that can be used to overlay the status bar.
161161
Note that this layout will automatically grow taller if displayed
162162
directly under the status bar (see the `ContentInsetting` protocol).
163163
*/
164-
case StatusLine = "StatusLine"
165-
164+
case statusLine = "StatusLine"
165+
166+
/**
167+
A floating card-style view with elements centered and arranged vertically.
168+
This view is typically used with `.center` presentation style.
169+
*/
170+
case centeredView = "CenteredView"
171+
166172
/**
167173
A standard message view like `MessageView`, but without
168174
stack views for iOS 8.
169175
*/
170-
case MessageViewIOS8 = "MessageViewIOS8"
176+
case messageViewIOS8 = "MessageViewIOS8"
171177
}
172178

173179
/**
@@ -386,3 +392,35 @@ extension MessageView {
386392
}
387393
}
388394

395+
/*
396+
MARK: - Configuring the width
397+
398+
This extension provides a few convenience functions for configuring the
399+
background view's width. You are encouraged to write your own such functions
400+
if these don't exactly meet your needs.
401+
*/
402+
403+
extension MessageView {
404+
405+
/**
406+
A shortcut for configuring the left and right layout margins. For views that
407+
have `backgroundView` as a subview of `MessageView`, the background view should
408+
be pinned to the left and right `layoutMargins` in order for this configuration to work.
409+
*/
410+
public func configureBackgroundView(sideMargin: CGFloat) {
411+
layoutMargins.left = sideMargin
412+
layoutMargins.right = sideMargin
413+
}
414+
415+
/**
416+
A shortcut for adding a width constraint to the `backgroundView`. When calling this
417+
method, it is important to ensure that the width constraint doesn't conflict with
418+
other constraints. The CardView.nib and TabView.nib layouts are compatible with
419+
this method.
420+
*/
421+
public func configureBackgroundView(width: CGFloat) {
422+
let constraint = NSLayoutConstraint(item: backgroundView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: width)
423+
backgroundView.addConstraint(constraint)
424+
}
425+
}
426+

‎SwiftMessages/PhysicsPanHandler.swift

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//
2+
// PhysicsPanHandler.swift
3+
// SwiftMessages
4+
//
5+
// Created by Timothy Moose on 6/25/17.
6+
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
open class PhysicsPanHandler {
12+
13+
public final class State {
14+
15+
weak var messageView: UIView?
16+
weak var containerView: UIView?
17+
var dynamicAnimator: UIDynamicAnimator
18+
var itemBehavior: UIDynamicItemBehavior
19+
var attachmentBehavior: UIAttachmentBehavior? {
20+
didSet {
21+
if let oldValue = oldValue {
22+
dynamicAnimator.removeBehavior(oldValue)
23+
}
24+
if let attachmentBehavior = attachmentBehavior {
25+
dynamicAnimator.addBehavior(attachmentBehavior)
26+
angle = messageView?.angle ?? angle
27+
time = CFAbsoluteTimeGetCurrent()
28+
}
29+
}
30+
}
31+
var time: CFAbsoluteTime = 0
32+
var angle: CGFloat = 0
33+
34+
init(messageView: UIView, containerView: UIView) {
35+
self.messageView = messageView
36+
self.containerView = containerView
37+
let dynamicAnimator = UIDynamicAnimator(referenceView: containerView)
38+
let itemBehavior = UIDynamicItemBehavior(items: [messageView])
39+
itemBehavior.allowsRotation = true
40+
dynamicAnimator.addBehavior(itemBehavior)
41+
self.itemBehavior = itemBehavior
42+
self.dynamicAnimator = dynamicAnimator
43+
}
44+
45+
func update(attachmentAnchorPoint anchorPoint: CGPoint) {
46+
angle = messageView?.angle ?? angle
47+
time = CFAbsoluteTimeGetCurrent()
48+
attachmentBehavior?.anchorPoint = anchorPoint
49+
}
50+
51+
public func stop() {
52+
guard let messageView = messageView else {
53+
dynamicAnimator.removeAllBehaviors()
54+
return
55+
}
56+
let center = messageView.center
57+
let transform = messageView.transform
58+
dynamicAnimator.removeAllBehaviors()
59+
messageView.center = center
60+
messageView.transform = transform
61+
}
62+
}
63+
64+
weak var animator: Animator?
65+
weak var messageView: UIView?
66+
weak var containerView: UIView?
67+
var state: State?
68+
private(set) var isOffScreen = false
69+
70+
public init(context: AnimationContext, animator: Animator) {
71+
messageView = context.messageView
72+
containerView = context.containerView
73+
self.animator = animator
74+
let pan = UIPanGestureRecognizer()
75+
pan.addTarget(self, action: #selector(pan(_:)))
76+
if let view = messageView as? BackgroundViewable {
77+
view.backgroundView.addGestureRecognizer(pan)
78+
} else {
79+
context.messageView.addGestureRecognizer(pan)
80+
}
81+
}
82+
83+
@objc func pan(_ pan: UIPanGestureRecognizer) {
84+
guard let messageView = messageView, let containerView = containerView, let animator = animator else { return }
85+
let anchorPoint = pan.location(in: containerView)
86+
switch pan.state {
87+
case .began:
88+
animator.delegate?.panStarted(animator: animator)
89+
let state = State(messageView: messageView, containerView: containerView)
90+
self.state = state
91+
let center = messageView.center
92+
let offset = UIOffset(horizontal: anchorPoint.x - center.x, vertical: anchorPoint.y - center.y)
93+
let attachmentBehavior = UIAttachmentBehavior(item: messageView, offsetFromCenter: offset, attachedToAnchor: anchorPoint)
94+
state.attachmentBehavior = attachmentBehavior
95+
state.itemBehavior.action = { [weak self, weak messageView, weak containerView] in
96+
guard let strongSelf = self, let messageView = messageView, let containerView = containerView, let animator = strongSelf.animator else { return }
97+
let view = (messageView as? BackgroundViewable)?.backgroundView ?? messageView
98+
let frame = containerView.convert(view.bounds, from: view)
99+
if !containerView.bounds.intersects(frame) {
100+
strongSelf.isOffScreen = true
101+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
102+
animator.delegate?.hide(animator: animator)
103+
}
104+
}
105+
}
106+
case .changed:
107+
guard let state = state else { return }
108+
state.update(attachmentAnchorPoint: anchorPoint)
109+
case .ended, .cancelled:
110+
guard let state = state else { return }
111+
let velocity = pan.velocity(in: containerView)
112+
let time = CFAbsoluteTimeGetCurrent()
113+
let angle = messageView.angle
114+
let angularVelocity: CGFloat
115+
if time > state.time {
116+
angularVelocity = (angle - state.angle) / CGFloat(time - state.time)
117+
} else {
118+
angularVelocity = 0
119+
}
120+
let speed = sqrt(pow(velocity.x, 2) + pow(velocity.y, 2))
121+
// The multiplier on angular velocity was determined by hand-tuning
122+
let energy = sqrt(pow(speed, 2) + pow(angularVelocity * 75, 2))
123+
if energy > 200 && speed > 600 {
124+
// Limit the speed and angular velocity to reasonable values
125+
let speedScale = speed > 0 ? min(1, 1800 / speed) : 1
126+
let escapeVelocity = CGPoint(x: velocity.x * speedScale, y: velocity.y * speedScale)
127+
let angularSpeedScale = min(1, 10 / fabs(angularVelocity))
128+
let escapeAngularVelocity = angularVelocity * angularSpeedScale
129+
state.itemBehavior.addLinearVelocity(escapeVelocity, for: messageView)
130+
state.itemBehavior.addAngularVelocity(escapeAngularVelocity, for: messageView)
131+
state.attachmentBehavior = nil
132+
} else {
133+
animator.delegate?.panEnded(animator: animator)
134+
state.stop()
135+
self.state = nil
136+
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0, options: .beginFromCurrentState, animations: {
137+
messageView.center = CGPoint(x: containerView.bounds.width / 2, y: containerView.bounds.height / 2)
138+
messageView.transform = CGAffineTransform.identity
139+
}, completion: nil)
140+
}
141+
default:
142+
break
143+
}
144+
}
145+
}
146+
147+
extension UIView {
148+
var angle: CGFloat {
149+
// http://stackoverflow.com/a/2051861/1271826
150+
return atan2(transform.b, transform.a)
151+
}
152+
}

‎SwiftMessages/Presenter.swift

+184-335
Large diffs are not rendered by default.

‎SwiftMessages/Resources/CardView.xib

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
33
<device id="retina4_7" orientation="portrait">
44
<adaptation id="fullscreen"/>
55
</device>
66
<dependencies>
77
<deployment version="2304" identifier="iOS"/>
8-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
8+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
99
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
1010
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
1111
</dependencies>
@@ -17,10 +17,10 @@
1717
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
1818
<subviews>
1919
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="dp3-Ae-zep" userLabel="Background view">
20-
<rect key="frame" x="18" y="18" width="572" height="133"/>
20+
<rect key="frame" x="18" y="18" width="564" height="133"/>
2121
<subviews>
2222
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="RJH-Fp-YDa" userLabel="Content view">
23-
<rect key="frame" x="20" y="20" width="532" height="93"/>
23+
<rect key="frame" x="20" y="20" width="524" height="93"/>
2424
<subviews>
2525
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" horizontalCompressionResistancePriority="752" text="😬" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pFx-Py-lZQ" userLabel="Icon label">
2626
<rect key="frame" x="0.0" y="24" width="43" height="45.5"/>
@@ -35,7 +35,7 @@
3535
<rect key="frame" x="53" y="29.5" width="33" height="34"/>
3636
</imageView>
3737
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" translatesAutoresizingMaskIntoConstraints="NO" id="cJi-r8-oeb">
38-
<rect key="frame" x="96" y="27.5" width="369" height="38.5"/>
38+
<rect key="frame" x="96" y="27.5" width="361" height="38.5"/>
3939
<subviews>
4040
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="[Title]" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="IWB-BS-FgD">
4141
<rect key="frame" x="0.0" y="0.0" width="48" height="20.5"/>
@@ -60,7 +60,7 @@
6060
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
6161
</stackView>
6262
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="252" horizontalCompressionResistancePriority="752" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VUM-rq-TVm">
63-
<rect key="frame" x="475" y="31.5" width="57" height="30"/>
63+
<rect key="frame" x="467" y="31.5" width="57" height="30"/>
6464
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
6565
<state key="normal" title="[Button]"/>
6666
</button>
@@ -83,8 +83,8 @@
8383
</subviews>
8484
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
8585
<constraints>
86-
<constraint firstAttribute="trailing" secondItem="dp3-Ae-zep" secondAttribute="trailing" constant="10" id="gGP-zK-6VU"/>
87-
<constraint firstItem="dp3-Ae-zep" firstAttribute="leading" secondItem="JI3-gM-XBO" secondAttribute="leadingMargin" constant="10" id="gyP-aD-uO3"/>
86+
<constraint firstItem="dp3-Ae-zep" firstAttribute="centerX" secondItem="JI3-gM-XBO" secondAttribute="centerX" id="gnw-SY-kB7"/>
87+
<constraint firstItem="dp3-Ae-zep" firstAttribute="leading" secondItem="JI3-gM-XBO" secondAttribute="leadingMargin" priority="900" constant="10" id="gyP-aD-uO3"/>
8888
<constraint firstAttribute="bottom" secondItem="dp3-Ae-zep" secondAttribute="bottom" constant="10" id="le8-gK-lcY"/>
8989
<constraint firstItem="dp3-Ae-zep" firstAttribute="top" secondItem="JI3-gM-XBO" secondAttribute="topMargin" constant="10" id="s15-z9-aHc"/>
9090
</constraints>
+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
3+
<device id="retina4_7" orientation="portrait">
4+
<adaptation id="fullscreen"/>
5+
</device>
6+
<dependencies>
7+
<deployment version="2304" identifier="iOS"/>
8+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
9+
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
10+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
11+
</dependencies>
12+
<objects>
13+
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
14+
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
15+
<view contentMode="scaleToFill" id="g5x-hF-D0u" userLabel="Card View" customClass="MessageView" customModule="SwiftMessages">
16+
<rect key="frame" x="0.0" y="0.0" width="337" height="270"/>
17+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
18+
<subviews>
19+
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0Mj-jC-BX6" userLabel="Background view">
20+
<rect key="frame" x="18" y="18" width="301" height="250"/>
21+
<subviews>
22+
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="YST-yc-0HK" userLabel="Content view">
23+
<rect key="frame" x="20" y="20" width="261" height="210"/>
24+
<subviews>
25+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" horizontalCompressionResistancePriority="752" text="😬" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="elb-HW-olH" userLabel="Icon label">
26+
<rect key="frame" x="109" y="0.0" width="43" height="45.5"/>
27+
<accessibility key="accessibilityConfiguration">
28+
<bool key="isElement" value="NO"/>
29+
</accessibility>
30+
<fontDescription key="fontDescription" type="system" pointSize="38"/>
31+
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
32+
<nil key="highlightedColor"/>
33+
</label>
34+
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="252" verticalHuggingPriority="251" horizontalCompressionResistancePriority="752" image="errorIcon" translatesAutoresizingMaskIntoConstraints="NO" id="hUK-sr-A3U" userLabel="Icon image view">
35+
<rect key="frame" x="114" y="55.5" width="33" height="34"/>
36+
</imageView>
37+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="[Title]" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ggW-gB-SWF">
38+
<rect key="frame" x="106.5" y="99.5" width="48" height="20.5"/>
39+
<accessibility key="accessibilityConfiguration">
40+
<bool key="isElement" value="NO"/>
41+
</accessibility>
42+
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
43+
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
44+
<nil key="highlightedColor"/>
45+
</label>
46+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="[Message Body]" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TGC-Td-alh">
47+
<rect key="frame" x="75" y="130" width="111.5" height="18"/>
48+
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
49+
<accessibility key="accessibilityConfiguration">
50+
<bool key="isElement" value="NO"/>
51+
</accessibility>
52+
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
53+
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
54+
<nil key="highlightedColor"/>
55+
</label>
56+
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="252" horizontalCompressionResistancePriority="752" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VOA-Iy-PCr">
57+
<rect key="frame" x="102" y="158" width="57" height="52"/>
58+
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
59+
<state key="normal" title="[Button]"/>
60+
</button>
61+
</subviews>
62+
</stackView>
63+
</subviews>
64+
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
65+
<constraints>
66+
<constraint firstItem="YST-yc-0HK" firstAttribute="leading" secondItem="0Mj-jC-BX6" secondAttribute="leading" constant="20" id="BRx-pe-2BP"/>
67+
<constraint firstAttribute="trailing" secondItem="YST-yc-0HK" secondAttribute="trailing" constant="20" id="JYB-gw-7Ol"/>
68+
<constraint firstAttribute="bottom" secondItem="YST-yc-0HK" secondAttribute="bottom" constant="20" id="Vsy-Y9-qzf"/>
69+
<constraint firstItem="YST-yc-0HK" firstAttribute="top" secondItem="0Mj-jC-BX6" secondAttribute="top" constant="20" id="yRl-kK-cQH"/>
70+
</constraints>
71+
<userDefinedRuntimeAttributes>
72+
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
73+
<integer key="value" value="5"/>
74+
</userDefinedRuntimeAttribute>
75+
</userDefinedRuntimeAttributes>
76+
</view>
77+
</subviews>
78+
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
79+
<constraints>
80+
<constraint firstItem="0Mj-jC-BX6" firstAttribute="top" secondItem="g5x-hF-D0u" secondAttribute="topMargin" constant="10" id="3zU-S0-d99"/>
81+
<constraint firstItem="0Mj-jC-BX6" firstAttribute="leading" secondItem="g5x-hF-D0u" secondAttribute="leadingMargin" priority="900" constant="10" id="Ebe-tX-wgR"/>
82+
<constraint firstItem="0Mj-jC-BX6" firstAttribute="centerX" secondItem="g5x-hF-D0u" secondAttribute="centerX" id="mMO-fy-KI8"/>
83+
<constraint firstAttribute="bottom" secondItem="0Mj-jC-BX6" secondAttribute="bottomMargin" constant="10" id="uzR-7t-pdN"/>
84+
</constraints>
85+
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
86+
<userDefinedRuntimeAttributes>
87+
<userDefinedRuntimeAttribute type="number" keyPath="bounceAnimationOffset">
88+
<real key="value" value="0.0"/>
89+
</userDefinedRuntimeAttribute>
90+
<userDefinedRuntimeAttribute type="number" keyPath="statusBarOffset">
91+
<real key="value" value="10"/>
92+
</userDefinedRuntimeAttribute>
93+
</userDefinedRuntimeAttributes>
94+
<connections>
95+
<outlet property="backgroundView" destination="0Mj-jC-BX6" id="idD-fU-aXH"/>
96+
<outlet property="bodyLabel" destination="TGC-Td-alh" id="swz-Qb-Dxw"/>
97+
<outlet property="button" destination="VOA-Iy-PCr" id="LGs-Ax-1Ds"/>
98+
<outlet property="iconImageView" destination="hUK-sr-A3U" id="Htw-4A-Adn"/>
99+
<outlet property="iconLabel" destination="elb-HW-olH" id="JXQ-xt-3t7"/>
100+
<outlet property="titleLabel" destination="ggW-gB-SWF" id="Coi-Gy-uPh"/>
101+
</connections>
102+
<point key="canvasLocation" x="345.5" y="320"/>
103+
</view>
104+
</objects>
105+
<resources>
106+
<image name="errorIcon" width="33" height="34"/>
107+
</resources>
108+
</document>

‎SwiftMessages/Resources/TabView.xib

+9-9
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
33
<device id="retina4_7" orientation="portrait">
44
<adaptation id="fullscreen"/>
55
</device>
66
<dependencies>
77
<deployment version="2304" identifier="iOS"/>
8-
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11757"/>
8+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
99
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
1010
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
1111
</dependencies>
@@ -17,10 +17,10 @@
1717
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
1818
<subviews>
1919
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8l1-Wp-omF" userLabel="Background view">
20-
<rect key="frame" x="18" y="0.0" width="572" height="123"/>
20+
<rect key="frame" x="18" y="0.0" width="564" height="123"/>
2121
<subviews>
2222
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="bdc-by-rjM" userLabel="Content view">
23-
<rect key="frame" x="20" y="28" width="532" height="67"/>
23+
<rect key="frame" x="20" y="28" width="524" height="67"/>
2424
<subviews>
2525
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" horizontalCompressionResistancePriority="752" text="😬" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="09t-tr-Q8T" userLabel="Icon label">
2626
<rect key="frame" x="0.0" y="11" width="43" height="45.5"/>
@@ -35,7 +35,7 @@
3535
<rect key="frame" x="53" y="16.5" width="33" height="34"/>
3636
</imageView>
3737
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="top" translatesAutoresizingMaskIntoConstraints="NO" id="h4V-Az-Wau">
38-
<rect key="frame" x="96" y="14.5" width="369" height="38.5"/>
38+
<rect key="frame" x="96" y="14.5" width="361" height="38.5"/>
3939
<subviews>
4040
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="751" text="[Title]" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RxN-E1-uzx">
4141
<rect key="frame" x="0.0" y="0.0" width="48" height="20.5"/>
@@ -60,7 +60,7 @@
6060
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
6161
</stackView>
6262
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="252" horizontalCompressionResistancePriority="752" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vBc-Qb-BBh">
63-
<rect key="frame" x="475" y="18.5" width="57" height="30"/>
63+
<rect key="frame" x="467" y="18.5" width="57" height="30"/>
6464
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
6565
<state key="normal" title="[Button]"/>
6666
</button>
@@ -70,8 +70,8 @@
7070
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
7171
<constraints>
7272
<constraint firstItem="bdc-by-rjM" firstAttribute="top" secondItem="8l1-Wp-omF" secondAttribute="topMargin" constant="20" id="3Q1-Vl-OFg"/>
73-
<constraint firstItem="bdc-by-rjM" firstAttribute="leading" secondItem="8l1-Wp-omF" secondAttribute="leading" constant="20" id="ZeN-Yg-DDA"/>
74-
<constraint firstAttribute="trailing" secondItem="bdc-by-rjM" secondAttribute="trailing" constant="20" id="Zuf-gb-AgS"/>
73+
<constraint firstItem="bdc-by-rjM" firstAttribute="leading" secondItem="8l1-Wp-omF" secondAttribute="leading" priority="900" constant="20" id="ZeN-Yg-DDA"/>
74+
<constraint firstAttribute="trailing" secondItem="bdc-by-rjM" secondAttribute="trailing" priority="900" constant="20" id="Zuf-gb-AgS"/>
7575
<constraint firstAttribute="bottomMargin" secondItem="bdc-by-rjM" secondAttribute="bottom" constant="20" id="jri-TS-nOa"/>
7676
</constraints>
7777
<edgeInsets key="layoutMargins" top="0.0" left="0.0" bottom="0.0" right="0.0"/>
@@ -84,7 +84,7 @@
8484
</subviews>
8585
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
8686
<constraints>
87-
<constraint firstAttribute="trailing" secondItem="8l1-Wp-omF" secondAttribute="trailing" constant="10" id="9zp-kw-f7h"/>
87+
<constraint firstItem="8l1-Wp-omF" firstAttribute="centerX" secondItem="Kgd-fq-RpT" secondAttribute="centerX" id="7tH-rr-EQZ"/>
8888
<constraint firstItem="8l1-Wp-omF" firstAttribute="top" secondItem="Kgd-fq-RpT" secondAttribute="top" id="WBW-gJ-lIP"/>
8989
<constraint firstAttribute="bottom" secondItem="8l1-Wp-omF" secondAttribute="bottom" id="cYv-6k-QSZ"/>
9090
<constraint firstItem="8l1-Wp-omF" firstAttribute="leading" secondItem="Kgd-fq-RpT" secondAttribute="leadingMargin" constant="10" id="tVC-3b-F2x"/>

‎SwiftMessages/SwiftMessages.swift

+50-16
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ private let globalInstance = SwiftMessages()
1515
It behaves like a queue, only showing one message at a time. Message views that
1616
implement the `Identifiable` protocol (as `MessageView` does) will have duplicates removed.
1717
*/
18-
open class SwiftMessages: PresenterDelegate {
18+
open class SwiftMessages {
1919

2020
/**
2121
Specifies whether the message view is displayed at the top or bottom
@@ -32,6 +32,16 @@ open class SwiftMessages: PresenterDelegate {
3232
Message view slides up from the bottom.
3333
*/
3434
case bottom
35+
36+
/**
37+
Message view fades into the center.
38+
*/
39+
case center
40+
41+
/**
42+
User-defined animation
43+
*/
44+
case custom(animator: Animator)
3545
}
3646

3747
/**
@@ -480,6 +490,10 @@ open class SwiftMessages: PresenterDelegate {
480490
guard queue.count > 0 else { return }
481491
let current = queue.removeFirst()
482492
self.current = current
493+
// Set `autohideToken` before the animation starts in case
494+
// the dismiss gesture begins before we've queued the autohide
495+
// block on animation completion.
496+
self.autohideToken = current
483497
current.showDate = Date()
484498
DispatchQueue.main.async { [weak self] in
485499
guard let strongSelf = self else { return }
@@ -493,7 +507,9 @@ open class SwiftMessages: PresenterDelegate {
493507
})
494508
return
495509
}
496-
strongSelf.queueAutoHide()
510+
if current === strongSelf.autohideToken {
511+
strongSelf.queueAutoHide()
512+
}
497513
}
498514
} catch {
499515
strongSelf.current = nil
@@ -532,34 +548,52 @@ open class SwiftMessages: PresenterDelegate {
532548
})
533549
}
534550
}
535-
536-
/*
537-
MARK: - PresenterDelegate
538-
*/
539-
551+
}
552+
553+
554+
/*
555+
MARK: - PresenterDelegate
556+
*/
557+
558+
extension SwiftMessages: PresenterDelegate {
559+
540560
func hide(presenter: Presenter) {
561+
if let current = current, presenter === current {
562+
hideCurrent()
563+
}
564+
queue = queue.filter { $0 !== presenter }
565+
delays.remove(presenter: presenter)
566+
}
567+
568+
public func hide(animator: Animator) {
541569
syncQueue.async { [weak self] in
542570
guard let strongSelf = self else { return }
543-
if let current = strongSelf.current, presenter === current {
544-
strongSelf.hideCurrent()
571+
if let presenter = strongSelf.presenter(forAnimator: animator) {
572+
strongSelf.hide(presenter: presenter)
545573
}
546-
strongSelf.queue = strongSelf.queue.filter { $0 !== presenter }
547-
strongSelf.delays.remove(presenter: presenter)
548574
}
549575
}
550-
551-
func panStarted(presenter: Presenter) {
576+
577+
public func panStarted(animator: Animator) {
552578
autohideToken = nil
553579
}
554-
555-
func panEnded(presenter: Presenter) {
580+
581+
public func panEnded(animator: Animator) {
556582
queueAutoHide()
557583
}
584+
585+
private func presenter(forAnimator animator: Animator) -> Presenter? {
586+
if let current = current, animator === current.animator {
587+
return current
588+
}
589+
let queued = queue.filter { $0.animator === animator }
590+
return queued.first
591+
}
558592
}
559593

560594
/**
561595
MARK: - Creating views from nibs
562-
596+
563597
This extension provides several convenience functions for instantiating views from nib files.
564598
SwiftMessages provides several default nib files in the Resources folder that can be
565599
drag-and-dropped into a project as a starting point and modified.

‎SwiftMessages/Theme.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,10 @@ public enum IconStyle {
4444
case `default`
4545
case light
4646
case subtle
47+
case none
4748

4849
/// Returns the image for the given theme
49-
public func image(theme: Theme) -> UIImage {
50+
public func image(theme: Theme) -> UIImage? {
5051
switch (theme, self) {
5152
case (.info, .default): return Icon.Info.image
5253
case (.info, .light): return Icon.InfoLight.image
@@ -60,6 +61,7 @@ public enum IconStyle {
6061
case (.error, .default): return Icon.Error.image
6162
case (.error, .light): return Icon.ErrorLight.image
6263
case (.error, .subtle): return Icon.ErrorSubtle.image
64+
default: return nil
6365
}
6466
}
6567
}
+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
//
2+
// TopBottomAnimation.swift
3+
// SwiftMessages
4+
//
5+
// Created by Timothy Moose on 6/4/17.
6+
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
7+
//
8+
9+
import UIKit
10+
11+
public class TopBottomAnimation: NSObject, Animator {
12+
13+
enum Style {
14+
case top
15+
case bottom
16+
}
17+
18+
public weak var delegate: AnimationDelegate?
19+
20+
var style: Style
21+
22+
var translationConstraint: NSLayoutConstraint! = nil
23+
24+
weak var messageView: UIView?
25+
26+
weak var containerView: UIView?
27+
28+
init(style: Style, delegate: AnimationDelegate) {
29+
self.style = style
30+
self.delegate = delegate
31+
}
32+
33+
public func show(context: AnimationContext, completion: @escaping AnimationCompletion) {
34+
install(context: context)
35+
showAnimation(completion: completion)
36+
}
37+
38+
public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) {
39+
let view = context.messageView
40+
let container = context.containerView
41+
UIView.animate(withDuration: 0.2, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: {
42+
let size = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
43+
self.translationConstraint.constant -= size.height
44+
container.layoutIfNeeded()
45+
}, completion: { completed in
46+
completion(completed)
47+
})
48+
}
49+
50+
func install(context: AnimationContext) {
51+
let view = context.messageView
52+
let container = context.containerView
53+
messageView = view
54+
containerView = container
55+
if let adjustable = context.messageView as? MarginAdjustable {
56+
bounceOffset = adjustable.bounceAnimationOffset
57+
}
58+
view.translatesAutoresizingMaskIntoConstraints = false
59+
container.addSubview(view)
60+
let leading = NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: container, attribute: .leading, multiplier: 1.00, constant: 0.0)
61+
let trailing = NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: container, attribute: .trailing, multiplier: 1.00, constant: 0.0)
62+
switch style {
63+
case .top:
64+
translationConstraint = NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: container, attribute: .top, multiplier: 1.00, constant: 0.0)
65+
case .bottom:
66+
translationConstraint = NSLayoutConstraint(item: container, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1.00, constant: 0.0)
67+
}
68+
container.addConstraints([leading, trailing, translationConstraint])
69+
if let adjustable = view as? MarginAdjustable {
70+
var top: CGFloat = 0.0
71+
var bottom: CGFloat = 0.0
72+
switch style {
73+
case .top:
74+
top += adjustable.bounceAnimationOffset
75+
if context.behindStatusBar {
76+
top += adjustable.statusBarOffset
77+
}
78+
case .bottom:
79+
bottom += adjustable.bounceAnimationOffset
80+
}
81+
view.layoutMargins = UIEdgeInsets(top: top, left: 0.0, bottom: bottom, right: 0.0)
82+
}
83+
let size = view.systemLayoutSizeFitting(UILayoutFittingCompressedSize)
84+
translationConstraint.constant -= size.height
85+
container.layoutIfNeeded()
86+
if context.interactiveHide {
87+
let pan = UIPanGestureRecognizer()
88+
pan.addTarget(self, action: #selector(pan(_:)))
89+
if let view = view as? BackgroundViewable {
90+
view.backgroundView.addGestureRecognizer(pan)
91+
} else {
92+
view.addGestureRecognizer(pan)
93+
}
94+
}
95+
}
96+
97+
func showAnimation(completion: @escaping AnimationCompletion) {
98+
guard let container = containerView else {
99+
completion(false)
100+
return
101+
}
102+
let animationDistance = self.translationConstraint.constant + bounceOffset
103+
// Cap the initial velocity at zero because the bounceOffset may not be great
104+
// enough to allow for greater bounce induced by a quick panning motion.
105+
let initialSpringVelocity = animationDistance == 0.0 ? 0.0 : min(0.0, closeSpeed / animationDistance)
106+
UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: initialSpringVelocity, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: {
107+
self.translationConstraint.constant = -self.bounceOffset
108+
container.layoutIfNeeded()
109+
}, completion: { completed in
110+
completion(completed)
111+
})
112+
}
113+
114+
fileprivate var bounceOffset: CGFloat = 5
115+
116+
/*
117+
MARK: - Pan to close
118+
*/
119+
120+
fileprivate var closing = false
121+
fileprivate var closeSpeed: CGFloat = 0.0
122+
fileprivate var closePercent: CGFloat = 0.0
123+
fileprivate var panTranslationY: CGFloat = 0.0
124+
125+
@objc func pan(_ pan: UIPanGestureRecognizer) {
126+
switch pan.state {
127+
case .changed:
128+
guard let view = pan.view else { return }
129+
let height = view.bounds.height - bounceOffset
130+
if height <= 0 { return }
131+
let point = pan.location(ofTouch: 0, in: view)
132+
var velocity = pan.velocity(in: view)
133+
var translation = pan.translation(in: view)
134+
if case .top = style {
135+
velocity.y *= -1.0
136+
translation.y *= -1.0
137+
}
138+
if !closing {
139+
if view.bounds.contains(point) && velocity.y > 0.0 && velocity.x / velocity.y < 5.0 {
140+
closing = true
141+
pan.setTranslation(CGPoint.zero, in: view)
142+
delegate?.panStarted(animator: self)
143+
}
144+
}
145+
if !closing { return }
146+
let translationAmount = -bounceOffset - max(0.0, translation.y)
147+
translationConstraint.constant = translationAmount
148+
closeSpeed = velocity.y
149+
closePercent = translation.y / height
150+
panTranslationY = translation.y
151+
case .ended, .cancelled:
152+
if closeSpeed > 750.0 || closePercent > 0.33 {
153+
delegate?.hide(animator: self)
154+
} else {
155+
closing = false
156+
closeSpeed = 0.0
157+
closePercent = 0.0
158+
panTranslationY = 0.0
159+
showAnimation(completion: { (completed) in
160+
self.delegate?.panEnded(animator: self)
161+
})
162+
}
163+
default:
164+
break
165+
}
166+
}
167+
}

‎SwiftMessages/Weak.swift

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//
2+
// Weak.swift
3+
// SwiftMessages
4+
//
5+
// Created by Timothy Moose on 6/4/17.
6+
// Copyright © 2017 SwiftKick Mobile. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public class Weak<T: AnyObject> {
12+
public weak var value : T?
13+
public init(value: T?) {
14+
self.value = value
15+
}
16+
}

0 commit comments

Comments
 (0)
Please sign in to comment.